@mindstudio-ai/remy 0.1.312 → 0.1.314

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/headless.js CHANGED
@@ -434,6 +434,13 @@ var MODEL_SURFACES = {
434
434
  modelType: "text",
435
435
  userPickable: true
436
436
  },
437
+ research: {
438
+ default: "claude-5-sonnet",
439
+ label: "Research Agent",
440
+ description: "Researches using the web and reports back with citations.",
441
+ modelType: "text",
442
+ userPickable: true
443
+ },
437
444
  copyEditor: {
438
445
  default: "claude-5-sonnet",
439
446
  label: "Copy Agent",
@@ -486,37 +493,59 @@ var MODEL_SURFACES = {
486
493
  userPickable: false
487
494
  }
488
495
  };
496
+ var DEFAULT_SUGGEST_COMPACT_AT = 3e5;
497
+ var TEXT_MODELS = {
498
+ // Anthropic 1M-context, flat-priced.
499
+ "claude-5-opus": { forceCompactAt: 85e4 },
500
+ "claude-4-8-opus": { forceCompactAt: 85e4 },
501
+ "claude-4-7-opus": { forceCompactAt: 85e4 },
502
+ // Anthropic 1M-context with 2x long-context pricing above 200K input.
503
+ "claude-4-6-opus": { forceCompactAt: 85e4, suggestCompactAt: 18e4 },
504
+ "claude-4-6-sonnet": { forceCompactAt: 85e4, suggestCompactAt: 18e4 },
505
+ "claude-fable-5": { forceCompactAt: 85e4 },
506
+ "claude-fable-5-1": { forceCompactAt: 85e4 },
507
+ "claude-5-sonnet": { forceCompactAt: 85e4 },
508
+ // OpenAI gpt-5.5/5.6: ~1M window, but the usable input ceiling under
509
+ // `truncation: 'auto'` is ~794K (output + reasoning reserve), and all
510
+ // rates double above 272K input.
511
+ "gpt-5.5": { forceCompactAt: 6e5, suggestCompactAt: 25e4 },
512
+ "gpt-5.6-sol": { forceCompactAt: 6e5, suggestCompactAt: 25e4 },
513
+ "gpt-5.6-terra": { forceCompactAt: 6e5, suggestCompactAt: 25e4 },
514
+ "gpt-5.6-luna": { forceCompactAt: 6e5, suggestCompactAt: 25e4 },
515
+ // Google ~1M-context; only 3.1-pro is tiered (higher rates above 200K).
516
+ "gemini-3-pro": { forceCompactAt: 85e4 },
517
+ "gemini-3.1-pro": { forceCompactAt: 85e4, suggestCompactAt: 18e4 },
518
+ "gemini-3-flash": { forceCompactAt: 85e4 },
519
+ "gemini-3.5-flash": { forceCompactAt: 85e4 },
520
+ "gemini-3.7-flash": { forceCompactAt: 85e4 },
521
+ // 256K window; its 200K pricing tier sits above the gate, so no nudge.
522
+ "grok-build-0.1": { forceCompactAt: 18e4 },
523
+ "grok-4.5": { forceCompactAt: 4e5 },
524
+ // 500K window
525
+ "grok-4.6": { forceCompactAt: 4e5 },
526
+ // 500K window
527
+ "glm-5.2": { forceCompactAt: 85e4 },
528
+ "muse-spark-1.1": { forceCompactAt: 85e4 },
529
+ "kimi-k2-7-code": { forceCompactAt: 2e5 },
530
+ // 262K window
531
+ "kimi-k3": { forceCompactAt: 85e4 },
532
+ "deepseek-v4-flash-0731": { forceCompactAt: 85e4 },
533
+ "qwen3.8-2.4t-a95b-deepinfra": { forceCompactAt: 2e5 },
534
+ // 262K window
535
+ "qwen3.8-27b-deepinfra": { forceCompactAt: 2e5 },
536
+ // 262K window
537
+ "minimax-m3": { forceCompactAt: 42e4 }
538
+ // 524K window
539
+ };
540
+ var DEFAULT_CONTEXT_LIMITS = { forceCompactAt: 85e4 };
541
+ function getContextLimits(modelId) {
542
+ return TEXT_MODELS[modelId] ?? DEFAULT_CONTEXT_LIMITS;
543
+ }
544
+ function getSuggestCompactAt(modelId) {
545
+ return getContextLimits(modelId).suggestCompactAt ?? DEFAULT_SUGGEST_COMPACT_AT;
546
+ }
489
547
  var ALLOWED_MODELS_BY_TYPE = {
490
- text: [
491
- "claude-5-opus",
492
- "claude-4-8-opus",
493
- "claude-4-7-opus",
494
- "claude-4-6-opus",
495
- "claude-4-6-sonnet",
496
- "claude-fable-5",
497
- "claude-fable-5-1",
498
- "claude-5-sonnet",
499
- "gpt-5.5",
500
- "gpt-5.6-sol",
501
- "gpt-5.6-terra",
502
- "gpt-5.6-luna",
503
- "gemini-3-pro",
504
- "gemini-3.1-pro",
505
- "gemini-3-flash",
506
- "gemini-3.5-flash",
507
- "gemini-3.7-flash",
508
- "grok-build-0.1",
509
- "grok-4.5",
510
- "grok-4.6",
511
- "glm-5.2",
512
- "muse-spark-1.1",
513
- "kimi-k2-7-code",
514
- "kimi-k3",
515
- "deepseek-v4-flash-0731",
516
- "qwen3.8-2.4t-a95b-deepinfra",
517
- "qwen3.8-27b-deepinfra",
518
- "minimax-m3"
519
- ]
548
+ text: Object.keys(TEXT_MODELS)
520
549
  // vision: undefined — unconstrained
521
550
  // image_generation: undefined — unconstrained
522
551
  };
@@ -559,6 +588,11 @@ function getEffectiveModelSurfaces() {
559
588
  function resolveModel(surfaceId, models, fallback) {
560
589
  return models?.[surfaceId] ?? fallback ?? orgDefaultModels[surfaceId] ?? MODEL_SURFACES[surfaceId].default;
561
590
  }
591
+ function resolveParentModel(models, fallback, buildModel) {
592
+ const override = buildModel ? filterModelPicks({ parent: buildModel }).parent : void 0;
593
+ const baseline = resolveModel("parent", models, fallback);
594
+ return { baseline, effective: override ?? baseline };
595
+ }
562
596
 
563
597
  // src/orgContext.ts
564
598
  var log3 = createLogger("orgContext");
@@ -1857,158 +1891,6 @@ var askMindStudioSdkTool = {
1857
1891
  }
1858
1892
  };
1859
1893
 
1860
- // src/usageLedger.ts
1861
- import fs12 from "fs";
1862
- var LEDGER_FILE = ".logs/usage.ndjson";
1863
- var fd = null;
1864
- function nanoToDollars(nano) {
1865
- return typeof nano === "number" ? nano / 1e9 : void 0;
1866
- }
1867
- function recordUsage(entry) {
1868
- try {
1869
- if (fd === null) {
1870
- fs12.mkdirSync(".logs", { recursive: true });
1871
- fd = fs12.openSync(LEDGER_FILE, "a");
1872
- }
1873
- fs12.writeSync(fd, JSON.stringify(entry) + "\n");
1874
- } catch {
1875
- }
1876
- }
1877
-
1878
- // src/subagents/common/runMindstudioCli.ts
1879
- function stripFlags(args) {
1880
- const out = [];
1881
- for (let i = 0; i < args.length; i++) {
1882
- const arg = args[i];
1883
- if (arg === "--no-meta") {
1884
- continue;
1885
- }
1886
- if (arg === "--output-key") {
1887
- i++;
1888
- continue;
1889
- }
1890
- out.push(arg);
1891
- }
1892
- return out;
1893
- }
1894
- async function runMindstudioCliResult(args, options) {
1895
- const cleanArgs = stripFlags(args);
1896
- const cliAction = args[0];
1897
- const agentName = options?.caller ?? "mindstudio-cli";
1898
- const start = Date.now();
1899
- const res = await runCli("mindstudio", cleanArgs, options);
1900
- if (!res.ok) {
1901
- return { ok: false, value: formatCliResult(res) };
1902
- }
1903
- const truncNote = res.truncated ? "\n\n[output truncated]" : "";
1904
- let envelope;
1905
- try {
1906
- envelope = JSON.parse(res.output);
1907
- } catch {
1908
- return { ok: true, value: res.output + truncNote };
1909
- }
1910
- if (envelope && typeof envelope === "object" && Array.isArray(envelope.results)) {
1911
- const durationMs = Date.now() - start;
1912
- for (const step of envelope.results) {
1913
- if (typeof step?.billingCost === "number") {
1914
- recordUsage({
1915
- ts: Date.now(),
1916
- agentName,
1917
- cliAction: `${cliAction}:${step.stepType ?? "step"}`,
1918
- cost: nanoToDollars(step.billingCost),
1919
- inputTokens: 0,
1920
- outputTokens: 0,
1921
- durationMs,
1922
- toolNames: []
1923
- });
1924
- }
1925
- }
1926
- return { ok: true, value: JSON.stringify(envelope.results) + truncNote };
1927
- }
1928
- if (typeof envelope?.$billingCost === "number") {
1929
- recordUsage({
1930
- ts: Date.now(),
1931
- agentName,
1932
- cliAction,
1933
- cost: nanoToDollars(envelope.$billingCost),
1934
- billingEvents: envelope.$billingEvents,
1935
- // CLI billing isn't expressed as input/output tokens for most actions
1936
- // (image gen is per-image, scrape per-page, etc). `numUnits` inside each
1937
- // billingEvent carries the per-event unit count.
1938
- inputTokens: 0,
1939
- outputTokens: 0,
1940
- durationMs: Date.now() - start,
1941
- toolNames: []
1942
- });
1943
- }
1944
- if (options?.outputKey) {
1945
- const v = envelope?.[options.outputKey];
1946
- if (v === void 0 || v === null) {
1947
- return { ok: false, value: JSON.stringify(stripDollarKeys(envelope)) };
1948
- }
1949
- const value = typeof v === "string" ? v : JSON.stringify(v);
1950
- return { ok: true, value: value + truncNote };
1951
- }
1952
- return {
1953
- ok: true,
1954
- value: JSON.stringify(stripDollarKeys(envelope)) + truncNote
1955
- };
1956
- }
1957
- async function runMindstudioCli(args, options) {
1958
- return (await runMindstudioCliResult(args, options)).value;
1959
- }
1960
- function stripDollarKeys(envelope) {
1961
- if (!envelope || typeof envelope !== "object" || Array.isArray(envelope)) {
1962
- return envelope;
1963
- }
1964
- const out = {};
1965
- for (const [k, v] of Object.entries(envelope)) {
1966
- if (!k.startsWith("$")) {
1967
- out[k] = v;
1968
- }
1969
- }
1970
- return out;
1971
- }
1972
-
1973
- // src/tools/common/searchGoogle.ts
1974
- var FETCH_TOP_N = 5;
1975
- var searchGoogleTool = {
1976
- definition: {
1977
- name: "searchGoogle",
1978
- description: "Search Google and return results. Use for research, finding documentation, looking up APIs, or any task where web search would help. The top results come back with their page content already included, so read those before reaching for scrapeWebUrl \u2014 you only need that for URLs this did not return, or for a result further down the list.",
1979
- inputSchema: {
1980
- type: "object",
1981
- properties: {
1982
- query: {
1983
- type: "string",
1984
- description: "The search query."
1985
- }
1986
- },
1987
- required: ["query"]
1988
- }
1989
- },
1990
- async execute(input, context) {
1991
- const query = input.query;
1992
- return runMindstudioCli(
1993
- [
1994
- "search-google",
1995
- "--query",
1996
- query,
1997
- "--export-type",
1998
- "json",
1999
- "--fetch-top-n",
2000
- String(FETCH_TOP_N)
2001
- ],
2002
- {
2003
- outputKey: "results",
2004
- maxBuffer: SEARCH_MAX_BUFFER,
2005
- onLog: context?.onLog,
2006
- caller: "parent"
2007
- }
2008
- );
2009
- }
2010
- };
2011
-
2012
1894
  // src/tools/common/setProjectMetadata.ts
2013
1895
  var setProjectMetadataTool = {
2014
1896
  definition: {
@@ -2133,7 +2015,7 @@ This reference lives at ${skill.path}. Re-read it with readFile if you need it a
2133
2015
  };
2134
2016
 
2135
2017
  // src/tools/code/readFile.ts
2136
- import fs13 from "fs/promises";
2018
+ import fs12 from "fs/promises";
2137
2019
  var DEFAULT_WINDOW = 500;
2138
2020
  var MAX_BYTES = 64 * 1024;
2139
2021
  function isBinary(buffer) {
@@ -2174,7 +2056,7 @@ var readFileTool = {
2174
2056
  },
2175
2057
  async execute(input) {
2176
2058
  try {
2177
- const buffer = await fs13.readFile(input.path);
2059
+ const buffer = await fs12.readFile(input.path);
2178
2060
  if (isBinary(buffer)) {
2179
2061
  const size = buffer.length;
2180
2062
  const unit = size > 1024 * 1024 ? `${(size / (1024 * 1024)).toFixed(1)}MB` : `${(size / 1024).toFixed(1)}KB`;
@@ -2246,7 +2128,7 @@ var readFileTool = {
2246
2128
  };
2247
2129
 
2248
2130
  // src/tools/code/writeFile.ts
2249
- import fs14 from "fs/promises";
2131
+ import fs13 from "fs/promises";
2250
2132
  import path6 from "path";
2251
2133
  var writeFileTool = {
2252
2134
  definition: {
@@ -2283,7 +2165,7 @@ var writeFileTool = {
2283
2165
  lastNewlineCount = newlineCount;
2284
2166
  const lastNewline = partial.content.lastIndexOf("\n");
2285
2167
  const completeContent = partial.content.substring(0, lastNewline + 1);
2286
- const oldContent = await fs14.readFile(partial.path, "utf-8").catch(() => "");
2168
+ const oldContent = await fs13.readFile(partial.path, "utf-8").catch(() => "");
2287
2169
  return `Writing ${partial.path} (${newlineCount} lines)
2288
2170
  ${unifiedDiff(partial.path, oldContent, completeContent)}`;
2289
2171
  }
@@ -2292,13 +2174,13 @@ ${unifiedDiff(partial.path, oldContent, completeContent)}`;
2292
2174
  async execute(input) {
2293
2175
  const release = await acquireFileLock(input.path);
2294
2176
  try {
2295
- await fs14.mkdir(path6.dirname(input.path), { recursive: true });
2177
+ await fs13.mkdir(path6.dirname(input.path), { recursive: true });
2296
2178
  let oldContent = null;
2297
2179
  try {
2298
- oldContent = await fs14.readFile(input.path, "utf-8");
2180
+ oldContent = await fs13.readFile(input.path, "utf-8");
2299
2181
  } catch {
2300
2182
  }
2301
- await fs14.writeFile(input.path, input.content, "utf-8");
2183
+ await fs13.writeFile(input.path, input.content, "utf-8");
2302
2184
  const lineCount = input.content.split("\n").length;
2303
2185
  const label = oldContent !== null ? "Wrote" : "Created";
2304
2186
  return `${label} ${input.path} (${lineCount} lines)
@@ -2312,7 +2194,7 @@ ${unifiedDiff(input.path, oldContent ?? "", input.content)}`;
2312
2194
  };
2313
2195
 
2314
2196
  // src/tools/code/editFile/index.ts
2315
- import fs15 from "fs/promises";
2197
+ import fs14 from "fs/promises";
2316
2198
  var editFileTool = {
2317
2199
  definition: {
2318
2200
  name: "editFile",
@@ -2343,7 +2225,7 @@ var editFileTool = {
2343
2225
  async execute(input) {
2344
2226
  const release = await acquireFileLock(input.path);
2345
2227
  try {
2346
- const content = await fs15.readFile(input.path, "utf-8");
2228
+ const content = await fs14.readFile(input.path, "utf-8");
2347
2229
  const { old_string, new_string, replace_all } = input;
2348
2230
  const occurrences = findOccurrences(content, old_string);
2349
2231
  if (replace_all) {
@@ -2359,7 +2241,7 @@ var editFileTool = {
2359
2241
  new_string
2360
2242
  );
2361
2243
  }
2362
- await fs15.writeFile(input.path, updated, "utf-8");
2244
+ await fs14.writeFile(input.path, updated, "utf-8");
2363
2245
  return `Replaced ${occurrences.length} occurrence${occurrences.length > 1 ? "s" : ""} in ${input.path}
2364
2246
  ${unifiedDiff(input.path, content, updated)}`;
2365
2247
  }
@@ -2370,7 +2252,7 @@ ${unifiedDiff(input.path, content, updated)}`;
2370
2252
  old_string.length,
2371
2253
  new_string
2372
2254
  );
2373
- await fs15.writeFile(input.path, updated, "utf-8");
2255
+ await fs14.writeFile(input.path, updated, "utf-8");
2374
2256
  return `Updated ${input.path}
2375
2257
  ${unifiedDiff(input.path, content, updated)}`;
2376
2258
  }
@@ -2386,7 +2268,7 @@ ${unifiedDiff(input.path, content, updated)}`;
2386
2268
  flex.matchedText.length,
2387
2269
  new_string
2388
2270
  );
2389
- await fs15.writeFile(input.path, updated, "utf-8");
2271
+ await fs14.writeFile(input.path, updated, "utf-8");
2390
2272
  return `Updated ${input.path} (matched with flexible whitespace at line ${flex.line})
2391
2273
  ${unifiedDiff(input.path, content, updated)}`;
2392
2274
  }
@@ -2712,12 +2594,12 @@ var globTool = {
2712
2594
  };
2713
2595
 
2714
2596
  // src/tools/code/listDir.ts
2715
- import fs16 from "fs/promises";
2597
+ import fs15 from "fs/promises";
2716
2598
  import path8 from "path";
2717
2599
  var EXCLUDE = /* @__PURE__ */ new Set([".git", "node_modules"]);
2718
2600
  var MAX_CHILDREN = 15;
2719
2601
  async function readAndSort(dirPath) {
2720
- const entries = await fs16.readdir(dirPath, { withFileTypes: true });
2602
+ const entries = await fs15.readdir(dirPath, { withFileTypes: true });
2721
2603
  return entries.filter((e) => !EXCLUDE.has(e.name)).sort((a, b) => {
2722
2604
  if (a.isDirectory() && !b.isDirectory()) {
2723
2605
  return -1;
@@ -2758,7 +2640,7 @@ function formatSize(bytes) {
2758
2640
  }
2759
2641
  async function formatFile(dirPath, name, indent) {
2760
2642
  try {
2761
- const stat4 = await fs16.stat(path8.join(dirPath, name));
2643
+ const stat4 = await fs15.stat(path8.join(dirPath, name));
2762
2644
  return `${indent}${name}${" ".repeat(Math.max(1, 30 - indent.length - name.length))}${formatSize(stat4.size)}`;
2763
2645
  } catch {
2764
2646
  return `${indent}${name}`;
@@ -3090,6 +2972,119 @@ var queryDatabaseTool = {
3090
2972
  }
3091
2973
  };
3092
2974
 
2975
+ // src/usageLedger.ts
2976
+ import fs16 from "fs";
2977
+ var LEDGER_FILE = ".logs/usage.ndjson";
2978
+ var fd = null;
2979
+ function nanoToDollars(nano) {
2980
+ return typeof nano === "number" ? nano / 1e9 : void 0;
2981
+ }
2982
+ function recordUsage(entry) {
2983
+ try {
2984
+ if (fd === null) {
2985
+ fs16.mkdirSync(".logs", { recursive: true });
2986
+ fd = fs16.openSync(LEDGER_FILE, "a");
2987
+ }
2988
+ fs16.writeSync(fd, JSON.stringify(entry) + "\n");
2989
+ } catch {
2990
+ }
2991
+ }
2992
+
2993
+ // src/subagents/common/runMindstudioCli.ts
2994
+ function stripFlags(args) {
2995
+ const out = [];
2996
+ for (let i = 0; i < args.length; i++) {
2997
+ const arg = args[i];
2998
+ if (arg === "--no-meta") {
2999
+ continue;
3000
+ }
3001
+ if (arg === "--output-key") {
3002
+ i++;
3003
+ continue;
3004
+ }
3005
+ out.push(arg);
3006
+ }
3007
+ return out;
3008
+ }
3009
+ async function runMindstudioCliResult(args, options) {
3010
+ const cleanArgs = stripFlags(args);
3011
+ const cliAction = args[0];
3012
+ const agentName = options?.caller ?? "mindstudio-cli";
3013
+ const start = Date.now();
3014
+ const res = await runCli("mindstudio", cleanArgs, options);
3015
+ if (!res.ok) {
3016
+ return { ok: false, value: formatCliResult(res) };
3017
+ }
3018
+ const truncNote = res.truncated ? "\n\n[output truncated]" : "";
3019
+ let envelope;
3020
+ try {
3021
+ envelope = JSON.parse(res.output);
3022
+ } catch {
3023
+ return { ok: true, value: res.output + truncNote };
3024
+ }
3025
+ if (envelope && typeof envelope === "object" && Array.isArray(envelope.results)) {
3026
+ const durationMs = Date.now() - start;
3027
+ for (const step of envelope.results) {
3028
+ if (typeof step?.billingCost === "number") {
3029
+ recordUsage({
3030
+ ts: Date.now(),
3031
+ agentName,
3032
+ cliAction: `${cliAction}:${step.stepType ?? "step"}`,
3033
+ cost: nanoToDollars(step.billingCost),
3034
+ inputTokens: 0,
3035
+ outputTokens: 0,
3036
+ durationMs,
3037
+ toolNames: []
3038
+ });
3039
+ }
3040
+ }
3041
+ return { ok: true, value: JSON.stringify(envelope.results) + truncNote };
3042
+ }
3043
+ if (typeof envelope?.$billingCost === "number") {
3044
+ recordUsage({
3045
+ ts: Date.now(),
3046
+ agentName,
3047
+ cliAction,
3048
+ cost: nanoToDollars(envelope.$billingCost),
3049
+ billingEvents: envelope.$billingEvents,
3050
+ // CLI billing isn't expressed as input/output tokens for most actions
3051
+ // (image gen is per-image, scrape per-page, etc). `numUnits` inside each
3052
+ // billingEvent carries the per-event unit count.
3053
+ inputTokens: 0,
3054
+ outputTokens: 0,
3055
+ durationMs: Date.now() - start,
3056
+ toolNames: []
3057
+ });
3058
+ }
3059
+ if (options?.outputKey) {
3060
+ const v = envelope?.[options.outputKey];
3061
+ if (v === void 0 || v === null) {
3062
+ return { ok: false, value: JSON.stringify(stripDollarKeys(envelope)) };
3063
+ }
3064
+ const value = typeof v === "string" ? v : JSON.stringify(v);
3065
+ return { ok: true, value: value + truncNote };
3066
+ }
3067
+ return {
3068
+ ok: true,
3069
+ value: JSON.stringify(stripDollarKeys(envelope)) + truncNote
3070
+ };
3071
+ }
3072
+ async function runMindstudioCli(args, options) {
3073
+ return (await runMindstudioCliResult(args, options)).value;
3074
+ }
3075
+ function stripDollarKeys(envelope) {
3076
+ if (!envelope || typeof envelope !== "object" || Array.isArray(envelope)) {
3077
+ return envelope;
3078
+ }
3079
+ const out = {};
3080
+ for (const [k, v] of Object.entries(envelope)) {
3081
+ if (!k.startsWith("$")) {
3082
+ out[k] = v;
3083
+ }
3084
+ }
3085
+ return out;
3086
+ }
3087
+
3093
3088
  // src/tools/_helpers/uploadImage.ts
3094
3089
  import { readFile, stat } from "fs/promises";
3095
3090
  import { basename, extname, resolve } from "path";
@@ -4568,7 +4563,7 @@ The first-party SDK (@mindstudio-ai/agent) provides access to 200+ AI models (Op
4568
4563
  ## What Remy apps are NOT good for
4569
4564
 
4570
4565
  - Native mobile apps (iOS/Android). Mobile-responsive web apps are fine.
4571
- - Fast-twitch multiplayer and live co-editing (shared cursors, 60fps sync) \u2014 client\u2192server is always a method invoke, so sub-100ms bidirectional interaction isn't a fit.
4566
+ - Real-time action games (server-authoritative simulation, guaranteed-order sync) \u2014 event delivery is at-most-once and ordering is the app's job, so there is no server tick to build one on.
4572
4567
  </platform_brief>`;
4573
4568
  }
4574
4569
 
@@ -4857,56 +4852,155 @@ var screenshotTool = {
4857
4852
  execute: (input, context) => executeScreenshot(input, context?.onLog, context)
4858
4853
  };
4859
4854
 
4860
- // src/subagents/designExpert/tools/searchGoogle.ts
4861
- var searchGoogle_exports = {};
4862
- __export(searchGoogle_exports, {
4863
- definition: () => definition,
4864
- execute: () => execute
4865
- });
4866
- var FETCH_TOP_N2 = 5;
4867
- var definition = {
4855
+ // src/subagents/research/tools.ts
4856
+ var searchGoogleDefinition = {
4868
4857
  name: "searchGoogle",
4869
- description: 'Search Google for web results. Reserch modern design trends in industries or verticals, "best [domain] apps 2026", ui patterns, or find something specific if the the user has an explicit reference. Searching for and reading case studies is a great way to get information and context about a project\'s domain. Prioritize authoritative sources like Figma and other design leaders, avoid random blog spam. The top results come back with their page content already included, so read those directly \u2014 only use `scrapeWebUrl` for a result further down the list, or for a URL this did not return.',
4858
+ description: "Search Google. Returns ~25 results (title, description, URL) \u2014 fast (~5s) when `fetchTopN` is omitted. Set `fetchTopN` (3\u20135) to also get the top results' page content inline, capped at ~4,000 chars per page \u2014 one call delivers excerpts, but it is much slower (~30\u201360s). Default pattern: fire SERP-only searches in parallel, then scrape the specific pages worth reading in full.",
4870
4859
  inputSchema: {
4871
4860
  type: "object",
4872
4861
  properties: {
4873
4862
  query: {
4874
4863
  type: "string",
4875
4864
  description: "The search query."
4865
+ },
4866
+ fetchTopN: {
4867
+ type: "number",
4868
+ description: "Also fetch page content (capped ~4,000 chars each) for this many top results. Omit for a fast SERP-only search."
4876
4869
  }
4877
4870
  },
4878
4871
  required: ["query"]
4879
4872
  }
4880
4873
  };
4881
- async function execute(input, onLog) {
4874
+ var scrapeWebUrlDefinition = {
4875
+ name: "scrapeWebUrl",
4876
+ description: "Fetch a web page as markdown. Renders JavaScript, so client-rendered pages come back complete. Returns the full page \u2014 long docs can run tens of thousands of tokens, so fetch only pages you have chosen to read, and fire independent fetches in parallel. For code hosted on GitHub/npm, prefer cloning with bash over scraping the repo page.",
4877
+ inputSchema: {
4878
+ type: "object",
4879
+ properties: {
4880
+ url: {
4881
+ type: "string",
4882
+ description: "The URL to fetch."
4883
+ }
4884
+ },
4885
+ required: ["url"]
4886
+ }
4887
+ };
4888
+ var RESEARCH_TOOLS = [
4889
+ ...COMMON_READ_TOOLS,
4890
+ searchGoogleDefinition,
4891
+ scrapeWebUrlDefinition,
4892
+ bashTool.definition
4893
+ ];
4894
+ async function executeSearchGoogle(input, onLog, caller) {
4895
+ const fetchTopN = Math.max(0, Math.round(Number(input.fetchTopN) || 0));
4882
4896
  return runMindstudioCli(
4883
4897
  [
4884
4898
  "search-google",
4885
4899
  "--query",
4886
- input.query,
4900
+ String(input.query),
4887
4901
  "--export-type",
4888
4902
  "json",
4889
4903
  "--fetch-top-n",
4890
- String(FETCH_TOP_N2)
4904
+ String(fetchTopN)
4891
4905
  ],
4892
4906
  {
4893
4907
  outputKey: "results",
4908
+ maxBuffer: SEARCH_MAX_BUFFER,
4894
4909
  onLog,
4895
- caller: "designExpert",
4896
- maxBuffer: SEARCH_MAX_BUFFER
4910
+ caller
4897
4911
  }
4898
4912
  );
4899
4913
  }
4914
+ async function executeScrapeWebUrl(input, onLog, caller) {
4915
+ return runMindstudioCli(
4916
+ [
4917
+ "scrape-url",
4918
+ "--url",
4919
+ String(input.url),
4920
+ "--page-options",
4921
+ JSON.stringify({ onlyMainContent: true })
4922
+ ],
4923
+ {
4924
+ maxBuffer: SCRAPE_MAX_BUFFER,
4925
+ onLog,
4926
+ caller
4927
+ }
4928
+ );
4929
+ }
4930
+
4931
+ // src/subagents/research/index.ts
4932
+ var BASE_PROMPT2 = readAsset("subagents/research", "prompt.md");
4933
+ async function runResearch(task, context) {
4934
+ const specIndex = loadSpecIndex();
4935
+ const parts = [BASE_PROMPT2, loadPlatformBrief()];
4936
+ parts.push("<!-- cache_breakpoint -->");
4937
+ if (specIndex) {
4938
+ parts.push(specIndex);
4939
+ }
4940
+ const system = parts.join("\n\n");
4941
+ const result = await runSubAgent({
4942
+ system,
4943
+ task,
4944
+ tools: RESEARCH_TOOLS,
4945
+ externalTools: /* @__PURE__ */ new Set(),
4946
+ executeTool: (name, toolInput, toolCallId, onLog, sams) => {
4947
+ const childCtx = toolCallId ? {
4948
+ ...deriveContext(context, toolCallId, onLog),
4949
+ subAgentMessages: sams
4950
+ } : context;
4951
+ if (name === "searchGoogle") {
4952
+ return executeSearchGoogle(toolInput, childCtx.onLog, "research");
4953
+ }
4954
+ if (name === "scrapeWebUrl") {
4955
+ return executeScrapeWebUrl(toolInput, childCtx.onLog, "research");
4956
+ }
4957
+ return executeTool(name, toolInput, childCtx);
4958
+ },
4959
+ apiConfig: context.apiConfig,
4960
+ model: resolveModel("research", context.models, context.model),
4961
+ subAgentId: "research",
4962
+ signal: context.signal,
4963
+ parentToolId: context.toolCallId,
4964
+ requestId: context.requestId,
4965
+ onEvent: context.onEvent,
4966
+ resolveExternalTool: context.resolveExternalTool,
4967
+ toolRegistry: context.toolRegistry
4968
+ });
4969
+ context.subAgentMessages?.set(context.toolCallId, result.messages);
4970
+ return result.text;
4971
+ }
4972
+ var researchTool = {
4973
+ definition: {
4974
+ name: "research",
4975
+ description: "Your researcher. Hand it a question and it searches the web, reads the pages and source code that matter, and returns a distilled, citation-backed report. Use it for both objective research like third-party APIs and services, as well as for subjective things like current trends, patterns, and approaches (from architecture to UI to even just high-level framings and ideas). Brief it neutrally: state the question and the relevant project context, not the answer you expect.",
4976
+ inputSchema: {
4977
+ type: "object",
4978
+ properties: {
4979
+ task: {
4980
+ type: "string",
4981
+ description: "What you need to find out, in natural language, plus any project context it cannot get from reading the spec. Phrase it as a question to answer, not a conclusion to confirm."
4982
+ }
4983
+ },
4984
+ required: ["task"]
4985
+ }
4986
+ },
4987
+ async execute(input, context) {
4988
+ if (!context) {
4989
+ return "Error: research requires execution context";
4990
+ }
4991
+ return runResearch(input.task, context);
4992
+ }
4993
+ };
4900
4994
 
4901
4995
  // src/subagents/designExpert/tools/scrapeWebUrl.ts
4902
4996
  var scrapeWebUrl_exports = {};
4903
4997
  __export(scrapeWebUrl_exports, {
4904
- definition: () => definition2,
4905
- execute: () => execute2
4998
+ definition: () => definition,
4999
+ execute: () => execute
4906
5000
  });
4907
- var definition2 = {
5001
+ var definition = {
4908
5002
  name: "scrapeWebUrl",
4909
- description: "Fetch the content of a web page as markdown. Use when reading sites from search results or specific things the user wants to incorporate.",
5003
+ description: "Fetch the content of a web page as markdown. Use for reading a specific URL \u2014 a site the user referenced, a brand to match, a page the researcher cited that you want in full.",
4910
5004
  inputSchema: {
4911
5005
  type: "object",
4912
5006
  properties: {
@@ -4918,7 +5012,7 @@ var definition2 = {
4918
5012
  required: ["url"]
4919
5013
  }
4920
5014
  };
4921
- async function execute2(input, onLog) {
5015
+ async function execute(input, onLog) {
4922
5016
  const pageOptions = { onlyMainContent: true };
4923
5017
  return runMindstudioCli(
4924
5018
  [
@@ -4935,8 +5029,8 @@ async function execute2(input, onLog) {
4935
5029
  // src/subagents/designExpert/tools/analyzeDesign.ts
4936
5030
  var analyzeDesign_exports = {};
4937
5031
  __export(analyzeDesign_exports, {
4938
- definition: () => definition4,
4939
- execute: () => execute4
5032
+ definition: () => definition3,
5033
+ execute: () => execute3
4940
5034
  });
4941
5035
  import { readFile as readFile2 } from "fs/promises";
4942
5036
  import { extname as extname2, join as join2 } from "path";
@@ -4945,8 +5039,8 @@ import { extname as extname2, join as join2 } from "path";
4945
5039
  var renderImage_exports = {};
4946
5040
  __export(renderImage_exports, {
4947
5041
  RENDER_ANALYZE_PROMPT: () => RENDER_ANALYZE_PROMPT,
4948
- definition: () => definition3,
4949
- execute: () => execute3
5042
+ definition: () => definition2,
5043
+ execute: () => execute2
4950
5044
  });
4951
5045
  import { mkdir, unlink, writeFile } from "fs/promises";
4952
5046
  import { tmpdir } from "os";
@@ -4955,7 +5049,7 @@ import { randomUUID } from "crypto";
4955
5049
  var MIN_DIMENSION = 16;
4956
5050
  var MAX_DIMENSION = 4096;
4957
5051
  var RENDER_ANALYZE_PROMPT = 'You are reviewing a browser-rendered graphic (composed from HTML/CSS by a designer) for fidelity. Report: whether the composition fills the full canvas or leaves unintended gaps at any edge, any clipped or overflowing text, whether custom webfonts appear to have loaded (distinctive letterforms vs generic fallback serif/sans), any misalignment or uneven spacing, any unintended scrollbars or default-styling artifacts, and \u2014 if the background is transparent \u2014 any fringing or stray opaque pixels at the edges. Then briefly describe the overall composition and how polished it looks. Be concise and practical. Respond only with your analysis as Markdown (starting with the title "Render Review") and absolutely no other text. Do not use emojis - use unicode if you need symbols.';
4958
- var definition3 = {
5052
+ var definition2 = {
4959
5053
  name: "renderImage",
4960
5054
  description: "Render a self-contained HTML document in a real browser and capture it as a hosted PNG at exact pixel dimensions, with a fidelity review included. Deterministic \u2014 exact hex colors, real loaded webfonts, precise geometry \u2014 unlike generateImages, which is an image model. Use for token-exact graphics: Open Graph share cards, wordmarks, flat/geometric icon tiles, badges, any composition where letterforms and spacing carry the design. Compose with HTML/CSS (link webfonts from CDNs \u2014 the renderer waits for them to load); inline existing SVG markup when needed, but never hand-write new SVG path data.",
4961
5055
  inputSchema: {
@@ -4989,7 +5083,7 @@ var definition3 = {
4989
5083
  required: ["html", "width", "height"]
4990
5084
  }
4991
5085
  };
4992
- async function execute3(input, onLog, context) {
5086
+ async function execute2(input, onLog, context) {
4993
5087
  const html = typeof input.html === "string" ? input.html : "";
4994
5088
  const width = Math.round(Number(input.width));
4995
5089
  const height = Math.round(Number(input.height));
@@ -5098,7 +5192,7 @@ Identify the specific design moves that make this page interesting and unique, d
5098
5192
 
5099
5193
  Respond only with your analysis as Markdown and absolutely no other text. Do not use emojis - use unicode if you need symbols.
5100
5194
  `;
5101
- var definition4 = {
5195
+ var definition3 = {
5102
5196
  name: "analyzeDesign",
5103
5197
  description: "Analyze the visual design of a website, an image (URL or file on disk), or an HTML document on disk. Websites are screenshotted first; an HTML file is rendered in a real browser first and the render is analyzed, which is how you look at a wireframe you authored. Provides static image analysis only, will not capture animations or video. With no prompt, a website or image gets a full design reference analysis (mood, color, typography, layout, distinctiveness) and a rendered HTML document gets a fidelity review (clipping, overflow, webfont loading, alignment). Provide a custom prompt to ask a specific design question instead. Use a bulleted list to ask many questions at once.",
5104
5198
  inputSchema: {
@@ -5138,7 +5232,7 @@ function splitFrontmatter(raw) {
5138
5232
  html: raw.slice(match[0].length)
5139
5233
  };
5140
5234
  }
5141
- async function execute4(input, onLog, context) {
5235
+ async function execute3(input, onLog, context) {
5142
5236
  const url = String(input.url ?? "").trim();
5143
5237
  if (!url) {
5144
5238
  return "Error: url is required.";
@@ -5246,10 +5340,10 @@ ${basePrompt}` : basePrompt;
5246
5340
  // src/subagents/designExpert/tools/analyzeImage.ts
5247
5341
  var analyzeImage_exports = {};
5248
5342
  __export(analyzeImage_exports, {
5249
- definition: () => definition5,
5250
- execute: () => execute5
5343
+ definition: () => definition4,
5344
+ execute: () => execute4
5251
5345
  });
5252
- var definition5 = {
5346
+ var definition4 = {
5253
5347
  name: "analyzeImage",
5254
5348
  description: "Analyze an image using a vision model. Provides static image analysis only, will not capture animations or video. Returns an objective description of what is visible \u2014 shapes, colors, layout, text, artifacts. Use for factual inventory of image contents, not for subjective design judgment - the vision model providing the analysis has no sense of design. You are the design expert - use the analysis tool for factual inventory, then apply your own expertise for quality and suitability assessments. Optionally provide specific questions about what you're looking for. Use a bulleted list to ask many questions at once. If you are analyzing a screenshot of the app preview, you can reuse the same screenshot URL multiple times to ask multiple questions.",
5255
5349
  inputSchema: {
@@ -5270,7 +5364,7 @@ var definition5 = {
5270
5364
  required: ["imageUrl"]
5271
5365
  }
5272
5366
  };
5273
- async function execute5(input, onLog, context) {
5367
+ async function execute4(input, onLog, context) {
5274
5368
  const prompt = buildScreenshotAnalysisPrompt({
5275
5369
  prompt: input.prompt
5276
5370
  });
@@ -5287,8 +5381,8 @@ async function execute5(input, onLog, context) {
5287
5381
  // src/subagents/designExpert/tools/images/generateImages.ts
5288
5382
  var generateImages_exports = {};
5289
5383
  __export(generateImages_exports, {
5290
- definition: () => definition6,
5291
- execute: () => execute6
5384
+ definition: () => definition5,
5385
+ execute: () => execute5
5292
5386
  });
5293
5387
 
5294
5388
  // src/subagents/designExpert/tools/images/enhancePrompt.ts
@@ -5515,7 +5609,7 @@ async function generateImageAssets(opts) {
5515
5609
  }
5516
5610
 
5517
5611
  // src/subagents/designExpert/tools/images/generateImages.ts
5518
- var definition6 = {
5612
+ var definition5 = {
5519
5613
  name: "generateImages",
5520
5614
  description: "Generate images. Returns CDN URLs with a quality analysis for each image. Produces high-quality results for everything from photorealistic images and abstract/creative visuals. Pass multiple prompts to generate in parallel. No need to analyze images separately after generating \u2014 the analysis is included.",
5521
5615
  inputSchema: {
@@ -5548,7 +5642,7 @@ var definition6 = {
5548
5642
  required: ["prompts"]
5549
5643
  }
5550
5644
  };
5551
- async function execute6(input, onLog, context) {
5645
+ async function execute5(input, onLog, context) {
5552
5646
  return generateImageAssets({
5553
5647
  prompts: input.prompts,
5554
5648
  width: input.width,
@@ -5579,10 +5673,10 @@ async function execute6(input, onLog, context) {
5579
5673
  // src/subagents/designExpert/tools/images/editImages.ts
5580
5674
  var editImages_exports = {};
5581
5675
  __export(editImages_exports, {
5582
- definition: () => definition7,
5583
- execute: () => execute7
5676
+ definition: () => definition6,
5677
+ execute: () => execute6
5584
5678
  });
5585
- var definition7 = {
5679
+ var definition6 = {
5586
5680
  name: "editImages",
5587
5681
  description: "Edit or transform existing images. Provide one or more source image URLs as reference and a prompt describing the desired edit. Use for compositing, style transfer, subject transformation, blending multiple references, or incorporating one or more references into something new. Returns CDN URLs with analysis.",
5588
5682
  inputSchema: {
@@ -5618,7 +5712,7 @@ var definition7 = {
5618
5712
  required: ["prompts", "sourceImages"]
5619
5713
  }
5620
5714
  };
5621
- async function execute7(input, onLog, context) {
5715
+ async function execute6(input, onLog, context) {
5622
5716
  return generateImageAssets({
5623
5717
  prompts: input.prompts,
5624
5718
  sourceImages: input.sourceImages,
@@ -5649,15 +5743,15 @@ async function execute7(input, onLog, context) {
5649
5743
  // src/subagents/designExpert/tools/polishCopy.ts
5650
5744
  var polishCopy_exports = {};
5651
5745
  __export(polishCopy_exports, {
5652
- definition: () => definition8,
5653
- execute: () => execute8
5746
+ definition: () => definition7,
5747
+ execute: () => execute7
5654
5748
  });
5655
5749
 
5656
5750
  // src/subagents/copyEditor/tools.ts
5657
5751
  var COPY_EDITOR_TOOLS = [...COMMON_READ_TOOLS];
5658
5752
 
5659
5753
  // src/subagents/copyEditor/index.ts
5660
- var BASE_PROMPT2 = readAsset("subagents/copyEditor", "prompt.md");
5754
+ var BASE_PROMPT3 = readAsset("subagents/copyEditor", "prompt.md");
5661
5755
  var copyEditorTool = {
5662
5756
  definition: {
5663
5757
  name: "copyEditor",
@@ -5678,7 +5772,7 @@ var copyEditorTool = {
5678
5772
  return "Error: copy editor requires execution context";
5679
5773
  }
5680
5774
  const specIndex = loadSpecIndex();
5681
- const parts = [BASE_PROMPT2];
5775
+ const parts = [BASE_PROMPT3];
5682
5776
  parts.push("<!-- cache_breakpoint -->");
5683
5777
  if (specIndex) {
5684
5778
  parts.push(specIndex);
@@ -5712,7 +5806,7 @@ var copyEditorTool = {
5712
5806
  };
5713
5807
 
5714
5808
  // src/subagents/designExpert/tools/polishCopy.ts
5715
- var definition8 = {
5809
+ var definition7 = {
5716
5810
  name: "polishCopy",
5717
5811
  description: "Hand off any user-facing copy you've written \u2014 headlines, captions, labels, body text \u2014 and get back a sharper version: better built for its audience and free of the fingerprints that make writing read as AI. It elevates how the copy communicates without inventing facts or claims you didn't give it. Give it the text plus what it's for (where it appears, the audience).",
5718
5812
  inputSchema: {
@@ -5726,15 +5820,15 @@ var definition8 = {
5726
5820
  required: ["task"]
5727
5821
  }
5728
5822
  };
5729
- async function execute8(input, _onLog, context) {
5823
+ async function execute7(input, _onLog, context) {
5730
5824
  return copyEditorTool.execute(input, context);
5731
5825
  }
5732
5826
 
5733
5827
  // src/subagents/designExpert/tools/loadSkill.ts
5734
5828
  var loadSkill_exports = {};
5735
5829
  __export(loadSkill_exports, {
5736
- definition: () => definition9,
5737
- execute: () => execute9
5830
+ definition: () => definition8,
5831
+ execute: () => execute8
5738
5832
  });
5739
5833
 
5740
5834
  // src/subagents/designExpert/skills/_catalog.ts
@@ -5748,7 +5842,7 @@ var designSkillCatalog = buildSkillCatalog({
5748
5842
  });
5749
5843
 
5750
5844
  // src/subagents/designExpert/tools/loadSkill.ts
5751
- var definition9 = {
5845
+ var definition8 = {
5752
5846
  name: "loadSkill",
5753
5847
  description: "Load the full craft reference for a design surface that isn't in your prompt. The available skills and the trigger for each are listed in <available_skills>. Load one before designing in its area, not after \u2014 these are hard-won technique recipes, and the defaulted version of these surfaces is exactly what they exist to prevent. Calling this is cheap and expected \u2014 if you're unsure whether you need it, load it.",
5754
5848
  inputSchema: {
@@ -5766,7 +5860,7 @@ var definition9 = {
5766
5860
  required: ["skill"]
5767
5861
  }
5768
5862
  };
5769
- async function execute9(input) {
5863
+ async function execute8(input) {
5770
5864
  const id = String(input.skill ?? "");
5771
5865
  const skill = designSkillCatalog.get(id);
5772
5866
  if (!skill) {
@@ -5787,15 +5881,15 @@ This reference lives at ${skill.path}. Re-read it with readFile if you need it a
5787
5881
  var createWireframe_exports = {};
5788
5882
  __export(createWireframe_exports, {
5789
5883
  WIREFRAMES_DIR: () => WIREFRAMES_DIR,
5790
- definition: () => definition10,
5791
- execute: () => execute10
5884
+ definition: () => definition9,
5885
+ execute: () => execute9
5792
5886
  });
5793
5887
  import { mkdir as mkdir2, stat as stat2, writeFile as writeFile2 } from "fs/promises";
5794
5888
  import { join as join3 } from "path";
5795
5889
  var log9 = createLogger("createWireframe");
5796
5890
  var WIREFRAMES_DIR = "src/.wireframes";
5797
5891
  var UPLOAD_TIMEOUT_MS2 = 3e4;
5798
- var definition10 = {
5892
+ var definition9 = {
5799
5893
  name: "createWireframe",
5800
5894
  description: "Generate a wireframe from self-contained HTML+CSS you author and write it to disk as a design artifact. This is how a wireframe comes to exist \u2014 the way generateImages is how an image comes to exist \u2014 and the developer builds from the file it creates. The result also hands back the reference line that embeds the wireframe in your response and in specs; paste it wherever the wireframe belongs and it renders as a live preview. Calling again with the same slug revises the wireframe in place, so existing references stay current \u2014 a revision is a call to this tool, never a prose description of changes to an earlier wireframe.",
5801
5895
  inputSchema: {
@@ -5889,7 +5983,7 @@ async function uploadMirror(context, slug, content) {
5889
5983
  return { ok: false, note: err.message };
5890
5984
  }
5891
5985
  }
5892
- async function execute10(input, onLog, context) {
5986
+ async function execute9(input, onLog, context) {
5893
5987
  if (!context) {
5894
5988
  return "Error: createWireframe requires execution context";
5895
5989
  }
@@ -5937,8 +6031,29 @@ async function execute10(input, onLog, context) {
5937
6031
  }
5938
6032
 
5939
6033
  // src/subagents/designExpert/tools/index.ts
6034
+ var research = {
6035
+ definition: {
6036
+ name: "research",
6037
+ description: "Your researcher, for design questions your built-in catalogs and expertise do not settle: how leading products present a specific kind of data or interaction, current conventions or best practices for a pattern, general-purpose ideas and inspiration. It searches the web, reads the sources that matter, and returns a distilled report with citations and concrete specifics. Brief it neutrally: the question or topic, not the answer you expect.",
6038
+ inputSchema: {
6039
+ type: "object",
6040
+ properties: {
6041
+ task: {
6042
+ type: "string",
6043
+ description: "What you need to find out, in natural language, with enough product context to make the findings relevant."
6044
+ }
6045
+ },
6046
+ required: ["task"]
6047
+ }
6048
+ },
6049
+ execute: (input, _onLog, context) => {
6050
+ if (!context) {
6051
+ return Promise.resolve("Error: research requires execution context");
6052
+ }
6053
+ return runResearch(input.task, context);
6054
+ }
6055
+ };
5940
6056
  var tools = {
5941
- searchGoogle: searchGoogle_exports,
5942
6057
  scrapeWebUrl: scrapeWebUrl_exports,
5943
6058
  analyzeDesign: analyzeDesign_exports,
5944
6059
  analyzeImage: analyzeImage_exports,
@@ -5952,7 +6067,8 @@ var tools = {
5952
6067
  polishCopy: polishCopy_exports,
5953
6068
  loadSkill: loadSkill_exports,
5954
6069
  // Appended last: tool order is part of the subagent's prompt-cache prefix.
5955
- createWireframe: createWireframe_exports
6070
+ createWireframe: createWireframe_exports,
6071
+ research
5956
6072
  };
5957
6073
  var DESIGN_EXPERT_TOOLS = [
5958
6074
  ...COMMON_READ_TOOLS,
@@ -6510,11 +6626,11 @@ ${result.text}`;
6510
6626
  }
6511
6627
 
6512
6628
  // src/subagents/productVision/prompt.ts
6513
- var BASE_PROMPT3 = readAsset("subagents/productVision", "prompt.md");
6629
+ var BASE_PROMPT4 = readAsset("subagents/productVision", "prompt.md");
6514
6630
  function getProductVisionPrompt() {
6515
6631
  const specIndex = loadSpecIndex();
6516
6632
  const roadmapIndex = loadRoadmapIndex();
6517
- const parts = [BASE_PROMPT3, loadPlatformBrief()];
6633
+ const parts = [BASE_PROMPT4, loadPlatformBrief()];
6518
6634
  parts.push("<!-- cache_breakpoint -->");
6519
6635
  if (specIndex) {
6520
6636
  parts.push(specIndex);
@@ -6638,11 +6754,26 @@ var SANITY_CHECK_TOOLS = [
6638
6754
  },
6639
6755
  required: ["command"]
6640
6756
  }
6757
+ },
6758
+ // Appended last: tool order is part of the subagent's prompt-cache prefix.
6759
+ {
6760
+ name: "research",
6761
+ description: "Deep researcher. When the plan hinges on an unfamiliar third-party service, API, or claim you cannot settle with a quick search, hand it the question \u2014 it researches properly (multiple sources, official docs, reading real source code) and returns a distilled, citation-backed report. Quick package-liveness checks stay on searchGoogle; use this when being wrong would be expensive. Brief it neutrally: the question, not the answer you expect.",
6762
+ inputSchema: {
6763
+ type: "object",
6764
+ properties: {
6765
+ task: {
6766
+ type: "string",
6767
+ description: "What you need to find out, in natural language."
6768
+ }
6769
+ },
6770
+ required: ["task"]
6771
+ }
6641
6772
  }
6642
6773
  ];
6643
6774
 
6644
6775
  // src/subagents/codeSanityCheck/index.ts
6645
- var BASE_PROMPT4 = readAsset("subagents/codeSanityCheck", "prompt.md");
6776
+ var BASE_PROMPT5 = readAsset("subagents/codeSanityCheck", "prompt.md");
6646
6777
  var codeSanityCheckTool = {
6647
6778
  definition: {
6648
6779
  name: "codeSanityCheck",
@@ -6663,7 +6794,7 @@ var codeSanityCheckTool = {
6663
6794
  return "Error: code sanity check requires execution context";
6664
6795
  }
6665
6796
  const specIndex = loadSpecIndex();
6666
- const parts = [BASE_PROMPT4, loadPlatformBrief()];
6797
+ const parts = [BASE_PROMPT5, loadPlatformBrief()];
6667
6798
  parts.push("<!-- cache_breakpoint -->");
6668
6799
  if (specIndex) {
6669
6800
  parts.push(specIndex);
@@ -6679,6 +6810,16 @@ var codeSanityCheckTool = {
6679
6810
  ...deriveContext(context, toolCallId, onLog),
6680
6811
  subAgentMessages: sams
6681
6812
  } : context;
6813
+ if (name === "searchGoogle") {
6814
+ return executeSearchGoogle(
6815
+ { ...toolInput, fetchTopN: 5 },
6816
+ childCtx.onLog,
6817
+ "codeSanityCheck"
6818
+ );
6819
+ }
6820
+ if (name === "research") {
6821
+ return runResearch(String(toolInput.task ?? ""), childCtx);
6822
+ }
6682
6823
  return executeTool(name, toolInput, childCtx);
6683
6824
  },
6684
6825
  apiConfig: context.apiConfig,
@@ -6832,7 +6973,7 @@ function acquireSpecSyncLock() {
6832
6973
  }
6833
6974
 
6834
6975
  // src/subagents/specSync/index.ts
6835
- var BASE_PROMPT5 = readAsset("subagents/specSync", "prompt.md");
6976
+ var BASE_PROMPT6 = readAsset("subagents/specSync", "prompt.md");
6836
6977
  var MSFM_DOCS = `<mindstudio_flavored_markdown_spec_docs>
6837
6978
  ${readAsset(
6838
6979
  "prompt",
@@ -6871,7 +7012,7 @@ var specSyncTool = {
6871
7012
  }
6872
7013
  const specIndex = loadSpecIndex();
6873
7014
  const parts = [
6874
- BASE_PROMPT5,
7015
+ BASE_PROMPT6,
6875
7016
  loadPlatformBrief(),
6876
7017
  MSFM_DOCS,
6877
7018
  "<!-- cache_breakpoint -->"
@@ -6986,7 +7127,6 @@ var ALL_TOOLS = [
6986
7127
  confirmDestructiveActionTool,
6987
7128
  askMindStudioSdkTool,
6988
7129
  scrapeWebUrlTool,
6989
- searchGoogleTool,
6990
7130
  setProjectMetadataTool,
6991
7131
  designExpertTool,
6992
7132
  productVisionTool,
@@ -7025,7 +7165,8 @@ var ALL_TOOLS = [
7025
7165
  // Appended rather than grouped: position is part of the cache prefix, so a
7026
7166
  // new tool goes at the end to leave every existing session's prefix intact.
7027
7167
  loadSkillTool,
7028
- testJewelTool
7168
+ testJewelTool,
7169
+ researchTool
7029
7170
  ];
7030
7171
  var SUBAGENT_TOOL_NAMES = /* @__PURE__ */ new Set([
7031
7172
  "visualDesignExpert",
@@ -7034,7 +7175,8 @@ var SUBAGENT_TOOL_NAMES = /* @__PURE__ */ new Set([
7034
7175
  "copyEditor",
7035
7176
  "specSync",
7036
7177
  "runAutomatedBrowserTest",
7037
- "askMindStudioSdk"
7178
+ "askMindStudioSdk",
7179
+ "research"
7038
7180
  ]);
7039
7181
  function getToolDefinitions() {
7040
7182
  return ALL_TOOLS.map((t) => t.definition);
@@ -8616,6 +8758,12 @@ function classify(line, matches) {
8616
8758
  tailClean = SEPARATORS_ONLY.test(gap);
8617
8759
  }
8618
8760
  }
8761
+ function chipCase(label) {
8762
+ if (label.startsWith("`")) {
8763
+ return label;
8764
+ }
8765
+ return label.charAt(0).toUpperCase() + label.slice(1);
8766
+ }
8619
8767
  function parseSuggestions(raw) {
8620
8768
  if (!raw.includes(SUGGEST_MARKER)) {
8621
8769
  return { text: raw, suggestions: [] };
@@ -8645,7 +8793,7 @@ function parseSuggestions(raw) {
8645
8793
  const key = `${m.label}::${m.message}`;
8646
8794
  if (!seen.has(key)) {
8647
8795
  seen.add(key);
8648
- suggestions.push({ label: m.label, message: m.message });
8796
+ suggestions.push({ label: chipCase(m.label), message: m.message });
8649
8797
  }
8650
8798
  }
8651
8799
  let rebuilt = "";
@@ -8745,10 +8893,12 @@ async function runTurn(params) {
8745
8893
  onBackgroundComplete
8746
8894
  } = params;
8747
8895
  const tools2 = getToolDefinitions();
8748
- const buildModelOverride = buildModel ? filterModelPicks({ parent: buildModel }).parent : void 0;
8749
- const baseline = resolveModel("parent", state.models, model);
8750
- const parentModel = buildModelOverride ?? baseline;
8751
- const modelOverride = buildModelOverride && buildModelOverride !== baseline ? { from: baseline } : void 0;
8896
+ const { baseline, effective: parentModel } = resolveParentModel(
8897
+ state.models,
8898
+ model,
8899
+ buildModel
8900
+ );
8901
+ const modelOverride = parentModel !== baseline ? { from: baseline } : void 0;
8752
8902
  const totalAttachments = entries.reduce(
8753
8903
  (n, e) => n + (e.attachments?.length ?? 0),
8754
8904
  0
@@ -8756,7 +8906,7 @@ async function runTurn(params) {
8756
8906
  log15.info("Turn started", {
8757
8907
  requestId,
8758
8908
  model,
8759
- buildModel: buildModelOverride,
8909
+ buildModel: modelOverride ? parentModel : void 0,
8760
8910
  toolCount: tools2.length,
8761
8911
  ...entries.length > 1 && { entryCount: entries.length },
8762
8912
  ...totalAttachments > 0 && { attachmentCount: totalAttachments }
@@ -9591,12 +9741,13 @@ function loadPassiveResults() {
9591
9741
  }
9592
9742
  return [];
9593
9743
  }
9594
- function writeStats(stats, queue, passiveResults) {
9744
+ function writeStats(stats, queue, passiveResults, suggestCompactAt) {
9595
9745
  try {
9596
9746
  writeFileAtomicSync(
9597
9747
  STATS_FILE,
9598
9748
  JSON.stringify({
9599
9749
  ...stats,
9750
+ suggestCompactAt,
9600
9751
  queue,
9601
9752
  passiveResults
9602
9753
  })
@@ -9798,7 +9949,6 @@ var USER_FACING_TOOLS = /* @__PURE__ */ new Set([
9798
9949
  "confirmDestructiveAction",
9799
9950
  "presentPublishPlan"
9800
9951
  ]);
9801
- var FORCED_COMPACTION_THRESHOLD_TOKENS = 85e4;
9802
9952
  var HeadlessSession = class {
9803
9953
  // Configuration
9804
9954
  opts;
@@ -10071,7 +10221,15 @@ var HeadlessSession = class {
10071
10221
  /** Persist sessionStats + queue snapshot + passive pen to .remy-stats.json. */
10072
10222
  persistStats() {
10073
10223
  this.sessionStats.updatedAt = Date.now();
10074
- writeStats(this.sessionStats, this.queue.snapshot(), this.passivePen);
10224
+ const suggestCompactAt = getSuggestCompactAt(
10225
+ resolveParentModel(this.state.models, this.opts.model).effective
10226
+ );
10227
+ writeStats(
10228
+ this.sessionStats,
10229
+ this.queue.snapshot(),
10230
+ this.passivePen,
10231
+ suggestCompactAt
10232
+ );
10075
10233
  }
10076
10234
  //////////////////////////////////////////////////////////////////////////////
10077
10235
  // Background completions (tool-block mutation; message delivery via queue)
@@ -10101,8 +10259,9 @@ var HeadlessSession = class {
10101
10259
  saveSession(this.state);
10102
10260
  }
10103
10261
  /**
10104
- * Forced compaction gate. If lastContextSize exceeds the threshold, compact
10105
- * before letting the upcoming turn run. Coalesces with any in-flight
10262
+ * Forced compaction gate. If lastContextSize exceeds the upcoming turn's
10263
+ * model threshold (per-model — see ModelContextLimits in models/surfaces),
10264
+ * compact before letting the turn run. Coalesces with any in-flight
10106
10265
  * compaction (e.g., one already started by /compact or a tool call). No
10107
10266
  * timeout — compaction takes as long as it takes.
10108
10267
  *
@@ -10113,13 +10272,15 @@ var HeadlessSession = class {
10113
10272
  * On compaction failure we don't bail — the turn proceeds and surfaces any
10114
10273
  * downstream overflow through the existing "prompt is too long" path.
10115
10274
  */
10116
- async runForcedCompactionIfNeeded(requestId) {
10117
- if (this.sessionStats.lastContextSize <= FORCED_COMPACTION_THRESHOLD_TOKENS) {
10275
+ async runForcedCompactionIfNeeded(requestId, parentModel) {
10276
+ const threshold = getContextLimits(parentModel).forceCompactAt;
10277
+ if (this.sessionStats.lastContextSize <= threshold) {
10118
10278
  return;
10119
10279
  }
10120
10280
  log17.info("Forced compaction gate triggered", {
10121
10281
  contextSize: this.sessionStats.lastContextSize,
10122
- threshold: FORCED_COMPACTION_THRESHOLD_TOKENS,
10282
+ threshold,
10283
+ model: parentModel,
10123
10284
  requestId
10124
10285
  });
10125
10286
  try {
@@ -10601,7 +10762,12 @@ var HeadlessSession = class {
10601
10762
  }
10602
10763
  return steered;
10603
10764
  };
10604
- await this.runForcedCompactionIfNeeded(requestId);
10765
+ const parentModel = resolveParentModel(
10766
+ this.state.models,
10767
+ this.opts.model,
10768
+ params.buildModel
10769
+ ).effective;
10770
+ await this.runForcedCompactionIfNeeded(requestId, parentModel);
10605
10771
  try {
10606
10772
  await runTurn({
10607
10773
  state: this.state,