newmark-agent 0.4.8 → 0.5.0

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.
@@ -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;
@@ -268,6 +271,7 @@ export declare class ConversationKernel {
268
271
  rewind(target: ConversationTargetInput, messageIndex: number): ReturnType<Agent['rewindConversation']>;
269
272
  prompt(message: string | AgentPromptMessage, target: ConversationTargetInput, options: ConversationKernelRunOptions, queueMode?: ConversationQueueMode): Promise<ConversationKernelRunResult>;
270
273
  private settleCooperativeStop;
274
+ private stopAutomaticContinuationAfterError;
271
275
  private run;
272
276
  /**
273
277
  * Apply a model selection recorded while a Build block was running. The
@@ -277,6 +281,7 @@ export declare class ConversationKernel {
277
281
  */
278
282
  private syncPendingModel;
279
283
  private runSingle;
284
+ private consumeFailedAutomaticContinuation;
280
285
  private processTimeoutMs;
281
286
  private runtime;
282
287
  private scheduleGoalContinuation;
@@ -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
  }
@@ -310,7 +353,7 @@ class ConversationKernel {
310
353
  const existing = runtime ? this.guideReceipt(runtime, clientMessageId) : undefined;
311
354
  if (existing)
312
355
  return existing;
313
- if (!runtime?.activePromise || !runtime.runId) {
356
+ if (!runtime?.runId) {
314
357
  return { ...base, reason: 'Target conversation is not running' };
315
358
  }
316
359
  if (requestedRunId && requestedRunId !== runtime.runId) {
@@ -318,6 +361,12 @@ class ConversationKernel {
318
361
  runtime.guideReceipts.set(clientMessageId, rejected);
319
362
  return runtime.runner.recordGuideReceipt(rejected);
320
363
  }
364
+ const canReactivateFinalizingRun = !runtime.activePromise
365
+ && runtime.guideAcceptanceClosedRunId === runtime.runId
366
+ && (!requestedRunId || requestedRunId === runtime.runId);
367
+ if (!runtime.activePromise && !canReactivateFinalizingRun) {
368
+ return { ...base, reason: 'Target conversation is not running' };
369
+ }
321
370
  let safeImages = [];
322
371
  let safeAttachments = [];
323
372
  try {
@@ -417,6 +466,8 @@ class ConversationKernel {
417
466
  attachments: safeAttachments.map(attachment => ({ ...attachment })),
418
467
  createdAt: deferred.createdAt,
419
468
  }]);
469
+ this.trackQueuedMessage(runtime, safeEnvelope.text, 'steer');
470
+ this.emitQueueUpdate(runtime);
420
471
  this.activateAcceptedGoal(runtime, envelope.goalObjective);
421
472
  this.schedulePendingRuntimeContinuation(runtime, runtime.runId);
422
473
  return deferred;
@@ -711,6 +762,7 @@ class ConversationKernel {
711
762
  activePromise = (async () => {
712
763
  let result = null;
713
764
  let stopped = false;
765
+ let failed = false;
714
766
  try {
715
767
  result = await this.run(runtime, message, options);
716
768
  stopped = runtime.runId === runId && runtime.stopRequestedRunId === runId;
@@ -720,6 +772,8 @@ class ConversationKernel {
720
772
  stopped = true;
721
773
  }
722
774
  else {
775
+ failed = true;
776
+ this.stopAutomaticContinuationAfterError(runtime, runId);
723
777
  runtime.runner.finishConversationWorkRun(runId, 'error', undefined, error instanceof Error ? error.message : String(error));
724
778
  throw error;
725
779
  }
@@ -731,7 +785,7 @@ class ConversationKernel {
731
785
  stopped = true;
732
786
  this.settleCooperativeStop(runtime, runId);
733
787
  }
734
- else if (!runtime.queuePaused && runtime.pendingNextTurn.length > 0) {
788
+ else if (!failed && !runtime.queuePaused && runtime.pendingNextTurn.length > 0) {
735
789
  // A renderer/IPC Guide can arrive after the final-drain barrier's
736
790
  // last check but before this promise settles. Do not leave the
737
791
  // deferred continuation queued on an idle runtime.
@@ -739,7 +793,7 @@ class ConversationKernel {
739
793
  }
740
794
  }
741
795
  }
742
- if (!stopped && runtime.runId === runId)
796
+ if (!failed && !stopped && runtime.runId === runId)
743
797
  this.scheduleGoalContinuation(runtime, runId);
744
798
  if (stopped) {
745
799
  const settled = this.result(runtime, []);
@@ -766,6 +820,30 @@ class ConversationKernel {
766
820
  this.emitQueueUpdate(runtime);
767
821
  return true;
768
822
  }
823
+ stopAutomaticContinuationAfterError(runtime, runId) {
824
+ if (runtime.runId !== runId)
825
+ return;
826
+ if (runtime.goalContinuationTimer) {
827
+ clearTimeout(runtime.goalContinuationTimer);
828
+ runtime.goalContinuationTimer = undefined;
829
+ }
830
+ runtime.pendingContinuationRunId = undefined;
831
+ this.rejectOutstandingGuides(runtime, 'The provider failed before this Guide could be applied; submit again to retry.');
832
+ runtime.pendingNextTurn = runtime.pendingNextTurn.filter(item => {
833
+ const automatic = item.queueMode === 'steer'
834
+ || (typeof item.message !== 'string' && item.message.hiddenUserInput === true);
835
+ if (automatic) {
836
+ runtime.runner.consumeConversationContinuation({
837
+ content: typeof item.message === 'string' ? item.message : item.message.text,
838
+ queueMode: item.queueMode,
839
+ clientMessageId: typeof item.message === 'string' ? undefined : item.message.clientMessageId,
840
+ });
841
+ }
842
+ return !automatic;
843
+ });
844
+ this.queueState(runtime);
845
+ this.emitQueueUpdate(runtime);
846
+ }
769
847
  async run(runtime, message, options) {
770
848
  this.applyOptions(runtime.runner, options);
771
849
  let lastTokens = await this.runSingle(runtime, message);
@@ -871,7 +949,14 @@ class ConversationKernel {
871
949
  this.consumeQueuedMessage(runtime, typeof message === 'string' ? message : message.text);
872
950
  const timeoutMs = this.processTimeoutMs(runtime);
873
951
  if (timeoutMs <= 0) {
874
- const tokens = await runtime.runner.process(message);
952
+ let tokens;
953
+ try {
954
+ tokens = await runtime.runner.process(message);
955
+ }
956
+ catch (error) {
957
+ this.consumeFailedAutomaticContinuation(runtime, message, continuationMode);
958
+ throw error;
959
+ }
875
960
  if (continuationMode)
876
961
  runtime.runner.consumeConversationContinuation({
877
962
  content: typeof message === 'string' ? message : message.text,
@@ -882,12 +967,19 @@ class ConversationKernel {
882
967
  }
883
968
  let timeout;
884
969
  try {
885
- const tokens = await Promise.race([
886
- runtime.runner.process(message),
887
- new Promise((_, reject) => {
888
- timeout = setTimeout(() => reject(new Error(`Process timeout (${Math.round(timeoutMs / 1000)}s)`)), timeoutMs);
889
- }),
890
- ]);
970
+ let tokens;
971
+ try {
972
+ tokens = await Promise.race([
973
+ runtime.runner.process(message),
974
+ new Promise((_, reject) => {
975
+ timeout = setTimeout(() => reject(new Error(`Process timeout (${Math.round(timeoutMs / 1000)}s)`)), timeoutMs);
976
+ }),
977
+ ]);
978
+ }
979
+ catch (error) {
980
+ this.consumeFailedAutomaticContinuation(runtime, message, continuationMode);
981
+ throw error;
982
+ }
891
983
  if (continuationMode)
892
984
  runtime.runner.consumeConversationContinuation({
893
985
  content: typeof message === 'string' ? message : message.text,
@@ -901,6 +993,15 @@ class ConversationKernel {
901
993
  clearTimeout(timeout);
902
994
  }
903
995
  }
996
+ consumeFailedAutomaticContinuation(runtime, message, continuationMode) {
997
+ if (!continuationMode || typeof message === 'string' || message.hiddenUserInput !== true)
998
+ return;
999
+ runtime.runner.consumeConversationContinuation({
1000
+ content: message.text,
1001
+ queueMode: continuationMode,
1002
+ clientMessageId: message.clientMessageId,
1003
+ });
1004
+ }
904
1005
  processTimeoutMs(runtime) {
905
1006
  const raw = runtime.runner.config.getNum('agent', 'process_timeout_ms') || this.host.config.getNum('agent', 'process_timeout_ms');
906
1007
  if (!Number.isFinite(raw) || raw <= 0)
@@ -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;
@@ -1,5 +1,5 @@
1
1
  import { AgentMode, AgentWorkEvent, ConversationInputEnvelope, GuideReceipt } from './types';
2
- import { ConversationQueueAction } from './conversationKernel';
2
+ import { ConversationQueueAction, ConversationQueueActionInput } from './conversationKernel';
3
3
  import { ConversationRuntimeTarget, NormalizedConversationTarget } from './conversationTarget';
4
4
  import { UtilityHostToolHandler } from './electronUtilityAgentClient';
5
5
  import { UtilityAgentPromptResult, UtilityAutoRouteRatingResult, UtilityConversationRewindResult, UtilityAgentSnapshotResult, UtilityAgentStopResult, UtilityPromptRequest } from './utilityAgentProtocol';
@@ -11,13 +11,7 @@ export interface ElectronTargetRuntimeClient {
11
11
  rewind(messageIndex: number): Promise<UtilityConversationRewindResult>;
12
12
  requestStop(runId?: string): Promise<UtilityAgentStopResult>;
13
13
  enqueueGuide(envelope: ConversationInputEnvelope): Promise<GuideReceipt>;
14
- queueAction?(action: ConversationQueueAction, input?: {
15
- id?: string;
16
- text?: string;
17
- requestedMode?: string;
18
- goalObjective?: string;
19
- createdAt?: string;
20
- }): Promise<Record<string, unknown>>;
14
+ queueAction?(action: ConversationQueueAction, input?: ConversationQueueActionInput): Promise<Record<string, unknown>>;
21
15
  checkpoint(): Promise<Record<string, unknown>>;
22
16
  contextCompress?(options?: {
23
17
  keepRecent?: number;
@@ -79,13 +73,7 @@ export declare class ElectronUtilityRuntimePool {
79
73
  rewind(target: ConversationRuntimeTarget, messageIndex: number): Promise<UtilityConversationRewindResult>;
80
74
  requestStop(target: ConversationRuntimeTarget, runId?: string): Promise<ElectronPoolStopResult>;
81
75
  enqueueGuide(envelope: ConversationInputEnvelope): Promise<GuideReceipt>;
82
- queueAction(target: ConversationRuntimeTarget, action: ConversationQueueAction, input?: {
83
- id?: string;
84
- text?: string;
85
- requestedMode?: string;
86
- goalObjective?: string;
87
- createdAt?: string;
88
- }): Promise<Record<string, unknown>>;
76
+ queueAction(target: ConversationRuntimeTarget, action: ConversationQueueAction, input?: ConversationQueueActionInput): Promise<Record<string, unknown>>;
89
77
  checkpoint(target: ConversationRuntimeTarget): Promise<Record<string, unknown>>;
90
78
  contextCompress(target: ConversationRuntimeTarget, options?: {
91
79
  keepRecent?: number;
@@ -1,6 +1,6 @@
1
1
  import { BrowserControlRequest, BrowserControlResult } from './browserControl';
2
2
  import { BrowserUseRequest } from './browserUse';
3
- import { AgentPromptMessage, ConversationContextCompressOptions, ConversationKernelRunOptions, ConversationKernelRunResult, ConversationQueueAction, ConversationQueueMode, ConversationRuntimeState, ConversationStopResult } from './conversationKernel';
3
+ import { AgentPromptMessage, ConversationContextCompressOptions, ConversationKernelRunOptions, ConversationKernelRunResult, ConversationQueueAction, ConversationQueueActionInput, ConversationQueueMode, ConversationRuntimeState, ConversationStopResult } from './conversationKernel';
4
4
  import { ConversationRuntimeTarget, NormalizedConversationTarget } from './conversationTarget';
5
5
  import { AgentMode, AgentWorkEvent, ConversationInputEnvelope, GuideReceipt } from './types';
6
6
  import type { AutoRouteRatingResult, ConversationSnapshot } from './agent';
@@ -106,13 +106,7 @@ export type UtilityAgentRequest = {
106
106
  params: {
107
107
  target: ConversationRuntimeTarget;
108
108
  action: ConversationQueueAction;
109
- input?: {
110
- id?: string;
111
- text?: string;
112
- requestedMode?: string;
113
- goalObjective?: string;
114
- createdAt?: string;
115
- };
109
+ input?: ConversationQueueActionInput;
116
110
  };
117
111
  } | {
118
112
  id: string;
@@ -0,0 +1,16 @@
1
+ import type { AgentWorkEvent } from './types';
2
+ /**
3
+ * Bounds cross-process traffic for high-rate streaming text without changing
4
+ * durable work-run events. Non-text events always flush pending text first.
5
+ */
6
+ export declare class WorkEventCoalescer {
7
+ private readonly emit;
8
+ private readonly windowMs;
9
+ private readonly pending;
10
+ constructor(emit: (event: AgentWorkEvent) => void, windowMs?: number);
11
+ push(event: AgentWorkEvent): void;
12
+ flush(key: string): void;
13
+ flushAll(): void;
14
+ pendingCount(): number;
15
+ }
16
+ //# sourceMappingURL=workEventCoalescer.d.ts.map