newmark-agent 0.5.4 → 0.5.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/conversation-utility-host.bundle.cjs +230 -66
- package/dist/core/agent.d.ts +1 -1
- package/dist/core/agent.js +55 -25
- package/dist/core/agentKernelRunner.js +55 -9
- package/dist/core/autoRouter.d.ts +1 -0
- package/dist/core/autoRouter.js +9 -0
- package/dist/core/conversationKernel.d.ts +12 -0
- package/dist/core/conversationKernel.js +32 -1
- package/dist/core/electronUtilityRuntimePool.js +7 -2
- package/dist/core/emptyResponseRetry.d.ts +16 -0
- package/dist/core/emptyResponseRetry.js +25 -0
- package/dist/core/terminalOutputBuffer.d.ts +24 -0
- package/dist/core/terminalOutputBuffer.js +92 -0
- package/dist/core/types.d.ts +21 -0
- package/dist/core/wslAgentRuntimePool.js +6 -2
- package/dist/llm/provider.d.ts +4 -4
- package/dist/llm/provider.js +55 -21
- package/dist/main.js +46 -3
- package/dist/preload.js +1 -0
- package/dist/providers/chat-completions.adapter.js +17 -4
- package/dist/providers/provider-events.d.ts +13 -0
- package/dist/providers/provider-events.js +55 -4
- package/dist/providers/responses.adapter.js +23 -9
- package/dist/server.js +21 -0
- package/dist/ui/index.html +2415 -216
- package/dist/ui/lucide-sprite.svg +4 -0
- package/dist/wsl-agent-host.bundle.cjs +230 -66
- package/package.json +10 -3
package/dist/core/agent.js
CHANGED
|
@@ -1016,7 +1016,13 @@ class Agent {
|
|
|
1016
1016
|
beginRouteAttempt() {
|
|
1017
1017
|
this.routeAttemptStartedAt = Date.now();
|
|
1018
1018
|
}
|
|
1019
|
-
async waitForPlannedRouteRetry() {
|
|
1019
|
+
async waitForPlannedRouteRetry(explicitDelayMs) {
|
|
1020
|
+
if (explicitDelayMs !== undefined) {
|
|
1021
|
+
if (explicitDelayMs <= 0)
|
|
1022
|
+
return;
|
|
1023
|
+
await new Promise(resolve => setTimeout(resolve, explicitDelayMs));
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1020
1026
|
const waitBudgetMs = Math.max(0, Math.min(15_000, this.lastRouteDecision?.retryBudgetMs ?? 5_000));
|
|
1021
1027
|
const delay = Math.max(0, Math.min(waitBudgetMs, this.lastRouteRetryDelayMs));
|
|
1022
1028
|
this.lastRouteRetryDelayMs = 0;
|
|
@@ -2298,6 +2304,7 @@ class Agent {
|
|
|
2298
2304
|
status: input.status,
|
|
2299
2305
|
guide: !isToolEvent && input.guide ? this.normalizeGuideReceipt(input.guide) : undefined,
|
|
2300
2306
|
displayImage: isToolEvent ? this.hydrateDisplayImage(input.displayImage) : undefined,
|
|
2307
|
+
fallback: input.fallback,
|
|
2301
2308
|
};
|
|
2302
2309
|
if (activeRun && this.isPersistablePublicWorkEvent(event)) {
|
|
2303
2310
|
activeRun.sequence = Number(sequence || activeRun.sequence + 1);
|
|
@@ -5587,10 +5594,22 @@ class Agent {
|
|
|
5587
5594
|
}
|
|
5588
5595
|
compressionBuildBlockStart(messages) {
|
|
5589
5596
|
const activeRunId = this.currentWorkRunId();
|
|
5590
|
-
if (
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5597
|
+
if (activeRunId) {
|
|
5598
|
+
const index = messages.findIndex(message => String(message.run_id || message.runId || '') === activeRunId);
|
|
5599
|
+
if (index >= 0)
|
|
5600
|
+
return index;
|
|
5601
|
+
}
|
|
5602
|
+
// dev-0.5.6: 空闲对话(无活动 run)时不得把全部历史算进当前 Build Block,
|
|
5603
|
+
// 否则上下文显示窗口的长期历史恒为 0。回退语义:
|
|
5604
|
+
// - 存在带 run_id 的消息:boundary 取最后一个 run 的起点之后(该 run 及其
|
|
5605
|
+
// 之前的历史属于长期历史,其后无归属的消息属于当前未命名区块);
|
|
5606
|
+
// - 完全没有 run_id:全部历史都是长期历史(不存在当前 Build Block)。
|
|
5607
|
+
let lastRunBoundary = -1;
|
|
5608
|
+
for (let index = 0; index < messages.length; index += 1) {
|
|
5609
|
+
if (String(messages[index]?.run_id || messages[index]?.runId || ''))
|
|
5610
|
+
lastRunBoundary = index + 1;
|
|
5611
|
+
}
|
|
5612
|
+
return lastRunBoundary >= 0 ? lastRunBoundary : messages.length;
|
|
5594
5613
|
}
|
|
5595
5614
|
recentContextSuffix(messages, maxMessages, tokenBudget) {
|
|
5596
5615
|
if (!messages.length)
|
|
@@ -6651,17 +6670,12 @@ class Agent {
|
|
|
6651
6670
|
if (this.routeAttemptStartedAt > 0)
|
|
6652
6671
|
currentAttempt.durationMs = Math.max(0, Date.now() - this.routeAttemptStartedAt);
|
|
6653
6672
|
}
|
|
6654
|
-
if (!fallbackEnabled) {
|
|
6655
|
-
this.lastRouteDecision.finalStatus = 'failed';
|
|
6656
|
-
this.routeAttemptStartedAt = 0;
|
|
6657
|
-
this.persistRouteDecision(this.lastRouteDecision);
|
|
6658
|
-
return null;
|
|
6659
|
-
}
|
|
6660
6673
|
if (!this.pendingAutoAttempts.length) {
|
|
6661
6674
|
this.pendingAutoAttempts = this.autoRouter.planAttempts(this.lastRouteDecision, this.autoRouteCandidates(), {
|
|
6662
6675
|
error: failure,
|
|
6663
6676
|
streamCommitted: this.routeStreamCommitted,
|
|
6664
6677
|
sideEffectCommitted: this.routeSideEffectCommitted,
|
|
6678
|
+
allowModelFallback: fallbackEnabled,
|
|
6665
6679
|
});
|
|
6666
6680
|
}
|
|
6667
6681
|
const next = this.pendingAutoAttempts.shift();
|
|
@@ -6702,8 +6716,11 @@ class Agent {
|
|
|
6702
6716
|
return null;
|
|
6703
6717
|
this.model = next.name;
|
|
6704
6718
|
this.fixedDeployment = this.deploymentRef(next);
|
|
6705
|
-
|
|
6706
|
-
|
|
6719
|
+
// Do NOT rewrite auto_switch_anchor_provider here. The anchor describes
|
|
6720
|
+
// the user's Auto-routing scope; a fixed-model recovery must never
|
|
6721
|
+
// repoint it (a single transient failure would otherwise silently move
|
|
6722
|
+
// every future Auto route — and every future fallback pool — to the
|
|
6723
|
+
// recovery provider, which is the cross-provider leak users observed).
|
|
6707
6724
|
return current;
|
|
6708
6725
|
}
|
|
6709
6726
|
isLlmErrorText(text) {
|
|
@@ -6711,18 +6728,23 @@ class Agent {
|
|
|
6711
6728
|
}
|
|
6712
6729
|
scopedSwitchModels(currentModelName) {
|
|
6713
6730
|
const all = this.config.allModels();
|
|
6714
|
-
|
|
6715
|
-
|
|
6716
|
-
|
|
6717
|
-
|
|
6718
|
-
|
|
6719
|
-
|
|
6720
|
-
|
|
6721
|
-
|
|
6731
|
+
// Fixed-model recovery is always provider-local. A same-named model on a
|
|
6732
|
+
// different provider is a different deployment and must never receive the
|
|
6733
|
+
// failed provider's credentials or become an implicit fallback.
|
|
6734
|
+
// Resolution order is strictly deployment-first: the pinned deployment,
|
|
6735
|
+
// then the exact active deployment. The global auto anchor is deliberately
|
|
6736
|
+
// NOT consulted here — it belongs to Auto routing only and can point at a
|
|
6737
|
+
// different provider (for example after a previous cross-provider switch),
|
|
6738
|
+
// which is exactly the same-name cross-provider leak this guard exists to
|
|
6739
|
+
// prevent. When no provider can be established unambiguously, return an
|
|
6740
|
+
// empty pool: failing the switch is safe, crossing providers is not.
|
|
6741
|
+
const current = currentModelName === 'auto' ? this.activeModelConfig() : this.config.findModel(currentModelName);
|
|
6742
|
+
const providerId = current?.provider_id
|
|
6743
|
+
|| this.fixedDeployment?.providerId
|
|
6744
|
+
|| (currentModelName !== 'auto' ? this.activeDeployment()?.providerId : undefined);
|
|
6722
6745
|
if (!providerId)
|
|
6723
|
-
return
|
|
6724
|
-
|
|
6725
|
-
return all.filter(m => m.provider_id === (provider?.id || providerId));
|
|
6746
|
+
return [];
|
|
6747
|
+
return all.filter(m => m.provider_id === providerId);
|
|
6726
6748
|
}
|
|
6727
6749
|
async validateModels(selectedNames, options = {}) {
|
|
6728
6750
|
if (this.modelValidationPromise)
|
|
@@ -8224,9 +8246,17 @@ class Agent {
|
|
|
8224
8246
|
return '[Subagent] Not found.';
|
|
8225
8247
|
const requestedModel = this.normalizeSubagentModelSelection(sa.model && sa.model !== 'default' ? sa.model : this.modelSelectionValue() || this.model);
|
|
8226
8248
|
const requestedDeployment = parseDeploymentSelectionValue(requestedModel);
|
|
8227
|
-
|
|
8249
|
+
let assignedModel = requestedModel === 'auto'
|
|
8228
8250
|
? this.activeModelConfig()
|
|
8229
8251
|
: (requestedDeployment ? this.config.findDeployment(requestedDeployment) : this.config.findModel(requestedModel));
|
|
8252
|
+
// Imported presets (and stale records) may reference a bare model name
|
|
8253
|
+
// that no longer exists in the configured catalog. Resolving by name must
|
|
8254
|
+
// never guess a provider; instead the peer inherits the parent deployment
|
|
8255
|
+
// so the job can still run. Qualified deployment values keep failing
|
|
8256
|
+
// closed — they are exact references, not guesses.
|
|
8257
|
+
if (!assignedModel && !requestedDeployment && requestedModel !== 'auto') {
|
|
8258
|
+
assignedModel = this.activeModelConfig();
|
|
8259
|
+
}
|
|
8230
8260
|
const model = assignedModel?.name || (requestedModel === 'auto' ? this.activeModelName() : requestedModel);
|
|
8231
8261
|
const activeModel = this.activeModelConfig();
|
|
8232
8262
|
const activeProvider = this.engineModel();
|
|
@@ -46,6 +46,7 @@ const toolPolicy_1 = require("./toolPolicy");
|
|
|
46
46
|
const performanceDiagnostics_1 = require("./performanceDiagnostics");
|
|
47
47
|
const agentKernelDiagnostics_1 = require("./agentKernelDiagnostics");
|
|
48
48
|
const toolchain_1 = require("../toolchain");
|
|
49
|
+
const emptyResponseRetry_1 = require("./emptyResponseRetry");
|
|
49
50
|
const publicStreamFilters = new WeakMap();
|
|
50
51
|
const brokerOnlyAssistantBuffers = new WeakMap();
|
|
51
52
|
const BROKER_PREFACE_BUFFER_CHARS = 96;
|
|
@@ -206,7 +207,7 @@ function kernelTurnFailed(agent, turn) {
|
|
|
206
207
|
return turn.stopReason === 'error' || agent.isLlmErrorText(turn.text);
|
|
207
208
|
}
|
|
208
209
|
function providerTurnIsEmpty(turn) {
|
|
209
|
-
return /provider returned an empty response/i.test(`${turn.errorMessage}\n${turn.text}`);
|
|
210
|
+
return !turn.activity && /provider returned an empty response/i.test(`${turn.errorMessage}\n${turn.text}`);
|
|
210
211
|
}
|
|
211
212
|
function removeTrailingFailedAssistant(agent, messages) {
|
|
212
213
|
const last = messages[messages.length - 1];
|
|
@@ -216,6 +217,14 @@ function removeTrailingFailedAssistant(agent, messages) {
|
|
|
216
217
|
if (last.stopReason === 'error' || agent.isLlmErrorText(text))
|
|
217
218
|
messages.pop();
|
|
218
219
|
}
|
|
220
|
+
function removeTrailingThoughtOnlyAssistant(messages) {
|
|
221
|
+
const last = messages[messages.length - 1];
|
|
222
|
+
if (last?.role !== 'assistant')
|
|
223
|
+
return;
|
|
224
|
+
const hasToolCall = last.content.some(content => content.type === 'toolCall');
|
|
225
|
+
if (!KernelMessageText(last).trim() && !hasToolCall)
|
|
226
|
+
messages.pop();
|
|
227
|
+
}
|
|
219
228
|
function normalizePublicProviderError(error, secrets = []) {
|
|
220
229
|
let raw = '';
|
|
221
230
|
if (error instanceof Error) {
|
|
@@ -369,8 +378,22 @@ async function runAgentKernel(agent) {
|
|
|
369
378
|
const tokens = [];
|
|
370
379
|
const runOnce = async (promptMessages, appendPromptToAgentHistory) => {
|
|
371
380
|
let lastAssistant = null;
|
|
381
|
+
let observedActivity = false;
|
|
382
|
+
let observedThought = false;
|
|
372
383
|
const unsubscribe = kernel.subscribe(async (event) => {
|
|
373
384
|
await handleKernelEvent(agent, event, tokens);
|
|
385
|
+
if (event.type === 'message_update') {
|
|
386
|
+
const delta = event.assistantMessageEvent;
|
|
387
|
+
const deltaText = typeof delta.delta === 'string'
|
|
388
|
+
? delta.delta
|
|
389
|
+
: '';
|
|
390
|
+
const thoughtDelta = delta.type === 'thinking_delta' && !!deltaText.trim();
|
|
391
|
+
observedThought = observedThought || thoughtDelta;
|
|
392
|
+
observedActivity = observedActivity ||
|
|
393
|
+
thoughtDelta ||
|
|
394
|
+
(delta.type === 'text_delta' && !!deltaText.trim()) ||
|
|
395
|
+
(delta.type === 'toolcall_end');
|
|
396
|
+
}
|
|
374
397
|
if (event.type === 'message_end' && event.message.role === 'assistant') {
|
|
375
398
|
lastAssistant = event.message;
|
|
376
399
|
}
|
|
@@ -390,11 +413,14 @@ async function runAgentKernel(agent) {
|
|
|
390
413
|
const text = assistant ? KernelMessageText(assistant) : '';
|
|
391
414
|
const hasToolCall = !!assistant?.content?.some(content => content.type === 'toolCall');
|
|
392
415
|
const emptyResponse = !assistant
|
|
393
|
-
|| (!text.trim() && !hasToolCall && String(assistant?.stopReason || '') !== 'aborted');
|
|
416
|
+
|| (!text.trim() && !hasToolCall && !observedActivity && String(assistant?.stopReason || '') !== 'aborted');
|
|
394
417
|
return {
|
|
395
418
|
text: emptyResponse ? '[Error] Provider returned an empty response.' : text,
|
|
396
419
|
stopReason: String(assistant?.stopReason || ''),
|
|
397
420
|
errorMessage: String(assistant?.errorMessage || (emptyResponse ? 'Provider returned an empty response.' : '')),
|
|
421
|
+
activity: observedActivity || !!text.trim() || hasToolCall,
|
|
422
|
+
thoughtOnly: observedThought && !text.trim() && !hasToolCall
|
|
423
|
+
&& !['error', 'aborted'].includes(String(assistant?.stopReason || '')),
|
|
398
424
|
};
|
|
399
425
|
}
|
|
400
426
|
finally {
|
|
@@ -440,16 +466,31 @@ async function runAgentKernel(agent) {
|
|
|
440
466
|
agent.recordWorkStatus('Final visual fallback used: local mini OCR plus conservative text correction.');
|
|
441
467
|
}
|
|
442
468
|
if (modelBeforeKernelRun && modelBeforeKernelRun !== agent.model && !tokens.some(t => t.text?.includes('[Model fallback]'))) {
|
|
443
|
-
|
|
469
|
+
const notice = `[Model fallback] ${modelBeforeKernelRun} unavailable; switched to ${agent.model}.`;
|
|
470
|
+
tokens.unshift({ type: 'text', text: notice });
|
|
471
|
+
agent.emitWorkEvent({
|
|
472
|
+
type: 'status',
|
|
473
|
+
content: notice,
|
|
474
|
+
fallback: { from: modelBeforeKernelRun, to: agent.model, providerId: agent.activeDeployment()?.providerId },
|
|
475
|
+
});
|
|
444
476
|
}
|
|
445
|
-
let
|
|
446
|
-
|
|
477
|
+
let consecutiveEmptyResponses = 0;
|
|
478
|
+
for (;;) {
|
|
479
|
+
const emptyResponseState = (0, emptyResponseRetry_1.observeEmptyResponseOutcome)(consecutiveEmptyResponses, providerTurnIsEmpty(lastTurn));
|
|
480
|
+
consecutiveEmptyResponses = emptyResponseState.consecutiveEmptyResponses;
|
|
481
|
+
if (lastTurn.thoughtOnly) {
|
|
482
|
+
removeTrailingThoughtOnlyAssistant(kernel.state.messages);
|
|
483
|
+
lastTurn = await runWithCompressionResume([], false);
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
if (!emptyResponseState.retry)
|
|
487
|
+
break;
|
|
447
488
|
removeTrailingFailedAssistant(agent, kernel.state.messages);
|
|
448
|
-
|
|
449
|
-
const notice = `[Model retry] Provider returned an empty response; retrying the same deployment (${
|
|
489
|
+
const retryNumber = consecutiveEmptyResponses;
|
|
490
|
+
const notice = `[Model retry] Provider returned an empty response; retrying the same deployment (${retryNumber}/${emptyResponseRetry_1.MAX_EMPTY_RESPONSE_RETRIES}) after ${(0, emptyResponseRetry_1.emptyResponseRetryDelayMs)(consecutiveEmptyResponses)}ms.`;
|
|
450
491
|
tokens.push({ type: 'text', text: notice });
|
|
451
492
|
agent.recordWorkStatus(notice);
|
|
452
|
-
await agent.waitForPlannedRouteRetry();
|
|
493
|
+
await agent.waitForPlannedRouteRetry((0, emptyResponseRetry_1.emptyResponseRetryDelayMs)(consecutiveEmptyResponses));
|
|
453
494
|
lastTurn = await runWithCompressionResume([], false);
|
|
454
495
|
}
|
|
455
496
|
let routeRetries = 0;
|
|
@@ -462,6 +503,11 @@ async function runAgentKernel(agent) {
|
|
|
462
503
|
const notice = routeTransitionNotice(agent, previous);
|
|
463
504
|
tokens.push({ type: 'text', text: notice });
|
|
464
505
|
agent.recordWorkStatus(notice);
|
|
506
|
+
agent.emitWorkEvent({
|
|
507
|
+
type: 'status',
|
|
508
|
+
content: notice,
|
|
509
|
+
fallback: { from: previous, to: agent.model, providerId: agent.activeDeployment()?.providerId },
|
|
510
|
+
});
|
|
465
511
|
kernel.state.model = toKernelModel(agent);
|
|
466
512
|
const fallbackToolSurface = refreshToolSurface(true);
|
|
467
513
|
kernel.state.systemPrompt = [agent.buildSystemPrompt(), fallbackToolSurface.systemPromptNotice].filter(Boolean).join('\n\n');
|
|
@@ -656,7 +702,7 @@ async function runAgentKernel(agent) {
|
|
|
656
702
|
}
|
|
657
703
|
if (textStarted)
|
|
658
704
|
finalContent.push({ type: 'text', text });
|
|
659
|
-
if (!finalContent.length) {
|
|
705
|
+
if (!finalContent.length && !thinking.trim()) {
|
|
660
706
|
text = '[Error] Provider returned an empty response.';
|
|
661
707
|
finalContent.push({ type: 'text', text });
|
|
662
708
|
}
|
|
@@ -153,6 +153,7 @@ export declare class AutoRouter {
|
|
|
153
153
|
error: RouteFailure;
|
|
154
154
|
streamCommitted: boolean;
|
|
155
155
|
sideEffectCommitted: boolean;
|
|
156
|
+
allowModelFallback?: boolean;
|
|
156
157
|
}): PlannedRouteAttempt[];
|
|
157
158
|
recordEndpointFailure(deployment: DeploymentRef, failure: RouteFailureType | RouteFailure): void;
|
|
158
159
|
recordEndpointSuccess(deployment: DeploymentRef, latencyMs?: number, throughput?: number): void;
|
package/dist/core/autoRouter.js
CHANGED
|
@@ -290,15 +290,24 @@ class AutoRouter {
|
|
|
290
290
|
retryDelayMs,
|
|
291
291
|
});
|
|
292
292
|
}
|
|
293
|
+
if (failure.allowModelFallback === false)
|
|
294
|
+
return attempts.slice(0, remainingAttempts);
|
|
293
295
|
const selection = decision.requestedSelection;
|
|
294
296
|
if (selection.kind === 'fixed')
|
|
295
297
|
return attempts.slice(0, remainingAttempts);
|
|
296
298
|
const scope = selection.kind === 'auto' ? selection.scope : { kind: 'provider', providerId: current.providerId };
|
|
297
299
|
const subset = selection.kind === 'auto' ? selection.subset : undefined;
|
|
298
300
|
const currentGroup = current.logicalModelGroupId;
|
|
301
|
+
const currentProviderId = current.providerId;
|
|
299
302
|
const now = this.now();
|
|
300
303
|
const attemptedDeployments = decision.attempts.map(attempt => attempt.deployment);
|
|
304
|
+
// Fallback is a recovery operation, not a new global routing decision.
|
|
305
|
+
// It must never cross the provider boundary, even when the original Auto
|
|
306
|
+
// selection has global scope. This prevents equal model ids from silently
|
|
307
|
+
// switching credentials/endpoints (for example provider A/model X to
|
|
308
|
+
// provider B/model X).
|
|
301
309
|
const eligible = candidates.filter(candidate => candidate.enabled
|
|
310
|
+
&& candidate.deployment.providerId === currentProviderId
|
|
302
311
|
&& inScope(candidate.deployment, scope)
|
|
303
312
|
&& inSubset(candidate.deployment, subset)
|
|
304
313
|
&& !sameDeployment(candidate.deployment, current)
|
|
@@ -194,6 +194,18 @@ export declare class ConversationKernel {
|
|
|
194
194
|
steering: string[];
|
|
195
195
|
followUp: string[];
|
|
196
196
|
};
|
|
197
|
+
/**
|
|
198
|
+
* dev-0.5.6: 排队 followUp 消息 drain 时转成普通用户消息载荷。
|
|
199
|
+
*
|
|
200
|
+
* 入队时(enqueueSameSession)为去重/编辑保留了 clientMessageId,并把
|
|
201
|
+
* runId 固定为入队时运行的 runId;若原样传给 process,写入 chatMessages
|
|
202
|
+
* 的用户消息会携带旧 runId + clientMessageId,渲染端(PC renderTranscript /
|
|
203
|
+
* 移动端 projectRemoteConversationItems)会把「clientMessageId + runId 命中
|
|
204
|
+
* workRuns」的消息误判为 guide 消息而跳过气泡,导致排队自动发送的消息不
|
|
205
|
+
* 显示在用户输入时间线。清除这两个字段后 process 走普通用户消息分支
|
|
206
|
+
* (chatMessages.push,mode 取 visibleMode,runId 取当前 run)。
|
|
207
|
+
*/
|
|
208
|
+
private drainQueuedFollowUpMessage;
|
|
197
209
|
queueItems(target: ConversationTargetInput): ConversationQueueItemSnapshot[];
|
|
198
210
|
enqueueNext(target: ConversationTargetInput, input: {
|
|
199
211
|
id: string;
|
|
@@ -60,6 +60,33 @@ class ConversationKernel {
|
|
|
60
60
|
followUp: queued?.followUp.slice() || [],
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* dev-0.5.6: 排队 followUp 消息 drain 时转成普通用户消息载荷。
|
|
65
|
+
*
|
|
66
|
+
* 入队时(enqueueSameSession)为去重/编辑保留了 clientMessageId,并把
|
|
67
|
+
* runId 固定为入队时运行的 runId;若原样传给 process,写入 chatMessages
|
|
68
|
+
* 的用户消息会携带旧 runId + clientMessageId,渲染端(PC renderTranscript /
|
|
69
|
+
* 移动端 projectRemoteConversationItems)会把「clientMessageId + runId 命中
|
|
70
|
+
* workRuns」的消息误判为 guide 消息而跳过气泡,导致排队自动发送的消息不
|
|
71
|
+
* 显示在用户输入时间线。清除这两个字段后 process 走普通用户消息分支
|
|
72
|
+
* (chatMessages.push,mode 取 visibleMode,runId 取当前 run)。
|
|
73
|
+
*/
|
|
74
|
+
drainQueuedFollowUpMessage(message) {
|
|
75
|
+
if (typeof message === 'string')
|
|
76
|
+
return message;
|
|
77
|
+
const text = String(message.text || '');
|
|
78
|
+
const images = message.images;
|
|
79
|
+
const attachments = message.attachments;
|
|
80
|
+
const visible = message.visibleUserInput;
|
|
81
|
+
const visibleMode = message.visibleMode;
|
|
82
|
+
return {
|
|
83
|
+
text,
|
|
84
|
+
...(images?.length ? { images } : {}),
|
|
85
|
+
...(attachments?.length ? { attachments } : {}),
|
|
86
|
+
...(visible ? { visibleUserInput: visible } : {}),
|
|
87
|
+
...(visibleMode ? { visibleMode } : {}),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
63
90
|
queueItems(target) {
|
|
64
91
|
const runtime = this.findRuntime(target);
|
|
65
92
|
if (!runtime)
|
|
@@ -888,7 +915,7 @@ class ConversationKernel {
|
|
|
888
915
|
lastTokens = await this.runSingle(runtime, batchMessage, 'steer');
|
|
889
916
|
}
|
|
890
917
|
else {
|
|
891
|
-
lastTokens = await this.runSingle(runtime, next.message, next.queueMode);
|
|
918
|
+
lastTokens = await this.runSingle(runtime, this.drainQueuedFollowUpMessage(next.message), next.queueMode);
|
|
892
919
|
}
|
|
893
920
|
}
|
|
894
921
|
const rootMessage = runtime.runner.subagents.readRootInbox()[0];
|
|
@@ -1414,6 +1441,10 @@ class ConversationKernel {
|
|
|
1414
1441
|
content: 'Conversation queue updated.',
|
|
1415
1442
|
conversationId: runtime.id,
|
|
1416
1443
|
queue: this.queued(runtime.target),
|
|
1444
|
+
// Structured rows with stable kernel ids so every consumer (PC UI and
|
|
1445
|
+
// the paired mobile client) can render/edit/delete the same items.
|
|
1446
|
+
queueItems: this.queueItems(runtime.target),
|
|
1447
|
+
queuePaused: runtime.queuePaused === true,
|
|
1417
1448
|
});
|
|
1418
1449
|
}
|
|
1419
1450
|
clearQueued(runtime) {
|
|
@@ -601,8 +601,13 @@ class ElectronUtilityRuntimePool {
|
|
|
601
601
|
return this.accessSequence;
|
|
602
602
|
}
|
|
603
603
|
maxResidentRuntimes() {
|
|
604
|
-
|
|
605
|
-
|
|
604
|
+
// The pool hosts one utility runtime per active conversation target.
|
|
605
|
+
// A small default (2) silently blocked users from running more than two
|
|
606
|
+
// conversations at once with "utility runtime pool capacity reached".
|
|
607
|
+
// Idle runtimes are still evicted on LRU after the idle TTL, so a higher
|
|
608
|
+
// default bounds memory by activity, not by an arbitrary conversation cap.
|
|
609
|
+
const configured = Number(this.options.maxResidentRuntimes ?? 8);
|
|
610
|
+
return Number.isFinite(configured) ? Math.max(1, Math.floor(configured)) : 8;
|
|
606
611
|
}
|
|
607
612
|
async serializeCapacity(operation) {
|
|
608
613
|
const previous = this.capacityTail;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const EMPTY_RESPONSE_RETRY_DELAYS_MS: readonly [200, 800, 2000, 10000, 60000];
|
|
2
|
+
/**
|
|
3
|
+
* A retry is scheduled only after an explicit provider empty-response
|
|
4
|
+
* failure. The initial failed request is not called a retry, so five retries
|
|
5
|
+
* means six consecutive explicit failures before termination.
|
|
6
|
+
*/
|
|
7
|
+
export declare const MAX_EMPTY_RESPONSE_RETRIES: 5;
|
|
8
|
+
export declare const MAX_CONSECUTIVE_EMPTY_RESPONSES: number;
|
|
9
|
+
export declare function emptyResponseRetryDelayMs(consecutiveEmptyResponses: number): number;
|
|
10
|
+
export interface EmptyResponseRetryState {
|
|
11
|
+
consecutiveEmptyResponses: number;
|
|
12
|
+
retry: boolean;
|
|
13
|
+
terminate: boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare function observeEmptyResponseOutcome(consecutiveEmptyResponses: number, emptyResponse: boolean): EmptyResponseRetryState;
|
|
16
|
+
//# sourceMappingURL=emptyResponseRetry.d.ts.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MAX_CONSECUTIVE_EMPTY_RESPONSES = exports.MAX_EMPTY_RESPONSE_RETRIES = exports.EMPTY_RESPONSE_RETRY_DELAYS_MS = void 0;
|
|
4
|
+
exports.emptyResponseRetryDelayMs = emptyResponseRetryDelayMs;
|
|
5
|
+
exports.observeEmptyResponseOutcome = observeEmptyResponseOutcome;
|
|
6
|
+
exports.EMPTY_RESPONSE_RETRY_DELAYS_MS = [200, 800, 2_000, 10_000, 60_000];
|
|
7
|
+
/**
|
|
8
|
+
* A retry is scheduled only after an explicit provider empty-response
|
|
9
|
+
* failure. The initial failed request is not called a retry, so five retries
|
|
10
|
+
* means six consecutive explicit failures before termination.
|
|
11
|
+
*/
|
|
12
|
+
exports.MAX_EMPTY_RESPONSE_RETRIES = exports.EMPTY_RESPONSE_RETRY_DELAYS_MS.length;
|
|
13
|
+
exports.MAX_CONSECUTIVE_EMPTY_RESPONSES = exports.MAX_EMPTY_RESPONSE_RETRIES + 1;
|
|
14
|
+
function emptyResponseRetryDelayMs(consecutiveEmptyResponses) {
|
|
15
|
+
return exports.EMPTY_RESPONSE_RETRY_DELAYS_MS[Math.max(0, consecutiveEmptyResponses - 1)] ?? 0;
|
|
16
|
+
}
|
|
17
|
+
function observeEmptyResponseOutcome(consecutiveEmptyResponses, emptyResponse) {
|
|
18
|
+
const nextCount = emptyResponse ? consecutiveEmptyResponses + 1 : 0;
|
|
19
|
+
return {
|
|
20
|
+
consecutiveEmptyResponses: nextCount,
|
|
21
|
+
retry: emptyResponse && nextCount <= exports.MAX_EMPTY_RESPONSE_RETRIES,
|
|
22
|
+
terminate: emptyResponse && nextCount > exports.MAX_EMPTY_RESPONSE_RETRIES,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=emptyResponseRetry.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare const TERMINAL_HISTORY_LIMIT: number;
|
|
2
|
+
export interface TerminalOutputBufferOptions {
|
|
3
|
+
flushIntervalMs?: number;
|
|
4
|
+
historyLimit?: number;
|
|
5
|
+
}
|
|
6
|
+
export declare class TerminalOutputBuffer {
|
|
7
|
+
private readonly send;
|
|
8
|
+
private readonly sessions;
|
|
9
|
+
private timer;
|
|
10
|
+
private readonly flushIntervalMs;
|
|
11
|
+
private readonly historyLimit;
|
|
12
|
+
constructor(send: (sessionId: string, text: string) => void, options?: TerminalOutputBufferOptions);
|
|
13
|
+
push(sessionId: string, text: string): void;
|
|
14
|
+
flush(sessionId: string): void;
|
|
15
|
+
flushAll(): void;
|
|
16
|
+
close(sessionId: string): string;
|
|
17
|
+
history(sessionId: string): string;
|
|
18
|
+
pendingChunkCount(): number;
|
|
19
|
+
hasScheduledFlush(): boolean;
|
|
20
|
+
private bound;
|
|
21
|
+
private schedule;
|
|
22
|
+
private clearTimerWhenIdle;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=terminalOutputBuffer.d.ts.map
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TerminalOutputBuffer = exports.TERMINAL_HISTORY_LIMIT = void 0;
|
|
4
|
+
exports.TERMINAL_HISTORY_LIMIT = 256 * 1024;
|
|
5
|
+
class TerminalOutputBuffer {
|
|
6
|
+
send;
|
|
7
|
+
sessions = new Map();
|
|
8
|
+
timer = null;
|
|
9
|
+
flushIntervalMs;
|
|
10
|
+
historyLimit;
|
|
11
|
+
constructor(send, options = {}) {
|
|
12
|
+
this.send = send;
|
|
13
|
+
this.flushIntervalMs = Math.max(1, options.flushIntervalMs ?? 20);
|
|
14
|
+
this.historyLimit = Math.max(1, options.historyLimit ?? exports.TERMINAL_HISTORY_LIMIT);
|
|
15
|
+
}
|
|
16
|
+
push(sessionId, text) {
|
|
17
|
+
if (!text)
|
|
18
|
+
return;
|
|
19
|
+
let state = this.sessions.get(sessionId);
|
|
20
|
+
if (!state) {
|
|
21
|
+
state = { chunks: [], length: 0, history: '' };
|
|
22
|
+
this.sessions.set(sessionId, state);
|
|
23
|
+
}
|
|
24
|
+
state.chunks.push(text);
|
|
25
|
+
state.length += text.length;
|
|
26
|
+
this.schedule();
|
|
27
|
+
}
|
|
28
|
+
flush(sessionId) {
|
|
29
|
+
const state = this.sessions.get(sessionId);
|
|
30
|
+
if (!state?.length)
|
|
31
|
+
return;
|
|
32
|
+
const text = state.chunks.length === 1 ? state.chunks[0] : state.chunks.join('');
|
|
33
|
+
state.chunks = [];
|
|
34
|
+
state.length = 0;
|
|
35
|
+
state.history = this.bound(`${state.history}${text}`);
|
|
36
|
+
this.send(sessionId, text);
|
|
37
|
+
this.clearTimerWhenIdle();
|
|
38
|
+
}
|
|
39
|
+
flushAll() {
|
|
40
|
+
for (const sessionId of this.sessions.keys())
|
|
41
|
+
this.flush(sessionId);
|
|
42
|
+
this.clearTimerWhenIdle(true);
|
|
43
|
+
}
|
|
44
|
+
close(sessionId) {
|
|
45
|
+
this.flush(sessionId);
|
|
46
|
+
const history = this.sessions.get(sessionId)?.history ?? '';
|
|
47
|
+
this.sessions.delete(sessionId);
|
|
48
|
+
this.clearTimerWhenIdle();
|
|
49
|
+
return history;
|
|
50
|
+
}
|
|
51
|
+
history(sessionId) {
|
|
52
|
+
const state = this.sessions.get(sessionId);
|
|
53
|
+
if (!state)
|
|
54
|
+
return '';
|
|
55
|
+
if (!state.length)
|
|
56
|
+
return state.history;
|
|
57
|
+
const pending = state.chunks.length === 1 ? state.chunks[0] : state.chunks.join('');
|
|
58
|
+
return this.bound(`${state.history}${pending}`);
|
|
59
|
+
}
|
|
60
|
+
pendingChunkCount() {
|
|
61
|
+
let count = 0;
|
|
62
|
+
for (const state of this.sessions.values())
|
|
63
|
+
count += state.chunks.length;
|
|
64
|
+
return count;
|
|
65
|
+
}
|
|
66
|
+
hasScheduledFlush() {
|
|
67
|
+
return this.timer !== null;
|
|
68
|
+
}
|
|
69
|
+
bound(text) {
|
|
70
|
+
return text.length > this.historyLimit ? text.slice(-this.historyLimit) : text;
|
|
71
|
+
}
|
|
72
|
+
schedule() {
|
|
73
|
+
if (this.timer)
|
|
74
|
+
return;
|
|
75
|
+
this.timer = setTimeout(() => {
|
|
76
|
+
this.timer = null;
|
|
77
|
+
this.flushAll();
|
|
78
|
+
}, this.flushIntervalMs);
|
|
79
|
+
this.timer.unref?.();
|
|
80
|
+
}
|
|
81
|
+
clearTimerWhenIdle(force = false) {
|
|
82
|
+
if (!this.timer)
|
|
83
|
+
return;
|
|
84
|
+
const hasPending = !force && Array.from(this.sessions.values()).some(state => state.length > 0);
|
|
85
|
+
if (hasPending)
|
|
86
|
+
return;
|
|
87
|
+
clearTimeout(this.timer);
|
|
88
|
+
this.timer = null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
exports.TerminalOutputBuffer = TerminalOutputBuffer;
|
|
92
|
+
//# sourceMappingURL=terminalOutputBuffer.js.map
|
package/dist/core/types.d.ts
CHANGED
|
@@ -112,6 +112,17 @@ export interface AgentWorkEvent {
|
|
|
112
112
|
steering: string[];
|
|
113
113
|
followUp: string[];
|
|
114
114
|
};
|
|
115
|
+
/** Structured queue rows with stable kernel ids (mobile/PC queue unification export). */
|
|
116
|
+
queueItems?: Array<{
|
|
117
|
+
id: string;
|
|
118
|
+
text: string;
|
|
119
|
+
queueMode: string;
|
|
120
|
+
requestedMode?: string;
|
|
121
|
+
goalObjective?: string;
|
|
122
|
+
runId?: string;
|
|
123
|
+
createdAt: string;
|
|
124
|
+
}>;
|
|
125
|
+
queuePaused?: boolean;
|
|
115
126
|
workspaceId?: string;
|
|
116
127
|
workspaceKey?: string;
|
|
117
128
|
runtimeKey?: string;
|
|
@@ -124,6 +135,16 @@ export interface AgentWorkEvent {
|
|
|
124
135
|
status?: GuideReceiptStatus | ConversationWorkRunStatus | 'stopping' | 'force_restarting';
|
|
125
136
|
guide?: GuideReceipt;
|
|
126
137
|
displayImage?: DisplayImageAttachment;
|
|
138
|
+
/**
|
|
139
|
+
* 结构化模型回退信号:from 为回退前的模型名,to 为实际使用的模型名。
|
|
140
|
+
* 前端据此把输入框下方的模型选择区同步为实际生效的模型,而不是隐藏的
|
|
141
|
+
* 参数回退。
|
|
142
|
+
*/
|
|
143
|
+
fallback?: {
|
|
144
|
+
from: string;
|
|
145
|
+
to: string;
|
|
146
|
+
providerId?: string;
|
|
147
|
+
};
|
|
127
148
|
}
|
|
128
149
|
export interface ConversationWorkRun {
|
|
129
150
|
runId: string;
|
|
@@ -569,8 +569,12 @@ class WslAgentRuntimePool {
|
|
|
569
569
|
return this.accessSequence;
|
|
570
570
|
}
|
|
571
571
|
maxResidentRuntimes() {
|
|
572
|
-
|
|
573
|
-
|
|
572
|
+
// Same rationale as the Electron utility pool: one runtime per active
|
|
573
|
+
// conversation target. The previous default of 2 blocked parallel
|
|
574
|
+
// conversations with a capacity error; idle LRU eviction still bounds
|
|
575
|
+
// resident memory.
|
|
576
|
+
const configured = Number(this.options.maxResidentRuntimes ?? 8);
|
|
577
|
+
return Number.isFinite(configured) ? Math.max(1, Math.floor(configured)) : 8;
|
|
574
578
|
}
|
|
575
579
|
async serializeCapacity(operation) {
|
|
576
580
|
const previous = this.capacityTail;
|
package/dist/llm/provider.d.ts
CHANGED
|
@@ -96,7 +96,7 @@ export declare class LLMProvider {
|
|
|
96
96
|
* `provider_adapters_v2` context flag. Request serialization and SSE
|
|
97
97
|
* normalization are delegated to the shared provider adapters while the
|
|
98
98
|
* transport orchestration (loopback node-http, fetch -> node-http fallback,
|
|
99
|
-
*
|
|
99
|
+
* cancellation-only streaming and the 4xx Chat -> Responses downgrade) stay here.
|
|
100
100
|
* The emitted request body and StreamToken stream are byte-equivalent to
|
|
101
101
|
* the legacy inlined path.
|
|
102
102
|
*/
|
|
@@ -106,9 +106,9 @@ export declare class LLMProvider {
|
|
|
106
106
|
private shouldDowngradeToResponses;
|
|
107
107
|
/**
|
|
108
108
|
* Loopback-aware transport injected into adapter `execute`. Streaming
|
|
109
|
-
* requests retain the fetch-to-node fallback for transport failures
|
|
110
|
-
*
|
|
111
|
-
*
|
|
109
|
+
* requests retain the fetch-to-node fallback for transport failures. They
|
|
110
|
+
* have no response deadline; only caller cancellation or a concrete
|
|
111
|
+
* transport/provider failure may end the request.
|
|
112
112
|
*/
|
|
113
113
|
private buildProviderAdapterTransport;
|
|
114
114
|
private toTransportResponse;
|