@mindstudio-ai/remy 0.1.177 → 0.1.178

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.
@@ -131,6 +131,12 @@ declare class HeadlessSession {
131
131
  */
132
132
  private kickDrain;
133
133
  private handleClear;
134
+ /** Archive the current session and seed a fresh one with the given
135
+ * per-agent model overrides. Models are immutable for the life of a
136
+ * session — this is the only way to change them. Omitting `models`
137
+ * (or sending an empty object) resets to "use server defaults for
138
+ * every agent". */
139
+ private handleNewSession;
134
140
  /** Cancel the running turn and drain the queue. Returns the drained items. */
135
141
  private handleCancel;
136
142
  private handleStdinLine;
package/dist/headless.js CHANGED
@@ -400,16 +400,17 @@ ${loadPlanStatus()}
400
400
  // src/api.ts
401
401
  var log2 = createLogger("api");
402
402
  async function* streamChat(params) {
403
- const { baseUrl: baseUrl2, apiKey, signal, requestId, ...body } = params;
403
+ const { baseUrl: baseUrl2, apiKey, signal, requestId, model, ...rest } = params;
404
404
  const url = `${baseUrl2}/_internal/v2/agent/remy/chat`;
405
405
  const startTime = Date.now();
406
- const subAgentId = body.subAgentId;
406
+ const subAgentId = rest.subAgentId;
407
+ const requestBody = { ...rest, ...model && { modelId: model } };
407
408
  log2.info("API request", {
408
409
  requestId,
409
410
  ...subAgentId && { subAgentId },
410
- model: body.model,
411
- messageCount: body.messages.length,
412
- toolCount: body.tools.length
411
+ model,
412
+ messageCount: rest.messages.length,
413
+ toolCount: rest.tools.length
413
414
  });
414
415
  let res;
415
416
  try {
@@ -419,7 +420,7 @@ async function* streamChat(params) {
419
420
  "Content-Type": "application/json",
420
421
  Authorization: `Bearer ${apiKey}`
421
422
  },
422
- body: JSON.stringify(body),
423
+ body: JSON.stringify(requestBody),
423
424
  signal
424
425
  });
425
426
  } catch (err) {
@@ -447,13 +448,21 @@ async function* streamChat(params) {
447
448
  });
448
449
  if (!res.ok) {
449
450
  let errorMessage = `HTTP ${res.status}`;
451
+ let errorCode;
452
+ let badModelId;
450
453
  try {
451
- const body2 = await res.json();
452
- if (body2.error) {
453
- errorMessage = body2.error;
454
+ const body = await res.json();
455
+ if (body.error) {
456
+ errorMessage = body.error;
457
+ }
458
+ if (body.errorMessage) {
459
+ errorMessage = body.errorMessage;
460
+ }
461
+ if (typeof body.code === "string") {
462
+ errorCode = body.code;
454
463
  }
455
- if (body2.errorMessage) {
456
- errorMessage = body2.errorMessage;
464
+ if (typeof body.modelId === "string") {
465
+ badModelId = body.modelId;
457
466
  }
458
467
  } catch {
459
468
  }
@@ -461,9 +470,16 @@ async function* streamChat(params) {
461
470
  requestId,
462
471
  ...subAgentId && { subAgentId },
463
472
  status: res.status,
464
- error: errorMessage
473
+ error: errorMessage,
474
+ ...errorCode && { code: errorCode },
475
+ ...badModelId && { badModelId }
465
476
  });
466
- yield { type: "error", error: errorMessage };
477
+ yield {
478
+ type: "error",
479
+ error: errorMessage,
480
+ ...errorCode && { code: errorCode },
481
+ ...badModelId && { badModelId }
482
+ };
467
483
  return;
468
484
  }
469
485
  const STALL_TIMEOUT_MS = 3e5;
@@ -654,7 +670,7 @@ var log3 = createLogger("compaction");
654
670
  var CONVERSATION_SUMMARY_PROMPT = readAsset("compaction", "conversation.md");
655
671
  var SUBAGENT_SUMMARY_PROMPT = readAsset("compaction", "subagent.md");
656
672
  var SUMMARIZABLE_SUBAGENTS = ["visualDesignExpert", "productVision"];
657
- async function compactConversation(messages, apiConfig, system, tools2) {
673
+ async function compactConversation(messages, apiConfig, system, tools2, model) {
658
674
  const endIndex = findSafeInsertionPoint(messages);
659
675
  const summaries = [];
660
676
  const tasks = [];
@@ -670,7 +686,8 @@ async function compactConversation(messages, apiConfig, system, tools2) {
670
686
  CONVERSATION_SUMMARY_PROMPT,
671
687
  conversationMessages,
672
688
  system,
673
- tools2
689
+ tools2,
690
+ model
674
691
  ).then((text) => {
675
692
  if (text) {
676
693
  summaries.push({ name: "conversation", text });
@@ -692,7 +709,8 @@ async function compactConversation(messages, apiConfig, system, tools2) {
692
709
  SUBAGENT_SUMMARY_PROMPT,
693
710
  subagentMessages,
694
711
  system,
695
- tools2
712
+ tools2,
713
+ model
696
714
  ).then((text) => {
697
715
  if (text) {
698
716
  summaries.push({ name, text });
@@ -814,7 +832,7 @@ function serializeForSummary(messages) {
814
832
  }).join("\n\n");
815
833
  }
816
834
  var CHUNK_CHAR_LIMIT = 24e5;
817
- async function generateSummary(apiConfig, name, compactionPrompt, messagesToSummarize, mainSystem, mainTools) {
835
+ async function generateSummary(apiConfig, name, compactionPrompt, messagesToSummarize, mainSystem, mainTools, model) {
818
836
  const serialized = serializeForSummary(messagesToSummarize);
819
837
  if (!serialized.trim()) {
820
838
  return null;
@@ -833,7 +851,8 @@ async function generateSummary(apiConfig, name, compactionPrompt, messagesToSumm
833
851
  compactionPrompt,
834
852
  messagesToSummarize.slice(0, mid),
835
853
  mainSystem,
836
- mainTools
854
+ mainTools,
855
+ model
837
856
  ),
838
857
  generateSummary(
839
858
  apiConfig,
@@ -841,7 +860,8 @@ async function generateSummary(apiConfig, name, compactionPrompt, messagesToSumm
841
860
  compactionPrompt,
842
861
  messagesToSummarize.slice(mid),
843
862
  mainSystem,
844
- mainTools
863
+ mainTools,
864
+ model
845
865
  )
846
866
  ]);
847
867
  const parts = [first, second].filter((p) => !!p);
@@ -866,6 +886,7 @@ ${serialized}` : serialized;
866
886
  const iterStart = Date.now();
867
887
  for await (const event of streamChat({
868
888
  ...apiConfig,
889
+ model,
869
890
  subAgentId: "conversationSummarizer",
870
891
  system,
871
892
  messages: [{ role: "user", content: userContent }],
@@ -3110,6 +3131,9 @@ ${content}` : attachmentHeader;
3110
3131
  if (thinking.length > 0) {
3111
3132
  cleaned2.thinking = thinking;
3112
3133
  }
3134
+ if (msg.providerMetadata) {
3135
+ cleaned2.providerMetadata = msg.providerMetadata;
3136
+ }
3113
3137
  if (msg.hidden) {
3114
3138
  cleaned2.hidden = true;
3115
3139
  }
@@ -3196,6 +3220,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3196
3220
  let stopReason = "end_turn";
3197
3221
  let currentToolNames = "";
3198
3222
  let lastUsage;
3223
+ let lastProviderMetadata;
3199
3224
  const statusWatcher = startStatusWatcher({
3200
3225
  apiConfig,
3201
3226
  getContext: () => {
@@ -3308,6 +3333,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3308
3333
  cacheReadTokens: event.usage.cacheReadTokens,
3309
3334
  llmCalls: 1
3310
3335
  };
3336
+ lastProviderMetadata = event.providerMetadata;
3311
3337
  recordUsage({
3312
3338
  ts: Date.now(),
3313
3339
  requestId,
@@ -3345,7 +3371,8 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3345
3371
  messages.push({
3346
3372
  role: "assistant",
3347
3373
  content: contentBlocks,
3348
- ...lastUsage ? { usage: lastUsage } : {}
3374
+ ...lastUsage ? { usage: lastUsage } : {},
3375
+ ...lastProviderMetadata ? { providerMetadata: lastProviderMetadata } : {}
3349
3376
  });
3350
3377
  const toolCalls = contentBlocks.filter(
3351
3378
  (b) => b.type === "tool"
@@ -3746,7 +3773,7 @@ var browserAutomationTool = {
3746
3773
  return `Error: unknown local tool "${name}"`;
3747
3774
  },
3748
3775
  apiConfig: context.apiConfig,
3749
- model: context.model,
3776
+ model: context.models?.browserAutomation ?? context.model,
3750
3777
  subAgentId: "browserAutomation",
3751
3778
  signal: context.signal,
3752
3779
  parentToolId: context.toolCallId,
@@ -4962,7 +4989,7 @@ var designExpertTool = {
4962
4989
  );
4963
4990
  },
4964
4991
  apiConfig: context.apiConfig,
4965
- model: context.model,
4992
+ model: context.models?.visualDesignExpert ?? context.model,
4966
4993
  subAgentId: "visualDesignExpert",
4967
4994
  signal: context.signal,
4968
4995
  parentToolId: context.toolCallId,
@@ -5180,7 +5207,7 @@ var productVisionTool = {
5180
5207
  return executeVisionTool(name, input2, childCtx);
5181
5208
  },
5182
5209
  apiConfig: context.apiConfig,
5183
- model: context.model,
5210
+ model: context.models?.productVision ?? context.model,
5184
5211
  subAgentId: "productVision",
5185
5212
  signal: context.signal,
5186
5213
  parentToolId: context.toolCallId,
@@ -5288,7 +5315,7 @@ var codeSanityCheckTool = {
5288
5315
  externalTools: /* @__PURE__ */ new Set(),
5289
5316
  executeTool: (name, toolInput) => executeTool(name, toolInput, context),
5290
5317
  apiConfig: context.apiConfig,
5291
- model: context.model,
5318
+ model: context.models?.codeSanityCheck ?? context.model,
5292
5319
  subAgentId: "codeSanityCheck",
5293
5320
  signal: context.signal,
5294
5321
  parentToolId: context.toolCallId,
@@ -5419,7 +5446,7 @@ function triggerCompaction(state, apiConfig, opts = {}) {
5419
5446
  if (inflightCompaction) {
5420
5447
  return inflightCompaction;
5421
5448
  }
5422
- const { blocking = false, requestId } = opts;
5449
+ const { blocking = false, requestId, model } = opts;
5423
5450
  listener?.({ type: "started", blocking, requestId });
5424
5451
  const system = buildSystemPrompt("onboardingFinished");
5425
5452
  const tools2 = getToolDefinitions("onboardingFinished");
@@ -5427,7 +5454,8 @@ function triggerCompaction(state, apiConfig, opts = {}) {
5427
5454
  state.messages,
5428
5455
  apiConfig,
5429
5456
  system,
5430
- tools2
5457
+ tools2,
5458
+ state.models?.conversationSummarizer ?? model
5431
5459
  ).then((summaries) => {
5432
5460
  pendingSummaries.push(...summaries);
5433
5461
  listener?.({ type: "complete", requestId });
@@ -5451,7 +5479,7 @@ var log8 = createLogger("brandExtraction");
5451
5479
  var EXTRACT_PROMPT = readAsset("brandExtraction", "extract.md");
5452
5480
  var BRAND_FILE = ".remy-brand.json";
5453
5481
  var CACHE_FILE = ".remy-brand.cache.json";
5454
- async function runExtraction(apiConfig) {
5482
+ async function runExtraction(apiConfig, model) {
5455
5483
  const inputHash = computeInputHash();
5456
5484
  const cached2 = readCache();
5457
5485
  if (cached2 && cached2.inputHash === inputHash) {
@@ -5459,7 +5487,7 @@ async function runExtraction(apiConfig) {
5459
5487
  return null;
5460
5488
  }
5461
5489
  log8.info("Extracting brand", { inputHash });
5462
- const brand = await extractBrand(apiConfig);
5490
+ const brand = await extractBrand(apiConfig, model);
5463
5491
  if (!brand) {
5464
5492
  log8.warn("Brand extraction failed \u2014 leaving cache untouched");
5465
5493
  return null;
@@ -5528,7 +5556,7 @@ function parseFrontmatter3(filePath) {
5528
5556
  return { type: "" };
5529
5557
  }
5530
5558
  }
5531
- async function extractBrand(apiConfig) {
5559
+ async function extractBrand(apiConfig, model) {
5532
5560
  const corpus = buildCorpus();
5533
5561
  if (!corpus.trim()) {
5534
5562
  log8.debug("No spec corpus \u2014 emitting empty brand");
@@ -5539,6 +5567,7 @@ async function extractBrand(apiConfig) {
5539
5567
  try {
5540
5568
  for await (const event of streamChat({
5541
5569
  ...apiConfig,
5570
+ model,
5542
5571
  subAgentId: "brandExtractor",
5543
5572
  system: EXTRACT_PROMPT,
5544
5573
  messages: [{ role: "user", content: corpus }],
@@ -5716,19 +5745,19 @@ function readCache() {
5716
5745
  var log9 = createLogger("brandExtraction:trigger");
5717
5746
  var inflight = false;
5718
5747
  var dirty = false;
5719
- function triggerBrandExtraction(apiConfig) {
5748
+ function triggerBrandExtraction(apiConfig, model) {
5720
5749
  if (inflight) {
5721
5750
  dirty = true;
5722
5751
  return;
5723
5752
  }
5724
5753
  inflight = true;
5725
- void runExtraction(apiConfig).catch((err) => {
5754
+ void runExtraction(apiConfig, model).catch((err) => {
5726
5755
  log9.error("Brand extraction failed", { error: err?.message });
5727
5756
  }).finally(() => {
5728
5757
  inflight = false;
5729
5758
  if (dirty) {
5730
5759
  dirty = false;
5731
- triggerBrandExtraction(apiConfig);
5760
+ triggerBrandExtraction(apiConfig, model);
5732
5761
  }
5733
5762
  });
5734
5763
  }
@@ -5743,9 +5772,15 @@ function loadSession(state) {
5743
5772
  try {
5744
5773
  const raw = fs21.readFileSync(SESSION_FILE, "utf-8");
5745
5774
  const data = JSON.parse(raw);
5775
+ if (data.models && typeof data.models === "object") {
5776
+ state.models = data.models;
5777
+ }
5746
5778
  if (Array.isArray(data.messages) && data.messages.length > 0) {
5747
5779
  state.messages = sanitizeMessages(data.messages);
5748
- log10.info("Session loaded", { messageCount: state.messages.length });
5780
+ log10.info("Session loaded", {
5781
+ messageCount: state.messages.length,
5782
+ ...state.models && { models: state.models }
5783
+ });
5749
5784
  return true;
5750
5785
  }
5751
5786
  } catch {
@@ -5790,11 +5825,11 @@ function sanitizeMessages(messages) {
5790
5825
  }
5791
5826
  function saveSession(state) {
5792
5827
  try {
5793
- fs21.writeFileSync(
5794
- SESSION_FILE,
5795
- JSON.stringify({ messages: state.messages }, null, 2),
5796
- "utf-8"
5797
- );
5828
+ const payload = { messages: state.messages };
5829
+ if (state.models && Object.keys(state.models).length > 0) {
5830
+ payload.models = state.models;
5831
+ }
5832
+ fs21.writeFileSync(SESSION_FILE, JSON.stringify(payload, null, 2), "utf-8");
5798
5833
  log10.info("Session saved", { messageCount: state.messages.length });
5799
5834
  } catch (err) {
5800
5835
  log10.warn("Session save failed", { error: err.message });
@@ -6135,6 +6170,7 @@ async function runTurn(params) {
6135
6170
  let textBlockOpen = false;
6136
6171
  const toolInputAccumulators = /* @__PURE__ */ new Map();
6137
6172
  let stopReason = "end_turn";
6173
+ let turnProviderMetadata;
6138
6174
  let subAgentText = "";
6139
6175
  let currentToolNames = "";
6140
6176
  const statusWatcher = isFirstMessage ? { stop() {
@@ -6216,11 +6252,12 @@ async function runTurn(params) {
6216
6252
  onEvent({ type: "tool_input_delta", id, name, result: content });
6217
6253
  }
6218
6254
  }
6255
+ const parentModel = state.models?.parent ?? model;
6219
6256
  try {
6220
6257
  for await (const event of streamChatWithRetry(
6221
6258
  {
6222
6259
  ...apiConfig,
6223
- model,
6260
+ model: parentModel,
6224
6261
  requestId,
6225
6262
  system,
6226
6263
  messages: cleanMessagesForApi(state.messages),
@@ -6332,6 +6369,7 @@ async function runTurn(params) {
6332
6369
  }
6333
6370
  case "done":
6334
6371
  stopReason = event.stopReason;
6372
+ turnProviderMetadata = event.providerMetadata;
6335
6373
  turnLlmCalls++;
6336
6374
  lastCallInputTokens = event.usage.inputTokens;
6337
6375
  lastCallCacheCreation = event.usage.cacheCreationTokens ?? 0;
@@ -6385,6 +6423,9 @@ async function runTurn(params) {
6385
6423
  cacheCreationTokens: turnCacheCreation || void 0,
6386
6424
  cacheReadTokens: turnCacheRead || void 0,
6387
6425
  llmCalls: turnLlmCalls
6426
+ },
6427
+ ...turnProviderMetadata && {
6428
+ providerMetadata: turnProviderMetadata
6388
6429
  }
6389
6430
  });
6390
6431
  }
@@ -6402,7 +6443,8 @@ async function runTurn(params) {
6402
6443
  cacheCreationTokens: turnCacheCreation || void 0,
6403
6444
  cacheReadTokens: turnCacheRead || void 0,
6404
6445
  llmCalls: turnLlmCalls
6405
- }
6446
+ },
6447
+ ...turnProviderMetadata && { providerMetadata: turnProviderMetadata }
6406
6448
  });
6407
6449
  }
6408
6450
  const toolCalls = getToolCalls(contentBlocks);
@@ -6491,6 +6533,7 @@ async function runTurn(params) {
6491
6533
  result = await executeTool(tc.name, input, {
6492
6534
  apiConfig,
6493
6535
  model,
6536
+ models: state.models,
6494
6537
  signal: toolAbort.signal,
6495
6538
  onEvent: wrappedOnEvent,
6496
6539
  resolveExternalTool,
@@ -6995,10 +7038,14 @@ var HeadlessSession = class {
6995
7038
  if (resumed) {
6996
7039
  this.emit("session_restored", {
6997
7040
  messageCount: this.state.messages.length,
7041
+ ...this.state.models && { models: this.state.models },
6998
7042
  ...this.queueFields()
6999
7043
  });
7000
7044
  }
7001
- triggerBrandExtraction(this.config);
7045
+ triggerBrandExtraction(
7046
+ this.config,
7047
+ this.state.models?.brandExtractor ?? this.opts.model
7048
+ );
7002
7049
  this.toolRegistry.onEvent = this.onEvent;
7003
7050
  setCompactionListener((event) => {
7004
7051
  if (event.type === "started") {
@@ -7151,7 +7198,8 @@ var HeadlessSession = class {
7151
7198
  try {
7152
7199
  await triggerCompaction(this.state, this.config, {
7153
7200
  blocking: true,
7154
- requestId
7201
+ requestId,
7202
+ model: this.opts.model
7155
7203
  });
7156
7204
  this.applyPendingSummaries();
7157
7205
  } catch {
@@ -7589,6 +7637,17 @@ var HeadlessSession = class {
7589
7637
  clearSession(this.state);
7590
7638
  return {};
7591
7639
  }
7640
+ /** Archive the current session and seed a fresh one with the given
7641
+ * per-agent model overrides. Models are immutable for the life of a
7642
+ * session — this is the only way to change them. Omitting `models`
7643
+ * (or sending an empty object) resets to "use server defaults for
7644
+ * every agent". */
7645
+ handleNewSession(models) {
7646
+ clearSession(this.state);
7647
+ this.state.models = models && Object.keys(models).length > 0 ? models : void 0;
7648
+ saveSession(this.state);
7649
+ return {};
7650
+ }
7592
7651
  /** Cancel the running turn and drain the queue. Returns the drained items. */
7593
7652
  handleCancel() {
7594
7653
  if (this.currentAbort) {
@@ -7662,6 +7721,7 @@ var HeadlessSession = class {
7662
7721
  totalMessageCount: total,
7663
7722
  running: this.running,
7664
7723
  ...this.running && this.currentRequestId ? { currentRequestId: this.currentRequestId } : {},
7724
+ ...this.state.models && { models: this.state.models },
7665
7725
  ...this.queueFields()
7666
7726
  }));
7667
7727
  return;
@@ -7674,6 +7734,15 @@ var HeadlessSession = class {
7674
7734
  );
7675
7735
  return;
7676
7736
  }
7737
+ if (action === "newSession") {
7738
+ const models = parsed.models;
7739
+ this.dispatchSimple(
7740
+ requestId,
7741
+ "session_cleared",
7742
+ () => this.handleNewSession(models)
7743
+ );
7744
+ return;
7745
+ }
7677
7746
  if (action === "cancel") {
7678
7747
  const cancelled = this.handleCancel();
7679
7748
  this.emit(
@@ -7720,7 +7789,8 @@ var HeadlessSession = class {
7720
7789
  try {
7721
7790
  await triggerCompaction(this.state, this.config, {
7722
7791
  blocking: false,
7723
- requestId
7792
+ requestId,
7793
+ model: this.opts.model
7724
7794
  });
7725
7795
  if (!this.running) {
7726
7796
  this.applyPendingSummaries();
package/dist/index.js CHANGED
@@ -86,16 +86,17 @@ var init_logger = __esm({
86
86
 
87
87
  // src/api.ts
88
88
  async function* streamChat(params) {
89
- const { baseUrl: baseUrl2, apiKey, signal, requestId, ...body } = params;
89
+ const { baseUrl: baseUrl2, apiKey, signal, requestId, model, ...rest } = params;
90
90
  const url = `${baseUrl2}/_internal/v2/agent/remy/chat`;
91
91
  const startTime = Date.now();
92
- const subAgentId = body.subAgentId;
92
+ const subAgentId = rest.subAgentId;
93
+ const requestBody = { ...rest, ...model && { modelId: model } };
93
94
  log.info("API request", {
94
95
  requestId,
95
96
  ...subAgentId && { subAgentId },
96
- model: body.model,
97
- messageCount: body.messages.length,
98
- toolCount: body.tools.length
97
+ model,
98
+ messageCount: rest.messages.length,
99
+ toolCount: rest.tools.length
99
100
  });
100
101
  let res;
101
102
  try {
@@ -105,7 +106,7 @@ async function* streamChat(params) {
105
106
  "Content-Type": "application/json",
106
107
  Authorization: `Bearer ${apiKey}`
107
108
  },
108
- body: JSON.stringify(body),
109
+ body: JSON.stringify(requestBody),
109
110
  signal
110
111
  });
111
112
  } catch (err) {
@@ -133,13 +134,21 @@ async function* streamChat(params) {
133
134
  });
134
135
  if (!res.ok) {
135
136
  let errorMessage = `HTTP ${res.status}`;
137
+ let errorCode;
138
+ let badModelId;
136
139
  try {
137
- const body2 = await res.json();
138
- if (body2.error) {
139
- errorMessage = body2.error;
140
+ const body = await res.json();
141
+ if (body.error) {
142
+ errorMessage = body.error;
143
+ }
144
+ if (body.errorMessage) {
145
+ errorMessage = body.errorMessage;
140
146
  }
141
- if (body2.errorMessage) {
142
- errorMessage = body2.errorMessage;
147
+ if (typeof body.code === "string") {
148
+ errorCode = body.code;
149
+ }
150
+ if (typeof body.modelId === "string") {
151
+ badModelId = body.modelId;
143
152
  }
144
153
  } catch {
145
154
  }
@@ -147,9 +156,16 @@ async function* streamChat(params) {
147
156
  requestId,
148
157
  ...subAgentId && { subAgentId },
149
158
  status: res.status,
150
- error: errorMessage
159
+ error: errorMessage,
160
+ ...errorCode && { code: errorCode },
161
+ ...badModelId && { badModelId }
151
162
  });
152
- yield { type: "error", error: errorMessage };
163
+ yield {
164
+ type: "error",
165
+ error: errorMessage,
166
+ ...errorCode && { code: errorCode },
167
+ ...badModelId && { badModelId }
168
+ };
153
169
  return;
154
170
  }
155
171
  const STALL_TIMEOUT_MS = 3e5;
@@ -1471,7 +1487,7 @@ var init_assets = __esm({
1471
1487
  });
1472
1488
 
1473
1489
  // src/compaction/index.ts
1474
- async function compactConversation(messages, apiConfig, system, tools2) {
1490
+ async function compactConversation(messages, apiConfig, system, tools2, model) {
1475
1491
  const endIndex = findSafeInsertionPoint(messages);
1476
1492
  const summaries = [];
1477
1493
  const tasks = [];
@@ -1487,7 +1503,8 @@ async function compactConversation(messages, apiConfig, system, tools2) {
1487
1503
  CONVERSATION_SUMMARY_PROMPT,
1488
1504
  conversationMessages,
1489
1505
  system,
1490
- tools2
1506
+ tools2,
1507
+ model
1491
1508
  ).then((text) => {
1492
1509
  if (text) {
1493
1510
  summaries.push({ name: "conversation", text });
@@ -1509,7 +1526,8 @@ async function compactConversation(messages, apiConfig, system, tools2) {
1509
1526
  SUBAGENT_SUMMARY_PROMPT,
1510
1527
  subagentMessages,
1511
1528
  system,
1512
- tools2
1529
+ tools2,
1530
+ model
1513
1531
  ).then((text) => {
1514
1532
  if (text) {
1515
1533
  summaries.push({ name, text });
@@ -1630,7 +1648,7 @@ function serializeForSummary(messages) {
1630
1648
  return `[${msg.role}]: ${parts.join("\n")}`;
1631
1649
  }).join("\n\n");
1632
1650
  }
1633
- async function generateSummary(apiConfig, name, compactionPrompt, messagesToSummarize, mainSystem, mainTools) {
1651
+ async function generateSummary(apiConfig, name, compactionPrompt, messagesToSummarize, mainSystem, mainTools, model) {
1634
1652
  const serialized = serializeForSummary(messagesToSummarize);
1635
1653
  if (!serialized.trim()) {
1636
1654
  return null;
@@ -1649,7 +1667,8 @@ async function generateSummary(apiConfig, name, compactionPrompt, messagesToSumm
1649
1667
  compactionPrompt,
1650
1668
  messagesToSummarize.slice(0, mid),
1651
1669
  mainSystem,
1652
- mainTools
1670
+ mainTools,
1671
+ model
1653
1672
  ),
1654
1673
  generateSummary(
1655
1674
  apiConfig,
@@ -1657,7 +1676,8 @@ async function generateSummary(apiConfig, name, compactionPrompt, messagesToSumm
1657
1676
  compactionPrompt,
1658
1677
  messagesToSummarize.slice(mid),
1659
1678
  mainSystem,
1660
- mainTools
1679
+ mainTools,
1680
+ model
1661
1681
  )
1662
1682
  ]);
1663
1683
  const parts = [first, second].filter((p) => !!p);
@@ -1682,6 +1702,7 @@ ${serialized}` : serialized;
1682
1702
  const iterStart = Date.now();
1683
1703
  for await (const event of streamChat({
1684
1704
  ...apiConfig,
1705
+ model,
1685
1706
  subAgentId: "conversationSummarizer",
1686
1707
  system,
1687
1708
  messages: [{ role: "user", content: userContent }],
@@ -2014,7 +2035,7 @@ function triggerCompaction(state, apiConfig, opts = {}) {
2014
2035
  if (inflightCompaction) {
2015
2036
  return inflightCompaction;
2016
2037
  }
2017
- const { blocking = false, requestId } = opts;
2038
+ const { blocking = false, requestId, model } = opts;
2018
2039
  listener?.({ type: "started", blocking, requestId });
2019
2040
  const system = buildSystemPrompt("onboardingFinished");
2020
2041
  const tools2 = getToolDefinitions("onboardingFinished");
@@ -2022,7 +2043,8 @@ function triggerCompaction(state, apiConfig, opts = {}) {
2022
2043
  state.messages,
2023
2044
  apiConfig,
2024
2045
  system,
2025
- tools2
2046
+ tools2,
2047
+ state.models?.conversationSummarizer ?? model
2026
2048
  ).then((summaries) => {
2027
2049
  pendingSummaries.push(...summaries);
2028
2050
  listener?.({ type: "complete", requestId });
@@ -3437,6 +3459,9 @@ ${content}` : attachmentHeader;
3437
3459
  if (thinking.length > 0) {
3438
3460
  cleaned2.thinking = thinking;
3439
3461
  }
3462
+ if (msg.providerMetadata) {
3463
+ cleaned2.providerMetadata = msg.providerMetadata;
3464
+ }
3440
3465
  if (msg.hidden) {
3441
3466
  cleaned2.hidden = true;
3442
3467
  }
@@ -3528,6 +3553,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3528
3553
  let stopReason = "end_turn";
3529
3554
  let currentToolNames = "";
3530
3555
  let lastUsage;
3556
+ let lastProviderMetadata;
3531
3557
  const statusWatcher = startStatusWatcher({
3532
3558
  apiConfig,
3533
3559
  getContext: () => {
@@ -3640,6 +3666,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3640
3666
  cacheReadTokens: event.usage.cacheReadTokens,
3641
3667
  llmCalls: 1
3642
3668
  };
3669
+ lastProviderMetadata = event.providerMetadata;
3643
3670
  recordUsage({
3644
3671
  ts: Date.now(),
3645
3672
  requestId,
@@ -3677,7 +3704,8 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3677
3704
  messages.push({
3678
3705
  role: "assistant",
3679
3706
  content: contentBlocks,
3680
- ...lastUsage ? { usage: lastUsage } : {}
3707
+ ...lastUsage ? { usage: lastUsage } : {},
3708
+ ...lastProviderMetadata ? { providerMetadata: lastProviderMetadata } : {}
3681
3709
  });
3682
3710
  const toolCalls = contentBlocks.filter(
3683
3711
  (b) => b.type === "tool"
@@ -4116,7 +4144,7 @@ var init_browserAutomation = __esm({
4116
4144
  return `Error: unknown local tool "${name}"`;
4117
4145
  },
4118
4146
  apiConfig: context.apiConfig,
4119
- model: context.model,
4147
+ model: context.models?.browserAutomation ?? context.model,
4120
4148
  subAgentId: "browserAutomation",
4121
4149
  signal: context.signal,
4122
4150
  parentToolId: context.toolCallId,
@@ -5494,7 +5522,7 @@ Visual design expert. Describe the situation and what you need \u2014 the agent
5494
5522
  );
5495
5523
  },
5496
5524
  apiConfig: context.apiConfig,
5497
- model: context.model,
5525
+ model: context.models?.visualDesignExpert ?? context.model,
5498
5526
  subAgentId: "visualDesignExpert",
5499
5527
  signal: context.signal,
5500
5528
  parentToolId: context.toolCallId,
@@ -5751,7 +5779,7 @@ var init_productVision = __esm({
5751
5779
  return executeVisionTool(name, input2, childCtx);
5752
5780
  },
5753
5781
  apiConfig: context.apiConfig,
5754
- model: context.model,
5782
+ model: context.models?.productVision ?? context.model,
5755
5783
  subAgentId: "productVision",
5756
5784
  signal: context.signal,
5757
5785
  parentToolId: context.toolCallId,
@@ -5877,7 +5905,7 @@ var init_codeSanityCheck = __esm({
5877
5905
  externalTools: /* @__PURE__ */ new Set(),
5878
5906
  executeTool: (name, toolInput) => executeTool(name, toolInput, context),
5879
5907
  apiConfig: context.apiConfig,
5880
- model: context.model,
5908
+ model: context.models?.codeSanityCheck ?? context.model,
5881
5909
  subAgentId: "codeSanityCheck",
5882
5910
  signal: context.signal,
5883
5911
  parentToolId: context.toolCallId,
@@ -6048,9 +6076,15 @@ function loadSession(state) {
6048
6076
  try {
6049
6077
  const raw = fs19.readFileSync(SESSION_FILE, "utf-8");
6050
6078
  const data = JSON.parse(raw);
6079
+ if (data.models && typeof data.models === "object") {
6080
+ state.models = data.models;
6081
+ }
6051
6082
  if (Array.isArray(data.messages) && data.messages.length > 0) {
6052
6083
  state.messages = sanitizeMessages(data.messages);
6053
- log7.info("Session loaded", { messageCount: state.messages.length });
6084
+ log7.info("Session loaded", {
6085
+ messageCount: state.messages.length,
6086
+ ...state.models && { models: state.models }
6087
+ });
6054
6088
  return true;
6055
6089
  }
6056
6090
  } catch {
@@ -6095,11 +6129,11 @@ function sanitizeMessages(messages) {
6095
6129
  }
6096
6130
  function saveSession(state) {
6097
6131
  try {
6098
- fs19.writeFileSync(
6099
- SESSION_FILE,
6100
- JSON.stringify({ messages: state.messages }, null, 2),
6101
- "utf-8"
6102
- );
6132
+ const payload = { messages: state.messages };
6133
+ if (state.models && Object.keys(state.models).length > 0) {
6134
+ payload.models = state.models;
6135
+ }
6136
+ fs19.writeFileSync(SESSION_FILE, JSON.stringify(payload, null, 2), "utf-8");
6103
6137
  log7.info("Session saved", { messageCount: state.messages.length });
6104
6138
  } catch (err) {
6105
6139
  log7.warn("Session save failed", { error: err.message });
@@ -6345,7 +6379,7 @@ var init_errors = __esm({
6345
6379
  import fs20 from "fs";
6346
6380
  import path10 from "path";
6347
6381
  import { createHash } from "crypto";
6348
- async function runExtraction(apiConfig) {
6382
+ async function runExtraction(apiConfig, model) {
6349
6383
  const inputHash = computeInputHash();
6350
6384
  const cached2 = readCache();
6351
6385
  if (cached2 && cached2.inputHash === inputHash) {
@@ -6353,7 +6387,7 @@ async function runExtraction(apiConfig) {
6353
6387
  return null;
6354
6388
  }
6355
6389
  log8.info("Extracting brand", { inputHash });
6356
- const brand = await extractBrand(apiConfig);
6390
+ const brand = await extractBrand(apiConfig, model);
6357
6391
  if (!brand) {
6358
6392
  log8.warn("Brand extraction failed \u2014 leaving cache untouched");
6359
6393
  return null;
@@ -6422,7 +6456,7 @@ function parseFrontmatter3(filePath) {
6422
6456
  return { type: "" };
6423
6457
  }
6424
6458
  }
6425
- async function extractBrand(apiConfig) {
6459
+ async function extractBrand(apiConfig, model) {
6426
6460
  const corpus = buildCorpus();
6427
6461
  if (!corpus.trim()) {
6428
6462
  log8.debug("No spec corpus \u2014 emitting empty brand");
@@ -6433,6 +6467,7 @@ async function extractBrand(apiConfig) {
6433
6467
  try {
6434
6468
  for await (const event of streamChat({
6435
6469
  ...apiConfig,
6470
+ model,
6436
6471
  subAgentId: "brandExtractor",
6437
6472
  system: EXTRACT_PROMPT,
6438
6473
  messages: [{ role: "user", content: corpus }],
@@ -6621,19 +6656,19 @@ var init_brandExtraction = __esm({
6621
6656
  });
6622
6657
 
6623
6658
  // src/brandExtraction/trigger.ts
6624
- function triggerBrandExtraction(apiConfig) {
6659
+ function triggerBrandExtraction(apiConfig, model) {
6625
6660
  if (inflight) {
6626
6661
  dirty = true;
6627
6662
  return;
6628
6663
  }
6629
6664
  inflight = true;
6630
- void runExtraction(apiConfig).catch((err) => {
6665
+ void runExtraction(apiConfig, model).catch((err) => {
6631
6666
  log9.error("Brand extraction failed", { error: err?.message });
6632
6667
  }).finally(() => {
6633
6668
  inflight = false;
6634
6669
  if (dirty) {
6635
6670
  dirty = false;
6636
- triggerBrandExtraction(apiConfig);
6671
+ triggerBrandExtraction(apiConfig, model);
6637
6672
  }
6638
6673
  });
6639
6674
  }
@@ -6752,6 +6787,7 @@ async function runTurn(params) {
6752
6787
  let textBlockOpen = false;
6753
6788
  const toolInputAccumulators = /* @__PURE__ */ new Map();
6754
6789
  let stopReason = "end_turn";
6790
+ let turnProviderMetadata;
6755
6791
  let subAgentText = "";
6756
6792
  let currentToolNames = "";
6757
6793
  const statusWatcher = isFirstMessage ? { stop() {
@@ -6833,11 +6869,12 @@ async function runTurn(params) {
6833
6869
  onEvent({ type: "tool_input_delta", id, name, result: content });
6834
6870
  }
6835
6871
  }
6872
+ const parentModel = state.models?.parent ?? model;
6836
6873
  try {
6837
6874
  for await (const event of streamChatWithRetry(
6838
6875
  {
6839
6876
  ...apiConfig,
6840
- model,
6877
+ model: parentModel,
6841
6878
  requestId,
6842
6879
  system,
6843
6880
  messages: cleanMessagesForApi(state.messages),
@@ -6949,6 +6986,7 @@ async function runTurn(params) {
6949
6986
  }
6950
6987
  case "done":
6951
6988
  stopReason = event.stopReason;
6989
+ turnProviderMetadata = event.providerMetadata;
6952
6990
  turnLlmCalls++;
6953
6991
  lastCallInputTokens = event.usage.inputTokens;
6954
6992
  lastCallCacheCreation = event.usage.cacheCreationTokens ?? 0;
@@ -7002,6 +7040,9 @@ async function runTurn(params) {
7002
7040
  cacheCreationTokens: turnCacheCreation || void 0,
7003
7041
  cacheReadTokens: turnCacheRead || void 0,
7004
7042
  llmCalls: turnLlmCalls
7043
+ },
7044
+ ...turnProviderMetadata && {
7045
+ providerMetadata: turnProviderMetadata
7005
7046
  }
7006
7047
  });
7007
7048
  }
@@ -7019,7 +7060,8 @@ async function runTurn(params) {
7019
7060
  cacheCreationTokens: turnCacheCreation || void 0,
7020
7061
  cacheReadTokens: turnCacheRead || void 0,
7021
7062
  llmCalls: turnLlmCalls
7022
- }
7063
+ },
7064
+ ...turnProviderMetadata && { providerMetadata: turnProviderMetadata }
7023
7065
  });
7024
7066
  }
7025
7067
  const toolCalls = getToolCalls(contentBlocks);
@@ -7108,6 +7150,7 @@ async function runTurn(params) {
7108
7150
  result = await executeTool(tc.name, input, {
7109
7151
  apiConfig,
7110
7152
  model,
7153
+ models: state.models,
7111
7154
  signal: toolAbort.signal,
7112
7155
  onEvent: wrappedOnEvent,
7113
7156
  resolveExternalTool,
@@ -7767,10 +7810,14 @@ var init_headless = __esm({
7767
7810
  if (resumed) {
7768
7811
  this.emit("session_restored", {
7769
7812
  messageCount: this.state.messages.length,
7813
+ ...this.state.models && { models: this.state.models },
7770
7814
  ...this.queueFields()
7771
7815
  });
7772
7816
  }
7773
- triggerBrandExtraction(this.config);
7817
+ triggerBrandExtraction(
7818
+ this.config,
7819
+ this.state.models?.brandExtractor ?? this.opts.model
7820
+ );
7774
7821
  this.toolRegistry.onEvent = this.onEvent;
7775
7822
  setCompactionListener((event) => {
7776
7823
  if (event.type === "started") {
@@ -7923,7 +7970,8 @@ var init_headless = __esm({
7923
7970
  try {
7924
7971
  await triggerCompaction(this.state, this.config, {
7925
7972
  blocking: true,
7926
- requestId
7973
+ requestId,
7974
+ model: this.opts.model
7927
7975
  });
7928
7976
  this.applyPendingSummaries();
7929
7977
  } catch {
@@ -8361,6 +8409,17 @@ var init_headless = __esm({
8361
8409
  clearSession(this.state);
8362
8410
  return {};
8363
8411
  }
8412
+ /** Archive the current session and seed a fresh one with the given
8413
+ * per-agent model overrides. Models are immutable for the life of a
8414
+ * session — this is the only way to change them. Omitting `models`
8415
+ * (or sending an empty object) resets to "use server defaults for
8416
+ * every agent". */
8417
+ handleNewSession(models) {
8418
+ clearSession(this.state);
8419
+ this.state.models = models && Object.keys(models).length > 0 ? models : void 0;
8420
+ saveSession(this.state);
8421
+ return {};
8422
+ }
8364
8423
  /** Cancel the running turn and drain the queue. Returns the drained items. */
8365
8424
  handleCancel() {
8366
8425
  if (this.currentAbort) {
@@ -8434,6 +8493,7 @@ var init_headless = __esm({
8434
8493
  totalMessageCount: total,
8435
8494
  running: this.running,
8436
8495
  ...this.running && this.currentRequestId ? { currentRequestId: this.currentRequestId } : {},
8496
+ ...this.state.models && { models: this.state.models },
8437
8497
  ...this.queueFields()
8438
8498
  }));
8439
8499
  return;
@@ -8446,6 +8506,15 @@ var init_headless = __esm({
8446
8506
  );
8447
8507
  return;
8448
8508
  }
8509
+ if (action === "newSession") {
8510
+ const models = parsed.models;
8511
+ this.dispatchSimple(
8512
+ requestId,
8513
+ "session_cleared",
8514
+ () => this.handleNewSession(models)
8515
+ );
8516
+ return;
8517
+ }
8449
8518
  if (action === "cancel") {
8450
8519
  const cancelled = this.handleCancel();
8451
8520
  this.emit(
@@ -8492,7 +8561,8 @@ var init_headless = __esm({
8492
8561
  try {
8493
8562
  await triggerCompaction(this.state, this.config, {
8494
8563
  blocking: false,
8495
- requestId
8564
+ requestId,
8565
+ model: this.opts.model
8496
8566
  });
8497
8567
  if (!this.running) {
8498
8568
  this.applyPendingSummaries();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindstudio-ai/remy",
3
- "version": "0.1.177",
3
+ "version": "0.1.178",
4
4
  "description": "MindStudio coding agent",
5
5
  "repository": {
6
6
  "type": "git",