newmark-agent 0.4.8 → 0.4.9

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.
@@ -340983,7 +340983,7 @@ async function runAgentKernel(agent) {
340983
340983
  return;
340984
340984
  }
340985
340985
  const publicError = normalizePublicProviderError(error, [currentAgent.activeModelConfig()?.api_key]);
340986
- if (/\b402\b|insufficient balance|insufficient funds|payment required|余额不足/i.test(publicError)) {
340986
+ if (/\b402\b|insufficient balance|insufficient funds|payment required|insufficient[_ -]?quota|quota (?:exceeded|exhausted)|credit(?:s| balance)? (?:exhausted|depleted)|billing hard limit|budget exhausted|余额不足|额度不足|配额(?:不足|耗尽|超限)/i.test(publicError)) {
340987
340987
  currentAgent.noteProviderBalanceFailure();
340988
340988
  }
340989
340989
  const final = assistantMessage2(model, [{ type: "text", text: `[Error] ${publicError}` }], "error");
@@ -343051,8 +343051,8 @@ function classifyRouteFailure(error) {
343051
343051
  if (statusCode === 400 || /bad request|invalid parameter|invalid request/i.test(text)) {
343052
343052
  return { type: "invalid_request", retryable: false, switchAllowed: false, statusCode };
343053
343053
  }
343054
- if (statusCode === 402 || /insufficient balance|insufficient funds|payment required|余额不足/i.test(text)) {
343055
- return { type: "balance_exhausted", retryable: false, switchAllowed: false, statusCode: 402 };
343054
+ if (statusCode === 402 || /insufficient balance|insufficient funds|payment required|insufficient[_ -]?quota|quota (?:exceeded|exhausted)|credit(?:s| balance)? (?:exhausted|depleted)|billing hard limit|budget exhausted|余额不足|额度不足|配额(?:不足|耗尽|超限)/i.test(text)) {
343055
+ return { type: "balance_exhausted", retryable: false, switchAllowed: true, statusCode: statusCode || 402 };
343056
343056
  }
343057
343057
  if (statusCode === 429 || /rate limit|too many requests/i.test(text)) {
343058
343058
  return { type: "rate_limited", retryable: true, switchAllowed: true, statusCode: 429, retryAfterMs };
@@ -343190,13 +343190,13 @@ var AutoRouter = class {
343190
343190
  }
343191
343191
  planAttempts(decision, candidates, failure) {
343192
343192
  const current = decision.resolvedDeployment;
343193
- if (!current || !failure.error.retryable || !failure.error.switchAllowed || failure.streamCommitted || failure.sideEffectCommitted) return [];
343193
+ if (!current || !failure.error.switchAllowed || failure.streamCommitted || failure.sideEffectCommitted) return [];
343194
343194
  const remainingAttempts = Math.max(0, 3 - decision.attempts.length);
343195
343195
  if (!remainingAttempts) return [];
343196
343196
  const attempts = [];
343197
343197
  const retryDelayMs = failure.error.retryAfterMs ?? 250;
343198
343198
  const alreadyRetriedCurrent = decision.attempts.some((attempt) => attempt.kind === "retry_same_deployment" && sameDeployment(attempt.deployment, current));
343199
- if (!alreadyRetriedCurrent && retryDelayMs <= (decision.retryBudgetMs ?? 5e3)) {
343199
+ if (failure.error.retryable && !alreadyRetriedCurrent && retryDelayMs <= (decision.retryBudgetMs ?? 5e3)) {
343200
343200
  attempts.push({
343201
343201
  deployment: { ...current },
343202
343202
  kind: "retry_same_deployment",
@@ -343217,11 +343217,16 @@ var AutoRouter = class {
343217
343217
  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");
343218
343218
  const equivalent = currentGroup ? eligible.find((candidate) => candidate.deployment.logicalModelGroupId === currentGroup && !candidate.fallbackOnly) : void 0;
343219
343219
  const fallback = eligible.find((candidate) => candidate.fallbackOnly);
343220
- for (const next of [equivalent, fallback]) {
343220
+ const rankedAlternates = decision.rankedCandidates.map((ranked) => eligible.find((candidate) => sameDeployment(candidate.deployment, ranked.deployment))).filter((candidate) => !!candidate && candidate !== equivalent && candidate !== fallback && !candidate.fallbackOnly);
343221
+ for (const candidate of eligible) {
343222
+ if (candidate === equivalent || candidate === fallback || candidate.fallbackOnly || rankedAlternates.some((existing) => sameDeployment(existing.deployment, candidate.deployment))) continue;
343223
+ rankedAlternates.push(candidate);
343224
+ }
343225
+ for (const next of [equivalent, fallback, ...rankedAlternates]) {
343221
343226
  if (!next || attempts.length >= 2) continue;
343222
343227
  attempts.push({
343223
343228
  deployment: { ...next.deployment },
343224
- kind: next === equivalent ? "equivalent_deployment" : "fallback_model",
343229
+ kind: next === equivalent ? "equivalent_deployment" : next === fallback ? "fallback_model" : "alternate_model",
343225
343230
  status: "planned",
343226
343231
  errorType: failure.error.type,
343227
343232
  streamCommitted: false,
@@ -343464,7 +343469,7 @@ function percentile(values, fraction) {
343464
343469
  }
343465
343470
  function failureFromType(type) {
343466
343471
  const retryable = type === "timeout" || type === "rate_limited" || type === "transport" || type === "server_error" || type === "empty_response";
343467
- return { type, retryable, switchAllowed: retryable };
343472
+ return { type, retryable, switchAllowed: retryable || type === "balance_exhausted" };
343468
343473
  }
343469
343474
 
343470
343475
  // src/context/services/agent-context-manager.ts
@@ -345570,7 +345575,9 @@ var Agent4 = class _Agent {
345570
345575
  }
345571
345576
  updateProviders(value) {
345572
345577
  const before = this.config.providers();
345573
- this.config.set("models", "providers", mergeProviderSecrets(value, before));
345578
+ const merged = mergeProviderSecrets(value, before);
345579
+ resetEditedModelValidationEvidence(merged, before);
345580
+ this.config.set("models", "providers", merged);
345574
345581
  const after = this.config.providers();
345575
345582
  const beforeById = new Map(before.map((provider) => [provider.id, provider]));
345576
345583
  const afterById = new Map(after.map((provider) => [provider.id, provider]));
@@ -347656,6 +347663,19 @@ Review this persisted peer result and summarize or continue the parent task as n
347656
347663
  this.writeStoredConversationState(stored, targetWs);
347657
347664
  return true;
347658
347665
  }
347666
+ reorderConversationContinuations(orderedIds) {
347667
+ const currentIds = this.continuations.filter((item) => item.queueMode === "followUp" && !!item.clientMessageId).map((item) => String(item.clientMessageId));
347668
+ const completeOrder = orderedIds.length === currentIds.length && new Set(orderedIds).size === orderedIds.length && orderedIds.every((id) => currentIds.includes(id));
347669
+ if (!completeOrder) throw new Error("A complete queue order with unique current item ids is required");
347670
+ const byId = new Map(this.continuations.flatMap((item) => item.queueMode === "followUp" && item.clientMessageId ? [[String(item.clientMessageId), item]] : []));
347671
+ let nextIndex = 0;
347672
+ this.continuations = this.continuations.map((item) => {
347673
+ if (item.queueMode !== "followUp" || !item.clientMessageId) return item;
347674
+ return byId.get(orderedIds[nextIndex++]);
347675
+ });
347676
+ this.saveWorkspaceConversationState(true);
347677
+ return this.conversationContinuations();
347678
+ }
347659
347679
  renameConversation(id, title, ws = this.workspace.current) {
347660
347680
  const targetWs = ws || this.workspace.current;
347661
347681
  if (!targetWs) return false;
@@ -350461,6 +350481,9 @@ ${msg.content}
350461
350481
  const fallbackEnabled = this.config.getBool("models", "fallback_on_unavailable");
350462
350482
  const observedFailure = classifyRouteFailure(errorText);
350463
350483
  const observedDeployment = this.activeDeployment();
350484
+ if (observedFailure.type === "balance_exhausted" && observedDeployment) {
350485
+ this.providerBalanceBlockedUntilByDeployment.set(deploymentIdentity(observedDeployment), Date.now() + 5 * 6e4);
350486
+ }
350464
350487
  const previousAttempt = observedDeployment && this.lastRouteDecision ? [...this.lastRouteDecision.attempts].reverse().find((attempt) => deploymentIdentity(attempt.deployment) === deploymentIdentity(observedDeployment)) : void 0;
350465
350488
  if (this.model === "auto" && this.routeAttemptStartedAt === 0 && (this.lastRouteDecision?.finalStatus === "failed" || this.lastRouteDecision?.finalStatus === "blocked") && previousAttempt?.status === "failed" && previousAttempt.errorType === observedFailure.type) {
350466
350489
  return null;
@@ -350511,14 +350534,12 @@ ${msg.content}
350511
350534
  return current2.modelId;
350512
350535
  }
350513
350536
  if (!fallbackEnabled) return null;
350514
- if (!observedFailure.retryable || !observedFailure.switchAllowed || this.routeStreamCommitted || this.routeSideEffectCommitted) return null;
350537
+ if (!observedFailure.switchAllowed || this.routeStreamCommitted || this.routeSideEffectCommitted) return null;
350515
350538
  const current = this.model;
350516
- const all = this.scopedSwitchModels(current).filter((m2) => m2.name !== current);
350539
+ const currentDeployment = observedDeployment;
350540
+ const all = this.scopedSwitchModels(current).filter((m2) => !currentDeployment || deploymentIdentity(this.deploymentRef(m2)) !== deploymentIdentity(currentDeployment));
350517
350541
  if (!all.length) return null;
350518
- const usable = all.filter((m2) => {
350519
- const status = String(m2.evaluation?.status || "unknown").toLowerCase();
350520
- return status !== "unavailable" && !status.startsWith("error");
350521
- });
350542
+ const usable = all.filter((m2) => !this.isBalanceBlockedDeployment(this.deploymentRef(m2)) && !modelConfigIsUnavailable(m2));
350522
350543
  if (!usable.length) return null;
350523
350544
  const pref = this.config.autoSwitchPreference();
350524
350545
  const ranked = [...usable].sort((a3, b2) => this.modelScore(b2, pref, false, false) - this.modelScore(a3, pref, false, false));
@@ -351194,7 +351215,7 @@ ${persisted.map((guide, index) => `Guide ${index + 1}: ${guide.text}`).join("\n"
351194
351215
  throw e3;
351195
351216
  }
351196
351217
  const msg = e3 instanceof Error ? e3.message : String(e3);
351197
- if (/\b402\b|insufficient balance|insufficient funds|payment required|余额不足/i.test(msg)) {
351218
+ if (/\b402\b|insufficient balance|insufficient funds|payment required|insufficient[_ -]?quota|quota (?:exceeded|exhausted)|credit(?:s| balance)? (?:exhausted|depleted)|billing hard limit|budget exhausted|余额不足|额度不足|配额(?:不足|耗尽|超限)/i.test(msg)) {
351198
351219
  this.noteProviderBalanceFailure();
351199
351220
  }
351200
351221
  this.status = "error";
@@ -353102,11 +353123,63 @@ function routeProviderFingerprint(provider) {
353102
353123
  enabled: provider.enabled,
353103
353124
  models: (provider.models || []).map((model) => ({
353104
353125
  name: model.name,
353126
+ display: model.display,
353127
+ description: model.description,
353128
+ maxTokens: model.max_tokens,
353129
+ vision: model.vision,
353130
+ thinking: !!model.thinking,
353131
+ imageOutput: !!model.image_output,
353105
353132
  enabled: model.enabled !== false,
353106
- logicalModelGroupId: model.logical_model_group_id || ""
353133
+ preview: !!model.preview,
353134
+ logicalModelGroupId: model.logical_model_group_id || "",
353135
+ privacy: model.privacy || [],
353136
+ capabilities: model.capabilities || [],
353137
+ supportedParameters: model.supported_parameters || [],
353138
+ routePreference: model.route_preference,
353139
+ fallbackOnly: !!model.fallback_only,
353140
+ thinkingTierMap: model.thinking_tier_map || {}
353107
353141
  }))
353108
353142
  });
353109
353143
  }
353144
+ function modelConfigurationFingerprint(model) {
353145
+ const { validation, evaluation, _previous_name, previous_name, ...configuration } = model;
353146
+ void validation;
353147
+ void evaluation;
353148
+ void _previous_name;
353149
+ void previous_name;
353150
+ return JSON.stringify(configuration);
353151
+ }
353152
+ function resetEditedModelValidationEvidence(incomingProviders, existingProviders) {
353153
+ const existingById = new Map(existingProviders.map((provider) => [provider.id, provider]));
353154
+ const existingByName = new Map(existingProviders.map((provider) => [provider.name, provider]));
353155
+ for (const rawProvider of incomingProviders) {
353156
+ if (!rawProvider || typeof rawProvider !== "object" || Array.isArray(rawProvider)) continue;
353157
+ const provider = rawProvider;
353158
+ const previousProvider = existingById.get(String(provider.id || "")) || existingByName.get(String(provider._previous_name || provider.previous_name || provider.name || ""));
353159
+ const models = Array.isArray(provider.models) ? provider.models : [];
353160
+ const providerConnectionChanged = !!previousProvider && (String(provider.base_url || provider.endpoint || "") !== previousProvider.base_url || String(provider.protocol || "") !== previousProvider.protocol);
353161
+ for (const rawModel of models) {
353162
+ if (!rawModel || typeof rawModel !== "object" || Array.isArray(rawModel)) continue;
353163
+ const model = rawModel;
353164
+ const previousName = String(model._previous_name || model.previous_name || model.name || "");
353165
+ const previousModel = previousProvider?.models.find((candidate) => candidate.name === previousName);
353166
+ const edited = providerConnectionChanged || !!previousModel && modelConfigurationFingerprint(model) !== modelConfigurationFingerprint(previousModel);
353167
+ delete model._previous_name;
353168
+ delete model.previous_name;
353169
+ if (!edited) continue;
353170
+ model.validation = { level: "discovered", status: "degraded", checked_at: "", capabilities: {} };
353171
+ delete model.evaluation;
353172
+ model.speed_rating = "unknown";
353173
+ model.capability_rating = "unknown";
353174
+ }
353175
+ }
353176
+ }
353177
+ function modelConfigIsUnavailable(model) {
353178
+ const validationStatus = effectiveModelValidationStatus(model);
353179
+ if (model.validation?.level !== "discovered" && (validationStatus === "unavailable" || validationStatus === "auth_error" || validationStatus === "invalid_config")) return true;
353180
+ const evaluationStatus = validationStatus === "degraded" ? "degraded" : String(model.evaluation?.status || "").toLowerCase();
353181
+ return evaluationStatus === "unavailable" || evaluationStatus.startsWith("error");
353182
+ }
353110
353183
  function parseDeploymentSelectionValue2(value) {
353111
353184
  const marker = String(value || "").trim();
353112
353185
  if (!marker.startsWith("deployment:")) return null;
@@ -353123,9 +353196,9 @@ function parseDeploymentSelectionValue2(value) {
353123
353196
  function effectiveModelValidationStatus(model) {
353124
353197
  const raw = String(model.validation?.status || "").toLowerCase();
353125
353198
  if (raw === "auth_error") return raw;
353199
+ if (String(model.validation?.level || "").toLowerCase() === "discovered") return "degraded";
353126
353200
  const textEvidence = model.validation?.capabilities?.text === true || model.validation?.capabilities?.text_input === true || model.validation?.capabilities?.text_output === true || model.evaluation?.text_input === true || model.evaluation?.text_output === true;
353127
353201
  if (textEvidence && raw === "unavailable") return "degraded";
353128
- if (!raw && String(model.validation?.level || "").toLowerCase() === "discovered") return "degraded";
353129
353202
  return ["verified", "degraded", "unavailable", "auth_error", "rate_limited", "invalid_config"].includes(raw) ? raw : "unavailable";
353130
353203
  }
353131
353204
  function routeToolIsReadOnly(name50, rawArgs) {
@@ -353332,6 +353405,33 @@ ${text}`;
353332
353405
  this.emitQueueUpdate(runtime);
353333
353406
  return true;
353334
353407
  }
353408
+ reorderQueueItems(target, orderedIdsInput) {
353409
+ const runtime = this.findRuntime(target);
353410
+ if (!runtime) throw new Error("Target conversation runtime is unavailable");
353411
+ const currentItems = this.queueItems(runtime.target);
353412
+ const orderedIds = Array.isArray(orderedIdsInput) ? orderedIdsInput.map((id) => String(id || "").trim()) : [];
353413
+ const currentIds = currentItems.map((item) => item.id);
353414
+ const completeOrder = orderedIds.length === currentIds.length && new Set(orderedIds).size === orderedIds.length && orderedIds.every((id) => currentIds.includes(id));
353415
+ if (!completeOrder) throw new Error("A complete queue order with unique current item ids is required");
353416
+ const persistedIds = runtime.runner.conversationContinuations().filter((item) => item.queueMode === "followUp" && !!item.clientMessageId).map((item) => String(item.clientMessageId));
353417
+ if (persistedIds.length !== currentIds.length || new Set(persistedIds).size !== persistedIds.length || persistedIds.some((id) => !currentIds.includes(id))) {
353418
+ throw new Error("Persisted queue does not match the complete queue order");
353419
+ }
353420
+ const pendingById = new Map(runtime.pendingNextTurn.flatMap((item) => {
353421
+ if (item.queueMode !== "followUp" || typeof item.message === "string" || !item.message.clientMessageId) return [];
353422
+ return [[String(item.message.clientMessageId), item]];
353423
+ }));
353424
+ const reorderedPending = orderedIds.map((id) => pendingById.get(id));
353425
+ let nextIndex = 0;
353426
+ runtime.pendingNextTurn = runtime.pendingNextTurn.map((item) => {
353427
+ if (item.queueMode !== "followUp" || typeof item.message === "string" || !item.message.clientMessageId) return item;
353428
+ return reorderedPending[nextIndex++];
353429
+ });
353430
+ runtime.runner.reorderConversationContinuations(orderedIds);
353431
+ runtime.queued.followUp = orderedIds.map((id) => pendingById.get(id)).map((item) => typeof item.message === "string" ? item.message : item.message.text);
353432
+ this.emitQueueUpdate(runtime);
353433
+ return this.queueItems(runtime.target);
353434
+ }
353335
353435
  setQueuePaused(target, paused) {
353336
353436
  const runtime = this.findRuntime(target);
353337
353437
  if (!runtime) throw new Error("Target conversation runtime is unavailable");
@@ -353358,6 +353458,8 @@ ${text}`;
353358
353458
  this.updateQueueItem(runtime.target, String(input.id || ""), String(input.text || ""));
353359
353459
  } else if (action === "delete") {
353360
353460
  if (!this.deleteQueueItem(runtime.target, String(input.id || ""))) throw new Error("Queue item was not found");
353461
+ } else if (action === "reorder") {
353462
+ this.reorderQueueItems(runtime.target, input.orderedIds || []);
353361
353463
  } else if (action === "toggle_pause") {
353362
353464
  this.setQueuePaused(runtime.target, !runtime.queuePaused);
353363
353465
  } else if (action === "guide") {
@@ -670,6 +670,7 @@ export declare class Agent {
670
670
  isBranchCommunicationEnabled(): boolean;
671
671
  switchConversationBranch(conversationId: string, branchId: string, branchGroupId?: string): ConversationSnapshot;
672
672
  setConversationPinned(id: string, pinned: boolean, ws?: WorkspaceInfo | null): boolean;
673
+ reorderConversationContinuations(orderedIds: string[]): ConversationContinuation[];
673
674
  renameConversation(id: string, title: string, ws?: WorkspaceInfo | null): boolean;
674
675
  /** 为指定工作区创建空白对话,不临时切换全局前台工作区。 */
675
676
  createConversationInWorkspace(ws: WorkspaceInfo, title?: string): {
@@ -1051,7 +1051,9 @@ class Agent {
1051
1051
  }
1052
1052
  updateProviders(value) {
1053
1053
  const before = this.config.providers();
1054
- this.config.set('models', 'providers', (0, config_1.mergeProviderSecrets)(value, before));
1054
+ const merged = (0, config_1.mergeProviderSecrets)(value, before);
1055
+ resetEditedModelValidationEvidence(merged, before);
1056
+ this.config.set('models', 'providers', merged);
1055
1057
  const after = this.config.providers();
1056
1058
  const beforeById = new Map(before.map(provider => [provider.id, provider]));
1057
1059
  const afterById = new Map(after.map(provider => [provider.id, provider]));
@@ -3479,6 +3481,27 @@ class Agent {
3479
3481
  this.writeStoredConversationState(stored, targetWs);
3480
3482
  return true;
3481
3483
  }
3484
+ reorderConversationContinuations(orderedIds) {
3485
+ const currentIds = this.continuations
3486
+ .filter(item => item.queueMode === 'followUp' && !!item.clientMessageId)
3487
+ .map(item => String(item.clientMessageId));
3488
+ const completeOrder = orderedIds.length === currentIds.length
3489
+ && new Set(orderedIds).size === orderedIds.length
3490
+ && orderedIds.every(id => currentIds.includes(id));
3491
+ if (!completeOrder)
3492
+ throw new Error('A complete queue order with unique current item ids is required');
3493
+ const byId = new Map(this.continuations.flatMap(item => item.queueMode === 'followUp' && item.clientMessageId
3494
+ ? [[String(item.clientMessageId), item]]
3495
+ : []));
3496
+ let nextIndex = 0;
3497
+ this.continuations = this.continuations.map(item => {
3498
+ if (item.queueMode !== 'followUp' || !item.clientMessageId)
3499
+ return item;
3500
+ return byId.get(orderedIds[nextIndex++]);
3501
+ });
3502
+ this.saveWorkspaceConversationState(true);
3503
+ return this.conversationContinuations();
3504
+ }
3482
3505
  renameConversation(id, title, ws = this.workspace.current) {
3483
3506
  const targetWs = ws || this.workspace.current;
3484
3507
  if (!targetWs)
@@ -6593,6 +6616,13 @@ class Agent {
6593
6616
  const fallbackEnabled = this.config.getBool('models', 'fallback_on_unavailable');
6594
6617
  const observedFailure = (0, autoRouter_1.classifyRouteFailure)(errorText);
6595
6618
  const observedDeployment = this.activeDeployment();
6619
+ // Keep balance exhaustion scoped to the deployment that actually failed.
6620
+ // Provider adapters normally record this before returning an error, but
6621
+ // fallback callers are also a public recovery boundary and must not rely
6622
+ // on every adapter/error path having performed that side effect first.
6623
+ if (observedFailure.type === 'balance_exhausted' && observedDeployment) {
6624
+ this.providerBalanceBlockedUntilByDeployment.set(deploymentIdentity(observedDeployment), Date.now() + 5 * 60_000);
6625
+ }
6596
6626
  const previousAttempt = observedDeployment && this.lastRouteDecision
6597
6627
  ? [...this.lastRouteDecision.attempts].reverse().find(attempt => deploymentIdentity(attempt.deployment) === deploymentIdentity(observedDeployment))
6598
6628
  : undefined;
@@ -6654,16 +6684,15 @@ class Agent {
6654
6684
  }
6655
6685
  if (!fallbackEnabled)
6656
6686
  return null;
6657
- if (!observedFailure.retryable || !observedFailure.switchAllowed || this.routeStreamCommitted || this.routeSideEffectCommitted)
6687
+ if (!observedFailure.switchAllowed || this.routeStreamCommitted || this.routeSideEffectCommitted)
6658
6688
  return null;
6659
6689
  const current = this.model;
6660
- const all = this.scopedSwitchModels(current).filter(m => m.name !== current);
6690
+ const currentDeployment = observedDeployment;
6691
+ const all = this.scopedSwitchModels(current).filter(m => !currentDeployment
6692
+ || deploymentIdentity(this.deploymentRef(m)) !== deploymentIdentity(currentDeployment));
6661
6693
  if (!all.length)
6662
6694
  return null;
6663
- const usable = all.filter(m => {
6664
- const status = String(m.evaluation?.status || 'unknown').toLowerCase();
6665
- return status !== 'unavailable' && !status.startsWith('error');
6666
- });
6695
+ const usable = all.filter(m => !this.isBalanceBlockedDeployment(this.deploymentRef(m)) && !modelConfigIsUnavailable(m));
6667
6696
  if (!usable.length)
6668
6697
  return null;
6669
6698
  const pref = this.config.autoSwitchPreference();
@@ -7436,7 +7465,7 @@ class Agent {
7436
7465
  throw e;
7437
7466
  }
7438
7467
  const msg = e instanceof Error ? e.message : String(e);
7439
- if (/\b402\b|insufficient balance|insufficient funds|payment required|余额不足/i.test(msg)) {
7468
+ if (/\b402\b|insufficient balance|insufficient funds|payment required|insufficient[_ -]?quota|quota (?:exceeded|exhausted)|credit(?:s| balance)? (?:exhausted|depleted)|billing hard limit|budget exhausted|余额不足|额度不足|配额(?:不足|耗尽|超限)/i.test(msg)) {
7440
7469
  this.noteProviderBalanceFailure();
7441
7470
  }
7442
7471
  this.status = 'error';
@@ -9539,11 +9568,74 @@ function routeProviderFingerprint(provider) {
9539
9568
  enabled: provider.enabled,
9540
9569
  models: (provider.models || []).map(model => ({
9541
9570
  name: model.name,
9571
+ display: model.display,
9572
+ description: model.description,
9573
+ maxTokens: model.max_tokens,
9574
+ vision: model.vision,
9575
+ thinking: !!model.thinking,
9576
+ imageOutput: !!model.image_output,
9542
9577
  enabled: model.enabled !== false,
9578
+ preview: !!model.preview,
9543
9579
  logicalModelGroupId: model.logical_model_group_id || '',
9580
+ privacy: model.privacy || [],
9581
+ capabilities: model.capabilities || [],
9582
+ supportedParameters: model.supported_parameters || [],
9583
+ routePreference: model.route_preference,
9584
+ fallbackOnly: !!model.fallback_only,
9585
+ thinkingTierMap: model.thinking_tier_map || {},
9544
9586
  })),
9545
9587
  });
9546
9588
  }
9589
+ function modelConfigurationFingerprint(model) {
9590
+ const { validation, evaluation, _previous_name, previous_name, ...configuration } = model;
9591
+ void validation;
9592
+ void evaluation;
9593
+ void _previous_name;
9594
+ void previous_name;
9595
+ return JSON.stringify(configuration);
9596
+ }
9597
+ function resetEditedModelValidationEvidence(incomingProviders, existingProviders) {
9598
+ const existingById = new Map(existingProviders.map(provider => [provider.id, provider]));
9599
+ const existingByName = new Map(existingProviders.map(provider => [provider.name, provider]));
9600
+ for (const rawProvider of incomingProviders) {
9601
+ if (!rawProvider || typeof rawProvider !== 'object' || Array.isArray(rawProvider))
9602
+ continue;
9603
+ const provider = rawProvider;
9604
+ const previousProvider = existingById.get(String(provider.id || ''))
9605
+ || existingByName.get(String(provider._previous_name || provider.previous_name || provider.name || ''));
9606
+ const models = Array.isArray(provider.models) ? provider.models : [];
9607
+ // Endpoint/protocol edits invalidate capability evidence. A display-name
9608
+ // change or credential rotation only resets runtime health/circuits via the
9609
+ // provider fingerprint and must not discard still-valid model capabilities.
9610
+ const providerConnectionChanged = !!previousProvider && (String(provider.base_url || provider.endpoint || '') !== previousProvider.base_url
9611
+ || String(provider.protocol || '') !== previousProvider.protocol);
9612
+ for (const rawModel of models) {
9613
+ if (!rawModel || typeof rawModel !== 'object' || Array.isArray(rawModel))
9614
+ continue;
9615
+ const model = rawModel;
9616
+ const previousName = String(model._previous_name || model.previous_name || model.name || '');
9617
+ const previousModel = previousProvider?.models.find(candidate => candidate.name === previousName);
9618
+ const edited = providerConnectionChanged || (!!previousModel
9619
+ && modelConfigurationFingerprint(model) !== modelConfigurationFingerprint(previousModel));
9620
+ delete model._previous_name;
9621
+ delete model.previous_name;
9622
+ if (!edited)
9623
+ continue;
9624
+ model.validation = { level: 'discovered', status: 'degraded', checked_at: '', capabilities: {} };
9625
+ delete model.evaluation;
9626
+ model.speed_rating = 'unknown';
9627
+ model.capability_rating = 'unknown';
9628
+ }
9629
+ }
9630
+ }
9631
+ function modelConfigIsUnavailable(model) {
9632
+ const validationStatus = effectiveModelValidationStatus(model);
9633
+ if (model.validation?.level !== 'discovered'
9634
+ && (validationStatus === 'unavailable' || validationStatus === 'auth_error' || validationStatus === 'invalid_config'))
9635
+ return true;
9636
+ const evaluationStatus = validationStatus === 'degraded' ? 'degraded' : String(model.evaluation?.status || '').toLowerCase();
9637
+ return evaluationStatus === 'unavailable' || evaluationStatus.startsWith('error');
9638
+ }
9547
9639
  function parseDeploymentSelectionValue(value) {
9548
9640
  const marker = String(value || '').trim();
9549
9641
  if (!marker.startsWith('deployment:'))
@@ -9564,6 +9656,8 @@ function effectiveModelValidationStatus(model) {
9564
9656
  const raw = String(model.validation?.status || '').toLowerCase();
9565
9657
  if (raw === 'auth_error')
9566
9658
  return raw;
9659
+ if (String(model.validation?.level || '').toLowerCase() === 'discovered')
9660
+ return 'degraded';
9567
9661
  const textEvidence = model.validation?.capabilities?.text === true
9568
9662
  || model.validation?.capabilities?.text_input === true
9569
9663
  || model.validation?.capabilities?.text_output === true
@@ -9571,8 +9665,6 @@ function effectiveModelValidationStatus(model) {
9571
9665
  || model.evaluation?.text_output === true;
9572
9666
  if (textEvidence && raw === 'unavailable')
9573
9667
  return 'degraded';
9574
- if (!raw && String(model.validation?.level || '').toLowerCase() === 'discovered')
9575
- return 'degraded';
9576
9668
  return (['verified', 'degraded', 'unavailable', 'auth_error', 'rate_limited', 'invalid_config'].includes(raw)
9577
9669
  ? raw
9578
9670
  : 'unavailable');
@@ -662,7 +662,7 @@ async function runAgentKernel(agent) {
662
662
  return;
663
663
  }
664
664
  const publicError = normalizePublicProviderError(error, [currentAgent.activeModelConfig()?.api_key]);
665
- if (/\b402\b|insufficient balance|insufficient funds|payment required|余额不足/i.test(publicError)) {
665
+ if (/\b402\b|insufficient balance|insufficient funds|payment required|insufficient[_ -]?quota|quota (?:exceeded|exhausted)|credit(?:s| balance)? (?:exhausted|depleted)|billing hard limit|budget exhausted|余额不足|额度不足|配额(?:不足|耗尽|超限)/i.test(publicError)) {
666
666
  currentAgent.noteProviderBalanceFailure();
667
667
  }
668
668
  const final = assistantMessage(model, [{ type: 'text', text: `[Error] ${publicError}` }], 'error');
@@ -85,7 +85,7 @@ export interface RankedRouteCandidate {
85
85
  export type RouteAttemptStatus = 'planned' | 'success' | 'failed' | 'blocked';
86
86
  export interface RouteAttempt {
87
87
  deployment: DeploymentRef;
88
- kind: 'initial' | 'retry_same_deployment' | 'equivalent_deployment' | 'fallback_model';
88
+ kind: 'initial' | 'retry_same_deployment' | 'equivalent_deployment' | 'fallback_model' | 'alternate_model';
89
89
  status: RouteAttemptStatus;
90
90
  errorType?: RouteFailureType;
91
91
  durationMs?: number;
@@ -115,8 +115,8 @@ function classifyRouteFailure(error) {
115
115
  if (statusCode === 400 || /bad request|invalid parameter|invalid request/i.test(text)) {
116
116
  return { type: 'invalid_request', retryable: false, switchAllowed: false, statusCode };
117
117
  }
118
- if (statusCode === 402 || /insufficient balance|insufficient funds|payment required|余额不足/i.test(text)) {
119
- return { type: 'balance_exhausted', retryable: false, switchAllowed: false, statusCode: 402 };
118
+ if (statusCode === 402 || /insufficient balance|insufficient funds|payment required|insufficient[_ -]?quota|quota (?:exceeded|exhausted)|credit(?:s| balance)? (?:exhausted|depleted)|billing hard limit|budget exhausted|余额不足|额度不足|配额(?:不足|耗尽|超限)/i.test(text)) {
119
+ return { type: 'balance_exhausted', retryable: false, switchAllowed: true, statusCode: statusCode || 402 };
120
120
  }
121
121
  if (statusCode === 429 || /rate limit|too many requests/i.test(text)) {
122
122
  return { type: 'rate_limited', retryable: true, switchAllowed: true, statusCode: 429, retryAfterMs };
@@ -270,7 +270,7 @@ class AutoRouter {
270
270
  }
271
271
  planAttempts(decision, candidates, failure) {
272
272
  const current = decision.resolvedDeployment;
273
- if (!current || !failure.error.retryable || !failure.error.switchAllowed || failure.streamCommitted || failure.sideEffectCommitted)
273
+ if (!current || !failure.error.switchAllowed || failure.streamCommitted || failure.sideEffectCommitted)
274
274
  return [];
275
275
  const remainingAttempts = Math.max(0, 3 - decision.attempts.length);
276
276
  if (!remainingAttempts)
@@ -279,7 +279,7 @@ class AutoRouter {
279
279
  const retryDelayMs = failure.error.retryAfterMs ?? 250;
280
280
  const alreadyRetriedCurrent = decision.attempts.some(attempt => attempt.kind === 'retry_same_deployment'
281
281
  && sameDeployment(attempt.deployment, current));
282
- if (!alreadyRetriedCurrent && retryDelayMs <= (decision.retryBudgetMs ?? 5_000)) {
282
+ if (failure.error.retryable && !alreadyRetriedCurrent && retryDelayMs <= (decision.retryBudgetMs ?? 5_000)) {
283
283
  attempts.push({
284
284
  deployment: { ...current },
285
285
  kind: 'retry_same_deployment',
@@ -310,12 +310,21 @@ class AutoRouter {
310
310
  ? eligible.find(candidate => candidate.deployment.logicalModelGroupId === currentGroup && !candidate.fallbackOnly)
311
311
  : undefined;
312
312
  const fallback = eligible.find(candidate => candidate.fallbackOnly);
313
- for (const next of [equivalent, fallback]) {
313
+ const rankedAlternates = decision.rankedCandidates
314
+ .map(ranked => eligible.find(candidate => sameDeployment(candidate.deployment, ranked.deployment)))
315
+ .filter((candidate) => !!candidate && candidate !== equivalent && candidate !== fallback && !candidate.fallbackOnly);
316
+ for (const candidate of eligible) {
317
+ if (candidate === equivalent || candidate === fallback || candidate.fallbackOnly
318
+ || rankedAlternates.some(existing => sameDeployment(existing.deployment, candidate.deployment)))
319
+ continue;
320
+ rankedAlternates.push(candidate);
321
+ }
322
+ for (const next of [equivalent, fallback, ...rankedAlternates]) {
314
323
  if (!next || attempts.length >= 2)
315
324
  continue;
316
325
  attempts.push({
317
326
  deployment: { ...next.deployment },
318
- kind: next === equivalent ? 'equivalent_deployment' : 'fallback_model',
327
+ kind: next === equivalent ? 'equivalent_deployment' : next === fallback ? 'fallback_model' : 'alternate_model',
319
328
  status: 'planned',
320
329
  errorType: failure.error.type,
321
330
  streamCommitted: false,
@@ -578,6 +587,6 @@ function percentile(values, fraction) {
578
587
  }
579
588
  function failureFromType(type) {
580
589
  const retryable = type === 'timeout' || type === 'rate_limited' || type === 'transport' || type === 'server_error' || type === 'empty_response';
581
- return { type, retryable, switchAllowed: retryable };
590
+ return { type, retryable, switchAllowed: retryable || type === 'balance_exhausted' };
582
591
  }
583
592
  //# sourceMappingURL=autoRouter.js.map
@@ -12,7 +12,15 @@ export interface ConversationQueueItemSnapshot {
12
12
  runId?: string;
13
13
  createdAt: string;
14
14
  }
15
- export type ConversationQueueAction = 'enqueue' | 'update' | 'delete' | 'toggle_pause' | 'guide';
15
+ export type ConversationQueueAction = 'enqueue' | 'update' | 'delete' | 'reorder' | 'toggle_pause' | 'guide';
16
+ export interface ConversationQueueActionInput {
17
+ id?: string;
18
+ text?: string;
19
+ requestedMode?: string;
20
+ goalObjective?: string;
21
+ createdAt?: string;
22
+ orderedIds?: string[];
23
+ }
16
24
  export interface AgentPromptMessage {
17
25
  text: string;
18
26
  /** Public transcript text when the execution prompt contains hidden orchestration instructions. */
@@ -196,14 +204,9 @@ export declare class ConversationKernel {
196
204
  }): ConversationQueueItemSnapshot;
197
205
  updateQueueItem(target: ConversationTargetInput, idInput: string, textInput: string): ConversationQueueItemSnapshot;
198
206
  deleteQueueItem(target: ConversationTargetInput, idInput: string): boolean;
207
+ reorderQueueItems(target: ConversationTargetInput, orderedIdsInput: string[]): ConversationQueueItemSnapshot[];
199
208
  setQueuePaused(target: ConversationTargetInput, paused: boolean): boolean;
200
- queueAction(target: ConversationTargetInput, action: ConversationQueueAction, input?: {
201
- id?: string;
202
- text?: string;
203
- requestedMode?: string;
204
- goalObjective?: string;
205
- createdAt?: string;
206
- }): {
209
+ queueAction(target: ConversationTargetInput, action: ConversationQueueAction, input?: ConversationQueueActionInput): {
207
210
  ok: boolean;
208
211
  queueItems: ConversationQueueItemSnapshot[];
209
212
  queuePaused: boolean;
@@ -160,6 +160,46 @@ class ConversationKernel {
160
160
  this.emitQueueUpdate(runtime);
161
161
  return true;
162
162
  }
163
+ reorderQueueItems(target, orderedIdsInput) {
164
+ const runtime = this.findRuntime(target);
165
+ if (!runtime)
166
+ throw new Error('Target conversation runtime is unavailable');
167
+ const currentItems = this.queueItems(runtime.target);
168
+ const orderedIds = Array.isArray(orderedIdsInput)
169
+ ? orderedIdsInput.map(id => String(id || '').trim())
170
+ : [];
171
+ const currentIds = currentItems.map(item => item.id);
172
+ const completeOrder = orderedIds.length === currentIds.length
173
+ && new Set(orderedIds).size === orderedIds.length
174
+ && orderedIds.every(id => currentIds.includes(id));
175
+ if (!completeOrder)
176
+ throw new Error('A complete queue order with unique current item ids is required');
177
+ const persistedIds = runtime.runner.conversationContinuations()
178
+ .filter(item => item.queueMode === 'followUp' && !!item.clientMessageId)
179
+ .map(item => String(item.clientMessageId));
180
+ if (persistedIds.length !== currentIds.length
181
+ || new Set(persistedIds).size !== persistedIds.length
182
+ || persistedIds.some(id => !currentIds.includes(id))) {
183
+ throw new Error('Persisted queue does not match the complete queue order');
184
+ }
185
+ const pendingById = new Map(runtime.pendingNextTurn.flatMap(item => {
186
+ if (item.queueMode !== 'followUp' || typeof item.message === 'string' || !item.message.clientMessageId)
187
+ return [];
188
+ return [[String(item.message.clientMessageId), item]];
189
+ }));
190
+ const reorderedPending = orderedIds.map(id => pendingById.get(id));
191
+ let nextIndex = 0;
192
+ runtime.pendingNextTurn = runtime.pendingNextTurn.map(item => {
193
+ if (item.queueMode !== 'followUp' || typeof item.message === 'string' || !item.message.clientMessageId)
194
+ return item;
195
+ return reorderedPending[nextIndex++];
196
+ });
197
+ runtime.runner.reorderConversationContinuations(orderedIds);
198
+ runtime.queued.followUp = orderedIds.map(id => pendingById.get(id))
199
+ .map(item => typeof item.message === 'string' ? item.message : item.message.text);
200
+ this.emitQueueUpdate(runtime);
201
+ return this.queueItems(runtime.target);
202
+ }
163
203
  setQueuePaused(target, paused) {
164
204
  const runtime = this.findRuntime(target);
165
205
  if (!runtime)
@@ -192,6 +232,9 @@ class ConversationKernel {
192
232
  if (!this.deleteQueueItem(runtime.target, String(input.id || '')))
193
233
  throw new Error('Queue item was not found');
194
234
  }
235
+ else if (action === 'reorder') {
236
+ this.reorderQueueItems(runtime.target, input.orderedIds || []);
237
+ }
195
238
  else if (action === 'toggle_pause') {
196
239
  this.setQueuePaused(runtime.target, !runtime.queuePaused);
197
240
  }
@@ -1,7 +1,7 @@
1
1
  import { spawn } from 'child_process';
2
2
  import { NormalizedConversationTarget } from './conversationTarget';
3
3
  import { AgentMode, AgentWorkEvent, ConversationInputEnvelope, GuideReceipt } from './types';
4
- import { ConversationQueueAction } from './conversationKernel';
4
+ import { ConversationQueueAction, ConversationQueueActionInput } from './conversationKernel';
5
5
  import { UtilityAgentPromptResult, UtilityAutoRouteRatingResult, UtilityAgentSnapshotResult, UtilityAgentStopResult, UtilityConversationRewindResult, UtilityHostToolRequest, UtilityPromptRequest } from './utilityAgentProtocol';
6
6
  type WindowsProcessTreeHelperRuntime = {
7
7
  kind: 'precompiled' | 'runtime_compile';
@@ -95,13 +95,7 @@ export declare class ElectronUtilityAgentClient {
95
95
  rewind(messageIndex: number): Promise<UtilityConversationRewindResult>;
96
96
  requestStop(runId?: string): Promise<UtilityAgentStopResult>;
97
97
  enqueueGuide(envelope: ConversationInputEnvelope): Promise<GuideReceipt>;
98
- queueAction(action: ConversationQueueAction, input?: {
99
- id?: string;
100
- text?: string;
101
- requestedMode?: string;
102
- goalObjective?: string;
103
- createdAt?: string;
104
- }): Promise<Record<string, unknown>>;
98
+ queueAction(action: ConversationQueueAction, input?: ConversationQueueActionInput): Promise<Record<string, unknown>>;
105
99
  checkpoint(): Promise<Record<string, unknown>>;
106
100
  contextCompress(options?: {
107
101
  keepRecent?: number;