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.
@@ -328670,7 +328670,7 @@ function providerStreamTimeoutError(timeoutMs) {
328670
328670
  error.message = `Stream read timeout after ${timeoutMs}ms`;
328671
328671
  return error;
328672
328672
  }
328673
- async function readProviderStreamChunk(reader, signal, timeoutMs = 3e4) {
328673
+ async function readProviderStreamChunk(reader, signal, timeoutMs = 0) {
328674
328674
  if (signal.aborted) throw providerAbortError(signal);
328675
328675
  let timer;
328676
328676
  let onAbort;
@@ -328678,9 +328678,9 @@ async function readProviderStreamChunk(reader, signal, timeoutMs = 3e4) {
328678
328678
  onAbort = () => reject(providerAbortError(signal));
328679
328679
  signal.addEventListener("abort", onAbort, { once: true });
328680
328680
  });
328681
- const timeoutPromise = new Promise((_3, reject) => {
328681
+ const timeoutPromise = timeoutMs > 0 ? new Promise((_3, reject) => {
328682
328682
  timer = setTimeout(() => reject(providerStreamTimeoutError(timeoutMs)), timeoutMs);
328683
- });
328683
+ }) : new Promise(() => void 0);
328684
328684
  try {
328685
328685
  return await Promise.race([reader.read(), abortPromise, timeoutPromise]);
328686
328686
  } catch (error) {
@@ -328709,6 +328709,30 @@ function parseProviderSse(raw) {
328709
328709
  }
328710
328710
  return events;
328711
328711
  }
328712
+ function assembleCompatibleToolArguments(parts) {
328713
+ const nonEmpty = (parts || []).map(String).filter((part) => part && part !== "null");
328714
+ if (!nonEmpty.length) return "{}";
328715
+ const isJsonObject = (value) => {
328716
+ try {
328717
+ const parsed = JSON.parse(value);
328718
+ return !!parsed && typeof parsed === "object" && !Array.isArray(parsed);
328719
+ } catch {
328720
+ return false;
328721
+ }
328722
+ };
328723
+ const incremental = nonEmpty.join("");
328724
+ if (isJsonObject(incremental)) return incremental;
328725
+ let compatible = "";
328726
+ for (const incoming of nonEmpty) {
328727
+ if (!compatible) compatible = incoming;
328728
+ else if (incoming === compatible) continue;
328729
+ else if (incoming.startsWith(compatible)) compatible = incoming;
328730
+ else if (compatible.startsWith(incoming)) continue;
328731
+ else compatible += incoming;
328732
+ }
328733
+ if (isJsonObject(compatible)) return compatible;
328734
+ return [...nonEmpty].reverse().find(isJsonObject) || compatible;
328735
+ }
328712
328736
  function isContentPolicyBlocked(json) {
328713
328737
  const choices = Array.isArray(json.choices) ? json.choices : [];
328714
328738
  const choice = choices[0] || {};
@@ -328937,6 +328961,8 @@ var ChatCompletionsAdapter = class {
328937
328961
  let contentPolicyBlocked = false;
328938
328962
  let emittedContent = false;
328939
328963
  let emittedTool = false;
328964
+ let emittedReasoning = false;
328965
+ let explicitCompletion = false;
328940
328966
  try {
328941
328967
  while (true) {
328942
328968
  const { done, value } = await readProviderStreamChunk(reader, signal);
@@ -328948,7 +328974,10 @@ var ChatCompletionsAdapter = class {
328948
328974
  const trimmed = line.trim();
328949
328975
  if (!trimmed.startsWith("data: ")) continue;
328950
328976
  const data = trimmed.slice(6);
328951
- if (data === "[DONE]") continue;
328977
+ if (data === "[DONE]") {
328978
+ explicitCompletion = true;
328979
+ continue;
328980
+ }
328952
328981
  let json;
328953
328982
  try {
328954
328983
  json = JSON.parse(data);
@@ -328961,11 +328990,16 @@ var ChatCompletionsAdapter = class {
328961
328990
  }
328962
328991
  if (isContentPolicyBlocked(json)) contentPolicyBlocked = true;
328963
328992
  const choices = Array.isArray(json.choices) ? json.choices : [];
328964
- const delta = choices[0]?.delta;
328993
+ const choice = choices[0];
328994
+ if (choice?.finish_reason !== void 0 && choice.finish_reason !== null) explicitCompletion = true;
328995
+ const delta = choice?.delta;
328965
328996
  if (!delta) continue;
328966
328997
  if (delta.reasoning_content) {
328967
328998
  const reasoning = this.extractText(delta.reasoning_content);
328968
- if (reasoning) yield { type: "reasoning.summary.delta", delta: reasoning };
328999
+ if (reasoning) {
329000
+ emittedReasoning = true;
329001
+ yield { type: "reasoning.summary.delta", delta: reasoning };
329002
+ }
328969
329003
  }
328970
329004
  const textDelta = this.extractText(delta.content);
328971
329005
  if (textDelta) {
@@ -329010,13 +329044,17 @@ var ChatCompletionsAdapter = class {
329010
329044
  type: "tool_call.completed",
329011
329045
  id: currentToolCall.id,
329012
329046
  name: currentToolCall.name,
329013
- arguments: currentToolCall.argumentParts.join("")
329047
+ arguments: assembleCompatibleToolArguments(currentToolCall.argumentParts)
329014
329048
  };
329015
329049
  }
329016
329050
  } else if (!emittedContent && !emittedTool && contentPolicyBlocked) {
329017
329051
  yield { type: "response.failed", error: "[Error] Content policy refusal (content_filter)." };
329018
329052
  return;
329019
329053
  }
329054
+ if (!explicitCompletion && !emittedContent && !emittedTool && !emittedReasoning) {
329055
+ yield { type: "response.failed", error: "[LLM Error] Chat stream ended before an explicit completion." };
329056
+ return;
329057
+ }
329020
329058
  yield { type: "response.completed" };
329021
329059
  } finally {
329022
329060
  reader.releaseLock();
@@ -329213,6 +329251,7 @@ var ResponsesAdapter = class {
329213
329251
  const key3 = `${String(payload.item_id || "")}:${String(payload.summary_index || 0)}`;
329214
329252
  const delta = this.extractText(payload.delta);
329215
329253
  if (delta) {
329254
+ emittedContent = true;
329216
329255
  reasoningSummaries.set(key3, (reasoningSummaries.get(key3) || "") + delta);
329217
329256
  yield { type: "reasoning.summary.delta", delta };
329218
329257
  }
@@ -329243,7 +329282,7 @@ var ResponsesAdapter = class {
329243
329282
  calls.set(key3, {
329244
329283
  id: String(item.call_id || item.id || key3),
329245
329284
  name: String(item.name || ""),
329246
- arguments: String(item.arguments || ""),
329285
+ argumentParts: item.arguments ? [String(item.arguments)] : [],
329247
329286
  emitted: false
329248
329287
  });
329249
329288
  }
@@ -329251,9 +329290,9 @@ var ResponsesAdapter = class {
329251
329290
  }
329252
329291
  if (eventType === "response.function_call_arguments.delta") {
329253
329292
  const key3 = String(payload.item_id || payload.call_id || payload.output_index || "");
329254
- const call = calls.get(key3) || { id: String(payload.call_id || key3), name: String(payload.name || ""), arguments: "", emitted: false };
329293
+ const call = calls.get(key3) || { id: String(payload.call_id || key3), name: String(payload.name || ""), argumentParts: [], emitted: false };
329255
329294
  const delta = String(payload.delta || "");
329256
- call.arguments += delta;
329295
+ if (delta) call.argumentParts.push(delta);
329257
329296
  calls.set(key3, call);
329258
329297
  yield { type: "tool_call.arguments.delta", id: call.id, delta };
329259
329298
  continue;
@@ -329265,19 +329304,20 @@ var ResponsesAdapter = class {
329265
329304
  const call = calls.get(key3) || {
329266
329305
  id: String(item.call_id || item.id || key3),
329267
329306
  name: String(item.name || ""),
329268
- arguments: String(item.arguments || ""),
329307
+ argumentParts: item.arguments ? [String(item.arguments)] : [],
329269
329308
  emitted: false
329270
329309
  };
329271
329310
  call.id = String(item.call_id || call.id);
329272
329311
  call.name = String(item.name || call.name);
329273
- call.arguments = typeof item.arguments === "string" ? item.arguments : call.arguments;
329312
+ if (typeof item.arguments === "string" && item.arguments) call.argumentParts.push(item.arguments);
329274
329313
  if (!call.emitted) {
329275
329314
  call.emitted = true;
329315
+ const argumentsJson = assembleCompatibleToolArguments(call.argumentParts);
329276
329316
  yield { type: "tool_call.started", id: call.id, name: call.name };
329277
- if (call.arguments && call.arguments !== "{}") {
329278
- yield { type: "tool_call.arguments.delta", id: call.id, delta: call.arguments };
329317
+ if (argumentsJson !== "{}") {
329318
+ yield { type: "tool_call.arguments.delta", id: call.id, delta: argumentsJson };
329279
329319
  }
329280
- yield { type: "tool_call.completed", id: call.id, name: call.name, arguments: call.arguments };
329320
+ yield { type: "tool_call.completed", id: call.id, name: call.name, arguments: argumentsJson };
329281
329321
  }
329282
329322
  calls.set(key3, call);
329283
329323
  }
@@ -329302,8 +329342,14 @@ var ResponsesAdapter = class {
329302
329342
  } else if (!completed) {
329303
329343
  yield { type: "response.failed", error: "[LLM Error] Responses stream ended before response.completed." };
329304
329344
  } else if (!emittedContent && calls.size === 0) {
329305
- yield { type: "response.failed", error: "[Error] Empty Responses stream." };
329345
+ yield { type: "response.failed", error: "[Error] Provider returned an empty response." };
329306
329346
  } else {
329347
+ for (const call of calls.values()) {
329348
+ if (call.emitted) continue;
329349
+ const argumentsJson = assembleCompatibleToolArguments(call.argumentParts);
329350
+ yield { type: "tool_call.started", id: call.id, name: call.name };
329351
+ yield { type: "tool_call.completed", id: call.id, name: call.name, arguments: argumentsJson };
329352
+ }
329307
329353
  yield { type: "response.completed" };
329308
329354
  }
329309
329355
  } finally {
@@ -330182,7 +330228,7 @@ ${responsePath}
330182
330228
  * `provider_adapters_v2` context flag. Request serialization and SSE
330183
330229
  * normalization are delegated to the shared provider adapters while the
330184
330230
  * transport orchestration (loopback node-http, fetch -> node-http fallback,
330185
- * 120s/30s timeouts) and the 4xx Chat -> Responses downgrade stay here.
330231
+ * cancellation-only streaming and the 4xx Chat -> Responses downgrade) stay here.
330186
330232
  * The emitted request body and StreamToken stream are byte-equivalent to
330187
330233
  * the legacy inlined path.
330188
330234
  */
@@ -330319,9 +330365,9 @@ ${responsePath}
330319
330365
  }
330320
330366
  /**
330321
330367
  * Loopback-aware transport injected into adapter `execute`. Streaming
330322
- * requests retain the fetch-to-node fallback for transport failures, while
330323
- * a local deadline is returned directly so one request cannot become a
330324
- * second Windows fallback request.
330368
+ * requests retain the fetch-to-node fallback for transport failures. They
330369
+ * have no response deadline; only caller cancellation or a concrete
330370
+ * transport/provider failure may end the request.
330325
330371
  */
330326
330372
  buildProviderAdapterTransport() {
330327
330373
  return async (request, signal) => {
@@ -330331,7 +330377,7 @@ ${responsePath}
330331
330377
  const forwardAbort = () => abort.abort(signal?.reason);
330332
330378
  if (signal?.aborted) forwardAbort();
330333
330379
  else signal?.addEventListener("abort", forwardAbort, { once: true });
330334
- const effectiveTimeout = this.effectiveRequestTimeout(12e4);
330380
+ const effectiveTimeout = 0;
330335
330381
  const timer = effectiveTimeout > 0 ? setTimeout(() => abort.abort(providerTimeoutError(effectiveTimeout)), effectiveTimeout) : void 0;
330336
330382
  try {
330337
330383
  try {
@@ -330476,7 +330522,7 @@ ${responsePath}
330476
330522
  const forwardAbort = () => abort.abort(signal?.reason);
330477
330523
  if (signal?.aborted) forwardAbort();
330478
330524
  else signal?.addEventListener("abort", forwardAbort, { once: true });
330479
- const effectiveTimeout = this.effectiveRequestTimeout(12e4);
330525
+ const effectiveTimeout = 0;
330480
330526
  const timeout = effectiveTimeout > 0 ? setTimeout(() => abort.abort(providerTimeoutError(effectiveTimeout)), effectiveTimeout) : void 0;
330481
330527
  let reader = null;
330482
330528
  try {
@@ -330508,10 +330554,14 @@ ${responsePath}
330508
330554
  }
330509
330555
  const decoder = new TextDecoder();
330510
330556
  let buffer = "";
330511
- let currentToolCall = null;
330557
+ const toolCalls = /* @__PURE__ */ new Map();
330558
+ const toolCallOrder = [];
330559
+ let syntheticToolIndex = 0;
330560
+ let lastToolIndex = 0;
330512
330561
  let currentReasoningContent = "";
330513
330562
  let contentPolicyBlocked = false;
330514
330563
  let emittedContent = false;
330564
+ let explicitCompletion = false;
330515
330565
  const streamSignal = signal || new AbortController().signal;
330516
330566
  while (true) {
330517
330567
  const { done, value } = await readProviderStreamChunk(reader, streamSignal);
@@ -330523,7 +330573,10 @@ ${responsePath}
330523
330573
  const trimmed = line.trim();
330524
330574
  if (!trimmed.startsWith("data: ")) continue;
330525
330575
  const data = trimmed.slice(6);
330526
- if (data === "[DONE]") continue;
330576
+ if (data === "[DONE]") {
330577
+ explicitCompletion = true;
330578
+ continue;
330579
+ }
330527
330580
  try {
330528
330581
  const json = JSON.parse(data);
330529
330582
  if (json.usage) yield { type: "usage", text: "", usage: extractProviderUsage(json) };
@@ -330541,24 +330594,44 @@ ${responsePath}
330541
330594
  }
330542
330595
  if (delta.tool_calls) {
330543
330596
  for (const tc of delta.tool_calls) {
330544
- if (tc.id) {
330545
- if (currentToolCall) {
330546
- yield { type: "tool_call", text: "", toolCall: currentToolCall, reasoningContent: currentReasoningContent || void 0 };
330547
- }
330548
- currentToolCall = { id: tc.id, name: tc.function?.name || "", arguments: tc.function?.arguments || "" };
330549
- } else if (tc.function?.arguments && currentToolCall) {
330550
- currentToolCall.arguments += tc.function.arguments;
330597
+ const rawIndex = Number(tc.index);
330598
+ const index = Number.isInteger(rawIndex) && rawIndex >= 0 ? rawIndex : tc.id ? syntheticToolIndex++ : lastToolIndex;
330599
+ lastToolIndex = index;
330600
+ let call = toolCalls.get(index);
330601
+ if (!call && (tc.id || tc.function?.name)) {
330602
+ call = { id: tc.id || "", name: tc.function?.name || "", argumentParts: [] };
330603
+ toolCalls.set(index, call);
330604
+ toolCallOrder.push(index);
330551
330605
  }
330606
+ if (!call) continue;
330607
+ if (tc.id && !call.id) call.id = tc.id;
330608
+ if (tc.function?.name && !call.name) call.name = tc.function.name;
330609
+ if (tc.function?.arguments) call.argumentParts.push(tc.function.arguments);
330552
330610
  }
330553
330611
  }
330554
330612
  } catch {
330555
330613
  }
330556
330614
  }
330557
330615
  }
330558
- if (currentToolCall && currentToolCall.arguments) {
330559
- yield { type: "tool_call", text: "", toolCall: currentToolCall, reasoningContent: currentReasoningContent || void 0 };
330616
+ if (toolCallOrder.length) {
330617
+ for (const index of toolCallOrder) {
330618
+ const call = toolCalls.get(index);
330619
+ if (!call) continue;
330620
+ yield {
330621
+ type: "tool_call",
330622
+ text: "",
330623
+ toolCall: {
330624
+ id: call.id,
330625
+ name: call.name,
330626
+ arguments: assembleCompatibleToolArguments(call.argumentParts)
330627
+ },
330628
+ reasoningContent: currentReasoningContent || void 0
330629
+ };
330630
+ }
330560
330631
  } else if (!emittedContent && contentPolicyBlocked) {
330561
330632
  yield { type: "text", text: "[Error] Content policy refusal (content_filter)." };
330633
+ } else if (!explicitCompletion && !emittedContent && !currentReasoningContent) {
330634
+ yield { type: "text", text: "[LLM Error] GitHub Models stream ended before an explicit completion." };
330562
330635
  }
330563
330636
  } finally {
330564
330637
  reader?.releaseLock();
@@ -340511,6 +340584,22 @@ function createToolchainCore() {
340511
340584
  return { registry: new ToolRegistry(), catalog: new CapabilityCatalog() };
340512
340585
  }
340513
340586
 
340587
+ // src/core/emptyResponseRetry.ts
340588
+ var EMPTY_RESPONSE_RETRY_DELAYS_MS = [200, 800, 2e3, 1e4, 6e4];
340589
+ var MAX_EMPTY_RESPONSE_RETRIES = EMPTY_RESPONSE_RETRY_DELAYS_MS.length;
340590
+ var MAX_CONSECUTIVE_EMPTY_RESPONSES = MAX_EMPTY_RESPONSE_RETRIES + 1;
340591
+ function emptyResponseRetryDelayMs(consecutiveEmptyResponses) {
340592
+ return EMPTY_RESPONSE_RETRY_DELAYS_MS[Math.max(0, consecutiveEmptyResponses - 1)] ?? 0;
340593
+ }
340594
+ function observeEmptyResponseOutcome(consecutiveEmptyResponses, emptyResponse) {
340595
+ const nextCount = emptyResponse ? consecutiveEmptyResponses + 1 : 0;
340596
+ return {
340597
+ consecutiveEmptyResponses: nextCount,
340598
+ retry: emptyResponse && nextCount <= MAX_EMPTY_RESPONSE_RETRIES,
340599
+ terminate: emptyResponse && nextCount > MAX_EMPTY_RESPONSE_RETRIES
340600
+ };
340601
+ }
340602
+
340514
340603
  // src/core/agentKernelRunner.ts
340515
340604
  var publicStreamFilters = /* @__PURE__ */ new WeakMap();
340516
340605
  var brokerOnlyAssistantBuffers = /* @__PURE__ */ new WeakMap();
@@ -340662,7 +340751,7 @@ function kernelTurnFailed(agent, turn) {
340662
340751
  return turn.stopReason === "error" || agent.isLlmErrorText(turn.text);
340663
340752
  }
340664
340753
  function providerTurnIsEmpty(turn) {
340665
- return /provider returned an empty response/i.test(`${turn.errorMessage}
340754
+ return !turn.activity && /provider returned an empty response/i.test(`${turn.errorMessage}
340666
340755
  ${turn.text}`);
340667
340756
  }
340668
340757
  function removeTrailingFailedAssistant(agent, messages) {
@@ -340671,6 +340760,12 @@ function removeTrailingFailedAssistant(agent, messages) {
340671
340760
  const text = KernelMessageText(last);
340672
340761
  if (last.stopReason === "error" || agent.isLlmErrorText(text)) messages.pop();
340673
340762
  }
340763
+ function removeTrailingThoughtOnlyAssistant(messages) {
340764
+ const last = messages[messages.length - 1];
340765
+ if (last?.role !== "assistant") return;
340766
+ const hasToolCall = last.content.some((content) => content.type === "toolCall");
340767
+ if (!KernelMessageText(last).trim() && !hasToolCall) messages.pop();
340768
+ }
340674
340769
  function normalizePublicProviderError(error, secrets = []) {
340675
340770
  let raw = "";
340676
340771
  if (error instanceof Error) {
@@ -340796,8 +340891,17 @@ async function runAgentKernel(agent) {
340796
340891
  const tokens = [];
340797
340892
  const runOnce = async (promptMessages, appendPromptToAgentHistory) => {
340798
340893
  let lastAssistant = null;
340894
+ let observedActivity = false;
340895
+ let observedThought = false;
340799
340896
  const unsubscribe = kernel2.subscribe(async (event) => {
340800
340897
  await handleKernelEvent(agent, event, tokens);
340898
+ if (event.type === "message_update") {
340899
+ const delta = event.assistantMessageEvent;
340900
+ const deltaText = typeof delta.delta === "string" ? delta.delta : "";
340901
+ const thoughtDelta = delta.type === "thinking_delta" && !!deltaText.trim();
340902
+ observedThought = observedThought || thoughtDelta;
340903
+ observedActivity = observedActivity || thoughtDelta || delta.type === "text_delta" && !!deltaText.trim() || delta.type === "toolcall_end";
340904
+ }
340801
340905
  if (event.type === "message_end" && event.message.role === "assistant") {
340802
340906
  lastAssistant = event.message;
340803
340907
  }
@@ -340815,11 +340919,13 @@ async function runAgentKernel(agent) {
340815
340919
  const assistant = lastAssistant;
340816
340920
  const text = assistant ? KernelMessageText(assistant) : "";
340817
340921
  const hasToolCall = !!assistant?.content?.some((content) => content.type === "toolCall");
340818
- const emptyResponse = !assistant || !text.trim() && !hasToolCall && String(assistant?.stopReason || "") !== "aborted";
340922
+ const emptyResponse = !assistant || !text.trim() && !hasToolCall && !observedActivity && String(assistant?.stopReason || "") !== "aborted";
340819
340923
  return {
340820
340924
  text: emptyResponse ? "[Error] Provider returned an empty response." : text,
340821
340925
  stopReason: String(assistant?.stopReason || ""),
340822
- errorMessage: String(assistant?.errorMessage || (emptyResponse ? "Provider returned an empty response." : ""))
340926
+ errorMessage: String(assistant?.errorMessage || (emptyResponse ? "Provider returned an empty response." : "")),
340927
+ activity: observedActivity || !!text.trim() || hasToolCall,
340928
+ thoughtOnly: observedThought && !text.trim() && !hasToolCall && !["error", "aborted"].includes(String(assistant?.stopReason || ""))
340823
340929
  };
340824
340930
  } finally {
340825
340931
  unsubscribe();
@@ -340857,16 +340963,30 @@ async function runAgentKernel(agent) {
340857
340963
  agent.recordWorkStatus("Final visual fallback used: local mini OCR plus conservative text correction.");
340858
340964
  }
340859
340965
  if (modelBeforeKernelRun && modelBeforeKernelRun !== agent.model && !tokens.some((t3) => t3.text?.includes("[Model fallback]"))) {
340860
- tokens.unshift({ type: "text", text: `[Model fallback] ${modelBeforeKernelRun} unavailable; switched to ${agent.model}.` });
340966
+ const notice = `[Model fallback] ${modelBeforeKernelRun} unavailable; switched to ${agent.model}.`;
340967
+ tokens.unshift({ type: "text", text: notice });
340968
+ agent.emitWorkEvent({
340969
+ type: "status",
340970
+ content: notice,
340971
+ fallback: { from: modelBeforeKernelRun, to: agent.model, providerId: agent.activeDeployment()?.providerId }
340972
+ });
340861
340973
  }
340862
- let emptyResponseRetries = 0;
340863
- while (providerTurnIsEmpty(lastTurn) && emptyResponseRetries < 2) {
340974
+ let consecutiveEmptyResponses = 0;
340975
+ for (; ; ) {
340976
+ const emptyResponseState = observeEmptyResponseOutcome(consecutiveEmptyResponses, providerTurnIsEmpty(lastTurn));
340977
+ consecutiveEmptyResponses = emptyResponseState.consecutiveEmptyResponses;
340978
+ if (lastTurn.thoughtOnly) {
340979
+ removeTrailingThoughtOnlyAssistant(kernel2.state.messages);
340980
+ lastTurn = await runWithCompressionResume([], false);
340981
+ continue;
340982
+ }
340983
+ if (!emptyResponseState.retry) break;
340864
340984
  removeTrailingFailedAssistant(agent, kernel2.state.messages);
340865
- emptyResponseRetries += 1;
340866
- const notice = `[Model retry] Provider returned an empty response; retrying the same deployment (${emptyResponseRetries}/2).`;
340985
+ const retryNumber = consecutiveEmptyResponses;
340986
+ const notice = `[Model retry] Provider returned an empty response; retrying the same deployment (${retryNumber}/${MAX_EMPTY_RESPONSE_RETRIES}) after ${emptyResponseRetryDelayMs(consecutiveEmptyResponses)}ms.`;
340867
340987
  tokens.push({ type: "text", text: notice });
340868
340988
  agent.recordWorkStatus(notice);
340869
- await agent.waitForPlannedRouteRetry();
340989
+ await agent.waitForPlannedRouteRetry(emptyResponseRetryDelayMs(consecutiveEmptyResponses));
340870
340990
  lastTurn = await runWithCompressionResume([], false);
340871
340991
  }
340872
340992
  let routeRetries = 0;
@@ -340878,6 +340998,11 @@ async function runAgentKernel(agent) {
340878
340998
  const notice = routeTransitionNotice(agent, previous);
340879
340999
  tokens.push({ type: "text", text: notice });
340880
341000
  agent.recordWorkStatus(notice);
341001
+ agent.emitWorkEvent({
341002
+ type: "status",
341003
+ content: notice,
341004
+ fallback: { from: previous, to: agent.model, providerId: agent.activeDeployment()?.providerId }
341005
+ });
340881
341006
  kernel2.state.model = toKernelModel(agent);
340882
341007
  const fallbackToolSurface = refreshToolSurface(true);
340883
341008
  kernel2.state.systemPrompt = [agent.buildSystemPrompt(), fallbackToolSurface.systemPromptNotice].filter(Boolean).join("\n\n");
@@ -341057,7 +341182,7 @@ async function runAgentKernel(agent) {
341057
341182
  return;
341058
341183
  }
341059
341184
  if (textStarted) finalContent.push({ type: "text", text });
341060
- if (!finalContent.length) {
341185
+ if (!finalContent.length && !thinking.trim()) {
341061
341186
  text = "[Error] Provider returned an empty response.";
341062
341187
  finalContent.push({ type: "text", text });
341063
341188
  }
@@ -343302,14 +343427,16 @@ var AutoRouter = class {
343302
343427
  retryDelayMs
343303
343428
  });
343304
343429
  }
343430
+ if (failure.allowModelFallback === false) return attempts.slice(0, remainingAttempts);
343305
343431
  const selection = decision.requestedSelection;
343306
343432
  if (selection.kind === "fixed") return attempts.slice(0, remainingAttempts);
343307
343433
  const scope = selection.kind === "auto" ? selection.scope : { kind: "provider", providerId: current.providerId };
343308
343434
  const subset = selection.kind === "auto" ? selection.subset : void 0;
343309
343435
  const currentGroup = current.logicalModelGroupId;
343436
+ const currentProviderId = current.providerId;
343310
343437
  const now2 = this.now();
343311
343438
  const attemptedDeployments = decision.attempts.map((attempt) => attempt.deployment);
343312
- 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");
343439
+ 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");
343313
343440
  const equivalent = currentGroup ? eligible.find((candidate) => candidate.deployment.logicalModelGroupId === currentGroup && !candidate.fallbackOnly) : void 0;
343314
343441
  const fallback = eligible.find((candidate) => candidate.fallbackOnly);
343315
343442
  const rankedAlternates = decision.rankedCandidates.map((ranked) => eligible.find((candidate) => sameDeployment(candidate.deployment, ranked.deployment))).filter((candidate) => !!candidate && candidate !== equivalent && candidate !== fallback && !candidate.fallbackOnly);
@@ -345639,7 +345766,12 @@ var Agent4 = class _Agent {
345639
345766
  beginRouteAttempt() {
345640
345767
  this.routeAttemptStartedAt = Date.now();
345641
345768
  }
345642
- async waitForPlannedRouteRetry() {
345769
+ async waitForPlannedRouteRetry(explicitDelayMs) {
345770
+ if (explicitDelayMs !== void 0) {
345771
+ if (explicitDelayMs <= 0) return;
345772
+ await new Promise((resolve16) => setTimeout(resolve16, explicitDelayMs));
345773
+ return;
345774
+ }
345643
345775
  const waitBudgetMs = Math.max(0, Math.min(15e3, this.lastRouteDecision?.retryBudgetMs ?? 5e3));
345644
345776
  const delay = Math.max(0, Math.min(waitBudgetMs, this.lastRouteRetryDelayMs));
345645
345777
  this.lastRouteRetryDelayMs = 0;
@@ -346747,7 +346879,8 @@ ${String(event.toolArgs || "")}`;
346747
346879
  sequence,
346748
346880
  status: input.status,
346749
346881
  guide: !isToolEvent && input.guide ? this.normalizeGuideReceipt(input.guide) : void 0,
346750
- displayImage: isToolEvent ? this.hydrateDisplayImage(input.displayImage) : void 0
346882
+ displayImage: isToolEvent ? this.hydrateDisplayImage(input.displayImage) : void 0,
346883
+ fallback: input.fallback
346751
346884
  };
346752
346885
  if (activeRun && this.isPersistablePublicWorkEvent(event)) {
346753
346886
  activeRun.sequence = Number(sequence || activeRun.sequence + 1);
@@ -349666,9 +349799,15 @@ ${summary}`, segment, "local-summarize", true);
349666
349799
  }
349667
349800
  compressionBuildBlockStart(messages) {
349668
349801
  const activeRunId = this.currentWorkRunId();
349669
- if (!activeRunId) return 0;
349670
- const index = messages.findIndex((message) => String(message.run_id || message.runId || "") === activeRunId);
349671
- return index >= 0 ? index : 0;
349802
+ if (activeRunId) {
349803
+ const index = messages.findIndex((message) => String(message.run_id || message.runId || "") === activeRunId);
349804
+ if (index >= 0) return index;
349805
+ }
349806
+ let lastRunBoundary = -1;
349807
+ for (let index = 0; index < messages.length; index += 1) {
349808
+ if (String(messages[index]?.run_id || messages[index]?.runId || "")) lastRunBoundary = index + 1;
349809
+ }
349810
+ return lastRunBoundary >= 0 ? lastRunBoundary : messages.length;
349672
349811
  }
349673
349812
  recentContextSuffix(messages, maxMessages, tokenBudget) {
349674
349813
  if (!messages.length) return [];
@@ -350597,17 +350736,12 @@ ${msg.content}
350597
350736
  currentAttempt.sideEffectBoundary = this.routeSideEffectCommitted;
350598
350737
  if (this.routeAttemptStartedAt > 0) currentAttempt.durationMs = Math.max(0, Date.now() - this.routeAttemptStartedAt);
350599
350738
  }
350600
- if (!fallbackEnabled) {
350601
- this.lastRouteDecision.finalStatus = "failed";
350602
- this.routeAttemptStartedAt = 0;
350603
- this.persistRouteDecision(this.lastRouteDecision);
350604
- return null;
350605
- }
350606
350739
  if (!this.pendingAutoAttempts.length) {
350607
350740
  this.pendingAutoAttempts = this.autoRouter.planAttempts(this.lastRouteDecision, this.autoRouteCandidates(), {
350608
350741
  error: failure,
350609
350742
  streamCommitted: this.routeStreamCommitted,
350610
- sideEffectCommitted: this.routeSideEffectCommitted
350743
+ sideEffectCommitted: this.routeSideEffectCommitted,
350744
+ allowModelFallback: fallbackEnabled
350611
350745
  });
350612
350746
  }
350613
350747
  const next2 = this.pendingAutoAttempts.shift();
@@ -350642,7 +350776,6 @@ ${msg.content}
350642
350776
  if (!next?.name) return null;
350643
350777
  this.model = next.name;
350644
350778
  this.fixedDeployment = this.deploymentRef(next);
350645
- if (next.provider) this.config.set("models", "auto_switch_anchor_provider", next.provider);
350646
350779
  return current;
350647
350780
  }
350648
350781
  isLlmErrorText(text) {
@@ -350650,12 +350783,10 @@ ${msg.content}
350650
350783
  }
350651
350784
  scopedSwitchModels(currentModelName) {
350652
350785
  const all = this.config.allModels();
350653
- if (this.config.autoSwitchScope() !== "provider") return all;
350654
- const current = currentModelName === "auto" ? void 0 : this.config.findModel(currentModelName);
350655
- const providerId = current?.provider_id || this.config.autoSwitchAnchorProvider() || this.config.findModel(this.config.getStr("models", "default_model"))?.provider_id || all[0]?.provider_id || "";
350656
- if (!providerId) return all;
350657
- const provider = this.config.findProvider(providerId);
350658
- return all.filter((m2) => m2.provider_id === (provider?.id || providerId));
350786
+ const current = currentModelName === "auto" ? this.activeModelConfig() : this.config.findModel(currentModelName);
350787
+ const providerId = current?.provider_id || this.fixedDeployment?.providerId || (currentModelName !== "auto" ? this.activeDeployment()?.providerId : void 0);
350788
+ if (!providerId) return [];
350789
+ return all.filter((m2) => m2.provider_id === providerId);
350659
350790
  }
350660
350791
  async validateModels(selectedNames, options = {}) {
350661
350792
  if (this.modelValidationPromise) return this.modelValidationPromise;
@@ -352037,7 +352168,10 @@ ${items.map((item) => this.formatAutomation(item)).join("\n")}`;
352037
352168
  if (!sa) return "[Subagent] Not found.";
352038
352169
  const requestedModel = this.normalizeSubagentModelSelection(sa.model && sa.model !== "default" ? sa.model : this.modelSelectionValue() || this.model);
352039
352170
  const requestedDeployment = parseDeploymentSelectionValue2(requestedModel);
352040
- const assignedModel = requestedModel === "auto" ? this.activeModelConfig() : requestedDeployment ? this.config.findDeployment(requestedDeployment) : this.config.findModel(requestedModel);
352171
+ let assignedModel = requestedModel === "auto" ? this.activeModelConfig() : requestedDeployment ? this.config.findDeployment(requestedDeployment) : this.config.findModel(requestedModel);
352172
+ if (!assignedModel && !requestedDeployment && requestedModel !== "auto") {
352173
+ assignedModel = this.activeModelConfig();
352174
+ }
352041
352175
  const model = assignedModel?.name || (requestedModel === "auto" ? this.activeModelName() : requestedModel);
352042
352176
  const activeModel = this.activeModelConfig();
352043
352177
  const activeProvider = this.engineModel();
@@ -353466,6 +353600,32 @@ var ConversationKernel = class {
353466
353600
  followUp: queued?.followUp.slice() || []
353467
353601
  };
353468
353602
  }
353603
+ /**
353604
+ * dev-0.5.6: 排队 followUp 消息 drain 时转成普通用户消息载荷。
353605
+ *
353606
+ * 入队时(enqueueSameSession)为去重/编辑保留了 clientMessageId,并把
353607
+ * runId 固定为入队时运行的 runId;若原样传给 process,写入 chatMessages
353608
+ * 的用户消息会携带旧 runId + clientMessageId,渲染端(PC renderTranscript /
353609
+ * 移动端 projectRemoteConversationItems)会把「clientMessageId + runId 命中
353610
+ * workRuns」的消息误判为 guide 消息而跳过气泡,导致排队自动发送的消息不
353611
+ * 显示在用户输入时间线。清除这两个字段后 process 走普通用户消息分支
353612
+ * (chatMessages.push,mode 取 visibleMode,runId 取当前 run)。
353613
+ */
353614
+ drainQueuedFollowUpMessage(message) {
353615
+ if (typeof message === "string") return message;
353616
+ const text = String(message.text || "");
353617
+ const images = message.images;
353618
+ const attachments = message.attachments;
353619
+ const visible = message.visibleUserInput;
353620
+ const visibleMode = message.visibleMode;
353621
+ return {
353622
+ text,
353623
+ ...images?.length ? { images } : {},
353624
+ ...attachments?.length ? { attachments } : {},
353625
+ ...visible ? { visibleUserInput: visible } : {},
353626
+ ...visibleMode ? { visibleMode } : {}
353627
+ };
353628
+ }
353469
353629
  queueItems(target) {
353470
353630
  const runtime = this.findRuntime(target);
353471
353631
  if (!runtime) return [];
@@ -354207,7 +354367,7 @@ ${batchText}`,
354207
354367
  };
354208
354368
  lastTokens = await this.runSingle(runtime, batchMessage, "steer");
354209
354369
  } else {
354210
- lastTokens = await this.runSingle(runtime, next.message, next.queueMode);
354370
+ lastTokens = await this.runSingle(runtime, this.drainQueuedFollowUpMessage(next.message), next.queueMode);
354211
354371
  }
354212
354372
  }
354213
354373
  const rootMessage = runtime.runner.subagents.readRootInbox()[0];
@@ -354666,7 +354826,11 @@ ${text}`;
354666
354826
  type: "queue_update",
354667
354827
  content: "Conversation queue updated.",
354668
354828
  conversationId: runtime.id,
354669
- queue: this.queued(runtime.target)
354829
+ queue: this.queued(runtime.target),
354830
+ // Structured rows with stable kernel ids so every consumer (PC UI and
354831
+ // the paired mobile client) can render/edit/delete the same items.
354832
+ queueItems: this.queueItems(runtime.target),
354833
+ queuePaused: runtime.queuePaused === true
354670
354834
  });
354671
354835
  }
354672
354836
  clearQueued(runtime) {
@@ -464,7 +464,7 @@ export declare class Agent {
464
464
  markRouteToolExecuted(name: string, rawArgs?: string): void;
465
465
  routeTransitionKind(): PlannedRouteAttempt['kind'] | '';
466
466
  beginRouteAttempt(): void;
467
- waitForPlannedRouteRetry(): Promise<void>;
467
+ waitForPlannedRouteRetry(explicitDelayMs?: number): Promise<void>;
468
468
  recordRouteSuccess(latencyMs?: number, throughput?: number): void;
469
469
  recordRouteToolOutcome(valid: boolean): void;
470
470
  updateProviders(value: unknown): void;