@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/index.js CHANGED
@@ -403,7 +403,6 @@ var init_readSpec = __esm({
403
403
  DEFAULT_MAX_LINES = 500;
404
404
  readSpecTool = {
405
405
  definition: {
406
- clearable: true,
407
406
  name: "readSpec",
408
407
  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).",
409
408
  inputSchema: {
@@ -536,7 +535,6 @@ var init_writeSpec = __esm({
536
535
  init_fileLock();
537
536
  writeSpecTool = {
538
537
  definition: {
539
- clearable: true,
540
538
  name: "writeSpec",
541
539
  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.",
542
540
  inputSchema: {
@@ -687,7 +685,6 @@ var init_editSpec = __esm({
687
685
  init_fileLock();
688
686
  editSpecTool = {
689
687
  definition: {
690
- clearable: true,
691
688
  name: "editSpec",
692
689
  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.",
693
690
  inputSchema: {
@@ -821,7 +818,6 @@ var init_listSpecFiles = __esm({
821
818
  "use strict";
822
819
  listSpecFilesTool = {
823
820
  definition: {
824
- clearable: false,
825
821
  name: "listSpecFiles",
826
822
  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.",
827
823
  inputSchema: {
@@ -855,7 +851,6 @@ var init_presentPublishPlan = __esm({
855
851
  "use strict";
856
852
  presentPublishPlanTool = {
857
853
  definition: {
858
- clearable: false,
859
854
  name: "presentPublishPlan",
860
855
  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.",
861
856
  inputSchema: {
@@ -886,7 +881,6 @@ var init_writePlan = __esm({
886
881
  PLAN_FILE = ".remy-plan.md";
887
882
  writePlanTool = {
888
883
  definition: {
889
- clearable: false,
890
884
  name: "writePlan",
891
885
  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.",
892
886
  inputSchema: {
@@ -923,7 +917,6 @@ var init_updatePlanStatus = __esm({
923
917
  PLAN_FILE2 = ".remy-plan.md";
924
918
  updatePlanStatusTool = {
925
919
  definition: {
926
- clearable: false,
927
920
  name: "updatePlanStatus",
928
921
  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.',
929
922
  inputSchema: {
@@ -971,7 +964,6 @@ var init_setProjectOnboardingState = __esm({
971
964
  "use strict";
972
965
  setProjectOnboardingStateTool = {
973
966
  definition: {
974
- clearable: false,
975
967
  name: "setProjectOnboardingState",
976
968
  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.",
977
969
  inputSchema: {
@@ -1000,7 +992,6 @@ var init_promptUser = __esm({
1000
992
  "use strict";
1001
993
  promptUserTool = {
1002
994
  definition: {
1003
- clearable: false,
1004
995
  name: "promptUser",
1005
996
  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.',
1006
997
  inputSchema: {
@@ -1138,7 +1129,6 @@ var init_confirmDestructiveAction = __esm({
1138
1129
  "use strict";
1139
1130
  confirmDestructiveActionTool = {
1140
1131
  definition: {
1141
- clearable: false,
1142
1132
  name: "confirmDestructiveAction",
1143
1133
  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.",
1144
1134
  inputSchema: {
@@ -1325,7 +1315,6 @@ var init_sdkConsultant = __esm({
1325
1315
  init_runCli();
1326
1316
  askMindStudioSdkTool = {
1327
1317
  definition: {
1328
- clearable: false,
1329
1318
  name: "askMindStudioSdk",
1330
1319
  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.",
1331
1320
  inputSchema: {
@@ -1487,7 +1476,6 @@ var init_searchGoogle = __esm({
1487
1476
  init_runCli();
1488
1477
  searchGoogleTool = {
1489
1478
  definition: {
1490
- clearable: false,
1491
1479
  name: "searchGoogle",
1492
1480
  description: "Search Google and return results. Use for research, finding documentation, looking up APIs, or any task where web search would help.",
1493
1481
  inputSchema: {
@@ -1524,7 +1512,6 @@ var init_setProjectMetadata = __esm({
1524
1512
  "use strict";
1525
1513
  setProjectMetadataTool = {
1526
1514
  definition: {
1527
- clearable: false,
1528
1515
  name: "setProjectMetadata",
1529
1516
  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.",
1530
1517
  inputSchema: {
@@ -1759,7 +1746,11 @@ function getConversationMessagesForSummary(messages, endIndex) {
1759
1746
  break;
1760
1747
  }
1761
1748
  }
1762
- return messages.slice(startIdx, endIndex);
1749
+ return messages.slice(startIdx, endIndex).filter(
1750
+ (msg) => !(Array.isArray(msg.content) && msg.content.some(
1751
+ (b) => b.type === "tool" && b.uiOnly
1752
+ ))
1753
+ );
1763
1754
  }
1764
1755
  function getSubAgentMessagesForSummary(messages, subAgentName, endIndex) {
1765
1756
  let checkpointIdx = -1;
@@ -2289,7 +2280,12 @@ ${summaryBlock.text}
2289
2280
  }
2290
2281
  startIdx = checkpointIdx + 1;
2291
2282
  }
2292
- const messagesToProcess = fixOrphanedToolCalls(messages.slice(startIdx));
2283
+ const apiMessages = messages.slice(startIdx).filter(
2284
+ (msg) => !(msg.role === "assistant" && Array.isArray(msg.content) && msg.content.some(
2285
+ (b) => b.type === "tool" && b.uiOnly
2286
+ ))
2287
+ );
2288
+ const messagesToProcess = fixOrphanedToolCalls(apiMessages);
2293
2289
  const toolUseIds = /* @__PURE__ */ new Set();
2294
2290
  for (const msg of messagesToProcess) {
2295
2291
  if (msg.role === "assistant" && Array.isArray(msg.content)) {
@@ -2393,6 +2389,11 @@ var init_toolResultCap = __esm({
2393
2389
  // src/session.ts
2394
2390
  import fs10 from "fs";
2395
2391
  import path4 from "path";
2392
+ function writeFileAtomicSync(file, data) {
2393
+ const tmp = `${file}.tmp`;
2394
+ fs10.writeFileSync(tmp, data, "utf-8");
2395
+ fs10.renameSync(tmp, file);
2396
+ }
2396
2397
  function loadSession(state) {
2397
2398
  pruneArchives();
2398
2399
  try {
@@ -2481,7 +2482,7 @@ function archiveMessages(messages, label, models) {
2481
2482
  if (models && Object.keys(models).length > 0) {
2482
2483
  payload.models = models;
2483
2484
  }
2484
- fs10.writeFileSync(dest, JSON.stringify(payload), "utf-8");
2485
+ writeFileAtomicSync(dest, JSON.stringify(payload));
2485
2486
  archiveCountCache.set(path4.basename(dest), count);
2486
2487
  log3.info("Session archived", { label, dest, messageCount: count });
2487
2488
  pruneArchives();
@@ -2686,7 +2687,7 @@ function saveSession(state) {
2686
2687
  if (Buffer.byteLength(serialized, "utf-8") > ROTATE_THRESHOLD_BYTES && rotate(state)) {
2687
2688
  serialized = JSON.stringify(buildPayload(state));
2688
2689
  }
2689
- fs10.writeFileSync(SESSION_FILE, serialized, "utf-8");
2690
+ writeFileAtomicSync(SESSION_FILE, serialized);
2690
2691
  log3.info("Session saved", { messageCount: state.messages.length });
2691
2692
  } catch (err) {
2692
2693
  log3.warn("Session save failed", { error: err.message });
@@ -2737,6 +2738,28 @@ var init_session = __esm({
2737
2738
  });
2738
2739
 
2739
2740
  // src/compaction/trigger.ts
2741
+ function getInflightCompaction() {
2742
+ return inflightCompaction;
2743
+ }
2744
+ function summariesOf(result) {
2745
+ const out = [];
2746
+ for (const msg of result.checkpoints) {
2747
+ if (!Array.isArray(msg.content)) {
2748
+ continue;
2749
+ }
2750
+ for (const block of msg.content) {
2751
+ if (block.type === "summary") {
2752
+ out.push({
2753
+ name: block.name,
2754
+ text: block.text,
2755
+ ...block.recent && { recent: block.recent },
2756
+ startedAt: block.startedAt
2757
+ });
2758
+ }
2759
+ }
2760
+ }
2761
+ return out;
2762
+ }
2740
2763
  function applyPendingSummaries(state) {
2741
2764
  const drained = pending;
2742
2765
  pending = null;
@@ -2757,6 +2780,20 @@ function applyPendingSummaries(state) {
2757
2780
  });
2758
2781
  saveSession(state);
2759
2782
  }
2783
+ function formatSummariesResult(summaries) {
2784
+ if (summaries === null) {
2785
+ return "A compaction checkpoint was already pending \u2014 no new compaction was needed.";
2786
+ }
2787
+ if (summaries.length === 0) {
2788
+ return "Nothing to compact \u2014 the conversation is already fully summarized.";
2789
+ }
2790
+ if (summaries.length === 1 && summaries[0].name === "conversation") {
2791
+ return summaries[0].text;
2792
+ }
2793
+ return summaries.map((s) => `## ${SUMMARY_SECTION_LABELS[s.name] ?? s.name}
2794
+
2795
+ ${s.text}`).join("\n\n");
2796
+ }
2760
2797
  function setCompactionListener(l) {
2761
2798
  listener = l;
2762
2799
  }
@@ -2766,18 +2803,24 @@ function triggerCompaction(state, apiConfig, opts = {}) {
2766
2803
  }
2767
2804
  if (pending) {
2768
2805
  log4.info("Compaction skipped \u2014 a checkpoint is already waiting to apply");
2769
- return Promise.resolve();
2806
+ return Promise.resolve(null);
2770
2807
  }
2771
- const { blocking = false, requestId, model } = opts;
2772
- listener?.({ type: "started", blocking, requestId });
2808
+ const { blocking = false, requestId, model, origin, toolCallId } = opts;
2809
+ listener?.({ type: "started", blocking, requestId, origin, toolCallId });
2773
2810
  inflightCompaction = compactConversation(
2774
2811
  state.messages,
2775
2812
  apiConfig,
2776
2813
  resolveModel("conversationSummarizer", state.models, model)
2777
2814
  ).then((result) => {
2778
2815
  pending = result;
2779
- listener?.({ type: "complete", requestId });
2816
+ const summaries = summariesOf(result);
2817
+ listener?.({
2818
+ type: "complete",
2819
+ requestId,
2820
+ ...summaries.length > 0 && { summaries }
2821
+ });
2780
2822
  log4.info("Compaction complete");
2823
+ return summaries;
2781
2824
  }).catch((err) => {
2782
2825
  const message = err.message || "Compaction failed";
2783
2826
  listener?.({ type: "complete", error: message, requestId });
@@ -2788,7 +2831,7 @@ function triggerCompaction(state, apiConfig, opts = {}) {
2788
2831
  });
2789
2832
  return inflightCompaction;
2790
2833
  }
2791
- var log4, pending, inflightCompaction, listener;
2834
+ var log4, pending, inflightCompaction, SUMMARY_SECTION_LABELS, listener;
2792
2835
  var init_trigger = __esm({
2793
2836
  "src/compaction/trigger.ts"() {
2794
2837
  "use strict";
@@ -2799,6 +2842,11 @@ var init_trigger = __esm({
2799
2842
  log4 = createLogger("compaction:trigger");
2800
2843
  pending = null;
2801
2844
  inflightCompaction = null;
2845
+ SUMMARY_SECTION_LABELS = {
2846
+ conversation: "Conversation",
2847
+ visualDesignExpert: "Design Agent Thread",
2848
+ productVision: "Roadmap Agent Thread"
2849
+ };
2802
2850
  listener = null;
2803
2851
  }
2804
2852
  });
@@ -2810,8 +2858,13 @@ var init_compactConversation = __esm({
2810
2858
  "use strict";
2811
2859
  init_trigger();
2812
2860
  compactConversationTool = {
2861
+ backgroundOnly: true,
2862
+ // Silent: the completed summary is written onto the tool block for the UI
2863
+ // detail view only. The model must never receive it as a message — it gets
2864
+ // the summary the correct way, as the checkpoint prefix in every later API
2865
+ // call (cleanMessagesForApi).
2866
+ backgroundNotify: "silent",
2813
2867
  definition: {
2814
- clearable: false,
2815
2868
  name: "compactConversation",
2816
2869
  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.",
2817
2870
  inputSchema: {
@@ -2823,11 +2876,32 @@ var init_compactConversation = __esm({
2823
2876
  if (!context?.conversationMessages || !context.apiConfig) {
2824
2877
  return "Error: compaction requires execution context.";
2825
2878
  }
2879
+ const { toolCallId, onBackgroundComplete, toolRegistry } = context;
2826
2880
  triggerCompaction(
2827
2881
  { messages: context.conversationMessages },
2828
2882
  context.apiConfig,
2829
- { blocking: false, requestId: context.requestId }
2830
- ).catch(() => {
2883
+ // origin/toolCallId tell the headless listener a real block exists —
2884
+ // it must not synthesize a UI-only one for this compaction.
2885
+ {
2886
+ blocking: false,
2887
+ requestId: context.requestId,
2888
+ origin: "tool",
2889
+ toolCallId
2890
+ }
2891
+ ).then((summaries) => {
2892
+ onBackgroundComplete?.(
2893
+ toolCallId,
2894
+ "compactConversation",
2895
+ formatSummariesResult(summaries)
2896
+ );
2897
+ }).catch((err) => {
2898
+ onBackgroundComplete?.(
2899
+ toolCallId,
2900
+ "compactConversation",
2901
+ `Error: ${err.message || "Compaction failed"}`
2902
+ );
2903
+ }).finally(() => {
2904
+ toolRegistry?.unregister(toolCallId);
2831
2905
  });
2832
2906
  return "Compaction started in the background.";
2833
2907
  }
@@ -2925,7 +2999,6 @@ var init_loadSkill = __esm({
2925
2999
  init_catalog();
2926
3000
  loadSkillTool = {
2927
3001
  definition: {
2928
- clearable: true,
2929
3002
  name: "loadSkill",
2930
3003
  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.",
2931
3004
  inputSchema: {
@@ -2982,7 +3055,6 @@ var init_readFile = __esm({
2982
3055
  MAX_BYTES = 64 * 1024;
2983
3056
  readFileTool = {
2984
3057
  definition: {
2985
- clearable: true,
2986
3058
  name: "readFile",
2987
3059
  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.",
2988
3060
  inputSchema: {
@@ -3094,7 +3166,6 @@ var init_writeFile = __esm({
3094
3166
  init_fileLock();
3095
3167
  writeFileTool = {
3096
3168
  definition: {
3097
- clearable: true,
3098
3169
  name: "writeFile",
3099
3170
  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.",
3100
3171
  inputSchema: {
@@ -3169,7 +3240,6 @@ var init_editFile = __esm({
3169
3240
  init_helpers2();
3170
3241
  editFileTool = {
3171
3242
  definition: {
3172
- clearable: true,
3173
3243
  name: "editFile",
3174
3244
  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.",
3175
3245
  inputSchema: {
@@ -3278,7 +3348,6 @@ var init_bash = __esm({
3278
3348
  MAX_OUTPUT_BYTES = 3e4;
3279
3349
  bashTool = {
3280
3350
  definition: {
3281
- clearable: true,
3282
3351
  name: "bash",
3283
3352
  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.",
3284
3353
  inputSchema: {
@@ -3406,7 +3475,6 @@ var init_grep = __esm({
3406
3475
  DEFAULT_MAX = 50;
3407
3476
  grepTool = {
3408
3477
  definition: {
3409
- clearable: true,
3410
3478
  name: "grep",
3411
3479
  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.",
3412
3480
  inputSchema: {
@@ -3524,7 +3592,6 @@ var init_glob = __esm({
3524
3592
  DEFAULT_MAX2 = 200;
3525
3593
  globTool = {
3526
3594
  definition: {
3527
- clearable: true,
3528
3595
  name: "glob",
3529
3596
  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.',
3530
3597
  inputSchema: {
@@ -3628,7 +3695,6 @@ var init_listDir = __esm({
3628
3695
  MAX_CHILDREN = 15;
3629
3696
  listDirTool = {
3630
3697
  definition: {
3631
- clearable: true,
3632
3698
  name: "listDir",
3633
3699
  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.",
3634
3700
  inputSchema: {
@@ -3711,7 +3777,6 @@ var init_editsFinished = __esm({
3711
3777
  "use strict";
3712
3778
  editsFinishedTool = {
3713
3779
  definition: {
3714
- clearable: false,
3715
3780
  name: "editsFinished",
3716
3781
  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.",
3717
3782
  inputSchema: {
@@ -3799,7 +3864,6 @@ var init_lspDiagnostics = __esm({
3799
3864
  init_lsp();
3800
3865
  lspDiagnosticsTool = {
3801
3866
  definition: {
3802
- clearable: true,
3803
3867
  name: "lspDiagnostics",
3804
3868
  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.",
3805
3869
  inputSchema: {
@@ -3855,7 +3919,6 @@ var init_restartProcess = __esm({
3855
3919
  init_lsp();
3856
3920
  restartProcessTool = {
3857
3921
  definition: {
3858
- clearable: false,
3859
3922
  name: "restartProcess",
3860
3923
  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.",
3861
3924
  inputSchema: {
@@ -3888,7 +3951,6 @@ var init_runScenario = __esm({
3888
3951
  "use strict";
3889
3952
  runScenarioTool = {
3890
3953
  definition: {
3891
- clearable: true,
3892
3954
  name: "runScenario",
3893
3955
  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.",
3894
3956
  inputSchema: {
@@ -3920,7 +3982,6 @@ var init_runMethod = __esm({
3920
3982
  "use strict";
3921
3983
  runMethodTool = {
3922
3984
  definition: {
3923
- clearable: true,
3924
3985
  name: "runMethod",
3925
3986
  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.',
3926
3987
  inputSchema: {
@@ -3961,7 +4022,6 @@ var init_queryDatabase = __esm({
3961
4022
  "use strict";
3962
4023
  queryDatabaseTool = {
3963
4024
  definition: {
3964
- clearable: true,
3965
4025
  name: "queryDatabase",
3966
4026
  description: "Execute a raw SQL query against the dev database and return the results. Use for inspecting data and debugging issues.",
3967
4027
  inputSchema: {
@@ -4337,16 +4397,48 @@ ${partial}` : "[INTERRUPTED] Tool execution was stopped.";
4337
4397
  });
4338
4398
 
4339
4399
  // src/statusWatcher.ts
4400
+ function sanitizeStatusText(text) {
4401
+ if (!text) {
4402
+ return "";
4403
+ }
4404
+ for (const marker of INTERNAL_PAYLOAD_MARKERS) {
4405
+ if (text.includes(marker)) {
4406
+ return "";
4407
+ }
4408
+ }
4409
+ const truncIdx = text.indexOf("(tool result truncated at");
4410
+ return truncIdx === -1 ? text : text.slice(0, truncIdx);
4411
+ }
4340
4412
  function startStatusWatcher(config) {
4341
4413
  const { apiConfig, getContext, onStatus, interval = 5e3, signal } = config;
4342
4414
  let inflight2 = false;
4343
4415
  let stopped = false;
4344
4416
  let pauseCount = 0;
4345
4417
  const url = `${apiConfig.baseUrl}/_internal/v2/agent/remy/generate-status`;
4418
+ let consecutiveFailures = 0;
4419
+ let backoffMs = 0;
4420
+ let nextAllowedAt = 0;
4421
+ const MAX_CONSECUTIVE_FAILURES = 10;
4422
+ const MAX_BACKOFF_MS = 6e4;
4423
+ function recordFailure() {
4424
+ consecutiveFailures++;
4425
+ backoffMs = Math.min(
4426
+ backoffMs === 0 ? interval : backoffMs * 2,
4427
+ MAX_BACKOFF_MS
4428
+ );
4429
+ nextAllowedAt = Date.now() + backoffMs;
4430
+ if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES && !stopped) {
4431
+ stopped = true;
4432
+ clearInterval(timer);
4433
+ }
4434
+ }
4346
4435
  async function tick() {
4347
4436
  if (stopped || signal?.aborted || inflight2 || pauseCount > 0) {
4348
4437
  return;
4349
4438
  }
4439
+ if (Date.now() < nextAllowedAt) {
4440
+ return;
4441
+ }
4350
4442
  inflight2 = true;
4351
4443
  try {
4352
4444
  const context = getContext();
@@ -4362,18 +4454,29 @@ function startStatusWatcher(config) {
4362
4454
  body: JSON.stringify({ appId: apiConfig.appId, context }),
4363
4455
  signal
4364
4456
  });
4365
- if (!res.ok || stopped) {
4457
+ if (stopped) {
4458
+ return;
4459
+ }
4460
+ if (!res.ok) {
4461
+ recordFailure();
4366
4462
  return;
4367
4463
  }
4368
4464
  const data = await res.json();
4369
4465
  if (!data.label) {
4466
+ recordFailure();
4370
4467
  return;
4371
4468
  }
4469
+ consecutiveFailures = 0;
4470
+ backoffMs = 0;
4471
+ nextAllowedAt = 0;
4372
4472
  if (pauseCount > 0 || stopped || signal?.aborted) {
4373
4473
  return;
4374
4474
  }
4375
4475
  onStatus(data.label);
4376
4476
  } catch {
4477
+ if (!stopped && !signal?.aborted) {
4478
+ recordFailure();
4479
+ }
4377
4480
  } finally {
4378
4481
  inflight2 = false;
4379
4482
  }
@@ -4394,9 +4497,17 @@ function startStatusWatcher(config) {
4394
4497
  }
4395
4498
  };
4396
4499
  }
4500
+ var INTERNAL_PAYLOAD_MARKERS;
4397
4501
  var init_statusWatcher = __esm({
4398
4502
  "src/statusWatcher.ts"() {
4399
4503
  "use strict";
4504
+ INTERNAL_PAYLOAD_MARKERS = [
4505
+ "[USER CANCELLED]",
4506
+ "[INTERRUPTED]",
4507
+ "[INTERRUPTED - PARTIAL OUTPUT RETRIEVED]",
4508
+ "<background_results>",
4509
+ "<tool_result"
4510
+ ];
4400
4511
  }
4401
4512
  });
4402
4513
 
@@ -4465,329 +4576,341 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
4465
4576
  return { text: USER_CANCELLED_RESULT, messages: thisInvocation() };
4466
4577
  }
4467
4578
  let lastToolResult = "";
4468
- while (true) {
4469
- turns++;
4470
- if (signal?.aborted) {
4471
- return abortResult([]);
4472
- }
4473
- const iterStart = Date.now();
4474
- const contentBlocks = [];
4475
- let thinkingStartedAt = 0;
4476
- let lastThinkingRelatedStartedAt;
4477
- let stopReason = "end_turn";
4478
- let currentToolNames = "";
4479
- let lastUsage;
4480
- let lastProviderMetadata;
4481
- let lastModelId;
4482
- const statusWatcher = startStatusWatcher({
4483
- apiConfig,
4484
- getContext: () => {
4485
- const parts = [];
4486
- if (task) {
4487
- parts.push(`Task: ${task.slice(-200)}`);
4488
- }
4489
- const text = getPartialText(contentBlocks);
4490
- if (text) {
4491
- parts.push(`Assistant text: ${text.slice(-500)}`);
4492
- }
4493
- if (currentToolNames) {
4494
- parts.push(`Tool: ${currentToolNames}`);
4495
- }
4496
- if (lastToolResult) {
4497
- parts.push(`Tool result: ${lastToolResult.slice(-200)}`);
4498
- }
4499
- return parts.join("\n");
4500
- },
4501
- onStatus: (label) => emit({ type: "status", message: label }),
4502
- signal
4503
- });
4504
- try {
4505
- for await (const event of streamChatWithRetry(
4506
- {
4507
- ...apiConfig,
4508
- model,
4509
- requestId,
4510
- subAgentId,
4511
- system: fullSystem,
4512
- messages: cleanMessagesForApi(messages),
4513
- tools: tools2,
4514
- signal
4515
- },
4516
- {
4517
- onRetry: (attempt) => emit({
4518
- type: "status",
4519
- message: `Lost connection, retrying (attempt ${attempt + 2} of 3)`
4520
- })
4579
+ let watchedBlocks = [];
4580
+ let watchedToolNames = "";
4581
+ const statusWatcher = startStatusWatcher({
4582
+ apiConfig,
4583
+ getContext: () => {
4584
+ const parts = [];
4585
+ if (task) {
4586
+ parts.push(`Task: ${sanitizeStatusText(task).slice(-200)}`);
4587
+ }
4588
+ const text = getPartialText(watchedBlocks);
4589
+ if (text) {
4590
+ parts.push(`Assistant text: ${text.slice(-500)}`);
4591
+ }
4592
+ if (watchedToolNames) {
4593
+ parts.push(`Tool: ${watchedToolNames}`);
4594
+ }
4595
+ const toolResult = sanitizeStatusText(lastToolResult);
4596
+ if (toolResult) {
4597
+ parts.push(`Tool result: ${toolResult.slice(-200)}`);
4598
+ }
4599
+ return parts.join("\n");
4600
+ },
4601
+ onStatus: (label) => emit({ type: "status", message: label }),
4602
+ signal
4603
+ });
4604
+ try {
4605
+ while (true) {
4606
+ turns++;
4607
+ if (signal?.aborted) {
4608
+ return abortResult([]);
4609
+ }
4610
+ const iterStart = Date.now();
4611
+ const contentBlocks = [];
4612
+ let thinkingStartedAt = 0;
4613
+ let lastThinkingRelatedStartedAt;
4614
+ let stopReason = "end_turn";
4615
+ let lastUsage;
4616
+ let lastProviderMetadata;
4617
+ let lastModelId;
4618
+ watchedBlocks = contentBlocks;
4619
+ watchedToolNames = "";
4620
+ try {
4621
+ for await (const event of streamChatWithRetry(
4622
+ {
4623
+ ...apiConfig,
4624
+ model,
4625
+ requestId,
4626
+ subAgentId,
4627
+ system: fullSystem,
4628
+ messages: cleanMessagesForApi(messages),
4629
+ tools: tools2,
4630
+ signal
4631
+ },
4632
+ {
4633
+ onRetry: (attempt) => emit({
4634
+ type: "status",
4635
+ message: `Lost connection, retrying (attempt ${attempt + 2} of 3)`
4636
+ })
4637
+ }
4638
+ )) {
4639
+ if (signal?.aborted) {
4640
+ break;
4641
+ }
4642
+ switch (event.type) {
4643
+ case "text": {
4644
+ const lastBlock = contentBlocks.at(-1);
4645
+ if (lastBlock?.type === "text") {
4646
+ lastBlock.text += event.text;
4647
+ } else {
4648
+ contentBlocks.push({
4649
+ type: "text",
4650
+ text: event.text,
4651
+ startedAt: event.ts
4652
+ });
4653
+ }
4654
+ emit({ type: "text", text: event.text });
4655
+ break;
4656
+ }
4657
+ case "thinking":
4658
+ if (!thinkingStartedAt) {
4659
+ thinkingStartedAt = event.ts;
4660
+ }
4661
+ emit({ type: "thinking", text: event.text });
4662
+ break;
4663
+ case "thinking_complete":
4664
+ contentBlocks.push({
4665
+ type: "thinking",
4666
+ thinking: event.thinking,
4667
+ signature: event.signature,
4668
+ startedAt: thinkingStartedAt,
4669
+ completedAt: event.ts
4670
+ });
4671
+ lastThinkingRelatedStartedAt = thinkingStartedAt;
4672
+ thinkingStartedAt = 0;
4673
+ break;
4674
+ case "redacted_thinking_complete": {
4675
+ const startedAt = lastThinkingRelatedStartedAt !== void 0 ? lastThinkingRelatedStartedAt + 1 : event.ts;
4676
+ contentBlocks.push({
4677
+ type: "redacted_thinking",
4678
+ data: event.data,
4679
+ startedAt,
4680
+ completedAt: event.ts
4681
+ });
4682
+ lastThinkingRelatedStartedAt = startedAt;
4683
+ break;
4684
+ }
4685
+ case "tool_use":
4686
+ contentBlocks.push({
4687
+ type: "tool",
4688
+ id: event.id,
4689
+ name: event.name,
4690
+ input: event.input,
4691
+ startedAt: Date.now()
4692
+ });
4693
+ emit({
4694
+ type: "tool_start",
4695
+ id: event.id,
4696
+ name: event.name,
4697
+ input: event.input
4698
+ });
4699
+ break;
4700
+ case "done":
4701
+ stopReason = event.stopReason;
4702
+ lastUsage = {
4703
+ inputTokens: event.usage.inputTokens,
4704
+ outputTokens: event.usage.outputTokens,
4705
+ cacheCreationTokens: event.usage.cacheCreationTokens,
4706
+ cacheReadTokens: event.usage.cacheReadTokens,
4707
+ llmCalls: 1
4708
+ };
4709
+ lastProviderMetadata = event.providerMetadata;
4710
+ lastModelId = event.modelId;
4711
+ recordUsage({
4712
+ ts: Date.now(),
4713
+ requestId,
4714
+ agentName: subAgentId || "sub-agent",
4715
+ parentToolId,
4716
+ modelId: event.modelId,
4717
+ inputTokens: event.usage.inputTokens,
4718
+ outputTokens: event.usage.outputTokens,
4719
+ cacheCreationTokens: event.usage.cacheCreationTokens,
4720
+ cacheReadTokens: event.usage.cacheReadTokens,
4721
+ cost: nanoToDollars(event.cost),
4722
+ billingEvents: event.billingEvents,
4723
+ durationMs: Date.now() - iterStart,
4724
+ toolNames: contentBlocks.filter(
4725
+ (b) => b.type === "tool"
4726
+ ).map((b) => b.name)
4727
+ });
4728
+ break;
4729
+ case "error":
4730
+ statusWatcher.stop();
4731
+ return {
4732
+ text: `Error: ${event.error}`,
4733
+ messages: thisInvocation()
4734
+ };
4735
+ }
4521
4736
  }
4522
- )) {
4523
- if (signal?.aborted) {
4524
- break;
4737
+ } catch (err) {
4738
+ if (!signal?.aborted) {
4739
+ throw err;
4525
4740
  }
4526
- switch (event.type) {
4527
- case "text": {
4528
- const lastBlock = contentBlocks.at(-1);
4529
- if (lastBlock?.type === "text") {
4530
- lastBlock.text += event.text;
4531
- } else {
4532
- contentBlocks.push({
4533
- type: "text",
4534
- text: event.text,
4535
- startedAt: event.ts
4741
+ }
4742
+ if (signal?.aborted) {
4743
+ statusWatcher.stop();
4744
+ return abortResult(contentBlocks);
4745
+ }
4746
+ messages.push({
4747
+ role: "assistant",
4748
+ content: contentBlocks,
4749
+ ...lastUsage ? { usage: lastUsage } : {},
4750
+ ...lastProviderMetadata ? { providerMetadata: lastProviderMetadata } : {},
4751
+ model: lastModelId ?? model
4752
+ });
4753
+ const toolCalls = contentBlocks.filter(
4754
+ (b) => b.type === "tool"
4755
+ );
4756
+ if (stopReason !== "tool_use" || toolCalls.length === 0) {
4757
+ statusWatcher.stop();
4758
+ const text = getPartialText(contentBlocks);
4759
+ const hasArtifacts = Object.keys(artifacts).length > 0;
4760
+ return {
4761
+ text,
4762
+ messages: thisInvocation(),
4763
+ ...hasArtifacts ? { artifacts } : {}
4764
+ };
4765
+ }
4766
+ log8.info("Tools executing", {
4767
+ requestId,
4768
+ parentToolId,
4769
+ count: toolCalls.length,
4770
+ tools: toolCalls.map((tc) => tc.name)
4771
+ });
4772
+ watchedToolNames = toolCalls.map((tc) => tc.name).join(", ");
4773
+ const results = await Promise.all(
4774
+ toolCalls.map(async (tc) => {
4775
+ if (signal?.aborted) {
4776
+ return {
4777
+ id: tc.id,
4778
+ result: USER_CANCELLED_RESULT,
4779
+ isError: true
4780
+ };
4781
+ }
4782
+ let settle;
4783
+ const resultPromise = new Promise((res) => {
4784
+ settle = (result, isError) => res({ id: tc.id, result, isError });
4785
+ });
4786
+ let toolAbort = new AbortController();
4787
+ const cascadeAbort = () => toolAbort.abort();
4788
+ signal?.addEventListener("abort", cascadeAbort, { once: true });
4789
+ let settled = false;
4790
+ const safeSettle = (result, isError) => {
4791
+ if (settled) {
4792
+ return;
4793
+ }
4794
+ settled = true;
4795
+ signal?.removeEventListener("abort", cascadeAbort);
4796
+ settle(result, isError);
4797
+ };
4798
+ const run2 = async (input) => {
4799
+ try {
4800
+ let result;
4801
+ if (externalTools.has(tc.name) && resolveExternalTool) {
4802
+ result = await resolveExternalTool(tc.id, tc.name, input);
4803
+ } else {
4804
+ const onLog = (line) => emit({
4805
+ type: "tool_input_delta",
4806
+ id: tc.id,
4807
+ name: tc.name,
4808
+ result: line
4809
+ });
4810
+ result = await executeTool2(
4811
+ tc.name,
4812
+ input,
4813
+ tc.id,
4814
+ onLog,
4815
+ subAgentMessages
4816
+ );
4817
+ }
4818
+ safeSettle(capToolResult(result), result.startsWith("Error"));
4819
+ } catch (err) {
4820
+ safeSettle(`Error: ${err.message}`, true);
4821
+ }
4822
+ };
4823
+ const entry = {
4824
+ id: tc.id,
4825
+ name: tc.name,
4826
+ input: tc.input,
4827
+ parentToolId,
4828
+ abortController: toolAbort,
4829
+ startedAt: Date.now(),
4830
+ settle: safeSettle,
4831
+ rerun: (newInput) => {
4832
+ settled = false;
4833
+ toolAbort = new AbortController();
4834
+ signal?.addEventListener("abort", () => toolAbort.abort(), {
4835
+ once: true
4536
4836
  });
4837
+ entry.abortController = toolAbort;
4838
+ entry.input = newInput;
4839
+ run2(newInput);
4537
4840
  }
4538
- emit({ type: "text", text: event.text });
4539
- break;
4841
+ };
4842
+ toolRegistry?.register(entry);
4843
+ const toolStart = Date.now();
4844
+ run2(tc.input);
4845
+ const r = await resultPromise;
4846
+ toolRegistry?.unregister(tc.id);
4847
+ log8.info("Tool completed", {
4848
+ requestId,
4849
+ parentToolId,
4850
+ toolCallId: tc.id,
4851
+ name: tc.name,
4852
+ durationMs: Date.now() - toolStart,
4853
+ isError: r.isError
4854
+ });
4855
+ emit({
4856
+ type: "tool_done",
4857
+ id: tc.id,
4858
+ name: tc.name,
4859
+ result: r.result,
4860
+ isError: r.isError
4861
+ });
4862
+ return r;
4863
+ })
4864
+ );
4865
+ lastToolResult = results.at(-1)?.result ?? "";
4866
+ for (const r of results) {
4867
+ const block = contentBlocks.find(
4868
+ (b) => b.type === "tool" && b.id === r.id
4869
+ );
4870
+ if (block?.type === "tool") {
4871
+ block.result = r.result;
4872
+ block.isError = r.isError;
4873
+ block.completedAt = Date.now();
4874
+ const innerMsgs = subAgentMessages.get(r.id);
4875
+ if (innerMsgs) {
4876
+ block.subAgentMessages = innerMsgs;
4540
4877
  }
4541
- case "thinking":
4542
- if (!thinkingStartedAt) {
4543
- thinkingStartedAt = event.ts;
4878
+ if (captureArtifacts?.includes(block.name) && !r.isError) {
4879
+ try {
4880
+ artifacts[block.name] = JSON.parse(r.result);
4881
+ } catch {
4544
4882
  }
4545
- emit({ type: "thinking", text: event.text });
4546
- break;
4547
- case "thinking_complete":
4548
- contentBlocks.push({
4549
- type: "thinking",
4550
- thinking: event.thinking,
4551
- signature: event.signature,
4552
- startedAt: thinkingStartedAt,
4553
- completedAt: event.ts
4554
- });
4555
- lastThinkingRelatedStartedAt = thinkingStartedAt;
4556
- thinkingStartedAt = 0;
4557
- break;
4558
- case "redacted_thinking_complete": {
4559
- const startedAt = lastThinkingRelatedStartedAt !== void 0 ? lastThinkingRelatedStartedAt + 1 : event.ts;
4560
- contentBlocks.push({
4561
- type: "redacted_thinking",
4562
- data: event.data,
4563
- startedAt,
4564
- completedAt: event.ts
4565
- });
4566
- lastThinkingRelatedStartedAt = startedAt;
4567
- break;
4568
4883
  }
4569
- case "tool_use":
4570
- contentBlocks.push({
4571
- type: "tool",
4572
- id: event.id,
4573
- name: event.name,
4574
- input: event.input,
4575
- startedAt: Date.now()
4576
- });
4577
- emit({
4578
- type: "tool_start",
4579
- id: event.id,
4580
- name: event.name,
4581
- input: event.input
4582
- });
4583
- break;
4584
- case "done":
4585
- stopReason = event.stopReason;
4586
- lastUsage = {
4587
- inputTokens: event.usage.inputTokens,
4588
- outputTokens: event.usage.outputTokens,
4589
- cacheCreationTokens: event.usage.cacheCreationTokens,
4590
- cacheReadTokens: event.usage.cacheReadTokens,
4591
- llmCalls: 1
4592
- };
4593
- lastProviderMetadata = event.providerMetadata;
4594
- lastModelId = event.modelId;
4595
- recordUsage({
4596
- ts: Date.now(),
4597
- requestId,
4598
- agentName: subAgentId || "sub-agent",
4599
- parentToolId,
4600
- modelId: event.modelId,
4601
- inputTokens: event.usage.inputTokens,
4602
- outputTokens: event.usage.outputTokens,
4603
- cacheCreationTokens: event.usage.cacheCreationTokens,
4604
- cacheReadTokens: event.usage.cacheReadTokens,
4605
- cost: nanoToDollars(event.cost),
4606
- billingEvents: event.billingEvents,
4607
- durationMs: Date.now() - iterStart,
4608
- toolNames: contentBlocks.filter(
4609
- (b) => b.type === "tool"
4610
- ).map((b) => b.name)
4611
- });
4612
- break;
4613
- case "error":
4614
- return {
4615
- text: `Error: ${event.error}`,
4616
- messages: thisInvocation()
4617
- };
4618
4884
  }
4619
- }
4620
- } catch (err) {
4621
- if (!signal?.aborted) {
4622
- throw err;
4885
+ messages.push({
4886
+ role: "user",
4887
+ content: r.result,
4888
+ toolCallId: r.id,
4889
+ isToolError: r.isError
4890
+ });
4623
4891
  }
4624
4892
  }
4625
- if (signal?.aborted) {
4626
- statusWatcher.stop();
4627
- return abortResult(contentBlocks);
4628
- }
4629
- messages.push({
4630
- role: "assistant",
4631
- content: contentBlocks,
4632
- ...lastUsage ? { usage: lastUsage } : {},
4633
- ...lastProviderMetadata ? { providerMetadata: lastProviderMetadata } : {},
4634
- model: lastModelId ?? model
4893
+ } finally {
4894
+ statusWatcher.stop();
4895
+ }
4896
+ };
4897
+ const wrapRun = async () => {
4898
+ try {
4899
+ const result = await run();
4900
+ log8.info("Sub-agent complete", {
4901
+ requestId,
4902
+ parentToolId,
4903
+ agentName,
4904
+ durationMs: Date.now() - runStart,
4905
+ turns
4635
4906
  });
4636
- const toolCalls = contentBlocks.filter(
4637
- (b) => b.type === "tool"
4638
- );
4639
- if (stopReason !== "tool_use" || toolCalls.length === 0) {
4640
- statusWatcher.stop();
4641
- const text = getPartialText(contentBlocks);
4642
- const hasArtifacts = Object.keys(artifacts).length > 0;
4643
- return {
4644
- text,
4645
- messages: thisInvocation(),
4646
- ...hasArtifacts ? { artifacts } : {}
4647
- };
4648
- }
4649
- log8.info("Tools executing", {
4907
+ return result;
4908
+ } catch (err) {
4909
+ log8.warn("Sub-agent error", {
4650
4910
  requestId,
4651
4911
  parentToolId,
4652
- count: toolCalls.length,
4653
- tools: toolCalls.map((tc) => tc.name)
4654
- });
4655
- currentToolNames = toolCalls.map((tc) => tc.name).join(", ");
4656
- const results = await Promise.all(
4657
- toolCalls.map(async (tc) => {
4658
- if (signal?.aborted) {
4659
- return { id: tc.id, result: USER_CANCELLED_RESULT, isError: true };
4660
- }
4661
- let settle;
4662
- const resultPromise = new Promise((res) => {
4663
- settle = (result, isError) => res({ id: tc.id, result, isError });
4664
- });
4665
- let toolAbort = new AbortController();
4666
- const cascadeAbort = () => toolAbort.abort();
4667
- signal?.addEventListener("abort", cascadeAbort, { once: true });
4668
- let settled = false;
4669
- const safeSettle = (result, isError) => {
4670
- if (settled) {
4671
- return;
4672
- }
4673
- settled = true;
4674
- signal?.removeEventListener("abort", cascadeAbort);
4675
- settle(result, isError);
4676
- };
4677
- const run2 = async (input) => {
4678
- try {
4679
- let result;
4680
- if (externalTools.has(tc.name) && resolveExternalTool) {
4681
- result = await resolveExternalTool(tc.id, tc.name, input);
4682
- } else {
4683
- const onLog = (line) => emit({
4684
- type: "tool_input_delta",
4685
- id: tc.id,
4686
- name: tc.name,
4687
- result: line
4688
- });
4689
- result = await executeTool2(
4690
- tc.name,
4691
- input,
4692
- tc.id,
4693
- onLog,
4694
- subAgentMessages
4695
- );
4696
- }
4697
- safeSettle(capToolResult(result), result.startsWith("Error"));
4698
- } catch (err) {
4699
- safeSettle(`Error: ${err.message}`, true);
4700
- }
4701
- };
4702
- const entry = {
4703
- id: tc.id,
4704
- name: tc.name,
4705
- input: tc.input,
4706
- parentToolId,
4707
- abortController: toolAbort,
4708
- startedAt: Date.now(),
4709
- settle: safeSettle,
4710
- rerun: (newInput) => {
4711
- settled = false;
4712
- toolAbort = new AbortController();
4713
- signal?.addEventListener("abort", () => toolAbort.abort(), {
4714
- once: true
4715
- });
4716
- entry.abortController = toolAbort;
4717
- entry.input = newInput;
4718
- run2(newInput);
4719
- }
4720
- };
4721
- toolRegistry?.register(entry);
4722
- const toolStart = Date.now();
4723
- run2(tc.input);
4724
- const r = await resultPromise;
4725
- toolRegistry?.unregister(tc.id);
4726
- log8.info("Tool completed", {
4727
- requestId,
4728
- parentToolId,
4729
- toolCallId: tc.id,
4730
- name: tc.name,
4731
- durationMs: Date.now() - toolStart,
4732
- isError: r.isError
4733
- });
4734
- emit({
4735
- type: "tool_done",
4736
- id: tc.id,
4737
- name: tc.name,
4738
- result: r.result,
4739
- isError: r.isError
4740
- });
4741
- return r;
4742
- })
4743
- );
4744
- statusWatcher.stop();
4745
- lastToolResult = results.at(-1)?.result ?? "";
4746
- for (const r of results) {
4747
- const block = contentBlocks.find(
4748
- (b) => b.type === "tool" && b.id === r.id
4749
- );
4750
- if (block?.type === "tool") {
4751
- block.result = r.result;
4752
- block.isError = r.isError;
4753
- block.completedAt = Date.now();
4754
- const innerMsgs = subAgentMessages.get(r.id);
4755
- if (innerMsgs) {
4756
- block.subAgentMessages = innerMsgs;
4757
- }
4758
- if (captureArtifacts?.includes(block.name) && !r.isError) {
4759
- try {
4760
- artifacts[block.name] = JSON.parse(r.result);
4761
- } catch {
4762
- }
4763
- }
4764
- }
4765
- messages.push({
4766
- role: "user",
4767
- content: r.result,
4768
- toolCallId: r.id,
4769
- isToolError: r.isError
4770
- });
4771
- }
4772
- }
4773
- };
4774
- const wrapRun = async () => {
4775
- try {
4776
- const result = await run();
4777
- log8.info("Sub-agent complete", {
4778
- requestId,
4779
- parentToolId,
4780
- agentName,
4781
- durationMs: Date.now() - runStart,
4782
- turns
4783
- });
4784
- return result;
4785
- } catch (err) {
4786
- log8.warn("Sub-agent error", {
4787
- requestId,
4788
- parentToolId,
4789
- agentName,
4790
- error: err.message
4912
+ agentName,
4913
+ error: err.message
4791
4914
  });
4792
4915
  throw err;
4793
4916
  }
@@ -4875,7 +4998,6 @@ var init_tools2 = __esm({
4875
4998
  init_readSpec();
4876
4999
  BROWSER_TOOLS = [
4877
5000
  {
4878
- clearable: false,
4879
5001
  name: "setupBrowser",
4880
5002
  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.",
4881
5003
  inputSchema: {
@@ -4908,7 +5030,6 @@ var init_tools2 = __esm({
4908
5030
  }
4909
5031
  },
4910
5032
  {
4911
- clearable: true,
4912
5033
  name: "browserCommand",
4913
5034
  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.",
4914
5035
  inputSchema: {
@@ -5356,7 +5477,6 @@ var init_browserAutomation = __esm({
5356
5477
  CAPTURE_COMMANDS = /* @__PURE__ */ new Set(["screenshotViewport", "screenshotFullPage"]);
5357
5478
  browserAutomationTool = {
5358
5479
  definition: {
5359
- clearable: true,
5360
5480
  name: "runAutomatedBrowserTest",
5361
5481
  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).",
5362
5482
  inputSchema: {
@@ -5447,7 +5567,6 @@ var init_screenshot2 = __esm({
5447
5567
  init_browserAutomation();
5448
5568
  init_surfaces();
5449
5569
  screenshotDefinition = {
5450
- clearable: true,
5451
5570
  name: "screenshot",
5452
5571
  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.",
5453
5572
  inputSchema: {
@@ -5521,7 +5640,6 @@ var init_searchGoogle2 = __esm({
5521
5640
  init_runMindstudioCli();
5522
5641
  init_runCli();
5523
5642
  definition = {
5524
- clearable: false,
5525
5643
  name: "searchGoogle",
5526
5644
  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.',
5527
5645
  inputSchema: {
@@ -5564,7 +5682,6 @@ var init_scrapeWebUrl = __esm({
5564
5682
  init_runMindstudioCli();
5565
5683
  init_runCli();
5566
5684
  definition2 = {
5567
- clearable: false,
5568
5685
  name: "scrapeWebUrl",
5569
5686
  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.",
5570
5687
  inputSchema: {
@@ -5657,7 +5774,6 @@ Identify the specific design moves that make this page interesting and unique, d
5657
5774
  Respond only with your analysis as Markdown and absolutely no other text. Do not use emojis - use unicode if you need symbols.
5658
5775
  `;
5659
5776
  definition3 = {
5660
- clearable: false,
5661
5777
  name: "analyzeDesign",
5662
5778
  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.",
5663
5779
  inputSchema: {
@@ -5705,7 +5821,6 @@ var init_analyzeImage2 = __esm({
5705
5821
  init_screenshot();
5706
5822
  init_surfaces();
5707
5823
  definition4 = {
5708
- clearable: true,
5709
5824
  name: "analyzeImage",
5710
5825
  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.",
5711
5826
  inputSchema: {
@@ -5977,7 +6092,6 @@ var init_generateImages = __esm({
5977
6092
  init_imageGenerator();
5978
6093
  init_surfaces();
5979
6094
  definition5 = {
5980
- clearable: false,
5981
6095
  name: "generateImages",
5982
6096
  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.",
5983
6097
  inputSchema: {
@@ -6053,7 +6167,6 @@ var init_editImages = __esm({
6053
6167
  init_imageGenerator();
6054
6168
  init_surfaces();
6055
6169
  definition6 = {
6056
- clearable: false,
6057
6170
  name: "editImages",
6058
6171
  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.",
6059
6172
  inputSchema: {
@@ -6116,7 +6229,6 @@ var init_copyEditor = __esm({
6116
6229
  BASE_PROMPT2 = readAsset("subagents/copyEditor", "prompt.md");
6117
6230
  copyEditorTool = {
6118
6231
  definition: {
6119
- clearable: false,
6120
6232
  name: "copyEditor",
6121
6233
  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.",
6122
6234
  inputSchema: {
@@ -6179,7 +6291,6 @@ var init_polishCopy = __esm({
6179
6291
  "use strict";
6180
6292
  init_copyEditor();
6181
6293
  definition7 = {
6182
- clearable: false,
6183
6294
  name: "polishCopy",
6184
6295
  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).",
6185
6296
  inputSchema: {
@@ -6673,7 +6784,6 @@ Visual design expert. Describe the situation and what you need \u2014 the agent
6673
6784
  ];
6674
6785
  designExpertTool = {
6675
6786
  definition: {
6676
- clearable: false,
6677
6787
  name: "visualDesignExpert",
6678
6788
  description: DESCRIPTION,
6679
6789
  inputSchema: {
@@ -6916,7 +7026,6 @@ var init_productVision = __esm({
6916
7026
  init_surfaces();
6917
7027
  productVisionTool = {
6918
7028
  definition: {
6919
- clearable: false,
6920
7029
  name: "productVision",
6921
7030
  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.",
6922
7031
  inputSchema: {
@@ -7048,7 +7157,6 @@ var init_codeSanityCheck = __esm({
7048
7157
  BASE_PROMPT4 = readAsset("subagents/codeSanityCheck", "prompt.md");
7049
7158
  codeSanityCheckTool = {
7050
7159
  definition: {
7051
- clearable: false,
7052
7160
  name: "codeSanityCheck",
7053
7161
  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.',
7054
7162
  inputSchema: {
@@ -7096,6 +7204,128 @@ var init_codeSanityCheck = __esm({
7096
7204
  }
7097
7205
  });
7098
7206
 
7207
+ // src/tools/spec/writeBuildOverview.ts
7208
+ import fs19 from "fs";
7209
+ function initialDelivery() {
7210
+ return `### Your deliverable
7211
+ 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.
7212
+
7213
+ <overview_shell>
7214
+ ${OVERVIEW_SHELL}
7215
+ </overview_shell>
7216
+
7217
+ Then reply with a one-line summary of what you wrote.`;
7218
+ }
7219
+ function refreshDelivery() {
7220
+ return `### Your deliverable
7221
+ 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.
7222
+
7223
+ Then reply with a one-line summary of what you changed.`;
7224
+ }
7225
+ async function renderBuildOverview(content, context, opts) {
7226
+ if (!content) {
7227
+ return "Error: writeBuildOverview requires non-empty `content` (the overview copy).";
7228
+ }
7229
+ const exists = fs19.existsSync(OVERVIEW_FILE);
7230
+ const task = `<overview_copy>${content}</overview_copy>
7231
+
7232
+ ${DESIGN_BRIEF}
7233
+
7234
+ ${exists ? refreshDelivery() : initialDelivery()}`;
7235
+ try {
7236
+ if (opts.background) {
7237
+ const result2 = await runDesignExpertRender(
7238
+ { task, background: true, reportingName: "writeBuildOverview" },
7239
+ context
7240
+ );
7241
+ context.subAgentMessages?.set(context.toolCallId, result2.messages);
7242
+ return result2.text;
7243
+ }
7244
+ const result = await runDesignExpertRender({ task }, context);
7245
+ context.subAgentMessages?.set(context.toolCallId, result.messages);
7246
+ if (!fs19.existsSync(OVERVIEW_FILE)) {
7247
+ return `Error: the design expert did not write ${OVERVIEW_FILE}. Its reply was:
7248
+ ${result.text}`;
7249
+ }
7250
+ return `Build overview written to ${OVERVIEW_FILE}. ${result.text}`;
7251
+ } catch (err) {
7252
+ return `Error generating build overview: ${err.message}`;
7253
+ }
7254
+ }
7255
+ var OVERVIEW_FILE, DESIGN_BRIEF, OVERVIEW_SHELL, buildOverviewTool;
7256
+ var init_writeBuildOverview = __esm({
7257
+ "src/tools/spec/writeBuildOverview.ts"() {
7258
+ "use strict";
7259
+ init_designExpert();
7260
+ OVERVIEW_FILE = "src/overview.html";
7261
+ 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).
7262
+
7263
+ 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.
7264
+
7265
+ ### The single hard rule
7266
+ 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.
7267
+
7268
+ ### What it is (and is not)
7269
+ - 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."
7270
+ - 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.
7271
+ - NOT a slide deck \u2014 no theatrics, no building to a climax, no persuasion.
7272
+ - NOT a docs site \u2014 no raised or shadowed cards, no hover-lift, no clickable-feeling surfaces, no sticky table of contents.
7273
+ - NOT a sparse memo \u2014 a restrained single-column page under-sells the work.
7274
+ - No header chrome and no footer: open straight on the app's logo and name so it sits flush with the IDE.
7275
+
7276
+ ### Constraints
7277
+ - A single self-contained HTML file. Fonts may load from a CDN; everything else (CSS, the logo SVG) is inline.
7278
+ - Responsive: fills the embedded panel width and collapses gracefully at narrow widths.`;
7279
+ OVERVIEW_SHELL = `<!DOCTYPE html>
7280
+ <html lang="en">
7281
+ <head>
7282
+ <meta charset="utf-8">
7283
+ <!-- This renders inside an iframe in the IDE. Keep it a simple, self-contained
7284
+ static document \u2014 no scroll/zoom/accessibility scaffolding beyond this head.
7285
+ The user-scalable=no viewport is intentional; leave it. -->
7286
+ <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
7287
+ <title>Build Overview</title>
7288
+ <!-- SKIN: link the app's fonts here (CDN \u2014 e.g. Fontshare / Google Fonts). -->
7289
+ <style>
7290
+ *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
7291
+ </style>
7292
+ </head>
7293
+ <body>
7294
+ <!-- Compose the Build Overview here. Layout, sections, type, and styling are
7295
+ yours \u2014 keep everything inline and self-contained, brand-skinned from the
7296
+ app's spec. -->
7297
+ </body>
7298
+ </html>`;
7299
+ buildOverviewTool = {
7300
+ definition: {
7301
+ name: "writeBuildOverview",
7302
+ 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.",
7303
+ inputSchema: {
7304
+ type: "object",
7305
+ properties: {
7306
+ content: {
7307
+ type: "string",
7308
+ 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."
7309
+ }
7310
+ },
7311
+ required: ["content"]
7312
+ }
7313
+ },
7314
+ async execute(input, context) {
7315
+ if (!context) {
7316
+ return "Error: writeBuildOverview requires execution context for design expert delegation";
7317
+ }
7318
+ const content = (input.content ?? "").trim();
7319
+ const background = Boolean(content) && fs19.existsSync(OVERVIEW_FILE);
7320
+ if (background) {
7321
+ input.background = true;
7322
+ }
7323
+ return renderBuildOverview(content, context, { background });
7324
+ }
7325
+ };
7326
+ }
7327
+ });
7328
+
7099
7329
  // src/subagents/specSync/tools.ts
7100
7330
  var SPEC_SYNC_TOOLS;
7101
7331
  var init_tools7 = __esm({
@@ -7141,6 +7371,7 @@ var init_specSync = __esm({
7141
7371
  init_runner();
7142
7372
  init_context();
7143
7373
  init_tools8();
7374
+ init_writeBuildOverview();
7144
7375
  init_tools7();
7145
7376
  init_lock();
7146
7377
  init_surfaces();
@@ -7157,15 +7388,18 @@ ${readAsset(
7157
7388
  // next real turn as a hidden background_results note instead.
7158
7389
  backgroundNotify: "passive",
7159
7390
  definition: {
7160
- clearable: false,
7161
7391
  name: "specSync",
7162
- 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.",
7392
+ 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.",
7163
7393
  inputSchema: {
7164
7394
  type: "object",
7165
7395
  properties: {
7166
7396
  task: {
7167
7397
  type: "string",
7168
7398
  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."
7399
+ },
7400
+ refreshBuildOverview: {
7401
+ type: "boolean",
7402
+ 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."
7169
7403
  }
7170
7404
  },
7171
7405
  required: ["task"]
@@ -7189,12 +7423,27 @@ ${readAsset(
7189
7423
  parts.push(specIndex);
7190
7424
  }
7191
7425
  const system = parts.join("\n\n");
7426
+ const refreshOverview = input.refreshBuildOverview === true;
7427
+ const tools2 = refreshOverview ? [...SPEC_SYNC_TOOLS, buildOverviewTool.definition] : SPEC_SYNC_TOOLS;
7428
+ const task = refreshOverview ? `${input.task}
7429
+
7430
+ 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;
7192
7431
  const result = await runSubAgent({
7193
7432
  system,
7194
- task: input.task,
7195
- tools: SPEC_SYNC_TOOLS,
7433
+ task,
7434
+ tools: tools2,
7196
7435
  externalTools: /* @__PURE__ */ new Set(),
7197
- executeTool: (name, toolInput) => executeTool(name, toolInput, context),
7436
+ executeTool: (name, toolInput, toolCallId, _onLog, sams) => {
7437
+ if (name === "writeBuildOverview") {
7438
+ const childCtx = toolCallId ? { ...deriveContext(context, toolCallId), subAgentMessages: sams } : { ...context, subAgentMessages: sams };
7439
+ return renderBuildOverview(
7440
+ String(toolInput.content ?? "").trim(),
7441
+ childCtx,
7442
+ { background: false }
7443
+ );
7444
+ }
7445
+ return executeTool(name, toolInput, context);
7446
+ },
7198
7447
  apiConfig: context.apiConfig,
7199
7448
  model: resolveModel("specSync", context.models, context.model),
7200
7449
  subAgentId: "specSync",
@@ -7233,7 +7482,6 @@ var init_scrapeWebUrl2 = __esm({
7233
7482
  init_runCli();
7234
7483
  scrapeWebUrlTool = {
7235
7484
  definition: {
7236
- clearable: false,
7237
7485
  name: "scrapeWebUrl",
7238
7486
  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",
7239
7487
  inputSchema: {
@@ -7277,143 +7525,26 @@ var init_scrapeWebUrl2 = __esm({
7277
7525
  }
7278
7526
  });
7279
7527
 
7280
- // src/tools/spec/writeBuildOverview.ts
7281
- import fs19 from "fs";
7282
- function initialDelivery() {
7283
- return `### Your deliverable
7284
- 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.
7285
-
7286
- <overview_shell>
7287
- ${OVERVIEW_SHELL}
7288
- </overview_shell>
7289
-
7290
- Then reply with a one-line summary of what you wrote.`;
7528
+ // src/tools/index.ts
7529
+ function deriveContext(parent, toolCallId) {
7530
+ return { ...parent, toolCallId };
7291
7531
  }
7292
- function refreshDelivery() {
7293
- return `### Your deliverable
7294
- 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.
7295
-
7296
- Then reply with a one-line summary of what you changed.`;
7532
+ function getToolDefinitions(_onboardingState) {
7533
+ return ALL_TOOLS.map((t) => t.definition);
7297
7534
  }
7298
- var OVERVIEW_FILE, DESIGN_BRIEF, OVERVIEW_SHELL, buildOverviewTool;
7299
- var init_writeBuildOverview = __esm({
7300
- "src/tools/spec/writeBuildOverview.ts"() {
7301
- "use strict";
7302
- init_designExpert();
7303
- OVERVIEW_FILE = "src/overview.html";
7304
- 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).
7305
-
7306
- 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.
7307
-
7308
- ### The single hard rule
7309
- 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.
7310
-
7311
- ### What it is (and is not)
7312
- - 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."
7313
- - 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.
7314
- - NOT a slide deck \u2014 no theatrics, no building to a climax, no persuasion.
7315
- - NOT a docs site \u2014 no raised or shadowed cards, no hover-lift, no clickable-feeling surfaces, no sticky table of contents.
7316
- - NOT a sparse memo \u2014 a restrained single-column page under-sells the work.
7317
- - No header chrome and no footer: open straight on the app's logo and name so it sits flush with the IDE.
7318
-
7319
- ### Constraints
7320
- - A single self-contained HTML file. Fonts may load from a CDN; everything else (CSS, the logo SVG) is inline.
7321
- - Responsive: fills the embedded panel width and collapses gracefully at narrow widths.`;
7322
- OVERVIEW_SHELL = `<!DOCTYPE html>
7323
- <html lang="en">
7324
- <head>
7325
- <meta charset="utf-8">
7326
- <!-- This renders inside an iframe in the IDE. Keep it a simple, self-contained
7327
- static document \u2014 no scroll/zoom/accessibility scaffolding beyond this head.
7328
- The user-scalable=no viewport is intentional; leave it. -->
7329
- <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
7330
- <title>Build Overview</title>
7331
- <!-- SKIN: link the app's fonts here (CDN \u2014 e.g. Fontshare / Google Fonts). -->
7332
- <style>
7333
- *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
7334
- </style>
7335
- </head>
7336
- <body>
7337
- <!-- Compose the Build Overview here. Layout, sections, type, and styling are
7338
- yours \u2014 keep everything inline and self-contained, brand-skinned from the
7339
- app's spec. -->
7340
- </body>
7341
- </html>`;
7342
- buildOverviewTool = {
7343
- definition: {
7344
- clearable: false,
7345
- name: "writeBuildOverview",
7346
- 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.",
7347
- inputSchema: {
7348
- type: "object",
7349
- properties: {
7350
- content: {
7351
- type: "string",
7352
- 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."
7353
- }
7354
- },
7355
- required: ["content"]
7356
- }
7357
- },
7358
- async execute(input, context) {
7359
- if (!context) {
7360
- return "Error: writeBuildOverview requires execution context for design expert delegation";
7361
- }
7362
- const content = (input.content ?? "").trim();
7363
- if (!content) {
7364
- return "Error: writeBuildOverview requires non-empty `content` (the overview copy).";
7365
- }
7366
- const exists = fs19.existsSync(OVERVIEW_FILE);
7367
- const task = `<overview_copy>${content}</overview_copy>
7368
-
7369
- ${DESIGN_BRIEF}
7370
-
7371
- ${exists ? refreshDelivery() : initialDelivery()}`;
7372
- try {
7373
- if (exists) {
7374
- input.background = true;
7375
- const result2 = await runDesignExpertRender(
7376
- { task, background: true, reportingName: "writeBuildOverview" },
7377
- context
7378
- );
7379
- context.subAgentMessages?.set(context.toolCallId, result2.messages);
7380
- return result2.text;
7381
- }
7382
- const result = await runDesignExpertRender({ task }, context);
7383
- context.subAgentMessages?.set(context.toolCallId, result.messages);
7384
- if (!fs19.existsSync(OVERVIEW_FILE)) {
7385
- return `Error: the design expert did not write ${OVERVIEW_FILE}. Its reply was:
7386
- ${result.text}`;
7387
- }
7388
- return `Build overview written to ${OVERVIEW_FILE}. ${result.text}`;
7389
- } catch (err) {
7390
- return `Error generating build overview: ${err.message}`;
7391
- }
7392
- }
7393
- };
7394
- }
7395
- });
7396
-
7397
- // src/tools/index.ts
7398
- function deriveContext(parent, toolCallId) {
7399
- return { ...parent, toolCallId };
7400
- }
7401
- function getToolDefinitions(_onboardingState) {
7402
- return ALL_TOOLS.map((t) => t.definition);
7403
- }
7404
- function getToolByName(name) {
7405
- return ALL_TOOLS.find((t) => t.definition.name === name);
7406
- }
7407
- function executeTool(name, input, context) {
7408
- const tool = getToolByName(name);
7409
- if (!tool) {
7410
- return Promise.resolve(`Error: Unknown tool "${name}"`);
7411
- }
7412
- return tool.execute(input, context);
7413
- }
7414
- var ALL_TOOLS, SUBAGENT_TOOL_NAMES;
7415
- var init_tools8 = __esm({
7416
- "src/tools/index.ts"() {
7535
+ function getToolByName(name) {
7536
+ return ALL_TOOLS.find((t) => t.definition.name === name);
7537
+ }
7538
+ function executeTool(name, input, context) {
7539
+ const tool = getToolByName(name);
7540
+ if (!tool) {
7541
+ return Promise.resolve(`Error: Unknown tool "${name}"`);
7542
+ }
7543
+ return tool.execute(input, context);
7544
+ }
7545
+ var ALL_TOOLS, SUBAGENT_TOOL_NAMES;
7546
+ var init_tools8 = __esm({
7547
+ "src/tools/index.ts"() {
7417
7548
  "use strict";
7418
7549
  init_readSpec();
7419
7550
  init_writeSpec();
@@ -7680,6 +7811,70 @@ var init_parsePartialJson = __esm({
7680
7811
  }
7681
7812
  });
7682
7813
 
7814
+ // src/automatedActions/resolve.ts
7815
+ function resolveAction(text) {
7816
+ const parsed = parseSentinel(text);
7817
+ if (!parsed) {
7818
+ return null;
7819
+ }
7820
+ const { name: triggerName, remainder } = parsed;
7821
+ if (NON_ACTION_SENTINELS.has(triggerName)) {
7822
+ return null;
7823
+ }
7824
+ let params = {};
7825
+ if (remainder) {
7826
+ try {
7827
+ params = JSON.parse(remainder.split("\n")[0]);
7828
+ } catch {
7829
+ }
7830
+ }
7831
+ let body = readAsset("automatedActions", `${triggerName}.md`);
7832
+ let next;
7833
+ const fmMatch = body.match(/^---\s*\n([\s\S]*?)\n---/);
7834
+ if (fmMatch) {
7835
+ const nextMatch = fmMatch[1].match(/^\s*next:\s*(\w+)\s*$/m);
7836
+ if (nextMatch) {
7837
+ next = nextMatch[1];
7838
+ }
7839
+ }
7840
+ body = body.replace(/^---[\s\S]*?---\s*/, "");
7841
+ for (const [key, value] of Object.entries(params)) {
7842
+ const str = typeof value === "string" ? value : JSON.stringify(value);
7843
+ body = body.replaceAll(`{{${key}}}`, str);
7844
+ }
7845
+ return {
7846
+ message: automatedMessage(triggerName, body),
7847
+ next
7848
+ };
7849
+ }
7850
+ function getActionChain(startName) {
7851
+ const chain = [];
7852
+ const seen = /* @__PURE__ */ new Set();
7853
+ let name = startName;
7854
+ while (name && !seen.has(name)) {
7855
+ seen.add(name);
7856
+ chain.push(name);
7857
+ let body;
7858
+ try {
7859
+ body = readAsset("automatedActions", `${name}.md`);
7860
+ } catch {
7861
+ break;
7862
+ }
7863
+ const fm = body.match(/^---\s*\n([\s\S]*?)\n---/);
7864
+ name = fm?.[1].match(/^\s*next:\s*(\w+)\s*$/m)?.[1];
7865
+ }
7866
+ return chain;
7867
+ }
7868
+ var NON_ACTION_SENTINELS;
7869
+ var init_resolve = __esm({
7870
+ "src/automatedActions/resolve.ts"() {
7871
+ "use strict";
7872
+ init_assets();
7873
+ init_sentinel();
7874
+ NON_ACTION_SENTINELS = /* @__PURE__ */ new Set(["background_results"]);
7875
+ }
7876
+ });
7877
+
7683
7878
  // src/errors.ts
7684
7879
  function friendlyError(raw) {
7685
7880
  for (const [pattern, message] of patterns) {
@@ -8182,6 +8377,9 @@ async function runTurn(params) {
8182
8377
  let lastCompletedTools = "";
8183
8378
  let lastCompletedInput = "";
8184
8379
  let lastCompletedResult = "";
8380
+ let subAgentText = "";
8381
+ let currentToolNames = "";
8382
+ let contentBlocksRef = [];
8185
8383
  let turnInputTokens = 0;
8186
8384
  let turnOutputTokens = 0;
8187
8385
  let turnCacheCreation = 0;
@@ -8190,281 +8388,328 @@ async function runTurn(params) {
8190
8388
  let lastCallInputTokens = 0;
8191
8389
  let lastCallCacheCreation = 0;
8192
8390
  let lastCallCacheRead = 0;
8193
- while (true) {
8194
- let getOrCreateAccumulator2 = function(id, name) {
8195
- let acc = toolInputAccumulators.get(id);
8196
- if (!acc) {
8197
- acc = { name, json: "", started: false, lastEmittedCount: 0 };
8198
- toolInputAccumulators.set(id, acc);
8199
- }
8200
- return acc;
8201
- };
8202
- var getOrCreateAccumulator = getOrCreateAccumulator2;
8203
- if (signal?.aborted) {
8204
- onEvent({ type: "turn_cancelled" });
8205
- saveSession(state);
8206
- return;
8207
- }
8208
- const iterStart = Date.now();
8209
- const contentBlocks = [];
8210
- const thinkingBlockStartTimes = [];
8211
- let thinkingCompleteCount = 0;
8212
- let lastThinkingRelatedStartedAt;
8213
- let textBlockOpen = false;
8214
- const toolInputAccumulators = /* @__PURE__ */ new Map();
8215
- let stopReason = "end_turn";
8216
- let turnProviderMetadata;
8217
- let turnModelId;
8218
- let subAgentText = "";
8219
- let currentToolNames = "";
8220
- const statusWatcher = isFirstMessage ? { stop() {
8221
- }, pause() {
8222
- }, resume() {
8223
- } } : startStatusWatcher({
8224
- apiConfig,
8225
- getContext: () => {
8226
- const parts = [];
8227
- const toolName = currentToolNames || getToolCalls(contentBlocks).filter((tc) => !STATUS_EXCLUDED_TOOLS.has(tc.name)).at(-1)?.name || lastCompletedTools;
8228
- if (toolName) {
8229
- parts.push(`Tool: ${toolName}`);
8230
- }
8231
- if (lastCompletedInput) {
8232
- parts.push(`Tool input: ${lastCompletedInput.slice(-1500)}`);
8233
- }
8234
- if (lastCompletedResult) {
8235
- parts.push(`Tool result: ${lastCompletedResult.slice(-1500)}`);
8236
- }
8237
- const text = subAgentText || getTextContent(contentBlocks).slice(-2e3);
8238
- if (text) {
8239
- parts.push(`Assistant text: ${text}`);
8240
- }
8241
- if (onboardingState && onboardingState !== "onboardingFinished") {
8242
- parts.push(`Build phase: ${onboardingState}`);
8391
+ const statusWatcher = isFirstMessage ? { stop() {
8392
+ }, pause() {
8393
+ }, resume() {
8394
+ } } : startStatusWatcher({
8395
+ apiConfig,
8396
+ getContext: () => {
8397
+ const parts = [];
8398
+ const toolName = currentToolNames || getToolCalls(contentBlocksRef).filter((tc) => !STATUS_EXCLUDED_TOOLS.has(tc.name)).at(-1)?.name || lastCompletedTools;
8399
+ if (toolName) {
8400
+ parts.push(`Tool: ${toolName}`);
8401
+ }
8402
+ const toolInput = sanitizeStatusText(lastCompletedInput);
8403
+ if (toolInput) {
8404
+ parts.push(`Tool input: ${toolInput.slice(-1500)}`);
8405
+ }
8406
+ const toolResult = sanitizeStatusText(lastCompletedResult);
8407
+ if (toolResult) {
8408
+ parts.push(`Tool result: ${toolResult.slice(-1500)}`);
8409
+ }
8410
+ const text = subAgentText || getTextContent(contentBlocksRef).slice(-2e3);
8411
+ if (text) {
8412
+ parts.push(`Assistant text: ${text}`);
8413
+ }
8414
+ if (onboardingState && onboardingState !== "onboardingFinished") {
8415
+ parts.push(`Build phase: ${onboardingState}`);
8416
+ }
8417
+ let hasUserSignal = false;
8418
+ for (const entry of keptEntries) {
8419
+ if (entry.hidden) {
8420
+ continue;
8243
8421
  }
8244
- for (const entry of keptEntries) {
8245
- const automated = parseSentinel(entry.text);
8246
- if (automated) {
8422
+ const automated = parseSentinel(entry.text);
8423
+ if (automated) {
8424
+ if (!NON_ACTION_SENTINELS.has(automated.name)) {
8247
8425
  parts.push(`Automated action: ${automated.name}`);
8248
- } else if (entry.text) {
8249
- parts.push(`User request: ${entry.text.slice(-500)}`);
8426
+ hasUserSignal = true;
8250
8427
  }
8428
+ } else if (entry.text) {
8429
+ parts.push(`User request: ${entry.text.slice(-500)}`);
8430
+ hasUserSignal = true;
8251
8431
  }
8252
- return parts.join("\n");
8253
- },
8254
- onStatus: (label) => onEvent({ type: "status", message: label }),
8255
- signal
8256
- });
8257
- async function handlePartialInput(acc, id, name, partial) {
8258
- const tool = getToolByName(name);
8259
- if (!tool?.streaming) {
8260
- return;
8261
8432
  }
8262
- const {
8263
- contentField = "content",
8264
- transform,
8265
- partialInput
8266
- } = tool.streaming;
8267
- if (partialInput) {
8268
- const result = partialInput(partial, acc.lastEmittedCount);
8269
- if (!result) {
8270
- return;
8271
- }
8272
- acc.lastEmittedCount = result.emittedCount;
8273
- acc.started = true;
8274
- onEvent({
8275
- type: "tool_start",
8276
- id,
8277
- name,
8278
- input: result.input,
8279
- partial: true
8280
- });
8281
- return;
8433
+ if (!hasUserSignal && !toolName && !text) {
8434
+ return "";
8282
8435
  }
8283
- const content = partial[contentField];
8284
- if (typeof content !== "string") {
8436
+ return parts.join("\n");
8437
+ },
8438
+ onStatus: (label) => onEvent({ type: "status", message: label }),
8439
+ signal
8440
+ });
8441
+ try {
8442
+ while (true) {
8443
+ let getOrCreateAccumulator2 = function(id, name) {
8444
+ let acc = toolInputAccumulators.get(id);
8445
+ if (!acc) {
8446
+ acc = { name, json: "", started: false, lastEmittedCount: 0 };
8447
+ toolInputAccumulators.set(id, acc);
8448
+ }
8449
+ return acc;
8450
+ };
8451
+ var getOrCreateAccumulator = getOrCreateAccumulator2;
8452
+ if (signal?.aborted) {
8453
+ onEvent({ type: "turn_cancelled" });
8454
+ saveSession(state);
8285
8455
  return;
8286
8456
  }
8287
- if (!acc.started) {
8288
- acc.started = true;
8289
- onEvent({ type: "tool_start", id, name, input: partial });
8290
- }
8291
- if (transform) {
8292
- const result = await transform(partial);
8293
- if (result === null) {
8457
+ const iterStart = Date.now();
8458
+ const contentBlocks = [];
8459
+ const thinkingBlockStartTimes = [];
8460
+ let thinkingCompleteCount = 0;
8461
+ let lastThinkingRelatedStartedAt;
8462
+ let textBlockOpen = false;
8463
+ const toolInputAccumulators = /* @__PURE__ */ new Map();
8464
+ let stopReason = "end_turn";
8465
+ let turnProviderMetadata;
8466
+ let turnModelId;
8467
+ contentBlocksRef = contentBlocks;
8468
+ subAgentText = "";
8469
+ currentToolNames = "";
8470
+ async function handlePartialInput(acc, id, name, partial) {
8471
+ const tool = getToolByName(name);
8472
+ if (!tool?.streaming) {
8294
8473
  return;
8295
8474
  }
8296
- onEvent({ type: "tool_input_delta", id, name, result });
8297
- } else {
8298
- onEvent({ type: "tool_input_delta", id, name, result: content });
8299
- }
8300
- }
8301
- try {
8302
- for await (const event of streamChatWithRetry(
8303
- {
8304
- ...apiConfig,
8305
- model: parentModel,
8306
- requestId,
8307
- system,
8308
- messages: cleanMessagesForApi(state.messages),
8309
- tools: tools2,
8310
- signal
8311
- },
8312
- {
8313
- onRetry: (attempt) => {
8314
- onEvent({
8315
- type: "status",
8316
- message: `Lost connection, retrying (attempt ${attempt + 2} of 3)`
8317
- });
8475
+ const {
8476
+ contentField = "content",
8477
+ transform,
8478
+ partialInput
8479
+ } = tool.streaming;
8480
+ if (partialInput) {
8481
+ const result = partialInput(partial, acc.lastEmittedCount);
8482
+ if (!result) {
8483
+ return;
8318
8484
  }
8485
+ acc.lastEmittedCount = result.emittedCount;
8486
+ acc.started = true;
8487
+ onEvent({
8488
+ type: "tool_start",
8489
+ id,
8490
+ name,
8491
+ input: result.input,
8492
+ partial: true
8493
+ });
8494
+ return;
8319
8495
  }
8320
- )) {
8321
- if (signal?.aborted) {
8322
- break;
8496
+ const content = partial[contentField];
8497
+ if (typeof content !== "string") {
8498
+ return;
8323
8499
  }
8324
- switch (event.type) {
8325
- case "text": {
8326
- const lastBlock = contentBlocks.at(-1);
8327
- if (lastBlock?.type === "text" && textBlockOpen) {
8328
- lastBlock.text += event.text;
8329
- } else {
8330
- contentBlocks.push({
8331
- type: "text",
8332
- text: event.text,
8333
- startedAt: event.ts
8334
- });
8335
- }
8336
- textBlockOpen = true;
8337
- onEvent({ type: "text", text: event.text });
8338
- break;
8500
+ if (!acc.started) {
8501
+ acc.started = true;
8502
+ onEvent({ type: "tool_start", id, name, input: partial });
8503
+ }
8504
+ if (transform) {
8505
+ const result = await transform(partial);
8506
+ if (result === null) {
8507
+ return;
8339
8508
  }
8340
- case "thinking":
8341
- if (event.text === "") {
8342
- thinkingBlockStartTimes.push(event.ts);
8343
- textBlockOpen = false;
8509
+ onEvent({ type: "tool_input_delta", id, name, result });
8510
+ } else {
8511
+ onEvent({ type: "tool_input_delta", id, name, result: content });
8512
+ }
8513
+ }
8514
+ try {
8515
+ for await (const event of streamChatWithRetry(
8516
+ {
8517
+ ...apiConfig,
8518
+ model: parentModel,
8519
+ requestId,
8520
+ system,
8521
+ messages: cleanMessagesForApi(state.messages),
8522
+ tools: tools2,
8523
+ signal
8524
+ },
8525
+ {
8526
+ onRetry: (attempt) => {
8527
+ onEvent({
8528
+ type: "status",
8529
+ message: `Lost connection, retrying (attempt ${attempt + 2} of 3)`
8530
+ });
8344
8531
  }
8345
- onEvent({ type: "thinking", text: event.text });
8346
- break;
8347
- case "thinking_complete": {
8348
- const startedAt = thinkingBlockStartTimes[thinkingCompleteCount] ?? event.ts;
8349
- contentBlocks.push({
8350
- type: "thinking",
8351
- thinking: event.thinking,
8352
- signature: event.signature,
8353
- startedAt,
8354
- completedAt: event.ts
8355
- });
8356
- thinkingCompleteCount++;
8357
- lastThinkingRelatedStartedAt = startedAt;
8358
- break;
8359
8532
  }
8360
- case "redacted_thinking_complete": {
8361
- const startedAt = lastThinkingRelatedStartedAt !== void 0 ? lastThinkingRelatedStartedAt + 1 : event.ts;
8362
- contentBlocks.push({
8363
- type: "redacted_thinking",
8364
- data: event.data,
8365
- startedAt,
8366
- completedAt: event.ts
8367
- });
8368
- lastThinkingRelatedStartedAt = startedAt;
8533
+ )) {
8534
+ if (signal?.aborted) {
8369
8535
  break;
8370
8536
  }
8371
- case "tool_input_delta": {
8372
- const acc = getOrCreateAccumulator2(event.id, event.name);
8373
- acc.json += event.delta;
8374
- try {
8375
- const partial = parsePartialJson(acc.json);
8376
- await handlePartialInput(acc, event.id, event.name, partial);
8377
- } catch {
8537
+ switch (event.type) {
8538
+ case "text": {
8539
+ const lastBlock = contentBlocks.at(-1);
8540
+ if (lastBlock?.type === "text" && textBlockOpen) {
8541
+ lastBlock.text += event.text;
8542
+ } else {
8543
+ contentBlocks.push({
8544
+ type: "text",
8545
+ text: event.text,
8546
+ startedAt: event.ts
8547
+ });
8548
+ }
8549
+ textBlockOpen = true;
8550
+ onEvent({ type: "text", text: event.text });
8551
+ break;
8378
8552
  }
8379
- break;
8380
- }
8381
- case "tool_input_args": {
8382
- const acc = getOrCreateAccumulator2(event.id, event.name);
8383
- await handlePartialInput(acc, event.id, event.name, event.args);
8384
- break;
8385
- }
8386
- case "tool_use": {
8387
- const tool = getToolByName(event.name);
8388
- contentBlocks.push({
8389
- type: "tool",
8390
- id: event.id,
8391
- name: event.name,
8392
- input: event.input,
8393
- startedAt: event.ts,
8394
- ...(event.input.background || tool?.backgroundOnly) && {
8395
- background: true
8553
+ case "thinking":
8554
+ if (event.text === "") {
8555
+ thinkingBlockStartTimes.push(event.ts);
8556
+ textBlockOpen = false;
8396
8557
  }
8397
- });
8398
- const acc = toolInputAccumulators.get(event.id);
8399
- const wasStreamed = acc?.started ?? false;
8400
- const isInputStreaming = !!tool?.streaming?.partialInput;
8401
- log13.info("Tool received", {
8402
- requestId,
8403
- toolCallId: event.id,
8404
- name: event.name
8405
- });
8406
- if (!wasStreamed || isInputStreaming) {
8407
- onEvent({
8408
- type: "tool_start",
8558
+ onEvent({ type: "thinking", text: event.text });
8559
+ break;
8560
+ case "thinking_complete": {
8561
+ const startedAt = thinkingBlockStartTimes[thinkingCompleteCount] ?? event.ts;
8562
+ contentBlocks.push({
8563
+ type: "thinking",
8564
+ thinking: event.thinking,
8565
+ signature: event.signature,
8566
+ startedAt,
8567
+ completedAt: event.ts
8568
+ });
8569
+ thinkingCompleteCount++;
8570
+ lastThinkingRelatedStartedAt = startedAt;
8571
+ break;
8572
+ }
8573
+ case "redacted_thinking_complete": {
8574
+ const startedAt = lastThinkingRelatedStartedAt !== void 0 ? lastThinkingRelatedStartedAt + 1 : event.ts;
8575
+ contentBlocks.push({
8576
+ type: "redacted_thinking",
8577
+ data: event.data,
8578
+ startedAt,
8579
+ completedAt: event.ts
8580
+ });
8581
+ lastThinkingRelatedStartedAt = startedAt;
8582
+ break;
8583
+ }
8584
+ case "tool_input_delta": {
8585
+ const acc = getOrCreateAccumulator2(event.id, event.name);
8586
+ acc.json += event.delta;
8587
+ try {
8588
+ const partial = parsePartialJson(acc.json);
8589
+ await handlePartialInput(acc, event.id, event.name, partial);
8590
+ } catch {
8591
+ }
8592
+ break;
8593
+ }
8594
+ case "tool_input_args": {
8595
+ const acc = getOrCreateAccumulator2(event.id, event.name);
8596
+ await handlePartialInput(acc, event.id, event.name, event.args);
8597
+ break;
8598
+ }
8599
+ case "tool_use": {
8600
+ const tool = getToolByName(event.name);
8601
+ contentBlocks.push({
8602
+ type: "tool",
8409
8603
  id: event.id,
8410
8604
  name: event.name,
8411
- input: event.input
8605
+ input: event.input,
8606
+ startedAt: event.ts,
8607
+ ...(event.input.background || tool?.backgroundOnly) && {
8608
+ background: true
8609
+ }
8610
+ });
8611
+ const acc = toolInputAccumulators.get(event.id);
8612
+ const wasStreamed = acc?.started ?? false;
8613
+ const isInputStreaming = !!tool?.streaming?.partialInput;
8614
+ log13.info("Tool received", {
8615
+ requestId,
8616
+ toolCallId: event.id,
8617
+ name: event.name
8412
8618
  });
8619
+ if (!wasStreamed || isInputStreaming) {
8620
+ onEvent({
8621
+ type: "tool_start",
8622
+ id: event.id,
8623
+ name: event.name,
8624
+ input: event.input,
8625
+ // Mirror the content-block stamp above so the live event
8626
+ // matches what a history reload shows — without it the
8627
+ // frontend renders background rows as already complete.
8628
+ ...isBackgroundCall({
8629
+ name: event.name,
8630
+ input: event.input
8631
+ }) && { background: true }
8632
+ });
8633
+ }
8634
+ break;
8413
8635
  }
8414
- break;
8636
+ case "done":
8637
+ stopReason = event.stopReason;
8638
+ turnProviderMetadata = event.providerMetadata;
8639
+ turnModelId = event.modelId;
8640
+ turnLlmCalls++;
8641
+ lastCallInputTokens = event.usage.inputTokens;
8642
+ lastCallCacheCreation = event.usage.cacheCreationTokens ?? 0;
8643
+ lastCallCacheRead = event.usage.cacheReadTokens ?? 0;
8644
+ turnInputTokens += lastCallInputTokens;
8645
+ turnOutputTokens += event.usage.outputTokens;
8646
+ turnCacheCreation += lastCallCacheCreation;
8647
+ turnCacheRead += lastCallCacheRead;
8648
+ recordUsage({
8649
+ ts: Date.now(),
8650
+ requestId,
8651
+ agentName: "parent",
8652
+ modelId: event.modelId,
8653
+ inputTokens: event.usage.inputTokens,
8654
+ outputTokens: event.usage.outputTokens,
8655
+ cacheCreationTokens: event.usage.cacheCreationTokens,
8656
+ cacheReadTokens: event.usage.cacheReadTokens,
8657
+ cost: nanoToDollars(event.cost),
8658
+ billingEvents: event.billingEvents,
8659
+ durationMs: Date.now() - iterStart,
8660
+ toolNames: contentBlocks.filter(
8661
+ (b) => b.type === "tool"
8662
+ ).map((b) => b.name)
8663
+ });
8664
+ break;
8665
+ case "error":
8666
+ statusWatcher.stop();
8667
+ onEvent({
8668
+ type: "error",
8669
+ error: friendlyError(event.error),
8670
+ ...event.code ? { code: event.code } : {}
8671
+ });
8672
+ return;
8415
8673
  }
8416
- case "done":
8417
- stopReason = event.stopReason;
8418
- turnProviderMetadata = event.providerMetadata;
8419
- turnModelId = event.modelId;
8420
- turnLlmCalls++;
8421
- lastCallInputTokens = event.usage.inputTokens;
8422
- lastCallCacheCreation = event.usage.cacheCreationTokens ?? 0;
8423
- lastCallCacheRead = event.usage.cacheReadTokens ?? 0;
8424
- turnInputTokens += lastCallInputTokens;
8425
- turnOutputTokens += event.usage.outputTokens;
8426
- turnCacheCreation += lastCallCacheCreation;
8427
- turnCacheRead += lastCallCacheRead;
8428
- recordUsage({
8429
- ts: Date.now(),
8430
- requestId,
8431
- agentName: "parent",
8432
- modelId: event.modelId,
8433
- inputTokens: event.usage.inputTokens,
8434
- outputTokens: event.usage.outputTokens,
8435
- cacheCreationTokens: event.usage.cacheCreationTokens,
8436
- cacheReadTokens: event.usage.cacheReadTokens,
8437
- cost: nanoToDollars(event.cost),
8438
- billingEvents: event.billingEvents,
8439
- durationMs: Date.now() - iterStart,
8440
- toolNames: contentBlocks.filter(
8441
- (b) => b.type === "tool"
8442
- ).map((b) => b.name)
8443
- });
8444
- break;
8445
- case "error":
8446
- onEvent({
8447
- type: "error",
8448
- error: friendlyError(event.error),
8449
- ...event.code ? { code: event.code } : {}
8450
- });
8451
- return;
8674
+ }
8675
+ } catch (err) {
8676
+ if (signal?.aborted) {
8677
+ } else {
8678
+ throw err;
8452
8679
  }
8453
8680
  }
8454
- } catch (err) {
8455
8681
  if (signal?.aborted) {
8456
- } else {
8457
- throw err;
8682
+ statusWatcher.stop();
8683
+ if (contentBlocks.length > 0) {
8684
+ contentBlocks.push({
8685
+ type: "text",
8686
+ text: "\n\n(cancelled)",
8687
+ startedAt: Date.now()
8688
+ });
8689
+ state.messages.push({
8690
+ role: "assistant",
8691
+ content: [...contentBlocks].sort(
8692
+ (a, b) => a.startedAt - b.startedAt
8693
+ ),
8694
+ usage: {
8695
+ inputTokens: turnInputTokens,
8696
+ outputTokens: turnOutputTokens,
8697
+ cacheCreationTokens: turnCacheCreation || void 0,
8698
+ cacheReadTokens: turnCacheRead || void 0,
8699
+ llmCalls: turnLlmCalls
8700
+ },
8701
+ ...turnProviderMetadata && {
8702
+ providerMetadata: turnProviderMetadata
8703
+ },
8704
+ model: turnModelId ?? parentModel,
8705
+ ...modelOverride && { modelOverride }
8706
+ });
8707
+ }
8708
+ onEvent({ type: "turn_cancelled" });
8709
+ saveSession(state);
8710
+ return;
8458
8711
  }
8459
- }
8460
- if (signal?.aborted) {
8461
- statusWatcher.stop();
8462
8712
  if (contentBlocks.length > 0) {
8463
- contentBlocks.push({
8464
- type: "text",
8465
- text: "\n\n(cancelled)",
8466
- startedAt: Date.now()
8467
- });
8468
8713
  state.messages.push({
8469
8714
  role: "assistant",
8470
8715
  content: [...contentBlocks].sort((a, b) => a.startedAt - b.startedAt),
@@ -8482,229 +8727,212 @@ async function runTurn(params) {
8482
8727
  ...modelOverride && { modelOverride }
8483
8728
  });
8484
8729
  }
8485
- onEvent({ type: "turn_cancelled" });
8486
- saveSession(state);
8487
- return;
8488
- }
8489
- if (contentBlocks.length > 0) {
8490
- state.messages.push({
8491
- role: "assistant",
8492
- content: [...contentBlocks].sort((a, b) => a.startedAt - b.startedAt),
8493
- usage: {
8494
- inputTokens: turnInputTokens,
8495
- outputTokens: turnOutputTokens,
8496
- cacheCreationTokens: turnCacheCreation || void 0,
8497
- cacheReadTokens: turnCacheRead || void 0,
8498
- llmCalls: turnLlmCalls
8499
- },
8500
- ...turnProviderMetadata && { providerMetadata: turnProviderMetadata },
8501
- model: turnModelId ?? parentModel,
8502
- ...modelOverride && { modelOverride }
8503
- });
8504
- }
8505
- const toolCalls = getToolCalls(contentBlocks);
8506
- if (stopReason !== "tool_use" || toolCalls.length === 0) {
8507
- statusWatcher.stop();
8508
- saveSession(state);
8509
- onEvent({
8510
- type: "turn_done",
8511
- stats: {
8512
- inputTokens: turnInputTokens,
8513
- outputTokens: turnOutputTokens,
8514
- cacheCreationTokens: turnCacheCreation || void 0,
8515
- cacheReadTokens: turnCacheRead || void 0,
8516
- llmCalls: turnLlmCalls,
8517
- lastCallInputTokens,
8518
- lastCallCacheCreation: lastCallCacheCreation || void 0,
8519
- lastCallCacheRead: lastCallCacheRead || void 0
8520
- }
8521
- });
8522
- return;
8523
- }
8524
- log13.info("Tools executing", {
8525
- requestId,
8526
- count: toolCalls.length,
8527
- tools: toolCalls.map((tc) => tc.name)
8528
- });
8529
- currentToolNames = toolCalls.filter((tc) => !STATUS_EXCLUDED_TOOLS.has(tc.name)).map((tc) => tc.name).join(", ");
8530
- const wrappedOnEvent = (e) => {
8531
- if ("parentToolId" in e && e.parentToolId) {
8532
- if (e.type === "text") {
8533
- subAgentText = e.text;
8534
- } else if (e.type === "tool_start") {
8535
- subAgentText = `Using ${e.name}`;
8536
- }
8730
+ const toolCalls = getToolCalls(contentBlocks);
8731
+ if (stopReason !== "tool_use" || toolCalls.length === 0) {
8732
+ statusWatcher.stop();
8733
+ saveSession(state);
8734
+ onEvent({
8735
+ type: "turn_done",
8736
+ stats: {
8737
+ inputTokens: turnInputTokens,
8738
+ outputTokens: turnOutputTokens,
8739
+ cacheCreationTokens: turnCacheCreation || void 0,
8740
+ cacheReadTokens: turnCacheRead || void 0,
8741
+ llmCalls: turnLlmCalls,
8742
+ lastCallInputTokens,
8743
+ lastCallCacheCreation: lastCallCacheCreation || void 0,
8744
+ lastCallCacheRead: lastCallCacheRead || void 0
8745
+ }
8746
+ });
8747
+ return;
8537
8748
  }
8538
- onEvent(e);
8539
- };
8540
- const subAgentMessages = /* @__PURE__ */ new Map();
8541
- const results = await Promise.all(
8542
- toolCalls.map(async (tc) => {
8543
- if (signal?.aborted) {
8544
- return { id: tc.id, result: USER_CANCELLED_RESULT, isError: true };
8749
+ log13.info("Tools executing", {
8750
+ requestId,
8751
+ count: toolCalls.length,
8752
+ tools: toolCalls.map((tc) => tc.name)
8753
+ });
8754
+ currentToolNames = toolCalls.filter((tc) => !STATUS_EXCLUDED_TOOLS.has(tc.name)).map((tc) => tc.name).join(", ");
8755
+ const wrappedOnEvent = (e) => {
8756
+ if ("parentToolId" in e && e.parentToolId) {
8757
+ if (e.type === "text") {
8758
+ subAgentText = e.text;
8759
+ } else if (e.type === "tool_start") {
8760
+ subAgentText = `Using ${e.name}`;
8761
+ }
8545
8762
  }
8546
- const toolStart = Date.now();
8547
- let settle;
8548
- const resultPromise = new Promise((res) => {
8549
- settle = (result, isError) => res({ id: tc.id, result, isError });
8550
- });
8551
- let toolAbort = new AbortController();
8552
- let settled = false;
8553
- const safeSettle = (result, isError) => {
8554
- if (settled) {
8555
- return;
8763
+ onEvent(e);
8764
+ };
8765
+ const subAgentMessages = /* @__PURE__ */ new Map();
8766
+ const results = await Promise.all(
8767
+ toolCalls.map(async (tc) => {
8768
+ if (signal?.aborted) {
8769
+ return { id: tc.id, result: USER_CANCELLED_RESULT, isError: true };
8556
8770
  }
8557
- settled = true;
8558
- signal?.removeEventListener("abort", cascadeAbort);
8559
- settle(result, isError);
8560
- };
8561
- const cascadeAbort = () => {
8562
- toolAbort.abort();
8563
- safeSettle(USER_CANCELLED_RESULT, true);
8564
- };
8565
- signal?.addEventListener("abort", cascadeAbort, { once: true });
8566
- const run = async (input) => {
8567
- try {
8568
- let result;
8569
- if (EXTERNAL_TOOLS.has(tc.name) && resolveExternalTool) {
8570
- saveSession(state);
8571
- log13.info("Waiting for external tool result", {
8572
- requestId,
8573
- toolCallId: tc.id,
8574
- name: tc.name
8575
- });
8576
- const blocksUser = USER_BLOCKING_EXTERNAL_TOOLS.has(tc.name);
8577
- if (blocksUser) {
8578
- statusWatcher.pause();
8579
- }
8580
- try {
8581
- result = await resolveExternalTool(tc.id, tc.name, input);
8582
- } finally {
8771
+ const toolStart = Date.now();
8772
+ let settle;
8773
+ const resultPromise = new Promise((res) => {
8774
+ settle = (result, isError) => res({ id: tc.id, result, isError });
8775
+ });
8776
+ let toolAbort = new AbortController();
8777
+ let settled = false;
8778
+ const safeSettle = (result, isError) => {
8779
+ if (settled) {
8780
+ return;
8781
+ }
8782
+ settled = true;
8783
+ signal?.removeEventListener("abort", cascadeAbort);
8784
+ settle(result, isError);
8785
+ };
8786
+ const cascadeAbort = () => {
8787
+ toolAbort.abort();
8788
+ safeSettle(USER_CANCELLED_RESULT, true);
8789
+ };
8790
+ signal?.addEventListener("abort", cascadeAbort, { once: true });
8791
+ const run = async (input) => {
8792
+ try {
8793
+ let result;
8794
+ if (EXTERNAL_TOOLS.has(tc.name) && resolveExternalTool) {
8795
+ saveSession(state);
8796
+ log13.info("Waiting for external tool result", {
8797
+ requestId,
8798
+ toolCallId: tc.id,
8799
+ name: tc.name
8800
+ });
8801
+ const blocksUser = USER_BLOCKING_EXTERNAL_TOOLS.has(tc.name);
8583
8802
  if (blocksUser) {
8584
- statusWatcher.resume();
8803
+ statusWatcher.pause();
8585
8804
  }
8805
+ try {
8806
+ result = await resolveExternalTool(tc.id, tc.name, input);
8807
+ } finally {
8808
+ if (blocksUser) {
8809
+ statusWatcher.resume();
8810
+ }
8811
+ }
8812
+ } else {
8813
+ result = await executeTool(tc.name, input, {
8814
+ apiConfig,
8815
+ model,
8816
+ models: state.models,
8817
+ signal: toolAbort.signal,
8818
+ onEvent: wrappedOnEvent,
8819
+ resolveExternalTool,
8820
+ toolCallId: tc.id,
8821
+ requestId,
8822
+ onboardingState,
8823
+ subAgentMessages,
8824
+ conversationMessages: state.messages,
8825
+ toolRegistry,
8826
+ onBackgroundComplete,
8827
+ onLog: (line) => wrappedOnEvent({
8828
+ type: "tool_input_delta",
8829
+ id: tc.id,
8830
+ name: tc.name,
8831
+ result: line
8832
+ })
8833
+ });
8586
8834
  }
8587
- } else {
8588
- result = await executeTool(tc.name, input, {
8589
- apiConfig,
8590
- model,
8591
- models: state.models,
8592
- signal: toolAbort.signal,
8593
- onEvent: wrappedOnEvent,
8594
- resolveExternalTool,
8595
- toolCallId: tc.id,
8596
- requestId,
8597
- onboardingState,
8598
- subAgentMessages,
8599
- conversationMessages: state.messages,
8600
- toolRegistry,
8601
- onBackgroundComplete,
8602
- onLog: (line) => wrappedOnEvent({
8603
- type: "tool_input_delta",
8604
- id: tc.id,
8605
- name: tc.name,
8606
- result: line
8607
- })
8835
+ safeSettle(capToolResult(result), result.startsWith("Error"));
8836
+ } catch (err) {
8837
+ safeSettle(`Error: ${err.message}`, true);
8838
+ }
8839
+ };
8840
+ const entry = {
8841
+ id: tc.id,
8842
+ name: tc.name,
8843
+ input: tc.input,
8844
+ abortController: toolAbort,
8845
+ startedAt: toolStart,
8846
+ settle: safeSettle,
8847
+ rerun: (newInput) => {
8848
+ settled = false;
8849
+ toolAbort = new AbortController();
8850
+ signal?.addEventListener("abort", () => toolAbort.abort(), {
8851
+ once: true
8608
8852
  });
8853
+ entry.abortController = toolAbort;
8854
+ entry.input = newInput;
8855
+ run(newInput);
8609
8856
  }
8610
- safeSettle(capToolResult(result), result.startsWith("Error"));
8611
- } catch (err) {
8612
- safeSettle(`Error: ${err.message}`, true);
8857
+ };
8858
+ toolRegistry?.register(entry);
8859
+ run(tc.input);
8860
+ const r = await resultPromise;
8861
+ if (!isBackgroundCall(tc)) {
8862
+ toolRegistry?.unregister(tc.id);
8613
8863
  }
8614
- };
8615
- const entry = {
8616
- id: tc.id,
8617
- name: tc.name,
8618
- input: tc.input,
8619
- abortController: toolAbort,
8620
- startedAt: toolStart,
8621
- settle: safeSettle,
8622
- rerun: (newInput) => {
8623
- settled = false;
8624
- toolAbort = new AbortController();
8625
- signal?.addEventListener("abort", () => toolAbort.abort(), {
8626
- once: true
8627
- });
8628
- entry.abortController = toolAbort;
8629
- entry.input = newInput;
8630
- run(newInput);
8864
+ log13.info("Tool completed", {
8865
+ requestId,
8866
+ toolCallId: tc.id,
8867
+ name: tc.name,
8868
+ durationMs: Date.now() - toolStart,
8869
+ isError: r.isError
8870
+ });
8871
+ onEvent({
8872
+ type: "tool_done",
8873
+ id: tc.id,
8874
+ name: tc.name,
8875
+ result: r.result,
8876
+ isError: r.isError
8877
+ });
8878
+ if (!r.isError && BRAND_TRIGGERING_TOOLS.has(tc.name)) {
8879
+ triggerBrandExtraction(
8880
+ apiConfig,
8881
+ resolveModel("brandExtractor", state.models, model)
8882
+ );
8631
8883
  }
8632
- };
8633
- toolRegistry?.register(entry);
8634
- run(tc.input);
8635
- const r = await resultPromise;
8636
- if (!isBackgroundCall(tc)) {
8637
- toolRegistry?.unregister(tc.id);
8638
- }
8639
- log13.info("Tool completed", {
8640
- requestId,
8641
- toolCallId: tc.id,
8642
- name: tc.name,
8643
- durationMs: Date.now() - toolStart,
8644
- isError: r.isError
8645
- });
8646
- onEvent({
8647
- type: "tool_done",
8648
- id: tc.id,
8649
- name: tc.name,
8650
- result: r.result,
8651
- isError: r.isError
8652
- });
8653
- if (!r.isError && BRAND_TRIGGERING_TOOLS.has(tc.name)) {
8654
- triggerBrandExtraction(
8655
- apiConfig,
8656
- resolveModel("brandExtractor", state.models, model)
8657
- );
8658
- }
8659
- return r;
8660
- })
8661
- );
8662
- statusWatcher.stop();
8663
- for (const r of results) {
8664
- const block = contentBlocks.find(
8665
- (b) => b.type === "tool" && b.id === r.id
8884
+ return r;
8885
+ })
8666
8886
  );
8667
- if (block?.type === "tool") {
8668
- block.result = r.result;
8669
- block.isError = r.isError;
8670
- block.completedAt = Date.now();
8671
- const msgs = subAgentMessages.get(r.id);
8672
- if (msgs) {
8673
- block.subAgentMessages = msgs;
8887
+ for (const r of results) {
8888
+ const block = contentBlocks.find(
8889
+ (b) => b.type === "tool" && b.id === r.id
8890
+ );
8891
+ if (block?.type === "tool") {
8892
+ block.result = r.result;
8893
+ block.isError = r.isError;
8894
+ block.completedAt = Date.now();
8895
+ const msgs = subAgentMessages.get(r.id);
8896
+ if (msgs) {
8897
+ block.subAgentMessages = msgs;
8898
+ }
8674
8899
  }
8675
8900
  }
8676
- }
8677
- const lastNonExcluded = toolCalls.filter(
8678
- (tc) => !STATUS_EXCLUDED_TOOLS.has(tc.name)
8679
- );
8680
- lastCompletedTools = lastNonExcluded.map((tc) => tc.name).join(", ");
8681
- lastCompletedInput = JSON.stringify(lastNonExcluded.at(-1)?.input ?? {});
8682
- lastCompletedResult = results.at(-1)?.result ?? "";
8683
- for (const r of results) {
8684
- state.messages.push({
8685
- role: "user",
8686
- content: r.result,
8687
- toolCallId: r.id,
8688
- isToolError: r.isError
8689
- });
8690
- }
8691
- if (takeSteering && !signal?.aborted) {
8692
- const injected = (await takeSteering()).filter(
8693
- (e) => e.text.trim().length > 0 || (e.attachments?.length ?? 0) > 0
8901
+ const lastNonExcluded = toolCalls.filter(
8902
+ (tc) => !STATUS_EXCLUDED_TOOLS.has(tc.name)
8694
8903
  );
8695
- if (injected.length > 0) {
8696
- for (const entry of injected) {
8697
- appendEntry(entry);
8698
- keptEntries.push(entry);
8904
+ lastCompletedTools = lastNonExcluded.map((tc) => tc.name).join(", ");
8905
+ lastCompletedInput = JSON.stringify(lastNonExcluded.at(-1)?.input ?? {});
8906
+ lastCompletedResult = results.at(-1)?.result ?? "";
8907
+ for (const r of results) {
8908
+ state.messages.push({
8909
+ role: "user",
8910
+ content: r.result,
8911
+ toolCallId: r.id,
8912
+ isToolError: r.isError
8913
+ });
8914
+ }
8915
+ if (takeSteering && !signal?.aborted) {
8916
+ const injected = (await takeSteering()).filter(
8917
+ (e) => e.text.trim().length > 0 || (e.attachments?.length ?? 0) > 0
8918
+ );
8919
+ if (injected.length > 0) {
8920
+ for (const entry of injected) {
8921
+ appendEntry(entry);
8922
+ keptEntries.push(entry);
8923
+ }
8924
+ saveSession(state);
8699
8925
  }
8926
+ }
8927
+ if (signal?.aborted) {
8928
+ statusWatcher.stop();
8929
+ onEvent({ type: "turn_cancelled" });
8700
8930
  saveSession(state);
8931
+ return;
8701
8932
  }
8702
8933
  }
8703
- if (signal?.aborted) {
8704
- onEvent({ type: "turn_cancelled" });
8705
- saveSession(state);
8706
- return;
8707
- }
8934
+ } finally {
8935
+ statusWatcher.stop();
8708
8936
  }
8709
8937
  }
8710
8938
  var log13, BRAND_TRIGGERING_TOOLS, EXTERNAL_TOOLS, USER_BLOCKING_EXTERNAL_TOOLS;
@@ -8718,6 +8946,7 @@ var init_agent = __esm({
8718
8946
  init_usageLedger();
8719
8947
  init_parsePartialJson();
8720
8948
  init_statusWatcher();
8949
+ init_resolve();
8721
8950
  init_errors();
8722
8951
  init_cleanMessages();
8723
8952
  init_sentinel();
@@ -9393,70 +9622,6 @@ var init_messageQueue = __esm({
9393
9622
  }
9394
9623
  });
9395
9624
 
9396
- // src/automatedActions/resolve.ts
9397
- function resolveAction(text) {
9398
- const parsed = parseSentinel(text);
9399
- if (!parsed) {
9400
- return null;
9401
- }
9402
- const { name: triggerName, remainder } = parsed;
9403
- if (NON_ACTION_SENTINELS.has(triggerName)) {
9404
- return null;
9405
- }
9406
- let params = {};
9407
- if (remainder) {
9408
- try {
9409
- params = JSON.parse(remainder.split("\n")[0]);
9410
- } catch {
9411
- }
9412
- }
9413
- let body = readAsset("automatedActions", `${triggerName}.md`);
9414
- let next;
9415
- const fmMatch = body.match(/^---\s*\n([\s\S]*?)\n---/);
9416
- if (fmMatch) {
9417
- const nextMatch = fmMatch[1].match(/^\s*next:\s*(\w+)\s*$/m);
9418
- if (nextMatch) {
9419
- next = nextMatch[1];
9420
- }
9421
- }
9422
- body = body.replace(/^---[\s\S]*?---\s*/, "");
9423
- for (const [key, value] of Object.entries(params)) {
9424
- const str = typeof value === "string" ? value : JSON.stringify(value);
9425
- body = body.replaceAll(`{{${key}}}`, str);
9426
- }
9427
- return {
9428
- message: automatedMessage(triggerName, body),
9429
- next
9430
- };
9431
- }
9432
- function getActionChain(startName) {
9433
- const chain = [];
9434
- const seen = /* @__PURE__ */ new Set();
9435
- let name = startName;
9436
- while (name && !seen.has(name)) {
9437
- seen.add(name);
9438
- chain.push(name);
9439
- let body;
9440
- try {
9441
- body = readAsset("automatedActions", `${name}.md`);
9442
- } catch {
9443
- break;
9444
- }
9445
- const fm = body.match(/^---\s*\n([\s\S]*?)\n---/);
9446
- name = fm?.[1].match(/^\s*next:\s*(\w+)\s*$/m)?.[1];
9447
- }
9448
- return chain;
9449
- }
9450
- var NON_ACTION_SENTINELS;
9451
- var init_resolve = __esm({
9452
- "src/automatedActions/resolve.ts"() {
9453
- "use strict";
9454
- init_assets();
9455
- init_sentinel();
9456
- NON_ACTION_SENTINELS = /* @__PURE__ */ new Set(["background_results"]);
9457
- }
9458
- });
9459
-
9460
9625
  // src/headless/index.ts
9461
9626
  var headless_exports = {};
9462
9627
  __export(headless_exports, {
@@ -9538,6 +9703,13 @@ var init_headless = __esm({
9538
9703
  earlyResults = /* @__PURE__ */ new Map();
9539
9704
  // Tool block updates from background completions (separate from the message queue)
9540
9705
  pendingBlockUpdates = [];
9706
+ /**
9707
+ * Id of the UI-only tool block synthesized for the in-flight user/gate
9708
+ * compaction (see the compaction listener). Model-invoked compactions have
9709
+ * a real block and never set this. Single slot — triggerCompaction is
9710
+ * single-flight.
9711
+ */
9712
+ syntheticCompactionId = null;
9541
9713
  // Tool lifecycle management — shared across all nesting depths
9542
9714
  toolRegistry = new ToolRegistry();
9543
9715
  // IO — accumulates stdin bytes between newline boundaries
@@ -9569,6 +9741,7 @@ var init_headless = __esm({
9569
9741
  this.emit("queue_changed", { queuedMessages: this.queue.snapshot() });
9570
9742
  });
9571
9743
  this.passivePen = loadPassiveResults();
9744
+ this.persistStats();
9572
9745
  if (resumed) {
9573
9746
  this.emit("session_restored", {
9574
9747
  messageCount: this.state.messages.length,
@@ -9591,15 +9764,61 @@ var init_headless = __esm({
9591
9764
  );
9592
9765
  this.sessionStats.compactionInProgress = true;
9593
9766
  this.persistStats();
9767
+ if (!event.toolCallId && (event.origin === "user" || event.origin === "gate")) {
9768
+ const id = `compact_${Date.now()}`;
9769
+ this.syntheticCompactionId = id;
9770
+ this.state.messages.push({
9771
+ role: "assistant",
9772
+ content: [
9773
+ {
9774
+ type: "tool",
9775
+ id,
9776
+ name: "compactConversation",
9777
+ input: {},
9778
+ startedAt: Date.now(),
9779
+ background: true,
9780
+ uiOnly: true,
9781
+ result: "Compaction started in the background."
9782
+ }
9783
+ ]
9784
+ });
9785
+ saveSession(this.state);
9786
+ this.onEvent({
9787
+ type: "tool_start",
9788
+ id,
9789
+ name: "compactConversation",
9790
+ input: {},
9791
+ background: true
9792
+ });
9793
+ this.onEvent({
9794
+ type: "tool_done",
9795
+ id,
9796
+ name: "compactConversation",
9797
+ result: "Compaction started in the background.",
9798
+ isError: false
9799
+ });
9800
+ }
9594
9801
  } else {
9595
9802
  const data = event.error ? { error: event.error } : {};
9596
9803
  this.emit("compaction_complete", data, event.requestId);
9804
+ if (this.syntheticCompactionId) {
9805
+ const id = this.syntheticCompactionId;
9806
+ this.syntheticCompactionId = null;
9807
+ this.onBackgroundComplete(
9808
+ id,
9809
+ "compactConversation",
9810
+ event.error ? `Error: ${event.error}` : formatSummariesResult(event.summaries ?? [])
9811
+ );
9812
+ }
9597
9813
  this.sessionStats.compactionInProgress = false;
9598
9814
  if (!event.error) {
9599
9815
  this.sessionStats.lastContextSize = 0;
9600
9816
  }
9601
9817
  this.sessionStats.messageCount = this.state.messages.length;
9602
9818
  this.persistStats();
9819
+ if (!this.running) {
9820
+ this.kickDrain();
9821
+ }
9603
9822
  }
9604
9823
  });
9605
9824
  process.stdin.setEncoding("utf-8");
@@ -9738,7 +9957,8 @@ var init_headless = __esm({
9738
9957
  await triggerCompaction(this.state, this.config, {
9739
9958
  blocking: true,
9740
9959
  requestId,
9741
- model: this.opts.model
9960
+ model: this.opts.model,
9961
+ origin: "gate"
9742
9962
  });
9743
9963
  applyPendingSummaries(this.state);
9744
9964
  } catch {
@@ -9767,6 +9987,12 @@ var init_headless = __esm({
9767
9987
  }
9768
9988
  return;
9769
9989
  }
9990
+ if (notify === "silent") {
9991
+ if (!this.running) {
9992
+ this.applyPendingBlockUpdates();
9993
+ }
9994
+ return;
9995
+ }
9770
9996
  this.queue.push({
9771
9997
  command: {
9772
9998
  action: "message",
@@ -10148,6 +10374,12 @@ var init_headless = __esm({
10148
10374
  this.completedEmitted = false;
10149
10375
  this.lastCompleted = null;
10150
10376
  this.turnStart = Date.now();
10377
+ const inflight2 = getInflightCompaction();
10378
+ if (inflight2) {
10379
+ await inflight2.catch(() => {
10380
+ });
10381
+ }
10382
+ applyPendingSummaries(this.state);
10151
10383
  if (this.passivePen.length > 0) {
10152
10384
  const swept = this.passivePen.splice(0);
10153
10385
  this.persistStats();
@@ -10237,7 +10469,7 @@ var init_headless = __esm({
10237
10469
  this.applyPendingBlockUpdates();
10238
10470
  }
10239
10471
  async handleMessage(parsed, requestId) {
10240
- if (this.running) {
10472
+ if (this.running || getInflightCompaction()) {
10241
10473
  const command = { ...parsed };
10242
10474
  if (requestId && command.requestId === void 0) {
10243
10475
  command.requestId = requestId;
@@ -10247,6 +10479,9 @@ var init_headless = __esm({
10247
10479
  source: "user",
10248
10480
  enqueuedAt: Date.now()
10249
10481
  });
10482
+ if (!this.running && !getInflightCompaction()) {
10483
+ this.kickDrain();
10484
+ }
10250
10485
  return;
10251
10486
  }
10252
10487
  this.running = true;
@@ -10254,6 +10489,7 @@ var init_headless = __esm({
10254
10489
  await this.runSingleTurn(parsed, requestId);
10255
10490
  await this.drainQueueLoop();
10256
10491
  } finally {
10492
+ this.currentAbort?.abort();
10257
10493
  this.currentAbort = null;
10258
10494
  this.currentRequestId = void 0;
10259
10495
  this.running = false;
@@ -10285,6 +10521,18 @@ var init_headless = __esm({
10285
10521
  async drainQueueLoop() {
10286
10522
  while (this.queue.length > 0) {
10287
10523
  const head = this.queue.peek();
10524
+ if (head.command.action === "compact") {
10525
+ this.queue.shift();
10526
+ await triggerCompaction(this.state, this.config, {
10527
+ blocking: true,
10528
+ requestId: head.command.requestId,
10529
+ model: this.opts.model,
10530
+ origin: "user"
10531
+ }).catch(() => {
10532
+ });
10533
+ applyPendingSummaries(this.state);
10534
+ continue;
10535
+ }
10288
10536
  if (head.source === "chain") {
10289
10537
  const item = this.queue.shift();
10290
10538
  const rid = item.command.requestId ?? `chain-${Date.now()}`;
@@ -10329,6 +10577,7 @@ var init_headless = __esm({
10329
10577
  try {
10330
10578
  await this.drainQueueLoop();
10331
10579
  } finally {
10580
+ this.currentAbort?.abort();
10332
10581
  this.currentAbort = null;
10333
10582
  this.currentRequestId = void 0;
10334
10583
  this.running = false;
@@ -10575,11 +10824,31 @@ var init_headless = __esm({
10575
10824
  return;
10576
10825
  }
10577
10826
  if (action === "compact") {
10827
+ if (this.running) {
10828
+ const alreadyQueued = this.queue.snapshot().some((it) => it.command.action === "compact");
10829
+ if (!alreadyQueued) {
10830
+ this.queue.push({
10831
+ command: {
10832
+ action: "compact",
10833
+ // Display label for the queue card (matches the automated
10834
+ // action idiom); never resolved as an action — drainQueueLoop
10835
+ // routes on command.action before any sentinel handling.
10836
+ text: sentinel("compact"),
10837
+ requestId
10838
+ },
10839
+ source: "user",
10840
+ enqueuedAt: Date.now()
10841
+ });
10842
+ }
10843
+ this.emit("completed", { success: true, queued: true }, requestId);
10844
+ return;
10845
+ }
10578
10846
  try {
10579
10847
  await triggerCompaction(this.state, this.config, {
10580
10848
  blocking: false,
10581
10849
  requestId,
10582
- model: this.opts.model
10850
+ model: this.opts.model,
10851
+ origin: "user"
10583
10852
  });
10584
10853
  if (!this.running) {
10585
10854
  applyPendingSummaries(this.state);