newmark-agent 0.5.4 → 0.5.8

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.
@@ -220,6 +220,10 @@
220
220
  <symbol id="minus" viewBox="0 0 24 24">
221
221
  <path d="M5 12h14" />
222
222
  </symbol>
223
+ <symbol id="mouse" viewBox="0 0 24 24">
224
+ <rect x="5" y="2" width="14" height="20" rx="7" />
225
+ <path d="M12 6v4" />
226
+ </symbol>
223
227
  <symbol id="move" viewBox="0 0 24 24">
224
228
  <path d="M12 2v20" />
225
229
  <path d="m15 19-3 3-3-3" />
@@ -328674,7 +328674,7 @@ function providerStreamTimeoutError(timeoutMs) {
328674
328674
  error.message = `Stream read timeout after ${timeoutMs}ms`;
328675
328675
  return error;
328676
328676
  }
328677
- async function readProviderStreamChunk(reader, signal, timeoutMs = 3e4) {
328677
+ async function readProviderStreamChunk(reader, signal, timeoutMs = 0) {
328678
328678
  if (signal.aborted) throw providerAbortError(signal);
328679
328679
  let timer;
328680
328680
  let onAbort;
@@ -328682,9 +328682,9 @@ async function readProviderStreamChunk(reader, signal, timeoutMs = 3e4) {
328682
328682
  onAbort = () => reject(providerAbortError(signal));
328683
328683
  signal.addEventListener("abort", onAbort, { once: true });
328684
328684
  });
328685
- const timeoutPromise = new Promise((_3, reject) => {
328685
+ const timeoutPromise = timeoutMs > 0 ? new Promise((_3, reject) => {
328686
328686
  timer = setTimeout(() => reject(providerStreamTimeoutError(timeoutMs)), timeoutMs);
328687
- });
328687
+ }) : new Promise(() => void 0);
328688
328688
  try {
328689
328689
  return await Promise.race([reader.read(), abortPromise, timeoutPromise]);
328690
328690
  } catch (error) {
@@ -328713,6 +328713,30 @@ function parseProviderSse(raw) {
328713
328713
  }
328714
328714
  return events;
328715
328715
  }
328716
+ function assembleCompatibleToolArguments(parts) {
328717
+ const nonEmpty = (parts || []).map(String).filter((part) => part && part !== "null");
328718
+ if (!nonEmpty.length) return "{}";
328719
+ const isJsonObject = (value) => {
328720
+ try {
328721
+ const parsed = JSON.parse(value);
328722
+ return !!parsed && typeof parsed === "object" && !Array.isArray(parsed);
328723
+ } catch {
328724
+ return false;
328725
+ }
328726
+ };
328727
+ const incremental = nonEmpty.join("");
328728
+ if (isJsonObject(incremental)) return incremental;
328729
+ let compatible = "";
328730
+ for (const incoming of nonEmpty) {
328731
+ if (!compatible) compatible = incoming;
328732
+ else if (incoming === compatible) continue;
328733
+ else if (incoming.startsWith(compatible)) compatible = incoming;
328734
+ else if (compatible.startsWith(incoming)) continue;
328735
+ else compatible += incoming;
328736
+ }
328737
+ if (isJsonObject(compatible)) return compatible;
328738
+ return [...nonEmpty].reverse().find(isJsonObject) || compatible;
328739
+ }
328716
328740
  function isContentPolicyBlocked(json) {
328717
328741
  const choices = Array.isArray(json.choices) ? json.choices : [];
328718
328742
  const choice = choices[0] || {};
@@ -328941,6 +328965,8 @@ var ChatCompletionsAdapter = class {
328941
328965
  let contentPolicyBlocked = false;
328942
328966
  let emittedContent = false;
328943
328967
  let emittedTool = false;
328968
+ let emittedReasoning = false;
328969
+ let explicitCompletion = false;
328944
328970
  try {
328945
328971
  while (true) {
328946
328972
  const { done, value } = await readProviderStreamChunk(reader, signal);
@@ -328952,7 +328978,10 @@ var ChatCompletionsAdapter = class {
328952
328978
  const trimmed = line.trim();
328953
328979
  if (!trimmed.startsWith("data: ")) continue;
328954
328980
  const data = trimmed.slice(6);
328955
- if (data === "[DONE]") continue;
328981
+ if (data === "[DONE]") {
328982
+ explicitCompletion = true;
328983
+ continue;
328984
+ }
328956
328985
  let json;
328957
328986
  try {
328958
328987
  json = JSON.parse(data);
@@ -328965,11 +328994,16 @@ var ChatCompletionsAdapter = class {
328965
328994
  }
328966
328995
  if (isContentPolicyBlocked(json)) contentPolicyBlocked = true;
328967
328996
  const choices = Array.isArray(json.choices) ? json.choices : [];
328968
- const delta = choices[0]?.delta;
328997
+ const choice = choices[0];
328998
+ if (choice?.finish_reason !== void 0 && choice.finish_reason !== null) explicitCompletion = true;
328999
+ const delta = choice?.delta;
328969
329000
  if (!delta) continue;
328970
329001
  if (delta.reasoning_content) {
328971
329002
  const reasoning = this.extractText(delta.reasoning_content);
328972
- if (reasoning) yield { type: "reasoning.summary.delta", delta: reasoning };
329003
+ if (reasoning) {
329004
+ emittedReasoning = true;
329005
+ yield { type: "reasoning.summary.delta", delta: reasoning };
329006
+ }
328973
329007
  }
328974
329008
  const textDelta = this.extractText(delta.content);
328975
329009
  if (textDelta) {
@@ -329014,13 +329048,17 @@ var ChatCompletionsAdapter = class {
329014
329048
  type: "tool_call.completed",
329015
329049
  id: currentToolCall.id,
329016
329050
  name: currentToolCall.name,
329017
- arguments: currentToolCall.argumentParts.join("")
329051
+ arguments: assembleCompatibleToolArguments(currentToolCall.argumentParts)
329018
329052
  };
329019
329053
  }
329020
329054
  } else if (!emittedContent && !emittedTool && contentPolicyBlocked) {
329021
329055
  yield { type: "response.failed", error: "[Error] Content policy refusal (content_filter)." };
329022
329056
  return;
329023
329057
  }
329058
+ if (!explicitCompletion && !emittedContent && !emittedTool && !emittedReasoning) {
329059
+ yield { type: "response.failed", error: "[LLM Error] Chat stream ended before an explicit completion." };
329060
+ return;
329061
+ }
329024
329062
  yield { type: "response.completed" };
329025
329063
  } finally {
329026
329064
  reader.releaseLock();
@@ -329217,6 +329255,7 @@ var ResponsesAdapter = class {
329217
329255
  const key3 = `${String(payload.item_id || "")}:${String(payload.summary_index || 0)}`;
329218
329256
  const delta = this.extractText(payload.delta);
329219
329257
  if (delta) {
329258
+ emittedContent = true;
329220
329259
  reasoningSummaries.set(key3, (reasoningSummaries.get(key3) || "") + delta);
329221
329260
  yield { type: "reasoning.summary.delta", delta };
329222
329261
  }
@@ -329247,7 +329286,7 @@ var ResponsesAdapter = class {
329247
329286
  calls.set(key3, {
329248
329287
  id: String(item.call_id || item.id || key3),
329249
329288
  name: String(item.name || ""),
329250
- arguments: String(item.arguments || ""),
329289
+ argumentParts: item.arguments ? [String(item.arguments)] : [],
329251
329290
  emitted: false
329252
329291
  });
329253
329292
  }
@@ -329255,9 +329294,9 @@ var ResponsesAdapter = class {
329255
329294
  }
329256
329295
  if (eventType === "response.function_call_arguments.delta") {
329257
329296
  const key3 = String(payload.item_id || payload.call_id || payload.output_index || "");
329258
- const call = calls.get(key3) || { id: String(payload.call_id || key3), name: String(payload.name || ""), arguments: "", emitted: false };
329297
+ const call = calls.get(key3) || { id: String(payload.call_id || key3), name: String(payload.name || ""), argumentParts: [], emitted: false };
329259
329298
  const delta = String(payload.delta || "");
329260
- call.arguments += delta;
329299
+ if (delta) call.argumentParts.push(delta);
329261
329300
  calls.set(key3, call);
329262
329301
  yield { type: "tool_call.arguments.delta", id: call.id, delta };
329263
329302
  continue;
@@ -329269,19 +329308,20 @@ var ResponsesAdapter = class {
329269
329308
  const call = calls.get(key3) || {
329270
329309
  id: String(item.call_id || item.id || key3),
329271
329310
  name: String(item.name || ""),
329272
- arguments: String(item.arguments || ""),
329311
+ argumentParts: item.arguments ? [String(item.arguments)] : [],
329273
329312
  emitted: false
329274
329313
  };
329275
329314
  call.id = String(item.call_id || call.id);
329276
329315
  call.name = String(item.name || call.name);
329277
- call.arguments = typeof item.arguments === "string" ? item.arguments : call.arguments;
329316
+ if (typeof item.arguments === "string" && item.arguments) call.argumentParts.push(item.arguments);
329278
329317
  if (!call.emitted) {
329279
329318
  call.emitted = true;
329319
+ const argumentsJson = assembleCompatibleToolArguments(call.argumentParts);
329280
329320
  yield { type: "tool_call.started", id: call.id, name: call.name };
329281
- if (call.arguments && call.arguments !== "{}") {
329282
- yield { type: "tool_call.arguments.delta", id: call.id, delta: call.arguments };
329321
+ if (argumentsJson !== "{}") {
329322
+ yield { type: "tool_call.arguments.delta", id: call.id, delta: argumentsJson };
329283
329323
  }
329284
- yield { type: "tool_call.completed", id: call.id, name: call.name, arguments: call.arguments };
329324
+ yield { type: "tool_call.completed", id: call.id, name: call.name, arguments: argumentsJson };
329285
329325
  }
329286
329326
  calls.set(key3, call);
329287
329327
  }
@@ -329306,8 +329346,14 @@ var ResponsesAdapter = class {
329306
329346
  } else if (!completed) {
329307
329347
  yield { type: "response.failed", error: "[LLM Error] Responses stream ended before response.completed." };
329308
329348
  } else if (!emittedContent && calls.size === 0) {
329309
- yield { type: "response.failed", error: "[Error] Empty Responses stream." };
329349
+ yield { type: "response.failed", error: "[Error] Provider returned an empty response." };
329310
329350
  } else {
329351
+ for (const call of calls.values()) {
329352
+ if (call.emitted) continue;
329353
+ const argumentsJson = assembleCompatibleToolArguments(call.argumentParts);
329354
+ yield { type: "tool_call.started", id: call.id, name: call.name };
329355
+ yield { type: "tool_call.completed", id: call.id, name: call.name, arguments: argumentsJson };
329356
+ }
329311
329357
  yield { type: "response.completed" };
329312
329358
  }
329313
329359
  } finally {
@@ -330186,7 +330232,7 @@ ${responsePath}
330186
330232
  * `provider_adapters_v2` context flag. Request serialization and SSE
330187
330233
  * normalization are delegated to the shared provider adapters while the
330188
330234
  * transport orchestration (loopback node-http, fetch -> node-http fallback,
330189
- * 120s/30s timeouts) and the 4xx Chat -> Responses downgrade stay here.
330235
+ * cancellation-only streaming and the 4xx Chat -> Responses downgrade) stay here.
330190
330236
  * The emitted request body and StreamToken stream are byte-equivalent to
330191
330237
  * the legacy inlined path.
330192
330238
  */
@@ -330323,9 +330369,9 @@ ${responsePath}
330323
330369
  }
330324
330370
  /**
330325
330371
  * Loopback-aware transport injected into adapter `execute`. Streaming
330326
- * requests retain the fetch-to-node fallback for transport failures, while
330327
- * a local deadline is returned directly so one request cannot become a
330328
- * second Windows fallback request.
330372
+ * requests retain the fetch-to-node fallback for transport failures. They
330373
+ * have no response deadline; only caller cancellation or a concrete
330374
+ * transport/provider failure may end the request.
330329
330375
  */
330330
330376
  buildProviderAdapterTransport() {
330331
330377
  return async (request, signal) => {
@@ -330335,7 +330381,7 @@ ${responsePath}
330335
330381
  const forwardAbort = () => abort.abort(signal?.reason);
330336
330382
  if (signal?.aborted) forwardAbort();
330337
330383
  else signal?.addEventListener("abort", forwardAbort, { once: true });
330338
- const effectiveTimeout = this.effectiveRequestTimeout(12e4);
330384
+ const effectiveTimeout = 0;
330339
330385
  const timer = effectiveTimeout > 0 ? setTimeout(() => abort.abort(providerTimeoutError(effectiveTimeout)), effectiveTimeout) : void 0;
330340
330386
  try {
330341
330387
  try {
@@ -330480,7 +330526,7 @@ ${responsePath}
330480
330526
  const forwardAbort = () => abort.abort(signal?.reason);
330481
330527
  if (signal?.aborted) forwardAbort();
330482
330528
  else signal?.addEventListener("abort", forwardAbort, { once: true });
330483
- const effectiveTimeout = this.effectiveRequestTimeout(12e4);
330529
+ const effectiveTimeout = 0;
330484
330530
  const timeout = effectiveTimeout > 0 ? setTimeout(() => abort.abort(providerTimeoutError(effectiveTimeout)), effectiveTimeout) : void 0;
330485
330531
  let reader = null;
330486
330532
  try {
@@ -330512,10 +330558,14 @@ ${responsePath}
330512
330558
  }
330513
330559
  const decoder = new TextDecoder();
330514
330560
  let buffer = "";
330515
- let currentToolCall = null;
330561
+ const toolCalls = /* @__PURE__ */ new Map();
330562
+ const toolCallOrder = [];
330563
+ let syntheticToolIndex = 0;
330564
+ let lastToolIndex = 0;
330516
330565
  let currentReasoningContent = "";
330517
330566
  let contentPolicyBlocked = false;
330518
330567
  let emittedContent = false;
330568
+ let explicitCompletion = false;
330519
330569
  const streamSignal = signal || new AbortController().signal;
330520
330570
  while (true) {
330521
330571
  const { done, value } = await readProviderStreamChunk(reader, streamSignal);
@@ -330527,7 +330577,10 @@ ${responsePath}
330527
330577
  const trimmed = line.trim();
330528
330578
  if (!trimmed.startsWith("data: ")) continue;
330529
330579
  const data = trimmed.slice(6);
330530
- if (data === "[DONE]") continue;
330580
+ if (data === "[DONE]") {
330581
+ explicitCompletion = true;
330582
+ continue;
330583
+ }
330531
330584
  try {
330532
330585
  const json = JSON.parse(data);
330533
330586
  if (json.usage) yield { type: "usage", text: "", usage: extractProviderUsage(json) };
@@ -330545,24 +330598,44 @@ ${responsePath}
330545
330598
  }
330546
330599
  if (delta.tool_calls) {
330547
330600
  for (const tc of delta.tool_calls) {
330548
- if (tc.id) {
330549
- if (currentToolCall) {
330550
- yield { type: "tool_call", text: "", toolCall: currentToolCall, reasoningContent: currentReasoningContent || void 0 };
330551
- }
330552
- currentToolCall = { id: tc.id, name: tc.function?.name || "", arguments: tc.function?.arguments || "" };
330553
- } else if (tc.function?.arguments && currentToolCall) {
330554
- currentToolCall.arguments += tc.function.arguments;
330601
+ const rawIndex = Number(tc.index);
330602
+ const index = Number.isInteger(rawIndex) && rawIndex >= 0 ? rawIndex : tc.id ? syntheticToolIndex++ : lastToolIndex;
330603
+ lastToolIndex = index;
330604
+ let call = toolCalls.get(index);
330605
+ if (!call && (tc.id || tc.function?.name)) {
330606
+ call = { id: tc.id || "", name: tc.function?.name || "", argumentParts: [] };
330607
+ toolCalls.set(index, call);
330608
+ toolCallOrder.push(index);
330555
330609
  }
330610
+ if (!call) continue;
330611
+ if (tc.id && !call.id) call.id = tc.id;
330612
+ if (tc.function?.name && !call.name) call.name = tc.function.name;
330613
+ if (tc.function?.arguments) call.argumentParts.push(tc.function.arguments);
330556
330614
  }
330557
330615
  }
330558
330616
  } catch {
330559
330617
  }
330560
330618
  }
330561
330619
  }
330562
- if (currentToolCall && currentToolCall.arguments) {
330563
- yield { type: "tool_call", text: "", toolCall: currentToolCall, reasoningContent: currentReasoningContent || void 0 };
330620
+ if (toolCallOrder.length) {
330621
+ for (const index of toolCallOrder) {
330622
+ const call = toolCalls.get(index);
330623
+ if (!call) continue;
330624
+ yield {
330625
+ type: "tool_call",
330626
+ text: "",
330627
+ toolCall: {
330628
+ id: call.id,
330629
+ name: call.name,
330630
+ arguments: assembleCompatibleToolArguments(call.argumentParts)
330631
+ },
330632
+ reasoningContent: currentReasoningContent || void 0
330633
+ };
330634
+ }
330564
330635
  } else if (!emittedContent && contentPolicyBlocked) {
330565
330636
  yield { type: "text", text: "[Error] Content policy refusal (content_filter)." };
330637
+ } else if (!explicitCompletion && !emittedContent && !currentReasoningContent) {
330638
+ yield { type: "text", text: "[LLM Error] GitHub Models stream ended before an explicit completion." };
330566
330639
  }
330567
330640
  } finally {
330568
330641
  reader?.releaseLock();
@@ -340515,6 +340588,22 @@ function createToolchainCore() {
340515
340588
  return { registry: new ToolRegistry(), catalog: new CapabilityCatalog() };
340516
340589
  }
340517
340590
 
340591
+ // src/core/emptyResponseRetry.ts
340592
+ var EMPTY_RESPONSE_RETRY_DELAYS_MS = [200, 800, 2e3, 1e4, 6e4];
340593
+ var MAX_EMPTY_RESPONSE_RETRIES = EMPTY_RESPONSE_RETRY_DELAYS_MS.length;
340594
+ var MAX_CONSECUTIVE_EMPTY_RESPONSES = MAX_EMPTY_RESPONSE_RETRIES + 1;
340595
+ function emptyResponseRetryDelayMs(consecutiveEmptyResponses) {
340596
+ return EMPTY_RESPONSE_RETRY_DELAYS_MS[Math.max(0, consecutiveEmptyResponses - 1)] ?? 0;
340597
+ }
340598
+ function observeEmptyResponseOutcome(consecutiveEmptyResponses, emptyResponse) {
340599
+ const nextCount = emptyResponse ? consecutiveEmptyResponses + 1 : 0;
340600
+ return {
340601
+ consecutiveEmptyResponses: nextCount,
340602
+ retry: emptyResponse && nextCount <= MAX_EMPTY_RESPONSE_RETRIES,
340603
+ terminate: emptyResponse && nextCount > MAX_EMPTY_RESPONSE_RETRIES
340604
+ };
340605
+ }
340606
+
340518
340607
  // src/core/agentKernelRunner.ts
340519
340608
  var publicStreamFilters = /* @__PURE__ */ new WeakMap();
340520
340609
  var brokerOnlyAssistantBuffers = /* @__PURE__ */ new WeakMap();
@@ -340666,7 +340755,7 @@ function kernelTurnFailed(agent, turn) {
340666
340755
  return turn.stopReason === "error" || agent.isLlmErrorText(turn.text);
340667
340756
  }
340668
340757
  function providerTurnIsEmpty(turn) {
340669
- return /provider returned an empty response/i.test(`${turn.errorMessage}
340758
+ return !turn.activity && /provider returned an empty response/i.test(`${turn.errorMessage}
340670
340759
  ${turn.text}`);
340671
340760
  }
340672
340761
  function removeTrailingFailedAssistant(agent, messages) {
@@ -340675,6 +340764,12 @@ function removeTrailingFailedAssistant(agent, messages) {
340675
340764
  const text = KernelMessageText(last);
340676
340765
  if (last.stopReason === "error" || agent.isLlmErrorText(text)) messages.pop();
340677
340766
  }
340767
+ function removeTrailingThoughtOnlyAssistant(messages) {
340768
+ const last = messages[messages.length - 1];
340769
+ if (last?.role !== "assistant") return;
340770
+ const hasToolCall = last.content.some((content) => content.type === "toolCall");
340771
+ if (!KernelMessageText(last).trim() && !hasToolCall) messages.pop();
340772
+ }
340678
340773
  function normalizePublicProviderError(error, secrets = []) {
340679
340774
  let raw = "";
340680
340775
  if (error instanceof Error) {
@@ -340800,8 +340895,17 @@ async function runAgentKernel(agent) {
340800
340895
  const tokens = [];
340801
340896
  const runOnce = async (promptMessages, appendPromptToAgentHistory) => {
340802
340897
  let lastAssistant = null;
340898
+ let observedActivity = false;
340899
+ let observedThought = false;
340803
340900
  const unsubscribe = kernel2.subscribe(async (event) => {
340804
340901
  await handleKernelEvent(agent, event, tokens);
340902
+ if (event.type === "message_update") {
340903
+ const delta = event.assistantMessageEvent;
340904
+ const deltaText = typeof delta.delta === "string" ? delta.delta : "";
340905
+ const thoughtDelta = delta.type === "thinking_delta" && !!deltaText.trim();
340906
+ observedThought = observedThought || thoughtDelta;
340907
+ observedActivity = observedActivity || thoughtDelta || delta.type === "text_delta" && !!deltaText.trim() || delta.type === "toolcall_end";
340908
+ }
340805
340909
  if (event.type === "message_end" && event.message.role === "assistant") {
340806
340910
  lastAssistant = event.message;
340807
340911
  }
@@ -340819,11 +340923,13 @@ async function runAgentKernel(agent) {
340819
340923
  const assistant = lastAssistant;
340820
340924
  const text = assistant ? KernelMessageText(assistant) : "";
340821
340925
  const hasToolCall = !!assistant?.content?.some((content) => content.type === "toolCall");
340822
- const emptyResponse = !assistant || !text.trim() && !hasToolCall && String(assistant?.stopReason || "") !== "aborted";
340926
+ const emptyResponse = !assistant || !text.trim() && !hasToolCall && !observedActivity && String(assistant?.stopReason || "") !== "aborted";
340823
340927
  return {
340824
340928
  text: emptyResponse ? "[Error] Provider returned an empty response." : text,
340825
340929
  stopReason: String(assistant?.stopReason || ""),
340826
- errorMessage: String(assistant?.errorMessage || (emptyResponse ? "Provider returned an empty response." : ""))
340930
+ errorMessage: String(assistant?.errorMessage || (emptyResponse ? "Provider returned an empty response." : "")),
340931
+ activity: observedActivity || !!text.trim() || hasToolCall,
340932
+ thoughtOnly: observedThought && !text.trim() && !hasToolCall && !["error", "aborted"].includes(String(assistant?.stopReason || ""))
340827
340933
  };
340828
340934
  } finally {
340829
340935
  unsubscribe();
@@ -340861,16 +340967,30 @@ async function runAgentKernel(agent) {
340861
340967
  agent.recordWorkStatus("Final visual fallback used: local mini OCR plus conservative text correction.");
340862
340968
  }
340863
340969
  if (modelBeforeKernelRun && modelBeforeKernelRun !== agent.model && !tokens.some((t3) => t3.text?.includes("[Model fallback]"))) {
340864
- tokens.unshift({ type: "text", text: `[Model fallback] ${modelBeforeKernelRun} unavailable; switched to ${agent.model}.` });
340970
+ const notice = `[Model fallback] ${modelBeforeKernelRun} unavailable; switched to ${agent.model}.`;
340971
+ tokens.unshift({ type: "text", text: notice });
340972
+ agent.emitWorkEvent({
340973
+ type: "status",
340974
+ content: notice,
340975
+ fallback: { from: modelBeforeKernelRun, to: agent.model, providerId: agent.activeDeployment()?.providerId }
340976
+ });
340865
340977
  }
340866
- let emptyResponseRetries = 0;
340867
- while (providerTurnIsEmpty(lastTurn) && emptyResponseRetries < 2) {
340978
+ let consecutiveEmptyResponses = 0;
340979
+ for (; ; ) {
340980
+ const emptyResponseState = observeEmptyResponseOutcome(consecutiveEmptyResponses, providerTurnIsEmpty(lastTurn));
340981
+ consecutiveEmptyResponses = emptyResponseState.consecutiveEmptyResponses;
340982
+ if (lastTurn.thoughtOnly) {
340983
+ removeTrailingThoughtOnlyAssistant(kernel2.state.messages);
340984
+ lastTurn = await runWithCompressionResume([], false);
340985
+ continue;
340986
+ }
340987
+ if (!emptyResponseState.retry) break;
340868
340988
  removeTrailingFailedAssistant(agent, kernel2.state.messages);
340869
- emptyResponseRetries += 1;
340870
- const notice = `[Model retry] Provider returned an empty response; retrying the same deployment (${emptyResponseRetries}/2).`;
340989
+ const retryNumber = consecutiveEmptyResponses;
340990
+ const notice = `[Model retry] Provider returned an empty response; retrying the same deployment (${retryNumber}/${MAX_EMPTY_RESPONSE_RETRIES}) after ${emptyResponseRetryDelayMs(consecutiveEmptyResponses)}ms.`;
340871
340991
  tokens.push({ type: "text", text: notice });
340872
340992
  agent.recordWorkStatus(notice);
340873
- await agent.waitForPlannedRouteRetry();
340993
+ await agent.waitForPlannedRouteRetry(emptyResponseRetryDelayMs(consecutiveEmptyResponses));
340874
340994
  lastTurn = await runWithCompressionResume([], false);
340875
340995
  }
340876
340996
  let routeRetries = 0;
@@ -340882,6 +341002,11 @@ async function runAgentKernel(agent) {
340882
341002
  const notice = routeTransitionNotice(agent, previous);
340883
341003
  tokens.push({ type: "text", text: notice });
340884
341004
  agent.recordWorkStatus(notice);
341005
+ agent.emitWorkEvent({
341006
+ type: "status",
341007
+ content: notice,
341008
+ fallback: { from: previous, to: agent.model, providerId: agent.activeDeployment()?.providerId }
341009
+ });
340885
341010
  kernel2.state.model = toKernelModel(agent);
340886
341011
  const fallbackToolSurface = refreshToolSurface(true);
340887
341012
  kernel2.state.systemPrompt = [agent.buildSystemPrompt(), fallbackToolSurface.systemPromptNotice].filter(Boolean).join("\n\n");
@@ -341061,7 +341186,7 @@ async function runAgentKernel(agent) {
341061
341186
  return;
341062
341187
  }
341063
341188
  if (textStarted) finalContent.push({ type: "text", text });
341064
- if (!finalContent.length) {
341189
+ if (!finalContent.length && !thinking.trim()) {
341065
341190
  text = "[Error] Provider returned an empty response.";
341066
341191
  finalContent.push({ type: "text", text });
341067
341192
  }
@@ -343306,14 +343431,16 @@ var AutoRouter = class {
343306
343431
  retryDelayMs
343307
343432
  });
343308
343433
  }
343434
+ if (failure.allowModelFallback === false) return attempts.slice(0, remainingAttempts);
343309
343435
  const selection = decision.requestedSelection;
343310
343436
  if (selection.kind === "fixed") return attempts.slice(0, remainingAttempts);
343311
343437
  const scope = selection.kind === "auto" ? selection.scope : { kind: "provider", providerId: current.providerId };
343312
343438
  const subset = selection.kind === "auto" ? selection.subset : void 0;
343313
343439
  const currentGroup = current.logicalModelGroupId;
343440
+ const currentProviderId = current.providerId;
343314
343441
  const now2 = this.now();
343315
343442
  const attemptedDeployments = decision.attempts.map((attempt) => attempt.deployment);
343316
- const eligible = candidates.filter((candidate) => candidate.enabled && inScope(candidate.deployment, scope) && inSubset(candidate.deployment, subset) && !sameDeployment(candidate.deployment, current) && !attemptedDeployments.some((attempted) => sameDeployment(candidate.deployment, attempted)) && validationEligible(candidate, now2).length === 0 && this.passedInitialHardFilters(decision, candidate) && this.circuitState(candidate.deployment, now2, false) !== "open");
343443
+ const eligible = candidates.filter((candidate) => candidate.enabled && candidate.deployment.providerId === currentProviderId && inScope(candidate.deployment, scope) && inSubset(candidate.deployment, subset) && !sameDeployment(candidate.deployment, current) && !attemptedDeployments.some((attempted) => sameDeployment(candidate.deployment, attempted)) && validationEligible(candidate, now2).length === 0 && this.passedInitialHardFilters(decision, candidate) && this.circuitState(candidate.deployment, now2, false) !== "open");
343317
343444
  const equivalent = currentGroup ? eligible.find((candidate) => candidate.deployment.logicalModelGroupId === currentGroup && !candidate.fallbackOnly) : void 0;
343318
343445
  const fallback = eligible.find((candidate) => candidate.fallbackOnly);
343319
343446
  const rankedAlternates = decision.rankedCandidates.map((ranked) => eligible.find((candidate) => sameDeployment(candidate.deployment, ranked.deployment))).filter((candidate) => !!candidate && candidate !== equivalent && candidate !== fallback && !candidate.fallbackOnly);
@@ -345643,7 +345770,12 @@ var Agent4 = class _Agent {
345643
345770
  beginRouteAttempt() {
345644
345771
  this.routeAttemptStartedAt = Date.now();
345645
345772
  }
345646
- async waitForPlannedRouteRetry() {
345773
+ async waitForPlannedRouteRetry(explicitDelayMs) {
345774
+ if (explicitDelayMs !== void 0) {
345775
+ if (explicitDelayMs <= 0) return;
345776
+ await new Promise((resolve16) => setTimeout(resolve16, explicitDelayMs));
345777
+ return;
345778
+ }
345647
345779
  const waitBudgetMs = Math.max(0, Math.min(15e3, this.lastRouteDecision?.retryBudgetMs ?? 5e3));
345648
345780
  const delay = Math.max(0, Math.min(waitBudgetMs, this.lastRouteRetryDelayMs));
345649
345781
  this.lastRouteRetryDelayMs = 0;
@@ -346751,7 +346883,8 @@ ${String(event.toolArgs || "")}`;
346751
346883
  sequence,
346752
346884
  status: input2.status,
346753
346885
  guide: !isToolEvent && input2.guide ? this.normalizeGuideReceipt(input2.guide) : void 0,
346754
- displayImage: isToolEvent ? this.hydrateDisplayImage(input2.displayImage) : void 0
346886
+ displayImage: isToolEvent ? this.hydrateDisplayImage(input2.displayImage) : void 0,
346887
+ fallback: input2.fallback
346755
346888
  };
346756
346889
  if (activeRun && this.isPersistablePublicWorkEvent(event)) {
346757
346890
  activeRun.sequence = Number(sequence || activeRun.sequence + 1);
@@ -349670,9 +349803,15 @@ ${summary}`, segment, "local-summarize", true);
349670
349803
  }
349671
349804
  compressionBuildBlockStart(messages) {
349672
349805
  const activeRunId = this.currentWorkRunId();
349673
- if (!activeRunId) return 0;
349674
- const index = messages.findIndex((message) => String(message.run_id || message.runId || "") === activeRunId);
349675
- return index >= 0 ? index : 0;
349806
+ if (activeRunId) {
349807
+ const index = messages.findIndex((message) => String(message.run_id || message.runId || "") === activeRunId);
349808
+ if (index >= 0) return index;
349809
+ }
349810
+ let lastRunBoundary = -1;
349811
+ for (let index = 0; index < messages.length; index += 1) {
349812
+ if (String(messages[index]?.run_id || messages[index]?.runId || "")) lastRunBoundary = index + 1;
349813
+ }
349814
+ return lastRunBoundary >= 0 ? lastRunBoundary : messages.length;
349676
349815
  }
349677
349816
  recentContextSuffix(messages, maxMessages, tokenBudget) {
349678
349817
  if (!messages.length) return [];
@@ -350601,17 +350740,12 @@ ${msg.content}
350601
350740
  currentAttempt.sideEffectBoundary = this.routeSideEffectCommitted;
350602
350741
  if (this.routeAttemptStartedAt > 0) currentAttempt.durationMs = Math.max(0, Date.now() - this.routeAttemptStartedAt);
350603
350742
  }
350604
- if (!fallbackEnabled) {
350605
- this.lastRouteDecision.finalStatus = "failed";
350606
- this.routeAttemptStartedAt = 0;
350607
- this.persistRouteDecision(this.lastRouteDecision);
350608
- return null;
350609
- }
350610
350743
  if (!this.pendingAutoAttempts.length) {
350611
350744
  this.pendingAutoAttempts = this.autoRouter.planAttempts(this.lastRouteDecision, this.autoRouteCandidates(), {
350612
350745
  error: failure,
350613
350746
  streamCommitted: this.routeStreamCommitted,
350614
- sideEffectCommitted: this.routeSideEffectCommitted
350747
+ sideEffectCommitted: this.routeSideEffectCommitted,
350748
+ allowModelFallback: fallbackEnabled
350615
350749
  });
350616
350750
  }
350617
350751
  const next2 = this.pendingAutoAttempts.shift();
@@ -350646,7 +350780,6 @@ ${msg.content}
350646
350780
  if (!next?.name) return null;
350647
350781
  this.model = next.name;
350648
350782
  this.fixedDeployment = this.deploymentRef(next);
350649
- if (next.provider) this.config.set("models", "auto_switch_anchor_provider", next.provider);
350650
350783
  return current;
350651
350784
  }
350652
350785
  isLlmErrorText(text) {
@@ -350654,12 +350787,10 @@ ${msg.content}
350654
350787
  }
350655
350788
  scopedSwitchModels(currentModelName) {
350656
350789
  const all = this.config.allModels();
350657
- if (this.config.autoSwitchScope() !== "provider") return all;
350658
- const current = currentModelName === "auto" ? void 0 : this.config.findModel(currentModelName);
350659
- const providerId = current?.provider_id || this.config.autoSwitchAnchorProvider() || this.config.findModel(this.config.getStr("models", "default_model"))?.provider_id || all[0]?.provider_id || "";
350660
- if (!providerId) return all;
350661
- const provider = this.config.findProvider(providerId);
350662
- return all.filter((m2) => m2.provider_id === (provider?.id || providerId));
350790
+ const current = currentModelName === "auto" ? this.activeModelConfig() : this.config.findModel(currentModelName);
350791
+ const providerId = current?.provider_id || this.fixedDeployment?.providerId || (currentModelName !== "auto" ? this.activeDeployment()?.providerId : void 0);
350792
+ if (!providerId) return [];
350793
+ return all.filter((m2) => m2.provider_id === providerId);
350663
350794
  }
350664
350795
  async validateModels(selectedNames, options = {}) {
350665
350796
  if (this.modelValidationPromise) return this.modelValidationPromise;
@@ -352041,7 +352172,10 @@ ${items.map((item) => this.formatAutomation(item)).join("\n")}`;
352041
352172
  if (!sa) return "[Subagent] Not found.";
352042
352173
  const requestedModel = this.normalizeSubagentModelSelection(sa.model && sa.model !== "default" ? sa.model : this.modelSelectionValue() || this.model);
352043
352174
  const requestedDeployment = parseDeploymentSelectionValue2(requestedModel);
352044
- const assignedModel = requestedModel === "auto" ? this.activeModelConfig() : requestedDeployment ? this.config.findDeployment(requestedDeployment) : this.config.findModel(requestedModel);
352175
+ let assignedModel = requestedModel === "auto" ? this.activeModelConfig() : requestedDeployment ? this.config.findDeployment(requestedDeployment) : this.config.findModel(requestedModel);
352176
+ if (!assignedModel && !requestedDeployment && requestedModel !== "auto") {
352177
+ assignedModel = this.activeModelConfig();
352178
+ }
352045
352179
  const model = assignedModel?.name || (requestedModel === "auto" ? this.activeModelName() : requestedModel);
352046
352180
  const activeModel = this.activeModelConfig();
352047
352181
  const activeProvider = this.engineModel();
@@ -353470,6 +353604,32 @@ var ConversationKernel = class {
353470
353604
  followUp: queued?.followUp.slice() || []
353471
353605
  };
353472
353606
  }
353607
+ /**
353608
+ * dev-0.5.6: 排队 followUp 消息 drain 时转成普通用户消息载荷。
353609
+ *
353610
+ * 入队时(enqueueSameSession)为去重/编辑保留了 clientMessageId,并把
353611
+ * runId 固定为入队时运行的 runId;若原样传给 process,写入 chatMessages
353612
+ * 的用户消息会携带旧 runId + clientMessageId,渲染端(PC renderTranscript /
353613
+ * 移动端 projectRemoteConversationItems)会把「clientMessageId + runId 命中
353614
+ * workRuns」的消息误判为 guide 消息而跳过气泡,导致排队自动发送的消息不
353615
+ * 显示在用户输入时间线。清除这两个字段后 process 走普通用户消息分支
353616
+ * (chatMessages.push,mode 取 visibleMode,runId 取当前 run)。
353617
+ */
353618
+ drainQueuedFollowUpMessage(message) {
353619
+ if (typeof message === "string") return message;
353620
+ const text = String(message.text || "");
353621
+ const images = message.images;
353622
+ const attachments = message.attachments;
353623
+ const visible = message.visibleUserInput;
353624
+ const visibleMode = message.visibleMode;
353625
+ return {
353626
+ text,
353627
+ ...images?.length ? { images } : {},
353628
+ ...attachments?.length ? { attachments } : {},
353629
+ ...visible ? { visibleUserInput: visible } : {},
353630
+ ...visibleMode ? { visibleMode } : {}
353631
+ };
353632
+ }
353473
353633
  queueItems(target) {
353474
353634
  const runtime = this.findRuntime(target);
353475
353635
  if (!runtime) return [];
@@ -354211,7 +354371,7 @@ ${batchText}`,
354211
354371
  };
354212
354372
  lastTokens = await this.runSingle(runtime, batchMessage, "steer");
354213
354373
  } else {
354214
- lastTokens = await this.runSingle(runtime, next.message, next.queueMode);
354374
+ lastTokens = await this.runSingle(runtime, this.drainQueuedFollowUpMessage(next.message), next.queueMode);
354215
354375
  }
354216
354376
  }
354217
354377
  const rootMessage = runtime.runner.subagents.readRootInbox()[0];
@@ -354670,7 +354830,11 @@ ${text}`;
354670
354830
  type: "queue_update",
354671
354831
  content: "Conversation queue updated.",
354672
354832
  conversationId: runtime.id,
354673
- queue: this.queued(runtime.target)
354833
+ queue: this.queued(runtime.target),
354834
+ // Structured rows with stable kernel ids so every consumer (PC UI and
354835
+ // the paired mobile client) can render/edit/delete the same items.
354836
+ queueItems: this.queueItems(runtime.target),
354837
+ queuePaused: runtime.queuePaused === true
354674
354838
  });
354675
354839
  }
354676
354840
  clearQueued(runtime) {