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.
- package/dist/conversation-utility-host.bundle.cjs +238 -29
- package/dist/core/agent.d.ts +1 -0
- package/dist/core/agent.js +102 -10
- package/dist/core/agentKernelRunner.js +1 -1
- package/dist/core/autoRouter.d.ts +1 -1
- package/dist/core/autoRouter.js +16 -7
- package/dist/core/conversationKernel.d.ts +13 -8
- package/dist/core/conversationKernel.js +111 -10
- package/dist/core/electronUtilityAgentClient.d.ts +2 -8
- package/dist/core/electronUtilityRuntimePool.d.ts +3 -15
- package/dist/core/utilityAgentProtocol.d.ts +2 -8
- package/dist/core/workEventCoalescer.d.ts +16 -0
- package/dist/core/workEventCoalescer.js +52 -0
- package/dist/core/wslAgentClient.d.ts +2 -8
- package/dist/core/wslAgentProtocol.d.ts +2 -8
- package/dist/core/wslAgentRuntimePool.d.ts +3 -15
- package/dist/llm/provider.d.ts +5 -0
- package/dist/llm/provider.js +63 -1
- package/dist/main.js +221 -175
- package/dist/preload.js +2 -0
- package/dist/server.d.ts +17 -1
- package/dist/server.js +331 -9
- package/dist/ui/index.html +295 -51
- package/dist/wsl-agent-host.bundle.cjs +238 -29
- package/package.json +10 -5
package/dist/core/agent.js
CHANGED
|
@@ -1051,7 +1051,9 @@ class Agent {
|
|
|
1051
1051
|
}
|
|
1052
1052
|
updateProviders(value) {
|
|
1053
1053
|
const before = this.config.providers();
|
|
1054
|
-
|
|
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.
|
|
6687
|
+
if (!observedFailure.switchAllowed || this.routeStreamCommitted || this.routeSideEffectCommitted)
|
|
6658
6688
|
return null;
|
|
6659
6689
|
const current = this.model;
|
|
6660
|
-
const
|
|
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
|
|
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
|
|
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;
|
package/dist/core/autoRouter.js
CHANGED
|
@@ -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
|
|
119
|
-
return { type: 'balance_exhausted', retryable: false, switchAllowed:
|
|
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.
|
|
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
|
-
|
|
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?.
|
|
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
|
-
|
|
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
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
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
|