dsh-bundle-antigravity-host 0.1.25 → 0.1.26
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/README.md +17 -4
- package/dist/adapter/antigravity-adapter.d.ts +34 -9
- package/dist/adapter/antigravity-adapter.js +505 -44
- package/dist/adapter/antigravity-adapter.js.map +1 -1
- package/dist/adapter/continuation-state.d.ts +5 -1
- package/dist/adapter/continuation-state.js +3 -2
- package/dist/adapter/continuation-state.js.map +1 -1
- package/dist/adapter/image-content.d.ts +7 -0
- package/dist/adapter/image-content.js +23 -2
- package/dist/adapter/image-content.js.map +1 -1
- package/dist/adapter/prompt-transcript.d.ts +36 -3
- package/dist/adapter/prompt-transcript.js +86 -3
- package/dist/adapter/prompt-transcript.js.map +1 -1
- package/dist/adapter/prompt-turn.d.ts +8 -3
- package/dist/adapter/prompt-turn.js +15 -5
- package/dist/adapter/prompt-turn.js.map +1 -1
- package/dist/adapter/session-reuse.d.ts +19 -0
- package/dist/adapter/session-reuse.js +102 -0
- package/dist/adapter/session-reuse.js.map +1 -0
- package/dist/bridge/mcp-bridge.d.ts +9 -2
- package/dist/bridge/mcp-bridge.js +15 -2
- package/dist/bridge/mcp-bridge.js.map +1 -1
- package/dist/client.js +16 -3
- package/dist/index.js +32 -8
- package/dist/index.js.map +1 -1
- package/dist/pool/pooled-adapter.d.ts +7 -0
- package/dist/pool/pooled-adapter.js +53 -3
- package/dist/pool/pooled-adapter.js.map +1 -1
- package/dist/protocol/agent-link.d.ts +11 -1
- package/dist/protocol/agent-link.js +25 -0
- package/dist/protocol/agent-link.js.map +1 -1
- package/dist/runtime/session-map.d.ts +53 -0
- package/dist/runtime/session-map.js +213 -0
- package/dist/runtime/session-map.js.map +1 -0
- package/dist/settings.d.ts +31 -0
- package/dist/settings.js +5 -0
- package/dist/settings.js.map +1 -1
- package/docs/changelog.md +11 -0
- package/docs/configuration.md +34 -2
- package/docs/development.md +3 -3
- package/docs/reference.md +21 -5
- package/package.json +16 -1
|
@@ -4,21 +4,19 @@
|
|
|
4
4
|
* 形态:官方 ACP server(Gemini 主模型)走 stdio,工具留在 DSH 侧,经插件
|
|
5
5
|
* 进程内的私有 loopback MCP 桥暴露给 agent(见 ../bridge/mcp-bridge.ts)。
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* 「park 中的 MCP tools/call」,它们都随回合结束而释放。
|
|
11
|
-
*
|
|
12
|
-
* 不支持项一律抛 `LlmError(..., 'UNSUPPORTED_OPTION')`(dsh-llm 0.2.1 的
|
|
13
|
-
* error.d.ts 未导出该码的官方常量,因此用字面量并在下方集中定义)。
|
|
7
|
+
* DSH 是历史权威:只有输入前缀、上轮实际交付输出与路由一致才复用会话;
|
|
8
|
+
* 不一致时完整重建。工具结果在同一个 ACP prompt 内经 MCP 回填。
|
|
9
|
+
* 可选磁盘映射只保存 ID、计数与指纹,不存消息正文。
|
|
14
10
|
*/
|
|
15
11
|
import { LlmAdapter, LlmError, ReasoningEffortId } from '@deepseek-ai/dsh-llm';
|
|
16
12
|
import { BRIDGE_MCP_SERVER_NAME, McpToolBridge } from '../bridge/mcp-bridge.js';
|
|
17
13
|
import { AGY_REASONING_EFFORTS, AGY_REASONING_EFFORT_NAMES, DEFAULT_AGY_REASONING_EFFORT, foldAgyModelFamilies, modelFamilyId, routeTieredModelId, tierForModelId, } from '../runtime/agy-cli.js';
|
|
18
14
|
import { findModelSelector, toModelCatalog } from './model-catalog.js';
|
|
19
|
-
import { buildPromptWithImages, buildToolResultContent } from './image-content.js';
|
|
15
|
+
import { buildPromptDeltaWithImages, buildPromptWithImages, buildToolResultContent } from './image-content.js';
|
|
20
16
|
import { AGY_TURN_ERROR, PromptTurn } from './prompt-turn.js';
|
|
21
17
|
import { ContinuationState } from './continuation-state.js';
|
|
18
|
+
import { incrementalMessages, outputHash, outputMatches, prefixHash, prefixMatches, reusableSessionKey, routeFingerprint } from './session-reuse.js';
|
|
19
|
+
import { SessionMapStore } from '../runtime/session-map.js';
|
|
22
20
|
/** DSH 侧 provider 标识符;与 cordis.patch.yml 的 id 及文档保持一致。 */
|
|
23
21
|
export const AGY_PROVIDER_ID = 'antigravity-acp';
|
|
24
22
|
/**
|
|
@@ -28,8 +26,7 @@ export const AGY_PROVIDER_ID = 'antigravity-acp';
|
|
|
28
26
|
*/
|
|
29
27
|
export const AGY_PLACEHOLDER_MODEL_ID = 'server-default';
|
|
30
28
|
/**
|
|
31
|
-
*
|
|
32
|
-
* `UNSUPPORTED_OPTION` 常量,故使用字面量并集中在此定义。
|
|
29
|
+
* @deprecated 兼容旧导出的错误码;temperature / stop 现在只警告并忽略,不再抛此码。
|
|
33
30
|
*/
|
|
34
31
|
export const AGY_UNSUPPORTED_OPTION = 'UNSUPPORTED_OPTION';
|
|
35
32
|
/** 本 adapter 的其余原创错误码(AGY_* 体系)。 */
|
|
@@ -38,6 +35,8 @@ export const AGY_ADAPTER_ERROR = {
|
|
|
38
35
|
BUSY: 'AGY_BUSY',
|
|
39
36
|
/** 回合需要一个模型选择器,但 ACP 会话未声明任何 model 类 configOption。 */
|
|
40
37
|
NO_MODEL_SELECTOR: 'AGY_NO_MODEL_SELECTOR',
|
|
38
|
+
/** agent 应答选模却没有确认请求的型号,不能静默使用其他模型。 */
|
|
39
|
+
MODEL_SELECTION_MISMATCH: 'AGY_MODEL_SELECTION_MISMATCH',
|
|
41
40
|
};
|
|
42
41
|
/*
|
|
43
42
|
* 关于失败重试:**刻意不 override `providerRetryPolicy`**。
|
|
@@ -78,6 +77,24 @@ export class AntigravityAcpAdapter extends LlmAdapter {
|
|
|
78
77
|
#loadCliCatalog;
|
|
79
78
|
#log;
|
|
80
79
|
#onMetadataChanged;
|
|
80
|
+
#reuseSessions;
|
|
81
|
+
#resumeSessions;
|
|
82
|
+
#sessionStore;
|
|
83
|
+
#sessionOwner = Symbol('acp-worker');
|
|
84
|
+
/** 同一 DSH 对话(普通聊天轮)→ 可复用的 ACP 会话,最多 64 个。 */
|
|
85
|
+
#sessions = new Map();
|
|
86
|
+
/** >0 时丢弃入站 session/update:`session/load` 会把整段历史重放回来。 */
|
|
87
|
+
#suppressUpdates = 0;
|
|
88
|
+
#keepProcessAfterCancel;
|
|
89
|
+
#cancelGraceMs;
|
|
90
|
+
#idleRecycleMs;
|
|
91
|
+
/** 取消后的确认定时器;agent 先确认停止就取消回收。 */
|
|
92
|
+
#cancelGrace;
|
|
93
|
+
/** 保留进程的空闲回收定时器;有新回合就撤销。 */
|
|
94
|
+
#idleRecycle;
|
|
95
|
+
#currentLink;
|
|
96
|
+
#ephemeral = new Map();
|
|
97
|
+
#sessionCloses = new Set();
|
|
81
98
|
#continuationState;
|
|
82
99
|
#readImage;
|
|
83
100
|
#canReadImages;
|
|
@@ -136,6 +153,15 @@ export class AntigravityAcpAdapter extends LlmAdapter {
|
|
|
136
153
|
this.#loadCliCatalog = options.loadCliCatalog;
|
|
137
154
|
this.#log = options.log;
|
|
138
155
|
this.#onMetadataChanged = options.onMetadataChanged;
|
|
156
|
+
this.#reuseSessions = options.reuseSessions ?? true;
|
|
157
|
+
this.#resumeSessions = options.resumeSessions ?? false;
|
|
158
|
+
this.#sessionStore = options.sessionStore ?? (options.loadSessionMap && options.saveSessionMap
|
|
159
|
+
? new SessionMapStore({ load: options.loadSessionMap, save: options.saveSessionMap,
|
|
160
|
+
onError: () => this.#log?.warn('antigravity-acp: 会话映射读写失败,跨进程恢复可能不可用') })
|
|
161
|
+
: undefined);
|
|
162
|
+
this.#keepProcessAfterCancel = options.keepProcessAfterCancel ?? false;
|
|
163
|
+
this.#cancelGraceMs = Math.max(0, options.cancelGraceMs ?? 30_000);
|
|
164
|
+
this.#idleRecycleMs = Math.max(0, options.idleRecycleMs ?? 300_000);
|
|
139
165
|
this.#readImage = options.readImage;
|
|
140
166
|
this.#canReadImages = options.canReadImages;
|
|
141
167
|
this.#bridge = new McpToolBridge({
|
|
@@ -144,23 +170,37 @@ export class AntigravityAcpAdapter extends LlmAdapter {
|
|
|
144
170
|
this.#registerBridgeTeardown(async () => {
|
|
145
171
|
this.#disposed = true;
|
|
146
172
|
this.#wakeAccessWaiters();
|
|
173
|
+
if (this.#turn && !this.#isTurnClosed(this.#turn))
|
|
174
|
+
this.#turn.abort('adapter 卸载');
|
|
147
175
|
this.#turn?.dispose();
|
|
148
176
|
this.#releaseTurnSubscription();
|
|
177
|
+
this.#dropAllSessions('adapter 卸载');
|
|
178
|
+
for (const [turn, link] of this.#ephemeral)
|
|
179
|
+
this.#closeSession(link, turn.sessionId);
|
|
180
|
+
this.#ephemeral.clear();
|
|
181
|
+
this.#clearCancelGrace();
|
|
182
|
+
this.#clearIdleRecycle();
|
|
149
183
|
await this.#bridge.close();
|
|
184
|
+
await Promise.allSettled(this.#sessionCloses);
|
|
185
|
+
this.#sessionStore?.release(this.#sessionOwner);
|
|
186
|
+
await this.#sessionStore?.flush();
|
|
150
187
|
});
|
|
151
188
|
this.#handlers = {
|
|
152
189
|
onSessionUpdate: (notification) => {
|
|
190
|
+
// session/load 会把整段历史重放回来;那不是本轮输出,必须丢弃。
|
|
191
|
+
if (this.#disposed || this.#suppressUpdates > 0)
|
|
192
|
+
return;
|
|
153
193
|
this.#observeUsageUpdate(notification);
|
|
154
194
|
return this.#turn?.handleSessionUpdate(notification);
|
|
155
195
|
},
|
|
156
|
-
onRequestPermission: (request) => this.#turn?.handleRequestPermission(request)
|
|
196
|
+
onRequestPermission: (request) => (!this.#disposed ? this.#turn?.handleRequestPermission(request) : undefined)
|
|
157
197
|
?? { outcome: { outcome: 'cancelled' } },
|
|
158
198
|
onUnexpectedAgentRequest: (method, request) => this.#handleUnexpectedRequest(method, request),
|
|
159
199
|
};
|
|
160
200
|
}
|
|
161
201
|
/** 设置页连接不能与正在进行的对话/工具回合竞争同一登录态。 */
|
|
162
202
|
get busy() {
|
|
163
|
-
return this.#streamActive || Boolean(this.#turn && !this.#isTurnClosed(this.#turn));
|
|
203
|
+
return this.#streamActive || Boolean(this.#cancelGrace) || Boolean(this.#turn && !this.#isTurnClosed(this.#turn));
|
|
164
204
|
}
|
|
165
205
|
get streamActive() { return this.#streamActive; }
|
|
166
206
|
get awaitingToolResults() { return this.#turn?.awaitingToolResults === true; }
|
|
@@ -194,7 +234,9 @@ export class AntigravityAcpAdapter extends LlmAdapter {
|
|
|
194
234
|
if (this.#disposed)
|
|
195
235
|
throw new LlmError('Antigravity ACP adapter 已卸载', 'AGY_LINK_CLOSED');
|
|
196
236
|
this.#streamActive = true;
|
|
237
|
+
this.#clearIdleRecycle();
|
|
197
238
|
let link;
|
|
239
|
+
let discoverySession;
|
|
198
240
|
try {
|
|
199
241
|
await withAbort(this.#settleRecycle(), signal);
|
|
200
242
|
link = await this.#getLink(this.#handlers, signal, interactive ? undefined : 'warmup');
|
|
@@ -204,6 +246,7 @@ export class AntigravityAcpAdapter extends LlmAdapter {
|
|
|
204
246
|
cwd: this.#cwd, mcpServers: [],
|
|
205
247
|
_meta: { antigravity: { enabledTools: [], eagerMcpServers: [] } },
|
|
206
248
|
}, { signal });
|
|
249
|
+
discoverySession = session.sessionId;
|
|
207
250
|
throwIfAborted(signal);
|
|
208
251
|
if (!findModelSelector(session.configOptions))
|
|
209
252
|
throw new LlmError('ACP 会话未提供模型目录', AGY_ADAPTER_ERROR.NO_MODEL_SELECTOR);
|
|
@@ -216,7 +259,10 @@ export class AntigravityAcpAdapter extends LlmAdapter {
|
|
|
216
259
|
throw error;
|
|
217
260
|
}
|
|
218
261
|
finally {
|
|
262
|
+
if (link && discoverySession)
|
|
263
|
+
this.#closeSession(link, discoverySession);
|
|
219
264
|
this.#streamActive = false;
|
|
265
|
+
this.#maybeIdle();
|
|
220
266
|
this.#wakeAccessWaiters();
|
|
221
267
|
}
|
|
222
268
|
}
|
|
@@ -226,11 +272,21 @@ export class AntigravityAcpAdapter extends LlmAdapter {
|
|
|
226
272
|
throw new LlmError('Antigravity ACP adapter 已卸载', 'AGY_LINK_CLOSED');
|
|
227
273
|
if (this.busy)
|
|
228
274
|
throw new LlmError('ACP 正在处理对话,无需再次预热', AGY_ADAPTER_ERROR.BUSY);
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
275
|
+
this.#streamActive = true;
|
|
276
|
+
this.#clearIdleRecycle();
|
|
277
|
+
try {
|
|
278
|
+
await withAbort(this.#settleRecycle(), signal);
|
|
279
|
+
const link = await this.#getLink(this.#handlers, signal, interactive ? undefined : 'warmup');
|
|
280
|
+
this.#observeCapabilities(link);
|
|
281
|
+
}
|
|
282
|
+
finally {
|
|
283
|
+
this.#streamActive = false;
|
|
284
|
+
this.#maybeIdle();
|
|
285
|
+
this.#wakeAccessWaiters();
|
|
286
|
+
}
|
|
232
287
|
}
|
|
233
288
|
#observeCapabilities(link) {
|
|
289
|
+
this.#currentLink = link;
|
|
234
290
|
const supported = link.initializeResponse?.agentCapabilities?.promptCapabilities?.image === true;
|
|
235
291
|
if (supported === this.#imageSupported)
|
|
236
292
|
return;
|
|
@@ -439,8 +495,12 @@ export class AntigravityAcpAdapter extends LlmAdapter {
|
|
|
439
495
|
* 之前串行化 stream。工具 park 保留回合所有权,辅助请求等待而不是抢占主回合。
|
|
440
496
|
*/
|
|
441
497
|
async *stream(options) {
|
|
442
|
-
//
|
|
443
|
-
|
|
498
|
+
// temperature / stop 在 ACP v1 没有会话通道:不因它们拒绝整个请求(否则宿主里
|
|
499
|
+
// 任何携带这两个字段的流程都会整单失败),只明确告知「值不生效」。
|
|
500
|
+
const unsupported = unsupportedOptions(options);
|
|
501
|
+
if (unsupported.length > 0) {
|
|
502
|
+
this.#log?.warn(`antigravity-acp: 忽略不支持的请求项 ${unsupported.join('、')}(ACP v1 无对应会话通道,值不会生效)`);
|
|
503
|
+
}
|
|
444
504
|
if (options.signal?.aborted) {
|
|
445
505
|
yield { type: 'finish', reason: { kind: 'aborted', failure: {
|
|
446
506
|
code: AGY_TURN_ERROR.ABORTED, message: 'Antigravity ACP 请求在发起前已取消',
|
|
@@ -448,6 +508,7 @@ export class AntigravityAcpAdapter extends LlmAdapter {
|
|
|
448
508
|
return;
|
|
449
509
|
}
|
|
450
510
|
await this.#acquireStream(options);
|
|
511
|
+
this.#clearIdleRecycle();
|
|
451
512
|
let handedOff = false;
|
|
452
513
|
try {
|
|
453
514
|
for await (const chunk of this.#streamExclusive(options)) {
|
|
@@ -464,14 +525,247 @@ export class AntigravityAcpAdapter extends LlmAdapter {
|
|
|
464
525
|
this.#abandonTurn('消费者提前关闭流,取消未完成回合');
|
|
465
526
|
}
|
|
466
527
|
this.#streamActive = false;
|
|
528
|
+
this.#maybeIdle();
|
|
467
529
|
this.#wakeAccessWaiters();
|
|
468
530
|
}
|
|
469
531
|
}
|
|
532
|
+
/* ── ACP 会话复用 ─────────────────────────────────────────────────── */
|
|
470
533
|
/**
|
|
471
|
-
*
|
|
472
|
-
*
|
|
534
|
+
* 跨进程恢复:重启后若磁盘上还留着该对话的 ACP 会话映射,尝试 `session/load`。
|
|
535
|
+
* 只信任「路由一致 + 历史仍以已发送前缀开头」;其余情况回退重建,不猜测。
|
|
473
536
|
*/
|
|
537
|
+
async #resumeSession(options, key, entry) {
|
|
538
|
+
if (!this.#reuseSessions || !this.#resumeSessions || !entry)
|
|
539
|
+
return undefined;
|
|
540
|
+
const discard = (reason) => {
|
|
541
|
+
this.#log?.info(`antigravity-acp: 放弃恢复 ACP 会话(${reason})`);
|
|
542
|
+
this.#sessionStore?.delete(key, this.#sessionOwner);
|
|
543
|
+
return undefined;
|
|
544
|
+
};
|
|
545
|
+
if (entry.cwd !== this.#cwd)
|
|
546
|
+
return discard('会话工作目录与当前不一致');
|
|
547
|
+
if (entry.route !== routeFingerprint(options))
|
|
548
|
+
return discard('模型、系统提示或工具目录已变化');
|
|
549
|
+
if (options.messages.length <= entry.sentCount)
|
|
550
|
+
return discard('没有新增消息');
|
|
551
|
+
if (!prefixMatches(options.messages, entry.sentCount, entry.sentHash))
|
|
552
|
+
return discard('历史不是已发送前缀的追加');
|
|
553
|
+
if (!outputMatches(options.messages.slice(entry.sentCount), entry.outputHash))
|
|
554
|
+
return discard('上一轮输出已变化或来自其他会话');
|
|
555
|
+
if (options.messages.slice(entry.sentCount).some((message) => message.role === 'tool'))
|
|
556
|
+
return discard('新增消息包含工具结果');
|
|
557
|
+
if (incrementalMessages(options.messages, entry.sentCount).length === 0)
|
|
558
|
+
return discard('没有可发送的增量,按完整历史重建');
|
|
559
|
+
const link = await this.#getLink(this.#handlers, options.signal);
|
|
560
|
+
this.#observeCapabilities(link);
|
|
561
|
+
const tools = options.tools ?? [];
|
|
562
|
+
let mcpServers = [];
|
|
563
|
+
if (tools.length > 0) {
|
|
564
|
+
this.#bridge.beginRound(tools, key);
|
|
565
|
+
if (!this.#bridge.listening)
|
|
566
|
+
await this.#bridge.start();
|
|
567
|
+
mcpServers = this.#bridge.acpRegistration();
|
|
568
|
+
}
|
|
569
|
+
this.#suppressUpdates += 1;
|
|
570
|
+
try {
|
|
571
|
+
const response = await link.loadSession({
|
|
572
|
+
sessionId: entry.sessionId,
|
|
573
|
+
cwd: this.#cwd,
|
|
574
|
+
mcpServers,
|
|
575
|
+
_meta: { antigravity: { enabledTools: [], eagerMcpServers: tools.length > 0 ? [BRIDGE_MCP_SERVER_NAME] : [] } },
|
|
576
|
+
}, { signal: options.signal, timeoutMs: Math.min(this.#promptTimeoutMs, 120_000) });
|
|
577
|
+
this.observeSessionConfigOptions(response.configOptions);
|
|
578
|
+
await this.#applyModelSelection(link, entry.sessionId, options.model, options.reasoningEffort, options.signal);
|
|
579
|
+
throwIfAborted(options.signal);
|
|
580
|
+
}
|
|
581
|
+
catch (error) {
|
|
582
|
+
this.#log?.warn(`antigravity-acp: session/load 恢复失败,按新轮重建 session=${entry.sessionId}`, error);
|
|
583
|
+
// 恢复失败可能仍有迟到回放;重建前回收连接。
|
|
584
|
+
this.#bridge.invalidateRound('恢复失败');
|
|
585
|
+
this.#scheduleRecycle(link);
|
|
586
|
+
await this.#settleRecycle();
|
|
587
|
+
throwIfAborted(options.signal);
|
|
588
|
+
return discard('session/load 失败');
|
|
589
|
+
}
|
|
590
|
+
finally {
|
|
591
|
+
this.#suppressUpdates -= 1;
|
|
592
|
+
}
|
|
593
|
+
const record = {
|
|
594
|
+
key,
|
|
595
|
+
sessionId: entry.sessionId,
|
|
596
|
+
link,
|
|
597
|
+
sentCount: entry.sentCount,
|
|
598
|
+
sentHash: entry.sentHash,
|
|
599
|
+
outputHash: entry.outputHash,
|
|
600
|
+
route: entry.route,
|
|
601
|
+
usesTools: tools.length > 0,
|
|
602
|
+
};
|
|
603
|
+
this.#sessions.set(key, record);
|
|
604
|
+
this.#log?.info(`antigravity-acp: 已恢复 ACP 会话 session=${entry.sessionId} 已发送消息=${entry.sentCount}`);
|
|
605
|
+
return record;
|
|
606
|
+
}
|
|
607
|
+
/** 找到可复用的会话;记录失效时顺手丢弃(关闭对端会话,下一轮重建)。 */
|
|
608
|
+
#findReusableSession(options, key) {
|
|
609
|
+
if (!this.#reuseSessions)
|
|
610
|
+
return undefined;
|
|
611
|
+
const record = this.#sessions.get(key);
|
|
612
|
+
if (!record)
|
|
613
|
+
return undefined;
|
|
614
|
+
if (this.#resumeSessions && this.#sessionStore && !this.#sessionStore.owns(key, this.#sessionOwner)) {
|
|
615
|
+
this.#dropSession(record, '会话已迁移到其他 worker');
|
|
616
|
+
return undefined;
|
|
617
|
+
}
|
|
618
|
+
// 进程崩溃/被回收后链接已关闭:记录不可用,按新轮重建。
|
|
619
|
+
if (record.link.closed) {
|
|
620
|
+
this.#dropSession(record, 'ACP 链接已关闭');
|
|
621
|
+
return undefined;
|
|
622
|
+
}
|
|
623
|
+
if (record.route !== routeFingerprint(options)) {
|
|
624
|
+
this.#dropSession(record, '模型、系统提示或工具目录已变化');
|
|
625
|
+
return undefined;
|
|
626
|
+
}
|
|
627
|
+
// 用过工具的会话依赖桥的授权代次:桥被另一个对话接管后它的 MCP 连接
|
|
628
|
+
// 持有旧 token,沿用只会让工具调用被拒;直接重建。
|
|
629
|
+
if (record.usesTools && this.#bridge.owner !== record.key) {
|
|
630
|
+
this.#dropSession(record, 'MCP 桥已被其他对话接管');
|
|
631
|
+
return undefined;
|
|
632
|
+
}
|
|
633
|
+
if (options.messages.length <= record.sentCount) {
|
|
634
|
+
this.#dropSession(record, '没有严格追加的新输入');
|
|
635
|
+
return undefined;
|
|
636
|
+
}
|
|
637
|
+
if (!prefixMatches(options.messages, record.sentCount, record.sentHash)) {
|
|
638
|
+
this.#dropSession(record, '历史已不是已发送前缀的追加(压缩、回退或编辑)');
|
|
639
|
+
return undefined;
|
|
640
|
+
}
|
|
641
|
+
if (!outputMatches(options.messages.slice(record.sentCount), record.outputHash)) {
|
|
642
|
+
this.#dropSession(record, '上一轮输出已变化或来自其他会话');
|
|
643
|
+
return undefined;
|
|
644
|
+
}
|
|
645
|
+
// 新增部分若含工具结果,只能走 MCP 回喂,不能当 prompt 文本重发。
|
|
646
|
+
if (options.messages.slice(record.sentCount).some((message) => message.role === 'tool')) {
|
|
647
|
+
this.#dropSession(record, '新增消息包含工具结果');
|
|
648
|
+
return undefined;
|
|
649
|
+
}
|
|
650
|
+
if (incrementalMessages(options.messages, record.sentCount).length === 0) {
|
|
651
|
+
this.#dropSession(record, '没有可发送的增量,按完整历史重建');
|
|
652
|
+
return undefined;
|
|
653
|
+
}
|
|
654
|
+
return record;
|
|
655
|
+
}
|
|
656
|
+
/** 记录一个刚建立会话 / 刚完成全量发送的 ACP 会话。 */
|
|
657
|
+
#rememberSession(options, key, link, sessionId) {
|
|
658
|
+
const record = {
|
|
659
|
+
key,
|
|
660
|
+
sessionId,
|
|
661
|
+
link,
|
|
662
|
+
sentCount: options.messages.length,
|
|
663
|
+
sentHash: prefixHash(options.messages),
|
|
664
|
+
route: routeFingerprint(options),
|
|
665
|
+
usesTools: (options.tools?.length ?? 0) > 0,
|
|
666
|
+
};
|
|
667
|
+
this.#sessions.set(key, record);
|
|
668
|
+
while (this.#sessions.size > 64) {
|
|
669
|
+
const oldest = this.#sessions.values().next().value;
|
|
670
|
+
if (!oldest)
|
|
671
|
+
break;
|
|
672
|
+
this.#dropSession(oldest, '内存会话 LRU 淘汰');
|
|
673
|
+
}
|
|
674
|
+
return record;
|
|
675
|
+
}
|
|
676
|
+
/** 在 finish 对外可见之前提交输入与实际交付输出,消费者可立即 return。 */
|
|
677
|
+
#advanceSession(record, options, blocks, settled) {
|
|
678
|
+
if (this.#sessions.get(record.key) !== record)
|
|
679
|
+
return;
|
|
680
|
+
record.sentCount = options.messages.length;
|
|
681
|
+
record.sentHash = prefixHash(options.messages);
|
|
682
|
+
record.outputHash = outputHash(blocks);
|
|
683
|
+
this.#sessions.delete(record.key);
|
|
684
|
+
this.#sessions.set(record.key, record);
|
|
685
|
+
if (settled)
|
|
686
|
+
this.#sessionStore?.set(record.key, this.#sessionOwner, {
|
|
687
|
+
sessionId: record.sessionId, cwd: this.#cwd, sentCount: record.sentCount,
|
|
688
|
+
sentHash: record.sentHash, outputHash: record.outputHash, route: record.route, updatedAt: Date.now(),
|
|
689
|
+
});
|
|
690
|
+
else
|
|
691
|
+
this.#sessionStore?.delete(record.key, this.#sessionOwner);
|
|
692
|
+
}
|
|
693
|
+
/** 丢弃会话记录并关闭对端会话;失败忽略——记录本来就已经不可信。 */
|
|
694
|
+
#dropSession(record, reason) {
|
|
695
|
+
if (this.#sessions.get(record.key) !== record)
|
|
696
|
+
return;
|
|
697
|
+
this.#sessions.delete(record.key);
|
|
698
|
+
this.#sessionStore?.delete(record.key, this.#sessionOwner);
|
|
699
|
+
this.#log?.info(`antigravity-acp: 放弃 ACP 会话复用(${reason})`);
|
|
700
|
+
this.#closeSession(record.link, record.sessionId);
|
|
701
|
+
}
|
|
702
|
+
#dropSessionsForLink(link, reason) {
|
|
703
|
+
for (const record of [...this.#sessions.values()]) {
|
|
704
|
+
if (record.link === link)
|
|
705
|
+
this.#dropSession(record, reason);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
#dropSessionsForTurn(sessionId, reason) {
|
|
709
|
+
for (const record of [...this.#sessions.values()]) {
|
|
710
|
+
if (record.sessionId === sessionId)
|
|
711
|
+
this.#dropSession(record, reason);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* 卸载时关闭所有对端会话,但**保留**磁盘映射:那正是下次启动 `session/load`
|
|
716
|
+
* 恢复的依据。真正失效的会话在 #dropSession 里连同映射一起删掉。
|
|
717
|
+
*/
|
|
718
|
+
#dropAllSessions(reason, link) {
|
|
719
|
+
for (const record of [...this.#sessions.values()]) {
|
|
720
|
+
if (link && record.link !== link)
|
|
721
|
+
continue;
|
|
722
|
+
this.#sessions.delete(record.key);
|
|
723
|
+
this.#log?.info(`antigravity-acp: 关闭 ACP 会话(${reason})`);
|
|
724
|
+
this.#closeSession(record.link, record.sessionId);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
/** 跟踪尽力关闭请求,卸载时等待它们结束;错误不影响当前对话。 */
|
|
728
|
+
#closeSession(link, sessionId) {
|
|
729
|
+
const pending = link.closeSession(sessionId).catch(() => {
|
|
730
|
+
if (!link.closed)
|
|
731
|
+
this.#log?.warn('antigravity-acp: session/close 失败,会话由进程退出清理');
|
|
732
|
+
}).finally(() => this.#sessionCloses.delete(pending));
|
|
733
|
+
this.#sessionCloses.add(pending);
|
|
734
|
+
}
|
|
735
|
+
#maybeIdle() {
|
|
736
|
+
const link = this.#currentLink;
|
|
737
|
+
if (this.#disposed || this.busy || this.#unrecycledLink || !link || link.closed)
|
|
738
|
+
return;
|
|
739
|
+
this.#scheduleIdleRecycle(link);
|
|
740
|
+
}
|
|
741
|
+
/** 只有没有活动请求、工具 park 或取消排空时才启动空闲计时。 */
|
|
742
|
+
#scheduleIdleRecycle(link) {
|
|
743
|
+
this.#clearIdleRecycle();
|
|
744
|
+
if (this.#idleRecycleMs <= 0 || this.#disposed || this.busy || link.closed)
|
|
745
|
+
return;
|
|
746
|
+
const timer = setTimeout(() => {
|
|
747
|
+
if (this.#idleRecycle?.timer !== timer)
|
|
748
|
+
return;
|
|
749
|
+
this.#idleRecycle = undefined;
|
|
750
|
+
if (this.busy || this.#disposed || this.#currentLink !== link || link.closed)
|
|
751
|
+
return;
|
|
752
|
+
this.#log?.info(`antigravity-acp: 进程空闲 ${this.#idleRecycleMs}ms 无新消息,回收`);
|
|
753
|
+
this.#scheduleRecycle(link, true);
|
|
754
|
+
}, this.#idleRecycleMs);
|
|
755
|
+
timer.unref?.();
|
|
756
|
+
this.#idleRecycle = { timer, link };
|
|
757
|
+
}
|
|
758
|
+
#clearIdleRecycle() {
|
|
759
|
+
const idle = this.#idleRecycle;
|
|
760
|
+
if (!idle)
|
|
761
|
+
return;
|
|
762
|
+
clearTimeout(idle.timer);
|
|
763
|
+
this.#idleRecycle = undefined;
|
|
764
|
+
}
|
|
765
|
+
/** 等待取消排空与回收;回收失败仍保留屏障,后续调用可重试同一连接。 */
|
|
474
766
|
async #settleRecycle() {
|
|
767
|
+
// 本地 aborted 不是停止证明;新回合不能撤销旧回合的排空屏障。
|
|
768
|
+
await this.#cancelGrace?.pending;
|
|
475
769
|
try {
|
|
476
770
|
await this.#cleanup;
|
|
477
771
|
}
|
|
@@ -488,8 +782,59 @@ export class AntigravityAcpAdapter extends LlmAdapter {
|
|
|
488
782
|
throw error;
|
|
489
783
|
}
|
|
490
784
|
}
|
|
785
|
+
/**
|
|
786
|
+
* 取消后的宽限:等 agent 应答 `cancelled` 再决定是否回收进程。
|
|
787
|
+
*
|
|
788
|
+
* 官方 `session/cancel` 是通知,不代表执行已停止;直接保留进程可能让它继续
|
|
789
|
+
* 跑旧会话。因此默认仍立即回收;开启 keepProcessAfterCancel 后:
|
|
790
|
+
* - 宽限内收到 prompt 应答(取消生效)→ 只丢弃会话,保留进程;
|
|
791
|
+
* - 超时未应答 → 按原策略回收;
|
|
792
|
+
* - cancelGraceMs = 0 → 不等待,立即回收。
|
|
793
|
+
*/
|
|
794
|
+
#scheduleCancelGrace(link, turn) {
|
|
795
|
+
this.#clearCancelGrace();
|
|
796
|
+
if (this.#cancelGraceMs <= 0) {
|
|
797
|
+
this.#scheduleRecycle(link);
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
let resolve;
|
|
801
|
+
const pending = new Promise(done => { resolve = done; });
|
|
802
|
+
const timer = setTimeout(() => {
|
|
803
|
+
this.#log?.warn(`antigravity-acp: 取消后 ${this.#cancelGraceMs}ms 内未收到 agent 确认,回收进程`);
|
|
804
|
+
this.#scheduleRecycle(link);
|
|
805
|
+
}, this.#cancelGraceMs);
|
|
806
|
+
timer.unref?.();
|
|
807
|
+
this.#cancelGrace = { timer, link, turn, pending, resolve };
|
|
808
|
+
this.#log?.info(`antigravity-acp: 取消后保留进程,最多等待 ${this.#cancelGraceMs}ms 供 agent 停止`);
|
|
809
|
+
}
|
|
810
|
+
/** agent 已应答(取消生效):撤销回收,只丢弃会话。 */
|
|
811
|
+
#confirmCancelGrace(turn) {
|
|
812
|
+
const grace = this.#cancelGrace;
|
|
813
|
+
if (!grace || grace.turn !== turn)
|
|
814
|
+
return;
|
|
815
|
+
this.#clearCancelGrace();
|
|
816
|
+
this.#log?.info('antigravity-acp: agent 已确认停止,保留 ACP 进程');
|
|
817
|
+
this.#maybeIdle();
|
|
818
|
+
this.#wakeAccessWaiters();
|
|
819
|
+
}
|
|
820
|
+
#clearCancelGrace() {
|
|
821
|
+
const grace = this.#cancelGrace;
|
|
822
|
+
if (!grace)
|
|
823
|
+
return;
|
|
824
|
+
clearTimeout(grace.timer);
|
|
825
|
+
this.#cancelGrace = undefined;
|
|
826
|
+
grace.resolve();
|
|
827
|
+
}
|
|
491
828
|
/** 先允许当前拒绝响应出站,再回收;同一连接的接管/取消不得重复覆盖屏障。 */
|
|
492
|
-
#scheduleRecycle(link) {
|
|
829
|
+
#scheduleRecycle(link, preserveMapping = false) {
|
|
830
|
+
this.#clearCancelGrace();
|
|
831
|
+
this.#clearIdleRecycle();
|
|
832
|
+
if (preserveMapping)
|
|
833
|
+
this.#dropAllSessions('空闲回收,保留恢复映射', link);
|
|
834
|
+
else
|
|
835
|
+
this.#dropSessionsForLink(link, 'ACP 链接回收');
|
|
836
|
+
if (this.#currentLink === link)
|
|
837
|
+
this.#currentLink = undefined;
|
|
493
838
|
if (this.#unrecycledLink === link)
|
|
494
839
|
return;
|
|
495
840
|
this.#unrecycledLink = link;
|
|
@@ -564,7 +909,7 @@ export class AntigravityAcpAdapter extends LlmAdapter {
|
|
|
564
909
|
}
|
|
565
910
|
}
|
|
566
911
|
/** 启动阶段也响应取消;迟到资源先回收,后续回合不能越过清理屏障。 */
|
|
567
|
-
async #prepareRound(options) {
|
|
912
|
+
async #prepareRound(options, owner) {
|
|
568
913
|
let link;
|
|
569
914
|
const check = () => {
|
|
570
915
|
if (this.#disposed)
|
|
@@ -577,8 +922,11 @@ export class AntigravityAcpAdapter extends LlmAdapter {
|
|
|
577
922
|
this.#observeCapabilities(link);
|
|
578
923
|
check();
|
|
579
924
|
const tools = options.tools ?? [];
|
|
580
|
-
this.#bridge.beginRound(tools);
|
|
581
925
|
let mcpServers = [];
|
|
926
|
+
// 复用会话时无工具的一次性请求(标题、压缩)不接管桥:轮换 token 会让同一
|
|
927
|
+
// worker 上仍在复用的 ACP 会话失去工具连接。关闭复用时恢复旧的每轮清桥。
|
|
928
|
+
if (tools.length > 0 || !this.#reuseSessions)
|
|
929
|
+
this.#bridge.beginRound(tools, owner);
|
|
582
930
|
if (tools.length > 0) {
|
|
583
931
|
if (!this.#bridge.listening)
|
|
584
932
|
await this.#bridge.start();
|
|
@@ -626,6 +974,7 @@ export class AntigravityAcpAdapter extends LlmAdapter {
|
|
|
626
974
|
}
|
|
627
975
|
}
|
|
628
976
|
async *#runNewRound(options) {
|
|
977
|
+
this.#clearIdleRecycle();
|
|
629
978
|
await withAbort(this.#settleRecycle(), options.signal);
|
|
630
979
|
if (this.#disposed)
|
|
631
980
|
throw new LlmError('Antigravity ACP adapter 已卸载', 'AGY_LINK_CLOSED');
|
|
@@ -637,22 +986,73 @@ export class AntigravityAcpAdapter extends LlmAdapter {
|
|
|
637
986
|
// usage_update 的上下文容量归属到本次请求的 DSH 路由键。
|
|
638
987
|
this.#routeModelId = options.model;
|
|
639
988
|
this.#lifecycle?.beginRound(options.model);
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
const
|
|
989
|
+
const key = reusableSessionKey(options);
|
|
990
|
+
const hadMemoryRecord = key !== undefined && this.#sessions.has(key);
|
|
991
|
+
// 必须先检查旧 owner,再 claim;反过来会把刚接回的陈旧内存记录误认为仍归自己。
|
|
992
|
+
let reused = key === undefined ? undefined : this.#findReusableSession(options, key);
|
|
993
|
+
const entry = key !== undefined && this.#reuseSessions && this.#resumeSessions
|
|
994
|
+
? await withAbort(this.#sessionStore?.claim(key, this.#sessionOwner) ?? Promise.resolve(undefined), options.signal) : undefined;
|
|
995
|
+
// 内存里没有记录时(插件刚重启),只有 resume 开启才尝试从磁盘映射恢复。
|
|
996
|
+
if (!reused && key !== undefined && !hadMemoryRecord)
|
|
997
|
+
reused = await this.#resumeSession(options, key, entry);
|
|
998
|
+
const tools = options.tools ?? [];
|
|
999
|
+
let link;
|
|
1000
|
+
let sessionId;
|
|
1001
|
+
if (reused) {
|
|
1002
|
+
// 复用:同一 ACP 会话已有原生历史,只发本轮新增消息。
|
|
1003
|
+
link = reused.link;
|
|
1004
|
+
sessionId = reused.sessionId;
|
|
1005
|
+
this.#log?.info(`antigravity-acp: 复用 ACP 会话 session=${sessionId} 已发送消息=${reused.sentCount}` +
|
|
1006
|
+
` 本轮新增=${options.messages.length - reused.sentCount}`);
|
|
1007
|
+
}
|
|
1008
|
+
else {
|
|
1009
|
+
const prepared = await this.#prepareRound(options, key).catch((error) => {
|
|
1010
|
+
this.#lifecycle?.failed(error);
|
|
1011
|
+
throw error;
|
|
1012
|
+
});
|
|
1013
|
+
link = prepared.link;
|
|
1014
|
+
sessionId = prepared.session.sessionId;
|
|
1015
|
+
}
|
|
1016
|
+
const turn = new PromptTurn(sessionId, {
|
|
645
1017
|
link,
|
|
646
1018
|
bridge: this.#bridge,
|
|
647
|
-
promptTimeoutMs: this.#promptTimeoutMs,
|
|
1019
|
+
promptTimeoutMs: purposeTimeoutMs(options.purpose, this.#promptTimeoutMs),
|
|
648
1020
|
parkSettleMs: this.#parkSettleMs,
|
|
1021
|
+
onPromptSettled: (settledTurn, response) => {
|
|
1022
|
+
if (this.#cancelGrace?.turn === settledTurn) {
|
|
1023
|
+
if (response && ['cancelled', 'end_turn', 'max_tokens', 'max_turn_requests', 'refusal'].includes(response.stopReason)) {
|
|
1024
|
+
this.#confirmCancelGrace(settledTurn);
|
|
1025
|
+
}
|
|
1026
|
+
else
|
|
1027
|
+
this.#scheduleRecycle(link);
|
|
1028
|
+
}
|
|
1029
|
+
},
|
|
649
1030
|
onTerminate: (reason, info) => {
|
|
1031
|
+
if (this.#disposed) {
|
|
1032
|
+
this.#bridge.invalidateRound(reason);
|
|
1033
|
+
this.#dropSessionsForTurn(turn.sessionId, '卸载时终止未完成回合');
|
|
1034
|
+
this.#wakeAccessWaiters();
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
650
1037
|
this.#lifecycle?.failed(reason);
|
|
651
|
-
this.#bridge.invalidateRound(reason);
|
|
652
1038
|
this.#releaseTurnSubscription(turn);
|
|
1039
|
+
// 干净失败(refusal / 空回复)不算异常:agent 已应答、状态自洽,桥与会话
|
|
1040
|
+
// 都保留(含带工具的会话),下一次仍可复用。
|
|
1041
|
+
if (info?.clean !== true) {
|
|
1042
|
+
this.#bridge.invalidateRound(reason);
|
|
1043
|
+
this.#dropSessionsForLink(link, '回合异常终止');
|
|
1044
|
+
}
|
|
653
1045
|
if (info?.clean === true) {
|
|
654
1046
|
// prompt 已正常应答的干净失败(refusal / 空回复等):agent 状态自洽,
|
|
655
1047
|
// 链接可复用——不为下一次对话重新 spawn 官方进程、不重弹登录。
|
|
1048
|
+
// 只清未回喂的 park,不轮换 token/owner(否则带工具的会话无法复用)。
|
|
1049
|
+
this.#bridge.resetPending(reason);
|
|
1050
|
+
this.#wakeAccessWaiters();
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
// 用户取消且开启保留进程:先等 agent 确认停止;确认了就只丢会话、不回收。
|
|
1054
|
+
if (info?.aborted === true && this.#keepProcessAfterCancel && turn.promptStarted) {
|
|
1055
|
+
this.#scheduleCancelGrace(link, turn);
|
|
656
1056
|
this.#wakeAccessWaiters();
|
|
657
1057
|
return;
|
|
658
1058
|
}
|
|
@@ -669,26 +1069,52 @@ export class AntigravityAcpAdapter extends LlmAdapter {
|
|
|
669
1069
|
`(${info.detail},本回合第 ${info.count} 次;来源=ACP插件,尚未转交DSH工具审批)`),
|
|
670
1070
|
});
|
|
671
1071
|
this.#turn = turn;
|
|
1072
|
+
if (key === undefined || !this.#reuseSessions)
|
|
1073
|
+
this.#ephemeral.set(turn, link);
|
|
672
1074
|
this.#subscribeTurn(turn);
|
|
673
1075
|
// 回合生命周期打点:选具体型号后「没有反应」这类问题,只靠这条能分清是
|
|
674
1076
|
// 「prompt 没送出去」「送出去了但没有块回来」还是「块回来了但没被消费」。
|
|
675
|
-
this.#log?.info(`antigravity-acp: 回合开始 model=${options.model} purpose=${options.purpose ?? 'chat'} session=${
|
|
676
|
-
`${options.reasoningEffort === undefined ? '' : ` effort=${String(options.reasoningEffort)}`}`
|
|
677
|
-
|
|
1077
|
+
this.#log?.info(`antigravity-acp: 回合开始 model=${options.model} purpose=${options.purpose ?? 'chat'} session=${sessionId} tools=${tools.length}` +
|
|
1078
|
+
`${options.reasoningEffort === undefined ? '' : ` effort=${String(options.reasoningEffort)}`}` +
|
|
1079
|
+
`${reused ? ' reused=true' : ''}`);
|
|
1080
|
+
// 复用会话只发增量;否则全量重建(含 system、工具策略与历史强调块)。
|
|
1081
|
+
const content = reused
|
|
1082
|
+
? await buildPromptDeltaWithImages(incrementalMessages(options.messages, reused.sentCount), this.#imageOptions(options.signal))
|
|
1083
|
+
: await buildPromptWithImages(options.system, options.messages, tools, this.#imageOptions(options.signal));
|
|
678
1084
|
this.#lifecycle?.promptSent();
|
|
1085
|
+
// 全量重建后先把「已发送」记到本轮完整历史,之后才有资格复用。
|
|
1086
|
+
const record = reused ?? (key !== undefined && this.#reuseSessions
|
|
1087
|
+
? this.#rememberSession(options, key, link, sessionId)
|
|
1088
|
+
: undefined);
|
|
1089
|
+
// 在新 prompt 在途期间,不把上一轮的旧游标继续当作可恢复状态。
|
|
1090
|
+
if (record) {
|
|
1091
|
+
this.#sessionStore?.delete(record.key, this.#sessionOwner);
|
|
1092
|
+
if (this.#sessionStore)
|
|
1093
|
+
await withAbort(this.#sessionStore.flush(), options.signal);
|
|
1094
|
+
}
|
|
1095
|
+
if (this.#disposed) {
|
|
1096
|
+
turn.abort('adapter 已卸载');
|
|
1097
|
+
throw new LlmError('Antigravity ACP adapter 已卸载', 'AGY_LINK_CLOSED');
|
|
1098
|
+
}
|
|
679
1099
|
turn.startPrompt(content, options.signal);
|
|
680
1100
|
try {
|
|
681
1101
|
let chunks = 0;
|
|
682
1102
|
let textChars = 0;
|
|
683
1103
|
let reasoningChars = 0;
|
|
684
1104
|
let finish = '';
|
|
1105
|
+
const emitted = [];
|
|
685
1106
|
for await (const chunk of drainTurn(turn)) {
|
|
686
1107
|
if (chunk.type === 'text-delta')
|
|
687
1108
|
textChars += chunk.text.length;
|
|
688
1109
|
if (chunk.type === 'reasoning-delta')
|
|
689
1110
|
reasoningChars += chunk.text.length;
|
|
690
|
-
if (chunk.type === '
|
|
1111
|
+
if (chunk.type === 'block-end')
|
|
1112
|
+
emitted.push(structuredClone(chunk.block));
|
|
1113
|
+
if (chunk.type === 'finish') {
|
|
691
1114
|
finish = chunk.reason.kind;
|
|
1115
|
+
if (record)
|
|
1116
|
+
this.#advanceSession(record, options, emitted, turn.promptSettled);
|
|
1117
|
+
}
|
|
692
1118
|
if (chunks === 0)
|
|
693
1119
|
this.#log?.info(`antigravity-acp: 回合首块到达(type=${chunk.type})`);
|
|
694
1120
|
if (chunk.type !== 'finish')
|
|
@@ -706,6 +1132,8 @@ export class AntigravityAcpAdapter extends LlmAdapter {
|
|
|
706
1132
|
}
|
|
707
1133
|
catch (error) {
|
|
708
1134
|
this.#log?.warn(`antigravity-acp: 回合失败 model=${options.model}`, error);
|
|
1135
|
+
if (record)
|
|
1136
|
+
this.#dropSession(record, '回合异常');
|
|
709
1137
|
throw error;
|
|
710
1138
|
}
|
|
711
1139
|
finally {
|
|
@@ -753,7 +1181,16 @@ export class AntigravityAcpAdapter extends LlmAdapter {
|
|
|
753
1181
|
if (!turn.terminated)
|
|
754
1182
|
throw error;
|
|
755
1183
|
}
|
|
1184
|
+
const emitted = [];
|
|
756
1185
|
for await (const chunk of drainTurn(turn)) {
|
|
1186
|
+
if (chunk.type === 'block-end')
|
|
1187
|
+
emitted.push(structuredClone(chunk.block));
|
|
1188
|
+
if (chunk.type === 'finish') {
|
|
1189
|
+
const key = reusableSessionKey(options);
|
|
1190
|
+
const record = key === undefined ? undefined : this.#sessions.get(key);
|
|
1191
|
+
if (record?.sessionId === turn.sessionId)
|
|
1192
|
+
this.#advanceSession(record, options, emitted, turn.promptSettled);
|
|
1193
|
+
}
|
|
757
1194
|
if (chunk.type !== 'finish')
|
|
758
1195
|
this.#lifecycle?.firstOutput(chunk.type === 'block-end' && chunk.block.type === 'tool-call');
|
|
759
1196
|
else
|
|
@@ -762,6 +1199,10 @@ export class AntigravityAcpAdapter extends LlmAdapter {
|
|
|
762
1199
|
}
|
|
763
1200
|
}
|
|
764
1201
|
finally {
|
|
1202
|
+
// 续轮带来的 assistant/tool 消息已经在 ACP 会话里体现过:成功时推进游标,
|
|
1203
|
+
// 让下一次用户轮只发送真正的新消息;回合被终止则作废该会话。
|
|
1204
|
+
if (turn.terminated)
|
|
1205
|
+
this.#dropSessionsForTurn(turn.sessionId, '续轮目标回合已终止');
|
|
765
1206
|
this.#releaseTurnSubscriptionIfClosed(turn);
|
|
766
1207
|
}
|
|
767
1208
|
}
|
|
@@ -793,13 +1234,17 @@ export class AntigravityAcpAdapter extends LlmAdapter {
|
|
|
793
1234
|
if (!this.#isTurnClosed(turn))
|
|
794
1235
|
return;
|
|
795
1236
|
this.#releaseTurnSubscription(turn);
|
|
796
|
-
|
|
1237
|
+
const link = this.#ephemeral.get(turn);
|
|
1238
|
+
if (link) {
|
|
1239
|
+
this.#ephemeral.delete(turn);
|
|
1240
|
+
this.#closeSession(link, turn.sessionId);
|
|
1241
|
+
}
|
|
1242
|
+
// 复用会话时不再 invalidateRound:轮换 token 会让 agent 手里的 MCP 连接
|
|
1243
|
+
// 立即失效。关闭复用时恢复旧行为(每轮结束清桥)。
|
|
1244
|
+
if (!this.#reuseSessions && this.#turn === turn)
|
|
797
1245
|
this.#bridge.invalidateRound('ACP 回合已结束');
|
|
798
1246
|
}
|
|
799
|
-
/**
|
|
800
|
-
* 放弃当前回合:取消 ACP 会话、封口 chunk 队列、清空 bridge 的 park。
|
|
801
|
-
* 不等待 prompt 应答(agent 可能已挂起),避免放弃动作被拖死。
|
|
802
|
-
*/
|
|
1247
|
+
/** 放弃当前回合:取消 ACP 会话、封口 chunk 队列、清空 bridge 的 park。 */
|
|
803
1248
|
#abandonTurn(reason) {
|
|
804
1249
|
const turn = this.#turn;
|
|
805
1250
|
this.#turn = undefined;
|
|
@@ -811,13 +1256,16 @@ export class AntigravityAcpAdapter extends LlmAdapter {
|
|
|
811
1256
|
turn.finishRound();
|
|
812
1257
|
turn.dispose();
|
|
813
1258
|
this.#bridge.resetPending(reason);
|
|
1259
|
+
// 续轮条件被放弃说明这个 ACP 会话已不可信,下一次按新轮重建。
|
|
1260
|
+
this.#dropSessionsForTurn(turn.sessionId, reason);
|
|
1261
|
+
this.#releaseTurnSubscriptionIfClosed(turn);
|
|
814
1262
|
}
|
|
815
1263
|
#isTurnClosed(turn) {
|
|
816
1264
|
// finished 的判定:回合已结束且不再等待工具结果。
|
|
817
1265
|
return turn.terminated || (turn.promptSettled && !turn.awaitingToolResults);
|
|
818
1266
|
}
|
|
819
1267
|
#handleUnexpectedRequest(method, _request) {
|
|
820
|
-
if (!this.#turn)
|
|
1268
|
+
if (this.#disposed || !this.#turn)
|
|
821
1269
|
return;
|
|
822
1270
|
const params = _request.params;
|
|
823
1271
|
if (params?.sessionId !== undefined && params.sessionId !== this.#turn.sessionId)
|
|
@@ -953,6 +1401,10 @@ export class AntigravityAcpAdapter extends LlmAdapter {
|
|
|
953
1401
|
this.#log?.warn(`antigravity-acp: session/set_config_option 失败(configId=${selector.configId} value=${requested})`, error);
|
|
954
1402
|
throw error;
|
|
955
1403
|
}
|
|
1404
|
+
// 不完整/不匹配的响应不能确认选模,也不能抹掉上一份有效目录。
|
|
1405
|
+
if (findModelSelector(response.configOptions)?.currentValue !== requested) {
|
|
1406
|
+
throw new LlmError('Antigravity ACP 未确认请求的模型配置,已停止发送 prompt,避免静默使用其他模型', AGY_ADAPTER_ERROR.MODEL_SELECTION_MISMATCH);
|
|
1407
|
+
}
|
|
956
1408
|
this.observeSessionConfigOptions(response.configOptions);
|
|
957
1409
|
}
|
|
958
1410
|
}
|
|
@@ -984,28 +1436,37 @@ async function* drainTurn(turn) {
|
|
|
984
1436
|
yield chunk;
|
|
985
1437
|
}
|
|
986
1438
|
}
|
|
1439
|
+
/**
|
|
1440
|
+
* 按用途收紧等待上限:标题生成与压缩都是一次性请求,挂住会占住 worker 并拖慢
|
|
1441
|
+
* 聊天;给它们各自更短的上限,而不是沿用聊天的 30 分钟。
|
|
1442
|
+
*/
|
|
1443
|
+
function purposeTimeoutMs(purpose, fallback) {
|
|
1444
|
+
if (purpose === 'session-title')
|
|
1445
|
+
return Math.min(fallback, 90_000);
|
|
1446
|
+
if (purpose === 'compaction')
|
|
1447
|
+
return Math.min(fallback, 600_000);
|
|
1448
|
+
return fallback;
|
|
1449
|
+
}
|
|
987
1450
|
/** 把 DSH 传来的思考强度归一到官方档位;无法识别时返回 undefined(走默认档位)。 */
|
|
988
1451
|
function normalizeEffort(effort) {
|
|
989
1452
|
const raw = effort === undefined ? undefined : effort.toString().toLowerCase();
|
|
990
1453
|
return AGY_REASONING_EFFORTS.find((candidate) => candidate === raw);
|
|
991
1454
|
}
|
|
992
1455
|
/**
|
|
993
|
-
* 不支持项检查。temperature / stop 没有 ACP
|
|
1456
|
+
* 不支持项检查。temperature / stop 没有 ACP 对应通道,记录警告并忽略。
|
|
994
1457
|
*
|
|
995
1458
|
* `reasoningEffort` **不是**不支持项:它现在是家族 → 档位模型的路由键
|
|
996
1459
|
* (2026-10-16 用户指示「菜单选择思考强度自动路由到对应模型」),见
|
|
997
1460
|
* `#applyModelSelection`。`maxTokens` 同样静默忽略(宿主会话标题等流程固定
|
|
998
1461
|
* 携带它,硬拒绝会让这些功能永久失败)。
|
|
999
1462
|
*/
|
|
1000
|
-
function
|
|
1463
|
+
function unsupportedOptions(options) {
|
|
1001
1464
|
const unsupported = [];
|
|
1002
1465
|
if (options.temperature !== undefined)
|
|
1003
1466
|
unsupported.push('temperature');
|
|
1004
1467
|
if (options.stop !== undefined)
|
|
1005
1468
|
unsupported.push('stop');
|
|
1006
|
-
|
|
1007
|
-
return;
|
|
1008
|
-
throw new LlmError(`Antigravity ACP provider 不支持这些请求项:${unsupported.join('、')}。ACP v1 未为它们定义会话通道,值由官方 agent 自身的会话配置决定`, AGY_UNSUPPORTED_OPTION);
|
|
1469
|
+
return unsupported;
|
|
1009
1470
|
}
|
|
1010
1471
|
/** 取 messages 尾部连续的 tool 结果消息(按原顺序)。 */
|
|
1011
1472
|
function trailingToolResults(messages) {
|