@mindstudio-ai/remy 0.1.266 → 0.1.267

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
@@ -1759,7 +1759,11 @@ function getConversationMessagesForSummary(messages, endIndex) {
1759
1759
  break;
1760
1760
  }
1761
1761
  }
1762
- return messages.slice(startIdx, endIndex);
1762
+ return messages.slice(startIdx, endIndex).filter(
1763
+ (msg) => !(Array.isArray(msg.content) && msg.content.some(
1764
+ (b) => b.type === "tool" && b.uiOnly
1765
+ ))
1766
+ );
1763
1767
  }
1764
1768
  function getSubAgentMessagesForSummary(messages, subAgentName, endIndex) {
1765
1769
  let checkpointIdx = -1;
@@ -2289,7 +2293,12 @@ ${summaryBlock.text}
2289
2293
  }
2290
2294
  startIdx = checkpointIdx + 1;
2291
2295
  }
2292
- const messagesToProcess = fixOrphanedToolCalls(messages.slice(startIdx));
2296
+ const apiMessages = messages.slice(startIdx).filter(
2297
+ (msg) => !(msg.role === "assistant" && Array.isArray(msg.content) && msg.content.some(
2298
+ (b) => b.type === "tool" && b.uiOnly
2299
+ ))
2300
+ );
2301
+ const messagesToProcess = fixOrphanedToolCalls(apiMessages);
2293
2302
  const toolUseIds = /* @__PURE__ */ new Set();
2294
2303
  for (const msg of messagesToProcess) {
2295
2304
  if (msg.role === "assistant" && Array.isArray(msg.content)) {
@@ -2393,6 +2402,11 @@ var init_toolResultCap = __esm({
2393
2402
  // src/session.ts
2394
2403
  import fs10 from "fs";
2395
2404
  import path4 from "path";
2405
+ function writeFileAtomicSync(file, data) {
2406
+ const tmp = `${file}.tmp`;
2407
+ fs10.writeFileSync(tmp, data, "utf-8");
2408
+ fs10.renameSync(tmp, file);
2409
+ }
2396
2410
  function loadSession(state) {
2397
2411
  pruneArchives();
2398
2412
  try {
@@ -2481,7 +2495,7 @@ function archiveMessages(messages, label, models) {
2481
2495
  if (models && Object.keys(models).length > 0) {
2482
2496
  payload.models = models;
2483
2497
  }
2484
- fs10.writeFileSync(dest, JSON.stringify(payload), "utf-8");
2498
+ writeFileAtomicSync(dest, JSON.stringify(payload));
2485
2499
  archiveCountCache.set(path4.basename(dest), count);
2486
2500
  log3.info("Session archived", { label, dest, messageCount: count });
2487
2501
  pruneArchives();
@@ -2686,7 +2700,7 @@ function saveSession(state) {
2686
2700
  if (Buffer.byteLength(serialized, "utf-8") > ROTATE_THRESHOLD_BYTES && rotate(state)) {
2687
2701
  serialized = JSON.stringify(buildPayload(state));
2688
2702
  }
2689
- fs10.writeFileSync(SESSION_FILE, serialized, "utf-8");
2703
+ writeFileAtomicSync(SESSION_FILE, serialized);
2690
2704
  log3.info("Session saved", { messageCount: state.messages.length });
2691
2705
  } catch (err) {
2692
2706
  log3.warn("Session save failed", { error: err.message });
@@ -2737,6 +2751,28 @@ var init_session = __esm({
2737
2751
  });
2738
2752
 
2739
2753
  // src/compaction/trigger.ts
2754
+ function getInflightCompaction() {
2755
+ return inflightCompaction;
2756
+ }
2757
+ function summariesOf(result) {
2758
+ const out = [];
2759
+ for (const msg of result.checkpoints) {
2760
+ if (!Array.isArray(msg.content)) {
2761
+ continue;
2762
+ }
2763
+ for (const block of msg.content) {
2764
+ if (block.type === "summary") {
2765
+ out.push({
2766
+ name: block.name,
2767
+ text: block.text,
2768
+ ...block.recent && { recent: block.recent },
2769
+ startedAt: block.startedAt
2770
+ });
2771
+ }
2772
+ }
2773
+ }
2774
+ return out;
2775
+ }
2740
2776
  function applyPendingSummaries(state) {
2741
2777
  const drained = pending;
2742
2778
  pending = null;
@@ -2757,6 +2793,20 @@ function applyPendingSummaries(state) {
2757
2793
  });
2758
2794
  saveSession(state);
2759
2795
  }
2796
+ function formatSummariesResult(summaries) {
2797
+ if (summaries === null) {
2798
+ return "A compaction checkpoint was already pending \u2014 no new compaction was needed.";
2799
+ }
2800
+ if (summaries.length === 0) {
2801
+ return "Nothing to compact \u2014 the conversation is already fully summarized.";
2802
+ }
2803
+ if (summaries.length === 1 && summaries[0].name === "conversation") {
2804
+ return summaries[0].text;
2805
+ }
2806
+ return summaries.map((s) => `## ${SUMMARY_SECTION_LABELS[s.name] ?? s.name}
2807
+
2808
+ ${s.text}`).join("\n\n");
2809
+ }
2760
2810
  function setCompactionListener(l) {
2761
2811
  listener = l;
2762
2812
  }
@@ -2766,18 +2816,24 @@ function triggerCompaction(state, apiConfig, opts = {}) {
2766
2816
  }
2767
2817
  if (pending) {
2768
2818
  log4.info("Compaction skipped \u2014 a checkpoint is already waiting to apply");
2769
- return Promise.resolve();
2819
+ return Promise.resolve(null);
2770
2820
  }
2771
- const { blocking = false, requestId, model } = opts;
2772
- listener?.({ type: "started", blocking, requestId });
2821
+ const { blocking = false, requestId, model, origin, toolCallId } = opts;
2822
+ listener?.({ type: "started", blocking, requestId, origin, toolCallId });
2773
2823
  inflightCompaction = compactConversation(
2774
2824
  state.messages,
2775
2825
  apiConfig,
2776
2826
  resolveModel("conversationSummarizer", state.models, model)
2777
2827
  ).then((result) => {
2778
2828
  pending = result;
2779
- listener?.({ type: "complete", requestId });
2829
+ const summaries = summariesOf(result);
2830
+ listener?.({
2831
+ type: "complete",
2832
+ requestId,
2833
+ ...summaries.length > 0 && { summaries }
2834
+ });
2780
2835
  log4.info("Compaction complete");
2836
+ return summaries;
2781
2837
  }).catch((err) => {
2782
2838
  const message = err.message || "Compaction failed";
2783
2839
  listener?.({ type: "complete", error: message, requestId });
@@ -2788,7 +2844,7 @@ function triggerCompaction(state, apiConfig, opts = {}) {
2788
2844
  });
2789
2845
  return inflightCompaction;
2790
2846
  }
2791
- var log4, pending, inflightCompaction, listener;
2847
+ var log4, pending, inflightCompaction, SUMMARY_SECTION_LABELS, listener;
2792
2848
  var init_trigger = __esm({
2793
2849
  "src/compaction/trigger.ts"() {
2794
2850
  "use strict";
@@ -2799,6 +2855,11 @@ var init_trigger = __esm({
2799
2855
  log4 = createLogger("compaction:trigger");
2800
2856
  pending = null;
2801
2857
  inflightCompaction = null;
2858
+ SUMMARY_SECTION_LABELS = {
2859
+ conversation: "Conversation",
2860
+ visualDesignExpert: "Design Agent Thread",
2861
+ productVision: "Roadmap Agent Thread"
2862
+ };
2802
2863
  listener = null;
2803
2864
  }
2804
2865
  });
@@ -2810,6 +2871,12 @@ var init_compactConversation = __esm({
2810
2871
  "use strict";
2811
2872
  init_trigger();
2812
2873
  compactConversationTool = {
2874
+ backgroundOnly: true,
2875
+ // Silent: the completed summary is written onto the tool block for the UI
2876
+ // detail view only. The model must never receive it as a message — it gets
2877
+ // the summary the correct way, as the checkpoint prefix in every later API
2878
+ // call (cleanMessagesForApi).
2879
+ backgroundNotify: "silent",
2813
2880
  definition: {
2814
2881
  clearable: false,
2815
2882
  name: "compactConversation",
@@ -2823,11 +2890,32 @@ var init_compactConversation = __esm({
2823
2890
  if (!context?.conversationMessages || !context.apiConfig) {
2824
2891
  return "Error: compaction requires execution context.";
2825
2892
  }
2893
+ const { toolCallId, onBackgroundComplete, toolRegistry } = context;
2826
2894
  triggerCompaction(
2827
2895
  { messages: context.conversationMessages },
2828
2896
  context.apiConfig,
2829
- { blocking: false, requestId: context.requestId }
2830
- ).catch(() => {
2897
+ // origin/toolCallId tell the headless listener a real block exists —
2898
+ // it must not synthesize a UI-only one for this compaction.
2899
+ {
2900
+ blocking: false,
2901
+ requestId: context.requestId,
2902
+ origin: "tool",
2903
+ toolCallId
2904
+ }
2905
+ ).then((summaries) => {
2906
+ onBackgroundComplete?.(
2907
+ toolCallId,
2908
+ "compactConversation",
2909
+ formatSummariesResult(summaries)
2910
+ );
2911
+ }).catch((err) => {
2912
+ onBackgroundComplete?.(
2913
+ toolCallId,
2914
+ "compactConversation",
2915
+ `Error: ${err.message || "Compaction failed"}`
2916
+ );
2917
+ }).finally(() => {
2918
+ toolRegistry?.unregister(toolCallId);
2831
2919
  });
2832
2920
  return "Compaction started in the background.";
2833
2921
  }
@@ -4337,16 +4425,48 @@ ${partial}` : "[INTERRUPTED] Tool execution was stopped.";
4337
4425
  });
4338
4426
 
4339
4427
  // src/statusWatcher.ts
4428
+ function sanitizeStatusText(text) {
4429
+ if (!text) {
4430
+ return "";
4431
+ }
4432
+ for (const marker of INTERNAL_PAYLOAD_MARKERS) {
4433
+ if (text.includes(marker)) {
4434
+ return "";
4435
+ }
4436
+ }
4437
+ const truncIdx = text.indexOf("(tool result truncated at");
4438
+ return truncIdx === -1 ? text : text.slice(0, truncIdx);
4439
+ }
4340
4440
  function startStatusWatcher(config) {
4341
4441
  const { apiConfig, getContext, onStatus, interval = 5e3, signal } = config;
4342
4442
  let inflight2 = false;
4343
4443
  let stopped = false;
4344
4444
  let pauseCount = 0;
4345
4445
  const url = `${apiConfig.baseUrl}/_internal/v2/agent/remy/generate-status`;
4446
+ let consecutiveFailures = 0;
4447
+ let backoffMs = 0;
4448
+ let nextAllowedAt = 0;
4449
+ const MAX_CONSECUTIVE_FAILURES = 10;
4450
+ const MAX_BACKOFF_MS = 6e4;
4451
+ function recordFailure() {
4452
+ consecutiveFailures++;
4453
+ backoffMs = Math.min(
4454
+ backoffMs === 0 ? interval : backoffMs * 2,
4455
+ MAX_BACKOFF_MS
4456
+ );
4457
+ nextAllowedAt = Date.now() + backoffMs;
4458
+ if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES && !stopped) {
4459
+ stopped = true;
4460
+ clearInterval(timer);
4461
+ }
4462
+ }
4346
4463
  async function tick() {
4347
4464
  if (stopped || signal?.aborted || inflight2 || pauseCount > 0) {
4348
4465
  return;
4349
4466
  }
4467
+ if (Date.now() < nextAllowedAt) {
4468
+ return;
4469
+ }
4350
4470
  inflight2 = true;
4351
4471
  try {
4352
4472
  const context = getContext();
@@ -4362,18 +4482,29 @@ function startStatusWatcher(config) {
4362
4482
  body: JSON.stringify({ appId: apiConfig.appId, context }),
4363
4483
  signal
4364
4484
  });
4365
- if (!res.ok || stopped) {
4485
+ if (stopped) {
4486
+ return;
4487
+ }
4488
+ if (!res.ok) {
4489
+ recordFailure();
4366
4490
  return;
4367
4491
  }
4368
4492
  const data = await res.json();
4369
4493
  if (!data.label) {
4494
+ recordFailure();
4370
4495
  return;
4371
4496
  }
4497
+ consecutiveFailures = 0;
4498
+ backoffMs = 0;
4499
+ nextAllowedAt = 0;
4372
4500
  if (pauseCount > 0 || stopped || signal?.aborted) {
4373
4501
  return;
4374
4502
  }
4375
4503
  onStatus(data.label);
4376
4504
  } catch {
4505
+ if (!stopped && !signal?.aborted) {
4506
+ recordFailure();
4507
+ }
4377
4508
  } finally {
4378
4509
  inflight2 = false;
4379
4510
  }
@@ -4394,9 +4525,17 @@ function startStatusWatcher(config) {
4394
4525
  }
4395
4526
  };
4396
4527
  }
4528
+ var INTERNAL_PAYLOAD_MARKERS;
4397
4529
  var init_statusWatcher = __esm({
4398
4530
  "src/statusWatcher.ts"() {
4399
4531
  "use strict";
4532
+ INTERNAL_PAYLOAD_MARKERS = [
4533
+ "[USER CANCELLED]",
4534
+ "[INTERRUPTED]",
4535
+ "[INTERRUPTED - PARTIAL OUTPUT RETRIEVED]",
4536
+ "<background_results>",
4537
+ "<tool_result"
4538
+ ];
4400
4539
  }
4401
4540
  });
4402
4541
 
@@ -4465,329 +4604,341 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
4465
4604
  return { text: USER_CANCELLED_RESULT, messages: thisInvocation() };
4466
4605
  }
4467
4606
  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
- })
4607
+ let watchedBlocks = [];
4608
+ let watchedToolNames = "";
4609
+ const statusWatcher = startStatusWatcher({
4610
+ apiConfig,
4611
+ getContext: () => {
4612
+ const parts = [];
4613
+ if (task) {
4614
+ parts.push(`Task: ${sanitizeStatusText(task).slice(-200)}`);
4615
+ }
4616
+ const text = getPartialText(watchedBlocks);
4617
+ if (text) {
4618
+ parts.push(`Assistant text: ${text.slice(-500)}`);
4619
+ }
4620
+ if (watchedToolNames) {
4621
+ parts.push(`Tool: ${watchedToolNames}`);
4622
+ }
4623
+ const toolResult = sanitizeStatusText(lastToolResult);
4624
+ if (toolResult) {
4625
+ parts.push(`Tool result: ${toolResult.slice(-200)}`);
4626
+ }
4627
+ return parts.join("\n");
4628
+ },
4629
+ onStatus: (label) => emit({ type: "status", message: label }),
4630
+ signal
4631
+ });
4632
+ try {
4633
+ while (true) {
4634
+ turns++;
4635
+ if (signal?.aborted) {
4636
+ return abortResult([]);
4637
+ }
4638
+ const iterStart = Date.now();
4639
+ const contentBlocks = [];
4640
+ let thinkingStartedAt = 0;
4641
+ let lastThinkingRelatedStartedAt;
4642
+ let stopReason = "end_turn";
4643
+ let lastUsage;
4644
+ let lastProviderMetadata;
4645
+ let lastModelId;
4646
+ watchedBlocks = contentBlocks;
4647
+ watchedToolNames = "";
4648
+ try {
4649
+ for await (const event of streamChatWithRetry(
4650
+ {
4651
+ ...apiConfig,
4652
+ model,
4653
+ requestId,
4654
+ subAgentId,
4655
+ system: fullSystem,
4656
+ messages: cleanMessagesForApi(messages),
4657
+ tools: tools2,
4658
+ signal
4659
+ },
4660
+ {
4661
+ onRetry: (attempt) => emit({
4662
+ type: "status",
4663
+ message: `Lost connection, retrying (attempt ${attempt + 2} of 3)`
4664
+ })
4665
+ }
4666
+ )) {
4667
+ if (signal?.aborted) {
4668
+ break;
4669
+ }
4670
+ switch (event.type) {
4671
+ case "text": {
4672
+ const lastBlock = contentBlocks.at(-1);
4673
+ if (lastBlock?.type === "text") {
4674
+ lastBlock.text += event.text;
4675
+ } else {
4676
+ contentBlocks.push({
4677
+ type: "text",
4678
+ text: event.text,
4679
+ startedAt: event.ts
4680
+ });
4681
+ }
4682
+ emit({ type: "text", text: event.text });
4683
+ break;
4684
+ }
4685
+ case "thinking":
4686
+ if (!thinkingStartedAt) {
4687
+ thinkingStartedAt = event.ts;
4688
+ }
4689
+ emit({ type: "thinking", text: event.text });
4690
+ break;
4691
+ case "thinking_complete":
4692
+ contentBlocks.push({
4693
+ type: "thinking",
4694
+ thinking: event.thinking,
4695
+ signature: event.signature,
4696
+ startedAt: thinkingStartedAt,
4697
+ completedAt: event.ts
4698
+ });
4699
+ lastThinkingRelatedStartedAt = thinkingStartedAt;
4700
+ thinkingStartedAt = 0;
4701
+ break;
4702
+ case "redacted_thinking_complete": {
4703
+ const startedAt = lastThinkingRelatedStartedAt !== void 0 ? lastThinkingRelatedStartedAt + 1 : event.ts;
4704
+ contentBlocks.push({
4705
+ type: "redacted_thinking",
4706
+ data: event.data,
4707
+ startedAt,
4708
+ completedAt: event.ts
4709
+ });
4710
+ lastThinkingRelatedStartedAt = startedAt;
4711
+ break;
4712
+ }
4713
+ case "tool_use":
4714
+ contentBlocks.push({
4715
+ type: "tool",
4716
+ id: event.id,
4717
+ name: event.name,
4718
+ input: event.input,
4719
+ startedAt: Date.now()
4720
+ });
4721
+ emit({
4722
+ type: "tool_start",
4723
+ id: event.id,
4724
+ name: event.name,
4725
+ input: event.input
4726
+ });
4727
+ break;
4728
+ case "done":
4729
+ stopReason = event.stopReason;
4730
+ lastUsage = {
4731
+ inputTokens: event.usage.inputTokens,
4732
+ outputTokens: event.usage.outputTokens,
4733
+ cacheCreationTokens: event.usage.cacheCreationTokens,
4734
+ cacheReadTokens: event.usage.cacheReadTokens,
4735
+ llmCalls: 1
4736
+ };
4737
+ lastProviderMetadata = event.providerMetadata;
4738
+ lastModelId = event.modelId;
4739
+ recordUsage({
4740
+ ts: Date.now(),
4741
+ requestId,
4742
+ agentName: subAgentId || "sub-agent",
4743
+ parentToolId,
4744
+ modelId: event.modelId,
4745
+ inputTokens: event.usage.inputTokens,
4746
+ outputTokens: event.usage.outputTokens,
4747
+ cacheCreationTokens: event.usage.cacheCreationTokens,
4748
+ cacheReadTokens: event.usage.cacheReadTokens,
4749
+ cost: nanoToDollars(event.cost),
4750
+ billingEvents: event.billingEvents,
4751
+ durationMs: Date.now() - iterStart,
4752
+ toolNames: contentBlocks.filter(
4753
+ (b) => b.type === "tool"
4754
+ ).map((b) => b.name)
4755
+ });
4756
+ break;
4757
+ case "error":
4758
+ statusWatcher.stop();
4759
+ return {
4760
+ text: `Error: ${event.error}`,
4761
+ messages: thisInvocation()
4762
+ };
4763
+ }
4521
4764
  }
4522
- )) {
4523
- if (signal?.aborted) {
4524
- break;
4765
+ } catch (err) {
4766
+ if (!signal?.aborted) {
4767
+ throw err;
4525
4768
  }
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
4769
+ }
4770
+ if (signal?.aborted) {
4771
+ statusWatcher.stop();
4772
+ return abortResult(contentBlocks);
4773
+ }
4774
+ messages.push({
4775
+ role: "assistant",
4776
+ content: contentBlocks,
4777
+ ...lastUsage ? { usage: lastUsage } : {},
4778
+ ...lastProviderMetadata ? { providerMetadata: lastProviderMetadata } : {},
4779
+ model: lastModelId ?? model
4780
+ });
4781
+ const toolCalls = contentBlocks.filter(
4782
+ (b) => b.type === "tool"
4783
+ );
4784
+ if (stopReason !== "tool_use" || toolCalls.length === 0) {
4785
+ statusWatcher.stop();
4786
+ const text = getPartialText(contentBlocks);
4787
+ const hasArtifacts = Object.keys(artifacts).length > 0;
4788
+ return {
4789
+ text,
4790
+ messages: thisInvocation(),
4791
+ ...hasArtifacts ? { artifacts } : {}
4792
+ };
4793
+ }
4794
+ log8.info("Tools executing", {
4795
+ requestId,
4796
+ parentToolId,
4797
+ count: toolCalls.length,
4798
+ tools: toolCalls.map((tc) => tc.name)
4799
+ });
4800
+ watchedToolNames = toolCalls.map((tc) => tc.name).join(", ");
4801
+ const results = await Promise.all(
4802
+ toolCalls.map(async (tc) => {
4803
+ if (signal?.aborted) {
4804
+ return {
4805
+ id: tc.id,
4806
+ result: USER_CANCELLED_RESULT,
4807
+ isError: true
4808
+ };
4809
+ }
4810
+ let settle;
4811
+ const resultPromise = new Promise((res) => {
4812
+ settle = (result, isError) => res({ id: tc.id, result, isError });
4813
+ });
4814
+ let toolAbort = new AbortController();
4815
+ const cascadeAbort = () => toolAbort.abort();
4816
+ signal?.addEventListener("abort", cascadeAbort, { once: true });
4817
+ let settled = false;
4818
+ const safeSettle = (result, isError) => {
4819
+ if (settled) {
4820
+ return;
4821
+ }
4822
+ settled = true;
4823
+ signal?.removeEventListener("abort", cascadeAbort);
4824
+ settle(result, isError);
4825
+ };
4826
+ const run2 = async (input) => {
4827
+ try {
4828
+ let result;
4829
+ if (externalTools.has(tc.name) && resolveExternalTool) {
4830
+ result = await resolveExternalTool(tc.id, tc.name, input);
4831
+ } else {
4832
+ const onLog = (line) => emit({
4833
+ type: "tool_input_delta",
4834
+ id: tc.id,
4835
+ name: tc.name,
4836
+ result: line
4837
+ });
4838
+ result = await executeTool2(
4839
+ tc.name,
4840
+ input,
4841
+ tc.id,
4842
+ onLog,
4843
+ subAgentMessages
4844
+ );
4845
+ }
4846
+ safeSettle(capToolResult(result), result.startsWith("Error"));
4847
+ } catch (err) {
4848
+ safeSettle(`Error: ${err.message}`, true);
4849
+ }
4850
+ };
4851
+ const entry = {
4852
+ id: tc.id,
4853
+ name: tc.name,
4854
+ input: tc.input,
4855
+ parentToolId,
4856
+ abortController: toolAbort,
4857
+ startedAt: Date.now(),
4858
+ settle: safeSettle,
4859
+ rerun: (newInput) => {
4860
+ settled = false;
4861
+ toolAbort = new AbortController();
4862
+ signal?.addEventListener("abort", () => toolAbort.abort(), {
4863
+ once: true
4536
4864
  });
4865
+ entry.abortController = toolAbort;
4866
+ entry.input = newInput;
4867
+ run2(newInput);
4537
4868
  }
4538
- emit({ type: "text", text: event.text });
4539
- break;
4869
+ };
4870
+ toolRegistry?.register(entry);
4871
+ const toolStart = Date.now();
4872
+ run2(tc.input);
4873
+ const r = await resultPromise;
4874
+ toolRegistry?.unregister(tc.id);
4875
+ log8.info("Tool completed", {
4876
+ requestId,
4877
+ parentToolId,
4878
+ toolCallId: tc.id,
4879
+ name: tc.name,
4880
+ durationMs: Date.now() - toolStart,
4881
+ isError: r.isError
4882
+ });
4883
+ emit({
4884
+ type: "tool_done",
4885
+ id: tc.id,
4886
+ name: tc.name,
4887
+ result: r.result,
4888
+ isError: r.isError
4889
+ });
4890
+ return r;
4891
+ })
4892
+ );
4893
+ lastToolResult = results.at(-1)?.result ?? "";
4894
+ for (const r of results) {
4895
+ const block = contentBlocks.find(
4896
+ (b) => b.type === "tool" && b.id === r.id
4897
+ );
4898
+ if (block?.type === "tool") {
4899
+ block.result = r.result;
4900
+ block.isError = r.isError;
4901
+ block.completedAt = Date.now();
4902
+ const innerMsgs = subAgentMessages.get(r.id);
4903
+ if (innerMsgs) {
4904
+ block.subAgentMessages = innerMsgs;
4540
4905
  }
4541
- case "thinking":
4542
- if (!thinkingStartedAt) {
4543
- thinkingStartedAt = event.ts;
4906
+ if (captureArtifacts?.includes(block.name) && !r.isError) {
4907
+ try {
4908
+ artifacts[block.name] = JSON.parse(r.result);
4909
+ } catch {
4544
4910
  }
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
4911
  }
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
4912
  }
4619
- }
4620
- } catch (err) {
4621
- if (!signal?.aborted) {
4622
- throw err;
4913
+ messages.push({
4914
+ role: "user",
4915
+ content: r.result,
4916
+ toolCallId: r.id,
4917
+ isToolError: r.isError
4918
+ });
4623
4919
  }
4624
4920
  }
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
4921
+ } finally {
4922
+ statusWatcher.stop();
4923
+ }
4924
+ };
4925
+ const wrapRun = async () => {
4926
+ try {
4927
+ const result = await run();
4928
+ log8.info("Sub-agent complete", {
4929
+ requestId,
4930
+ parentToolId,
4931
+ agentName,
4932
+ durationMs: Date.now() - runStart,
4933
+ turns
4635
4934
  });
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", {
4935
+ return result;
4936
+ } catch (err) {
4937
+ log8.warn("Sub-agent error", {
4650
4938
  requestId,
4651
4939
  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
4940
+ agentName,
4941
+ error: err.message
4791
4942
  });
4792
4943
  throw err;
4793
4944
  }
@@ -7096,6 +7247,129 @@ var init_codeSanityCheck = __esm({
7096
7247
  }
7097
7248
  });
7098
7249
 
7250
+ // src/tools/spec/writeBuildOverview.ts
7251
+ import fs19 from "fs";
7252
+ function initialDelivery() {
7253
+ return `### Your deliverable
7254
+ 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.
7255
+
7256
+ <overview_shell>
7257
+ ${OVERVIEW_SHELL}
7258
+ </overview_shell>
7259
+
7260
+ Then reply with a one-line summary of what you wrote.`;
7261
+ }
7262
+ function refreshDelivery() {
7263
+ return `### Your deliverable
7264
+ 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.
7265
+
7266
+ Then reply with a one-line summary of what you changed.`;
7267
+ }
7268
+ async function renderBuildOverview(content, context, opts) {
7269
+ if (!content) {
7270
+ return "Error: writeBuildOverview requires non-empty `content` (the overview copy).";
7271
+ }
7272
+ const exists = fs19.existsSync(OVERVIEW_FILE);
7273
+ const task = `<overview_copy>${content}</overview_copy>
7274
+
7275
+ ${DESIGN_BRIEF}
7276
+
7277
+ ${exists ? refreshDelivery() : initialDelivery()}`;
7278
+ try {
7279
+ if (opts.background) {
7280
+ const result2 = await runDesignExpertRender(
7281
+ { task, background: true, reportingName: "writeBuildOverview" },
7282
+ context
7283
+ );
7284
+ context.subAgentMessages?.set(context.toolCallId, result2.messages);
7285
+ return result2.text;
7286
+ }
7287
+ const result = await runDesignExpertRender({ task }, context);
7288
+ context.subAgentMessages?.set(context.toolCallId, result.messages);
7289
+ if (!fs19.existsSync(OVERVIEW_FILE)) {
7290
+ return `Error: the design expert did not write ${OVERVIEW_FILE}. Its reply was:
7291
+ ${result.text}`;
7292
+ }
7293
+ return `Build overview written to ${OVERVIEW_FILE}. ${result.text}`;
7294
+ } catch (err) {
7295
+ return `Error generating build overview: ${err.message}`;
7296
+ }
7297
+ }
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; routine refreshes ride specSync's `refreshBuildOverview` flag instead, so call this directly only for the initial generation or when the user explicitly asks.",
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
+ const background = Boolean(content) && fs19.existsSync(OVERVIEW_FILE);
7364
+ if (background) {
7365
+ input.background = true;
7366
+ }
7367
+ return renderBuildOverview(content, context, { background });
7368
+ }
7369
+ };
7370
+ }
7371
+ });
7372
+
7099
7373
  // src/subagents/specSync/tools.ts
7100
7374
  var SPEC_SYNC_TOOLS;
7101
7375
  var init_tools7 = __esm({
@@ -7141,6 +7415,7 @@ var init_specSync = __esm({
7141
7415
  init_runner();
7142
7416
  init_context();
7143
7417
  init_tools8();
7418
+ init_writeBuildOverview();
7144
7419
  init_tools7();
7145
7420
  init_lock();
7146
7421
  init_surfaces();
@@ -7159,13 +7434,17 @@ ${readAsset(
7159
7434
  definition: {
7160
7435
  clearable: false,
7161
7436
  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.",
7437
+ 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
7438
  inputSchema: {
7164
7439
  type: "object",
7165
7440
  properties: {
7166
7441
  task: {
7167
7442
  type: "string",
7168
7443
  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."
7444
+ },
7445
+ refreshBuildOverview: {
7446
+ type: "boolean",
7447
+ 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
7448
  }
7170
7449
  },
7171
7450
  required: ["task"]
@@ -7189,12 +7468,27 @@ ${readAsset(
7189
7468
  parts.push(specIndex);
7190
7469
  }
7191
7470
  const system = parts.join("\n\n");
7471
+ const refreshOverview = input.refreshBuildOverview === true;
7472
+ const tools2 = refreshOverview ? [...SPEC_SYNC_TOOLS, buildOverviewTool.definition] : SPEC_SYNC_TOOLS;
7473
+ const task = refreshOverview ? `${input.task}
7474
+
7475
+ 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
7476
  const result = await runSubAgent({
7193
7477
  system,
7194
- task: input.task,
7195
- tools: SPEC_SYNC_TOOLS,
7478
+ task,
7479
+ tools: tools2,
7196
7480
  externalTools: /* @__PURE__ */ new Set(),
7197
- executeTool: (name, toolInput) => executeTool(name, toolInput, context),
7481
+ executeTool: (name, toolInput, toolCallId, _onLog, sams) => {
7482
+ if (name === "writeBuildOverview") {
7483
+ const childCtx = toolCallId ? { ...deriveContext(context, toolCallId), subAgentMessages: sams } : { ...context, subAgentMessages: sams };
7484
+ return renderBuildOverview(
7485
+ String(toolInput.content ?? "").trim(),
7486
+ childCtx,
7487
+ { background: false }
7488
+ );
7489
+ }
7490
+ return executeTool(name, toolInput, context);
7491
+ },
7198
7492
  apiConfig: context.apiConfig,
7199
7493
  model: resolveModel("specSync", context.models, context.model),
7200
7494
  subAgentId: "specSync",
@@ -7277,143 +7571,26 @@ var init_scrapeWebUrl2 = __esm({
7277
7571
  }
7278
7572
  });
7279
7573
 
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.`;
7574
+ // src/tools/index.ts
7575
+ function deriveContext(parent, toolCallId) {
7576
+ return { ...parent, toolCallId };
7291
7577
  }
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.`;
7578
+ function getToolDefinitions(_onboardingState) {
7579
+ return ALL_TOOLS.map((t) => t.definition);
7297
7580
  }
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"() {
7581
+ function getToolByName(name) {
7582
+ return ALL_TOOLS.find((t) => t.definition.name === name);
7583
+ }
7584
+ function executeTool(name, input, context) {
7585
+ const tool = getToolByName(name);
7586
+ if (!tool) {
7587
+ return Promise.resolve(`Error: Unknown tool "${name}"`);
7588
+ }
7589
+ return tool.execute(input, context);
7590
+ }
7591
+ var ALL_TOOLS, SUBAGENT_TOOL_NAMES;
7592
+ var init_tools8 = __esm({
7593
+ "src/tools/index.ts"() {
7417
7594
  "use strict";
7418
7595
  init_readSpec();
7419
7596
  init_writeSpec();
@@ -7680,6 +7857,70 @@ var init_parsePartialJson = __esm({
7680
7857
  }
7681
7858
  });
7682
7859
 
7860
+ // src/automatedActions/resolve.ts
7861
+ function resolveAction(text) {
7862
+ const parsed = parseSentinel(text);
7863
+ if (!parsed) {
7864
+ return null;
7865
+ }
7866
+ const { name: triggerName, remainder } = parsed;
7867
+ if (NON_ACTION_SENTINELS.has(triggerName)) {
7868
+ return null;
7869
+ }
7870
+ let params = {};
7871
+ if (remainder) {
7872
+ try {
7873
+ params = JSON.parse(remainder.split("\n")[0]);
7874
+ } catch {
7875
+ }
7876
+ }
7877
+ let body = readAsset("automatedActions", `${triggerName}.md`);
7878
+ let next;
7879
+ const fmMatch = body.match(/^---\s*\n([\s\S]*?)\n---/);
7880
+ if (fmMatch) {
7881
+ const nextMatch = fmMatch[1].match(/^\s*next:\s*(\w+)\s*$/m);
7882
+ if (nextMatch) {
7883
+ next = nextMatch[1];
7884
+ }
7885
+ }
7886
+ body = body.replace(/^---[\s\S]*?---\s*/, "");
7887
+ for (const [key, value] of Object.entries(params)) {
7888
+ const str = typeof value === "string" ? value : JSON.stringify(value);
7889
+ body = body.replaceAll(`{{${key}}}`, str);
7890
+ }
7891
+ return {
7892
+ message: automatedMessage(triggerName, body),
7893
+ next
7894
+ };
7895
+ }
7896
+ function getActionChain(startName) {
7897
+ const chain = [];
7898
+ const seen = /* @__PURE__ */ new Set();
7899
+ let name = startName;
7900
+ while (name && !seen.has(name)) {
7901
+ seen.add(name);
7902
+ chain.push(name);
7903
+ let body;
7904
+ try {
7905
+ body = readAsset("automatedActions", `${name}.md`);
7906
+ } catch {
7907
+ break;
7908
+ }
7909
+ const fm = body.match(/^---\s*\n([\s\S]*?)\n---/);
7910
+ name = fm?.[1].match(/^\s*next:\s*(\w+)\s*$/m)?.[1];
7911
+ }
7912
+ return chain;
7913
+ }
7914
+ var NON_ACTION_SENTINELS;
7915
+ var init_resolve = __esm({
7916
+ "src/automatedActions/resolve.ts"() {
7917
+ "use strict";
7918
+ init_assets();
7919
+ init_sentinel();
7920
+ NON_ACTION_SENTINELS = /* @__PURE__ */ new Set(["background_results"]);
7921
+ }
7922
+ });
7923
+
7683
7924
  // src/errors.ts
7684
7925
  function friendlyError(raw) {
7685
7926
  for (const [pattern, message] of patterns) {
@@ -8182,6 +8423,9 @@ async function runTurn(params) {
8182
8423
  let lastCompletedTools = "";
8183
8424
  let lastCompletedInput = "";
8184
8425
  let lastCompletedResult = "";
8426
+ let subAgentText = "";
8427
+ let currentToolNames = "";
8428
+ let contentBlocksRef = [];
8185
8429
  let turnInputTokens = 0;
8186
8430
  let turnOutputTokens = 0;
8187
8431
  let turnCacheCreation = 0;
@@ -8190,281 +8434,328 @@ async function runTurn(params) {
8190
8434
  let lastCallInputTokens = 0;
8191
8435
  let lastCallCacheCreation = 0;
8192
8436
  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}`);
8437
+ const statusWatcher = isFirstMessage ? { stop() {
8438
+ }, pause() {
8439
+ }, resume() {
8440
+ } } : startStatusWatcher({
8441
+ apiConfig,
8442
+ getContext: () => {
8443
+ const parts = [];
8444
+ const toolName = currentToolNames || getToolCalls(contentBlocksRef).filter((tc) => !STATUS_EXCLUDED_TOOLS.has(tc.name)).at(-1)?.name || lastCompletedTools;
8445
+ if (toolName) {
8446
+ parts.push(`Tool: ${toolName}`);
8447
+ }
8448
+ const toolInput = sanitizeStatusText(lastCompletedInput);
8449
+ if (toolInput) {
8450
+ parts.push(`Tool input: ${toolInput.slice(-1500)}`);
8451
+ }
8452
+ const toolResult = sanitizeStatusText(lastCompletedResult);
8453
+ if (toolResult) {
8454
+ parts.push(`Tool result: ${toolResult.slice(-1500)}`);
8455
+ }
8456
+ const text = subAgentText || getTextContent(contentBlocksRef).slice(-2e3);
8457
+ if (text) {
8458
+ parts.push(`Assistant text: ${text}`);
8459
+ }
8460
+ if (onboardingState && onboardingState !== "onboardingFinished") {
8461
+ parts.push(`Build phase: ${onboardingState}`);
8462
+ }
8463
+ let hasUserSignal = false;
8464
+ for (const entry of keptEntries) {
8465
+ if (entry.hidden) {
8466
+ continue;
8243
8467
  }
8244
- for (const entry of keptEntries) {
8245
- const automated = parseSentinel(entry.text);
8246
- if (automated) {
8468
+ const automated = parseSentinel(entry.text);
8469
+ if (automated) {
8470
+ if (!NON_ACTION_SENTINELS.has(automated.name)) {
8247
8471
  parts.push(`Automated action: ${automated.name}`);
8248
- } else if (entry.text) {
8249
- parts.push(`User request: ${entry.text.slice(-500)}`);
8472
+ hasUserSignal = true;
8250
8473
  }
8474
+ } else if (entry.text) {
8475
+ parts.push(`User request: ${entry.text.slice(-500)}`);
8476
+ hasUserSignal = true;
8251
8477
  }
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
8478
  }
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;
8479
+ if (!hasUserSignal && !toolName && !text) {
8480
+ return "";
8282
8481
  }
8283
- const content = partial[contentField];
8284
- if (typeof content !== "string") {
8482
+ return parts.join("\n");
8483
+ },
8484
+ onStatus: (label) => onEvent({ type: "status", message: label }),
8485
+ signal
8486
+ });
8487
+ try {
8488
+ while (true) {
8489
+ let getOrCreateAccumulator2 = function(id, name) {
8490
+ let acc = toolInputAccumulators.get(id);
8491
+ if (!acc) {
8492
+ acc = { name, json: "", started: false, lastEmittedCount: 0 };
8493
+ toolInputAccumulators.set(id, acc);
8494
+ }
8495
+ return acc;
8496
+ };
8497
+ var getOrCreateAccumulator = getOrCreateAccumulator2;
8498
+ if (signal?.aborted) {
8499
+ onEvent({ type: "turn_cancelled" });
8500
+ saveSession(state);
8285
8501
  return;
8286
8502
  }
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) {
8503
+ const iterStart = Date.now();
8504
+ const contentBlocks = [];
8505
+ const thinkingBlockStartTimes = [];
8506
+ let thinkingCompleteCount = 0;
8507
+ let lastThinkingRelatedStartedAt;
8508
+ let textBlockOpen = false;
8509
+ const toolInputAccumulators = /* @__PURE__ */ new Map();
8510
+ let stopReason = "end_turn";
8511
+ let turnProviderMetadata;
8512
+ let turnModelId;
8513
+ contentBlocksRef = contentBlocks;
8514
+ subAgentText = "";
8515
+ currentToolNames = "";
8516
+ async function handlePartialInput(acc, id, name, partial) {
8517
+ const tool = getToolByName(name);
8518
+ if (!tool?.streaming) {
8294
8519
  return;
8295
8520
  }
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
- });
8521
+ const {
8522
+ contentField = "content",
8523
+ transform,
8524
+ partialInput
8525
+ } = tool.streaming;
8526
+ if (partialInput) {
8527
+ const result = partialInput(partial, acc.lastEmittedCount);
8528
+ if (!result) {
8529
+ return;
8318
8530
  }
8531
+ acc.lastEmittedCount = result.emittedCount;
8532
+ acc.started = true;
8533
+ onEvent({
8534
+ type: "tool_start",
8535
+ id,
8536
+ name,
8537
+ input: result.input,
8538
+ partial: true
8539
+ });
8540
+ return;
8319
8541
  }
8320
- )) {
8321
- if (signal?.aborted) {
8322
- break;
8542
+ const content = partial[contentField];
8543
+ if (typeof content !== "string") {
8544
+ return;
8323
8545
  }
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;
8546
+ if (!acc.started) {
8547
+ acc.started = true;
8548
+ onEvent({ type: "tool_start", id, name, input: partial });
8549
+ }
8550
+ if (transform) {
8551
+ const result = await transform(partial);
8552
+ if (result === null) {
8553
+ return;
8339
8554
  }
8340
- case "thinking":
8341
- if (event.text === "") {
8342
- thinkingBlockStartTimes.push(event.ts);
8343
- textBlockOpen = false;
8555
+ onEvent({ type: "tool_input_delta", id, name, result });
8556
+ } else {
8557
+ onEvent({ type: "tool_input_delta", id, name, result: content });
8558
+ }
8559
+ }
8560
+ try {
8561
+ for await (const event of streamChatWithRetry(
8562
+ {
8563
+ ...apiConfig,
8564
+ model: parentModel,
8565
+ requestId,
8566
+ system,
8567
+ messages: cleanMessagesForApi(state.messages),
8568
+ tools: tools2,
8569
+ signal
8570
+ },
8571
+ {
8572
+ onRetry: (attempt) => {
8573
+ onEvent({
8574
+ type: "status",
8575
+ message: `Lost connection, retrying (attempt ${attempt + 2} of 3)`
8576
+ });
8344
8577
  }
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
8578
  }
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;
8579
+ )) {
8580
+ if (signal?.aborted) {
8369
8581
  break;
8370
8582
  }
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 {
8583
+ switch (event.type) {
8584
+ case "text": {
8585
+ const lastBlock = contentBlocks.at(-1);
8586
+ if (lastBlock?.type === "text" && textBlockOpen) {
8587
+ lastBlock.text += event.text;
8588
+ } else {
8589
+ contentBlocks.push({
8590
+ type: "text",
8591
+ text: event.text,
8592
+ startedAt: event.ts
8593
+ });
8594
+ }
8595
+ textBlockOpen = true;
8596
+ onEvent({ type: "text", text: event.text });
8597
+ break;
8378
8598
  }
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
8599
+ case "thinking":
8600
+ if (event.text === "") {
8601
+ thinkingBlockStartTimes.push(event.ts);
8602
+ textBlockOpen = false;
8396
8603
  }
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",
8604
+ onEvent({ type: "thinking", text: event.text });
8605
+ break;
8606
+ case "thinking_complete": {
8607
+ const startedAt = thinkingBlockStartTimes[thinkingCompleteCount] ?? event.ts;
8608
+ contentBlocks.push({
8609
+ type: "thinking",
8610
+ thinking: event.thinking,
8611
+ signature: event.signature,
8612
+ startedAt,
8613
+ completedAt: event.ts
8614
+ });
8615
+ thinkingCompleteCount++;
8616
+ lastThinkingRelatedStartedAt = startedAt;
8617
+ break;
8618
+ }
8619
+ case "redacted_thinking_complete": {
8620
+ const startedAt = lastThinkingRelatedStartedAt !== void 0 ? lastThinkingRelatedStartedAt + 1 : event.ts;
8621
+ contentBlocks.push({
8622
+ type: "redacted_thinking",
8623
+ data: event.data,
8624
+ startedAt,
8625
+ completedAt: event.ts
8626
+ });
8627
+ lastThinkingRelatedStartedAt = startedAt;
8628
+ break;
8629
+ }
8630
+ case "tool_input_delta": {
8631
+ const acc = getOrCreateAccumulator2(event.id, event.name);
8632
+ acc.json += event.delta;
8633
+ try {
8634
+ const partial = parsePartialJson(acc.json);
8635
+ await handlePartialInput(acc, event.id, event.name, partial);
8636
+ } catch {
8637
+ }
8638
+ break;
8639
+ }
8640
+ case "tool_input_args": {
8641
+ const acc = getOrCreateAccumulator2(event.id, event.name);
8642
+ await handlePartialInput(acc, event.id, event.name, event.args);
8643
+ break;
8644
+ }
8645
+ case "tool_use": {
8646
+ const tool = getToolByName(event.name);
8647
+ contentBlocks.push({
8648
+ type: "tool",
8409
8649
  id: event.id,
8410
8650
  name: event.name,
8411
- input: event.input
8651
+ input: event.input,
8652
+ startedAt: event.ts,
8653
+ ...(event.input.background || tool?.backgroundOnly) && {
8654
+ background: true
8655
+ }
8656
+ });
8657
+ const acc = toolInputAccumulators.get(event.id);
8658
+ const wasStreamed = acc?.started ?? false;
8659
+ const isInputStreaming = !!tool?.streaming?.partialInput;
8660
+ log13.info("Tool received", {
8661
+ requestId,
8662
+ toolCallId: event.id,
8663
+ name: event.name
8412
8664
  });
8665
+ if (!wasStreamed || isInputStreaming) {
8666
+ onEvent({
8667
+ type: "tool_start",
8668
+ id: event.id,
8669
+ name: event.name,
8670
+ input: event.input,
8671
+ // Mirror the content-block stamp above so the live event
8672
+ // matches what a history reload shows — without it the
8673
+ // frontend renders background rows as already complete.
8674
+ ...isBackgroundCall({
8675
+ name: event.name,
8676
+ input: event.input
8677
+ }) && { background: true }
8678
+ });
8679
+ }
8680
+ break;
8413
8681
  }
8414
- break;
8682
+ case "done":
8683
+ stopReason = event.stopReason;
8684
+ turnProviderMetadata = event.providerMetadata;
8685
+ turnModelId = event.modelId;
8686
+ turnLlmCalls++;
8687
+ lastCallInputTokens = event.usage.inputTokens;
8688
+ lastCallCacheCreation = event.usage.cacheCreationTokens ?? 0;
8689
+ lastCallCacheRead = event.usage.cacheReadTokens ?? 0;
8690
+ turnInputTokens += lastCallInputTokens;
8691
+ turnOutputTokens += event.usage.outputTokens;
8692
+ turnCacheCreation += lastCallCacheCreation;
8693
+ turnCacheRead += lastCallCacheRead;
8694
+ recordUsage({
8695
+ ts: Date.now(),
8696
+ requestId,
8697
+ agentName: "parent",
8698
+ modelId: event.modelId,
8699
+ inputTokens: event.usage.inputTokens,
8700
+ outputTokens: event.usage.outputTokens,
8701
+ cacheCreationTokens: event.usage.cacheCreationTokens,
8702
+ cacheReadTokens: event.usage.cacheReadTokens,
8703
+ cost: nanoToDollars(event.cost),
8704
+ billingEvents: event.billingEvents,
8705
+ durationMs: Date.now() - iterStart,
8706
+ toolNames: contentBlocks.filter(
8707
+ (b) => b.type === "tool"
8708
+ ).map((b) => b.name)
8709
+ });
8710
+ break;
8711
+ case "error":
8712
+ statusWatcher.stop();
8713
+ onEvent({
8714
+ type: "error",
8715
+ error: friendlyError(event.error),
8716
+ ...event.code ? { code: event.code } : {}
8717
+ });
8718
+ return;
8415
8719
  }
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;
8720
+ }
8721
+ } catch (err) {
8722
+ if (signal?.aborted) {
8723
+ } else {
8724
+ throw err;
8452
8725
  }
8453
8726
  }
8454
- } catch (err) {
8455
8727
  if (signal?.aborted) {
8456
- } else {
8457
- throw err;
8728
+ statusWatcher.stop();
8729
+ if (contentBlocks.length > 0) {
8730
+ contentBlocks.push({
8731
+ type: "text",
8732
+ text: "\n\n(cancelled)",
8733
+ startedAt: Date.now()
8734
+ });
8735
+ state.messages.push({
8736
+ role: "assistant",
8737
+ content: [...contentBlocks].sort(
8738
+ (a, b) => a.startedAt - b.startedAt
8739
+ ),
8740
+ usage: {
8741
+ inputTokens: turnInputTokens,
8742
+ outputTokens: turnOutputTokens,
8743
+ cacheCreationTokens: turnCacheCreation || void 0,
8744
+ cacheReadTokens: turnCacheRead || void 0,
8745
+ llmCalls: turnLlmCalls
8746
+ },
8747
+ ...turnProviderMetadata && {
8748
+ providerMetadata: turnProviderMetadata
8749
+ },
8750
+ model: turnModelId ?? parentModel,
8751
+ ...modelOverride && { modelOverride }
8752
+ });
8753
+ }
8754
+ onEvent({ type: "turn_cancelled" });
8755
+ saveSession(state);
8756
+ return;
8458
8757
  }
8459
- }
8460
- if (signal?.aborted) {
8461
- statusWatcher.stop();
8462
8758
  if (contentBlocks.length > 0) {
8463
- contentBlocks.push({
8464
- type: "text",
8465
- text: "\n\n(cancelled)",
8466
- startedAt: Date.now()
8467
- });
8468
8759
  state.messages.push({
8469
8760
  role: "assistant",
8470
8761
  content: [...contentBlocks].sort((a, b) => a.startedAt - b.startedAt),
@@ -8482,229 +8773,212 @@ async function runTurn(params) {
8482
8773
  ...modelOverride && { modelOverride }
8483
8774
  });
8484
8775
  }
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
- }
8776
+ const toolCalls = getToolCalls(contentBlocks);
8777
+ if (stopReason !== "tool_use" || toolCalls.length === 0) {
8778
+ statusWatcher.stop();
8779
+ saveSession(state);
8780
+ onEvent({
8781
+ type: "turn_done",
8782
+ stats: {
8783
+ inputTokens: turnInputTokens,
8784
+ outputTokens: turnOutputTokens,
8785
+ cacheCreationTokens: turnCacheCreation || void 0,
8786
+ cacheReadTokens: turnCacheRead || void 0,
8787
+ llmCalls: turnLlmCalls,
8788
+ lastCallInputTokens,
8789
+ lastCallCacheCreation: lastCallCacheCreation || void 0,
8790
+ lastCallCacheRead: lastCallCacheRead || void 0
8791
+ }
8792
+ });
8793
+ return;
8537
8794
  }
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 };
8795
+ log13.info("Tools executing", {
8796
+ requestId,
8797
+ count: toolCalls.length,
8798
+ tools: toolCalls.map((tc) => tc.name)
8799
+ });
8800
+ currentToolNames = toolCalls.filter((tc) => !STATUS_EXCLUDED_TOOLS.has(tc.name)).map((tc) => tc.name).join(", ");
8801
+ const wrappedOnEvent = (e) => {
8802
+ if ("parentToolId" in e && e.parentToolId) {
8803
+ if (e.type === "text") {
8804
+ subAgentText = e.text;
8805
+ } else if (e.type === "tool_start") {
8806
+ subAgentText = `Using ${e.name}`;
8807
+ }
8545
8808
  }
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;
8809
+ onEvent(e);
8810
+ };
8811
+ const subAgentMessages = /* @__PURE__ */ new Map();
8812
+ const results = await Promise.all(
8813
+ toolCalls.map(async (tc) => {
8814
+ if (signal?.aborted) {
8815
+ return { id: tc.id, result: USER_CANCELLED_RESULT, isError: true };
8556
8816
  }
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 {
8817
+ const toolStart = Date.now();
8818
+ let settle;
8819
+ const resultPromise = new Promise((res) => {
8820
+ settle = (result, isError) => res({ id: tc.id, result, isError });
8821
+ });
8822
+ let toolAbort = new AbortController();
8823
+ let settled = false;
8824
+ const safeSettle = (result, isError) => {
8825
+ if (settled) {
8826
+ return;
8827
+ }
8828
+ settled = true;
8829
+ signal?.removeEventListener("abort", cascadeAbort);
8830
+ settle(result, isError);
8831
+ };
8832
+ const cascadeAbort = () => {
8833
+ toolAbort.abort();
8834
+ safeSettle(USER_CANCELLED_RESULT, true);
8835
+ };
8836
+ signal?.addEventListener("abort", cascadeAbort, { once: true });
8837
+ const run = async (input) => {
8838
+ try {
8839
+ let result;
8840
+ if (EXTERNAL_TOOLS.has(tc.name) && resolveExternalTool) {
8841
+ saveSession(state);
8842
+ log13.info("Waiting for external tool result", {
8843
+ requestId,
8844
+ toolCallId: tc.id,
8845
+ name: tc.name
8846
+ });
8847
+ const blocksUser = USER_BLOCKING_EXTERNAL_TOOLS.has(tc.name);
8583
8848
  if (blocksUser) {
8584
- statusWatcher.resume();
8849
+ statusWatcher.pause();
8585
8850
  }
8851
+ try {
8852
+ result = await resolveExternalTool(tc.id, tc.name, input);
8853
+ } finally {
8854
+ if (blocksUser) {
8855
+ statusWatcher.resume();
8856
+ }
8857
+ }
8858
+ } else {
8859
+ result = await executeTool(tc.name, input, {
8860
+ apiConfig,
8861
+ model,
8862
+ models: state.models,
8863
+ signal: toolAbort.signal,
8864
+ onEvent: wrappedOnEvent,
8865
+ resolveExternalTool,
8866
+ toolCallId: tc.id,
8867
+ requestId,
8868
+ onboardingState,
8869
+ subAgentMessages,
8870
+ conversationMessages: state.messages,
8871
+ toolRegistry,
8872
+ onBackgroundComplete,
8873
+ onLog: (line) => wrappedOnEvent({
8874
+ type: "tool_input_delta",
8875
+ id: tc.id,
8876
+ name: tc.name,
8877
+ result: line
8878
+ })
8879
+ });
8586
8880
  }
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
- })
8881
+ safeSettle(capToolResult(result), result.startsWith("Error"));
8882
+ } catch (err) {
8883
+ safeSettle(`Error: ${err.message}`, true);
8884
+ }
8885
+ };
8886
+ const entry = {
8887
+ id: tc.id,
8888
+ name: tc.name,
8889
+ input: tc.input,
8890
+ abortController: toolAbort,
8891
+ startedAt: toolStart,
8892
+ settle: safeSettle,
8893
+ rerun: (newInput) => {
8894
+ settled = false;
8895
+ toolAbort = new AbortController();
8896
+ signal?.addEventListener("abort", () => toolAbort.abort(), {
8897
+ once: true
8608
8898
  });
8899
+ entry.abortController = toolAbort;
8900
+ entry.input = newInput;
8901
+ run(newInput);
8609
8902
  }
8610
- safeSettle(capToolResult(result), result.startsWith("Error"));
8611
- } catch (err) {
8612
- safeSettle(`Error: ${err.message}`, true);
8903
+ };
8904
+ toolRegistry?.register(entry);
8905
+ run(tc.input);
8906
+ const r = await resultPromise;
8907
+ if (!isBackgroundCall(tc)) {
8908
+ toolRegistry?.unregister(tc.id);
8613
8909
  }
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);
8910
+ log13.info("Tool completed", {
8911
+ requestId,
8912
+ toolCallId: tc.id,
8913
+ name: tc.name,
8914
+ durationMs: Date.now() - toolStart,
8915
+ isError: r.isError
8916
+ });
8917
+ onEvent({
8918
+ type: "tool_done",
8919
+ id: tc.id,
8920
+ name: tc.name,
8921
+ result: r.result,
8922
+ isError: r.isError
8923
+ });
8924
+ if (!r.isError && BRAND_TRIGGERING_TOOLS.has(tc.name)) {
8925
+ triggerBrandExtraction(
8926
+ apiConfig,
8927
+ resolveModel("brandExtractor", state.models, model)
8928
+ );
8631
8929
  }
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
8930
+ return r;
8931
+ })
8666
8932
  );
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;
8933
+ for (const r of results) {
8934
+ const block = contentBlocks.find(
8935
+ (b) => b.type === "tool" && b.id === r.id
8936
+ );
8937
+ if (block?.type === "tool") {
8938
+ block.result = r.result;
8939
+ block.isError = r.isError;
8940
+ block.completedAt = Date.now();
8941
+ const msgs = subAgentMessages.get(r.id);
8942
+ if (msgs) {
8943
+ block.subAgentMessages = msgs;
8944
+ }
8674
8945
  }
8675
8946
  }
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
8947
+ const lastNonExcluded = toolCalls.filter(
8948
+ (tc) => !STATUS_EXCLUDED_TOOLS.has(tc.name)
8694
8949
  );
8695
- if (injected.length > 0) {
8696
- for (const entry of injected) {
8697
- appendEntry(entry);
8698
- keptEntries.push(entry);
8950
+ lastCompletedTools = lastNonExcluded.map((tc) => tc.name).join(", ");
8951
+ lastCompletedInput = JSON.stringify(lastNonExcluded.at(-1)?.input ?? {});
8952
+ lastCompletedResult = results.at(-1)?.result ?? "";
8953
+ for (const r of results) {
8954
+ state.messages.push({
8955
+ role: "user",
8956
+ content: r.result,
8957
+ toolCallId: r.id,
8958
+ isToolError: r.isError
8959
+ });
8960
+ }
8961
+ if (takeSteering && !signal?.aborted) {
8962
+ const injected = (await takeSteering()).filter(
8963
+ (e) => e.text.trim().length > 0 || (e.attachments?.length ?? 0) > 0
8964
+ );
8965
+ if (injected.length > 0) {
8966
+ for (const entry of injected) {
8967
+ appendEntry(entry);
8968
+ keptEntries.push(entry);
8969
+ }
8970
+ saveSession(state);
8699
8971
  }
8972
+ }
8973
+ if (signal?.aborted) {
8974
+ statusWatcher.stop();
8975
+ onEvent({ type: "turn_cancelled" });
8700
8976
  saveSession(state);
8977
+ return;
8701
8978
  }
8702
8979
  }
8703
- if (signal?.aborted) {
8704
- onEvent({ type: "turn_cancelled" });
8705
- saveSession(state);
8706
- return;
8707
- }
8980
+ } finally {
8981
+ statusWatcher.stop();
8708
8982
  }
8709
8983
  }
8710
8984
  var log13, BRAND_TRIGGERING_TOOLS, EXTERNAL_TOOLS, USER_BLOCKING_EXTERNAL_TOOLS;
@@ -8718,6 +8992,7 @@ var init_agent = __esm({
8718
8992
  init_usageLedger();
8719
8993
  init_parsePartialJson();
8720
8994
  init_statusWatcher();
8995
+ init_resolve();
8721
8996
  init_errors();
8722
8997
  init_cleanMessages();
8723
8998
  init_sentinel();
@@ -9393,70 +9668,6 @@ var init_messageQueue = __esm({
9393
9668
  }
9394
9669
  });
9395
9670
 
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
9671
  // src/headless/index.ts
9461
9672
  var headless_exports = {};
9462
9673
  __export(headless_exports, {
@@ -9538,6 +9749,13 @@ var init_headless = __esm({
9538
9749
  earlyResults = /* @__PURE__ */ new Map();
9539
9750
  // Tool block updates from background completions (separate from the message queue)
9540
9751
  pendingBlockUpdates = [];
9752
+ /**
9753
+ * Id of the UI-only tool block synthesized for the in-flight user/gate
9754
+ * compaction (see the compaction listener). Model-invoked compactions have
9755
+ * a real block and never set this. Single slot — triggerCompaction is
9756
+ * single-flight.
9757
+ */
9758
+ syntheticCompactionId = null;
9541
9759
  // Tool lifecycle management — shared across all nesting depths
9542
9760
  toolRegistry = new ToolRegistry();
9543
9761
  // IO — accumulates stdin bytes between newline boundaries
@@ -9569,6 +9787,7 @@ var init_headless = __esm({
9569
9787
  this.emit("queue_changed", { queuedMessages: this.queue.snapshot() });
9570
9788
  });
9571
9789
  this.passivePen = loadPassiveResults();
9790
+ this.persistStats();
9572
9791
  if (resumed) {
9573
9792
  this.emit("session_restored", {
9574
9793
  messageCount: this.state.messages.length,
@@ -9591,15 +9810,61 @@ var init_headless = __esm({
9591
9810
  );
9592
9811
  this.sessionStats.compactionInProgress = true;
9593
9812
  this.persistStats();
9813
+ if (!event.toolCallId && (event.origin === "user" || event.origin === "gate")) {
9814
+ const id = `compact_${Date.now()}`;
9815
+ this.syntheticCompactionId = id;
9816
+ this.state.messages.push({
9817
+ role: "assistant",
9818
+ content: [
9819
+ {
9820
+ type: "tool",
9821
+ id,
9822
+ name: "compactConversation",
9823
+ input: {},
9824
+ startedAt: Date.now(),
9825
+ background: true,
9826
+ uiOnly: true,
9827
+ result: "Compaction started in the background."
9828
+ }
9829
+ ]
9830
+ });
9831
+ saveSession(this.state);
9832
+ this.onEvent({
9833
+ type: "tool_start",
9834
+ id,
9835
+ name: "compactConversation",
9836
+ input: {},
9837
+ background: true
9838
+ });
9839
+ this.onEvent({
9840
+ type: "tool_done",
9841
+ id,
9842
+ name: "compactConversation",
9843
+ result: "Compaction started in the background.",
9844
+ isError: false
9845
+ });
9846
+ }
9594
9847
  } else {
9595
9848
  const data = event.error ? { error: event.error } : {};
9596
9849
  this.emit("compaction_complete", data, event.requestId);
9850
+ if (this.syntheticCompactionId) {
9851
+ const id = this.syntheticCompactionId;
9852
+ this.syntheticCompactionId = null;
9853
+ this.onBackgroundComplete(
9854
+ id,
9855
+ "compactConversation",
9856
+ event.error ? `Error: ${event.error}` : formatSummariesResult(event.summaries ?? [])
9857
+ );
9858
+ }
9597
9859
  this.sessionStats.compactionInProgress = false;
9598
9860
  if (!event.error) {
9599
9861
  this.sessionStats.lastContextSize = 0;
9600
9862
  }
9601
9863
  this.sessionStats.messageCount = this.state.messages.length;
9602
9864
  this.persistStats();
9865
+ if (!this.running) {
9866
+ this.kickDrain();
9867
+ }
9603
9868
  }
9604
9869
  });
9605
9870
  process.stdin.setEncoding("utf-8");
@@ -9738,7 +10003,8 @@ var init_headless = __esm({
9738
10003
  await triggerCompaction(this.state, this.config, {
9739
10004
  blocking: true,
9740
10005
  requestId,
9741
- model: this.opts.model
10006
+ model: this.opts.model,
10007
+ origin: "gate"
9742
10008
  });
9743
10009
  applyPendingSummaries(this.state);
9744
10010
  } catch {
@@ -9767,6 +10033,12 @@ var init_headless = __esm({
9767
10033
  }
9768
10034
  return;
9769
10035
  }
10036
+ if (notify === "silent") {
10037
+ if (!this.running) {
10038
+ this.applyPendingBlockUpdates();
10039
+ }
10040
+ return;
10041
+ }
9770
10042
  this.queue.push({
9771
10043
  command: {
9772
10044
  action: "message",
@@ -10148,6 +10420,12 @@ var init_headless = __esm({
10148
10420
  this.completedEmitted = false;
10149
10421
  this.lastCompleted = null;
10150
10422
  this.turnStart = Date.now();
10423
+ const inflight2 = getInflightCompaction();
10424
+ if (inflight2) {
10425
+ await inflight2.catch(() => {
10426
+ });
10427
+ }
10428
+ applyPendingSummaries(this.state);
10151
10429
  if (this.passivePen.length > 0) {
10152
10430
  const swept = this.passivePen.splice(0);
10153
10431
  this.persistStats();
@@ -10237,7 +10515,7 @@ var init_headless = __esm({
10237
10515
  this.applyPendingBlockUpdates();
10238
10516
  }
10239
10517
  async handleMessage(parsed, requestId) {
10240
- if (this.running) {
10518
+ if (this.running || getInflightCompaction()) {
10241
10519
  const command = { ...parsed };
10242
10520
  if (requestId && command.requestId === void 0) {
10243
10521
  command.requestId = requestId;
@@ -10247,6 +10525,9 @@ var init_headless = __esm({
10247
10525
  source: "user",
10248
10526
  enqueuedAt: Date.now()
10249
10527
  });
10528
+ if (!this.running && !getInflightCompaction()) {
10529
+ this.kickDrain();
10530
+ }
10250
10531
  return;
10251
10532
  }
10252
10533
  this.running = true;
@@ -10254,6 +10535,7 @@ var init_headless = __esm({
10254
10535
  await this.runSingleTurn(parsed, requestId);
10255
10536
  await this.drainQueueLoop();
10256
10537
  } finally {
10538
+ this.currentAbort?.abort();
10257
10539
  this.currentAbort = null;
10258
10540
  this.currentRequestId = void 0;
10259
10541
  this.running = false;
@@ -10285,6 +10567,18 @@ var init_headless = __esm({
10285
10567
  async drainQueueLoop() {
10286
10568
  while (this.queue.length > 0) {
10287
10569
  const head = this.queue.peek();
10570
+ if (head.command.action === "compact") {
10571
+ this.queue.shift();
10572
+ await triggerCompaction(this.state, this.config, {
10573
+ blocking: true,
10574
+ requestId: head.command.requestId,
10575
+ model: this.opts.model,
10576
+ origin: "user"
10577
+ }).catch(() => {
10578
+ });
10579
+ applyPendingSummaries(this.state);
10580
+ continue;
10581
+ }
10288
10582
  if (head.source === "chain") {
10289
10583
  const item = this.queue.shift();
10290
10584
  const rid = item.command.requestId ?? `chain-${Date.now()}`;
@@ -10329,6 +10623,7 @@ var init_headless = __esm({
10329
10623
  try {
10330
10624
  await this.drainQueueLoop();
10331
10625
  } finally {
10626
+ this.currentAbort?.abort();
10332
10627
  this.currentAbort = null;
10333
10628
  this.currentRequestId = void 0;
10334
10629
  this.running = false;
@@ -10575,11 +10870,31 @@ var init_headless = __esm({
10575
10870
  return;
10576
10871
  }
10577
10872
  if (action === "compact") {
10873
+ if (this.running) {
10874
+ const alreadyQueued = this.queue.snapshot().some((it) => it.command.action === "compact");
10875
+ if (!alreadyQueued) {
10876
+ this.queue.push({
10877
+ command: {
10878
+ action: "compact",
10879
+ // Display label for the queue card (matches the automated
10880
+ // action idiom); never resolved as an action — drainQueueLoop
10881
+ // routes on command.action before any sentinel handling.
10882
+ text: sentinel("compact"),
10883
+ requestId
10884
+ },
10885
+ source: "user",
10886
+ enqueuedAt: Date.now()
10887
+ });
10888
+ }
10889
+ this.emit("completed", { success: true, queued: true }, requestId);
10890
+ return;
10891
+ }
10578
10892
  try {
10579
10893
  await triggerCompaction(this.state, this.config, {
10580
10894
  blocking: false,
10581
10895
  requestId,
10582
- model: this.opts.model
10896
+ model: this.opts.model,
10897
+ origin: "user"
10583
10898
  });
10584
10899
  if (!this.running) {
10585
10900
  applyPendingSummaries(this.state);