maka-agent 0.2.0-dev.67.20261002 → 0.2.0-dev.69.20261004
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/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
- package/node_modules/@maka/acp-executor-plugin/dist/acp-catalog.js +132 -0
- package/node_modules/@maka/acp-executor-plugin/dist/index.js +251 -113
- package/node_modules/@maka/acp-executor-plugin/dist/plugin.mjs +431 -108
- package/node_modules/@maka/core/dist/executor-catalog.js +25 -3
- package/node_modules/@maka/core/dist/recall.js +13 -14
- package/node_modules/@maka/runtime/dist/admission-limiter.js +3 -2
- package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +0 -3
- package/node_modules/@maka/runtime/dist/model-history.js +4 -14
- package/node_modules/@maka/runtime/dist/plugin-data-services.js +7 -0
- package/node_modules/@maka/runtime/dist/plugin-executor-service.js +19 -3
- package/node_modules/@maka/runtime/dist/runtime-read-model.js +23 -4
- package/node_modules/@maka/runtime/dist/session-manager.js +1 -0
- package/node_modules/@maka/runtime/package.json +1 -0
- package/node_modules/@maka/runtime-host/dist/antigravity-program.js +49 -0
- package/node_modules/@maka/runtime-host/dist/protocol/index.js +3 -1
- package/node_modules/@maka/runtime-host/dist/protocol/plugin-platform.js +8 -2
- package/node_modules/@maka/runtime-host/dist/server/acp/antigravity.js +5 -19
- package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +14 -3
- package/node_modules/@maka/runtime-host/dist/server/external-agent-setup-coordinator.js +3 -0
- package/node_modules/@maka/runtime-host/dist/server/plugin-data-runtime.js +6 -0
- package/node_modules/@maka/runtime-host/dist/server/plugin-scratch-directory.js +55 -0
- package/node_modules/@maka/runtime-host/dist/server/session-catalog-coordinator.js +40 -6
- package/node_modules/@maka/runtime-host/package.json +1 -0
- package/node_modules/@maka/storage/dist/execution-stores.js +10 -0
- package/node_modules/@maka/storage/dist/runtime-event-persistence.js +1 -0
- package/node_modules/@maka/storage/dist/sqlite-runtime-store.js +74 -5
- package/package.json +1 -1
|
@@ -18,10 +18,13 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { createHash } from 'node:crypto';
|
|
20
20
|
import { constants, createReadStream } from 'node:fs';
|
|
21
|
-
import { access,
|
|
21
|
+
import { access, mkdir, mkdtemp, realpath, rm, stat } from 'node:fs/promises';
|
|
22
22
|
import { tmpdir } from 'node:os';
|
|
23
|
-
import {
|
|
23
|
+
import { AdmissionLimiter } from '@maka/runtime/admission-limiter';
|
|
24
|
+
import { AcpCatalog } from './acp-catalog.js';
|
|
25
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
24
26
|
import { methods, } from '@agentclientprotocol/sdk';
|
|
27
|
+
import { pluginIdentity } from '@maka/runtime/plugin-runtime';
|
|
25
28
|
import { AcpRuntimeError } from './acp-errors.js';
|
|
26
29
|
import { readWorkspaceTextFile, writeWorkspaceTextFile } from './acp-filesystem.js';
|
|
27
30
|
import { createAcpConnection } from './acp-process.js';
|
|
@@ -39,7 +42,8 @@ export class AcpExecutor {
|
|
|
39
42
|
#sessions = new Map();
|
|
40
43
|
#disposed = false;
|
|
41
44
|
#catalog;
|
|
42
|
-
#
|
|
45
|
+
#withCatalogDirectory;
|
|
46
|
+
#temporaryDirectory;
|
|
43
47
|
constructor(adapter, config, options = {}) {
|
|
44
48
|
this.#adapter = validateAdapter(adapter);
|
|
45
49
|
this.id = adapter.id;
|
|
@@ -47,88 +51,118 @@ export class AcpExecutor {
|
|
|
47
51
|
this.#configured = validateConfiguredAgent(adapter.configure(config));
|
|
48
52
|
this.#createConnection = options.createConnection ?? createAcpConnection;
|
|
49
53
|
this.#state = options.state;
|
|
54
|
+
this.#withCatalogDirectory =
|
|
55
|
+
options.withCatalogDirectory ??
|
|
56
|
+
(async (signal, use) => {
|
|
57
|
+
// Standalone embeddings have no Host storage; keep their cwd stable for
|
|
58
|
+
// this executor's lifetime. Registered adapters use durable scratch paths.
|
|
59
|
+
signal.throwIfAborted();
|
|
60
|
+
this.#temporaryDirectory ??= mkdtemp(join(tmpdir(), 'maka-acp-catalog-'));
|
|
61
|
+
const directory = await this.#temporaryDirectory;
|
|
62
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
63
|
+
try {
|
|
64
|
+
return await use(await realpath(directory));
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
await rm(directory, { recursive: true, force: true });
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
this.#catalog = new AcpCatalog(options.catalogAdmission ?? new AdmissionLimiter(1), (signal) => this.#probeCatalog(signal), () => this.#catalogEntry('unavailable'));
|
|
50
71
|
}
|
|
51
72
|
async discover(input) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
73
|
+
// The generic Host query still supplies its target cwd. ACP draft candidates
|
|
74
|
+
// belong to this configured executor, and never initialize the user's workspace.
|
|
75
|
+
return await this.#catalog.get(input.signal, input.refresh);
|
|
76
|
+
}
|
|
77
|
+
async #probeCatalog(signal) {
|
|
78
|
+
return await this.#withCatalogDirectory(signal, (directory) => this.#readCatalog(directory, signal));
|
|
79
|
+
}
|
|
80
|
+
async #readCatalog(directory, signal) {
|
|
81
|
+
let session;
|
|
82
|
+
try {
|
|
83
|
+
signal.throwIfAborted();
|
|
84
|
+
session = {
|
|
62
85
|
conversationKey: 'catalog-probe',
|
|
63
|
-
cwd,
|
|
86
|
+
cwd: await realpath(directory),
|
|
64
87
|
configOptions: [],
|
|
65
88
|
lost: false,
|
|
66
89
|
};
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
this.#discovery = probe().finally(() => {
|
|
84
|
-
this.#discovery = undefined;
|
|
85
|
-
});
|
|
86
|
-
return this.#discovery;
|
|
90
|
+
await this.#initialize(session, signal, true);
|
|
91
|
+
return this.#catalogEntry('ready', session.configOptions);
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
return this.#catalogEntry(isAuthenticationFailure(error) ? 'authentication_required' : 'unavailable');
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
// The directory lease outlives process cleanup, including failure cleanup.
|
|
98
|
+
if (session)
|
|
99
|
+
await (session.loss ?? this.#disposeSession(session));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
invalidateCatalog() {
|
|
103
|
+
this.#catalog.invalidate();
|
|
87
104
|
}
|
|
88
105
|
async inspectConversation(input) {
|
|
89
106
|
const session = this.#sessions.get(input.conversationKey);
|
|
90
107
|
if (this.#disposed)
|
|
91
108
|
return this.#catalogEntry('unavailable');
|
|
92
109
|
if (session && session.cwd !== resolve(input.cwd))
|
|
93
|
-
return this.#catalogEntry('history_only', [], input.configuration
|
|
110
|
+
return this.#catalogEntry('history_only', [], input.configuration);
|
|
94
111
|
if (session?.restoring)
|
|
95
|
-
return this.#catalogEntry('restoring', session.configOptions, input.configuration
|
|
112
|
+
return this.#catalogEntry('restoring', session.configOptions, input.configuration);
|
|
96
113
|
if (session?.restoreFailed)
|
|
97
|
-
return this.#catalogEntry('restore_failed', session.configOptions, input.configuration
|
|
114
|
+
return this.#catalogEntry('restore_failed', session.configOptions, input.configuration);
|
|
98
115
|
if (session?.historyGap)
|
|
99
|
-
return this.#catalogEntry('history_gap', session.configOptions, input.configuration
|
|
116
|
+
return this.#catalogEntry('history_gap', session.configOptions, input.configuration);
|
|
100
117
|
if (session && !session.lost)
|
|
101
|
-
return this.#catalogEntry('ready', session.configOptions, input.configuration
|
|
118
|
+
return this.#catalogEntry('ready', session.configOptions, input.configuration);
|
|
102
119
|
if (session?.lost && !session.record)
|
|
103
|
-
return this.#catalogEntry('history_only', session.configOptions, input.configuration
|
|
120
|
+
return this.#catalogEntry('history_only', session.configOptions, input.configuration);
|
|
104
121
|
const stored = this.#state?.read
|
|
105
122
|
? decodeContinuity(await this.#state.read(input.conversationKey))
|
|
106
123
|
: (await this.#state?.has(input.conversationKey, input.cwd))
|
|
107
124
|
? 'legacy'
|
|
108
125
|
: undefined;
|
|
109
126
|
if (!stored)
|
|
110
|
-
return this.#catalogEntry('ready', [], input.configuration
|
|
127
|
+
return this.#catalogEntry('ready', [], input.configuration);
|
|
111
128
|
if (stored === 'invalid' || stored === 'legacy' || stored.cwd !== resolve(input.cwd))
|
|
112
|
-
return this.#catalogEntry('history_only', [], input.configuration
|
|
129
|
+
return this.#catalogEntry('history_only', [], input.configuration);
|
|
113
130
|
if (stored.phase === 'reserved')
|
|
114
|
-
return this.#catalogEntry('history_only', [], input.configuration
|
|
131
|
+
return this.#catalogEntry('history_only', [], input.configuration);
|
|
115
132
|
return this.#catalogEntry(stored.phase === 'prompt_pending' || stored.phase === 'history_gap'
|
|
116
133
|
? 'history_gap'
|
|
117
|
-
: 'restorable', [],
|
|
134
|
+
: 'restorable', [], {
|
|
135
|
+
model: input.configuration?.model ?? stored.confirmedModel,
|
|
136
|
+
mode: input.configuration?.mode ?? stored.confirmedMode,
|
|
137
|
+
});
|
|
118
138
|
}
|
|
119
139
|
async configureConversation(input, signal) {
|
|
120
|
-
if (!input.configuration?.model)
|
|
121
|
-
throw new Error('ACP
|
|
140
|
+
if (!input.configuration?.model && !input.configuration?.mode)
|
|
141
|
+
throw new Error('ACP configuration change is unavailable');
|
|
122
142
|
const session = await this.#session(input);
|
|
123
|
-
if (session.active || session.configuring)
|
|
124
|
-
throw new Error('ACP
|
|
143
|
+
if (session.active || session.configuring || session.restoring || session.awaitingAck)
|
|
144
|
+
throw new Error('ACP configuration change is unavailable while the Session is busy');
|
|
125
145
|
session.configuring = true;
|
|
126
146
|
const wasConnected = !!session.connection;
|
|
127
147
|
try {
|
|
128
|
-
|
|
129
|
-
|
|
148
|
+
// A fresh Session must select the requested model before checking which
|
|
149
|
+
// modes that model exposes. Do not apply the launch defaults separately.
|
|
150
|
+
await this.#ensureInitialized(session, signal, true);
|
|
151
|
+
await this.#applyInitialConfig(session, !wasConnected && !session.restoredFromState
|
|
152
|
+
? { ...this.#configured.launch.initialConfig, ...input.configuration }
|
|
153
|
+
: input.configuration, signal, true);
|
|
154
|
+
return {
|
|
155
|
+
...(currentAcpModel(session.configOptions)
|
|
156
|
+
? { model: currentAcpModel(session.configOptions) }
|
|
157
|
+
: {}),
|
|
158
|
+
...(acpOption(session.configOptions, 'mode')?.currentValue
|
|
159
|
+
? { mode: acpOption(session.configOptions, 'mode').currentValue }
|
|
160
|
+
: {}),
|
|
161
|
+
};
|
|
130
162
|
}
|
|
131
163
|
catch (error) {
|
|
164
|
+
if (isAuthenticationFailure(error))
|
|
165
|
+
this.invalidateCatalog();
|
|
132
166
|
if (!wasConnected || session.lost) {
|
|
133
167
|
await this.#lose(session);
|
|
134
168
|
if (session.record?.sessionId && !session.historyGap)
|
|
@@ -149,7 +183,17 @@ export class AcpExecutor {
|
|
|
149
183
|
? model.options
|
|
150
184
|
.flatMap((entry) => ('options' in entry ? entry.options : [entry]))
|
|
151
185
|
.map((entry) => ({ id: entry.value, name: entry.name }))
|
|
152
|
-
:
|
|
186
|
+
: selected?.model
|
|
187
|
+
? [{ id: selected.model, name: selected.model }]
|
|
188
|
+
: [];
|
|
189
|
+
const mode = options.find((option) => option.type === 'select' && (option.category === 'mode' || option.id === 'mode'));
|
|
190
|
+
const modes = mode?.type === 'select'
|
|
191
|
+
? mode.options
|
|
192
|
+
.flatMap((entry) => ('options' in entry ? entry.options : [entry]))
|
|
193
|
+
.map((entry) => ({ id: entry.value, name: entry.name }))
|
|
194
|
+
: selected?.mode
|
|
195
|
+
? [{ id: selected.mode, name: selected.mode }]
|
|
196
|
+
: [];
|
|
153
197
|
return {
|
|
154
198
|
id: this.id,
|
|
155
199
|
displayName: this.displayName,
|
|
@@ -158,13 +202,19 @@ export class AcpExecutor {
|
|
|
158
202
|
? (this.#adapter.describeModels?.(models) ?? { models })
|
|
159
203
|
: {
|
|
160
204
|
models,
|
|
161
|
-
...(this.#catalog?.modelGroups ? { modelGroups: this.#catalog.modelGroups } : {}),
|
|
162
205
|
}),
|
|
163
206
|
...(model?.type === 'select'
|
|
164
207
|
? { currentModel: model.currentValue }
|
|
165
|
-
: selected
|
|
166
|
-
? { currentModel: selected }
|
|
208
|
+
: selected?.model
|
|
209
|
+
? { currentModel: selected.model }
|
|
167
210
|
: {}),
|
|
211
|
+
...(mode?.type === 'select'
|
|
212
|
+
? { modes, currentMode: mode.currentValue, supportsModeChange: true }
|
|
213
|
+
: {
|
|
214
|
+
modes,
|
|
215
|
+
...(selected?.mode ? { currentMode: selected.mode } : {}),
|
|
216
|
+
supportsModeChange: false,
|
|
217
|
+
}),
|
|
168
218
|
supportsAttachments: false,
|
|
169
219
|
supportsModelChange: model?.type === 'select',
|
|
170
220
|
};
|
|
@@ -182,7 +232,7 @@ export class AcpExecutor {
|
|
|
182
232
|
return failure('Conflicting executor models', 'acp_config_invalid');
|
|
183
233
|
const model = request.configuration?.model ?? (request.model === this.id ? undefined : request.model);
|
|
184
234
|
if (model)
|
|
185
|
-
request = { ...request, configuration: { model } };
|
|
235
|
+
request = { ...request, configuration: { ...request.configuration, model } };
|
|
186
236
|
let session;
|
|
187
237
|
try {
|
|
188
238
|
session = await this.#session(request);
|
|
@@ -197,8 +247,8 @@ export class AcpExecutor {
|
|
|
197
247
|
try {
|
|
198
248
|
await this.#ensureInitialized(session, context.signal);
|
|
199
249
|
context.signal.throwIfAborted();
|
|
200
|
-
if (request.configuration?.model)
|
|
201
|
-
await this.#applyInitialConfig(session,
|
|
250
|
+
if (request.configuration?.model || request.configuration?.mode)
|
|
251
|
+
await this.#applyInitialConfig(session, request.configuration, context.signal);
|
|
202
252
|
await this.#beginPrompt(session, request.turnId);
|
|
203
253
|
const prompt = session.connection.agent.request(methods.agent.session.prompt, {
|
|
204
254
|
sessionId: session.acpSessionId,
|
|
@@ -224,6 +274,8 @@ export class AcpExecutor {
|
|
|
224
274
|
return { status: 'completed', text: active.text };
|
|
225
275
|
}
|
|
226
276
|
catch (error) {
|
|
277
|
+
if (isAuthenticationFailure(error))
|
|
278
|
+
this.invalidateCatalog();
|
|
227
279
|
if (errorCode(error) === 'acp_history_gap')
|
|
228
280
|
session.historyGap = true;
|
|
229
281
|
await this.#lose(session);
|
|
@@ -259,6 +311,7 @@ export class AcpExecutor {
|
|
|
259
311
|
if (this.#disposed)
|
|
260
312
|
return;
|
|
261
313
|
this.#disposed = true;
|
|
314
|
+
await this.#catalog.dispose();
|
|
262
315
|
const sessions = [...this.#sessions.values()];
|
|
263
316
|
this.#sessions.clear();
|
|
264
317
|
const settlements = await Promise.allSettled(sessions.map((session) => session.loss ?? this.#disposeSession(session)));
|
|
@@ -293,10 +346,10 @@ export class AcpExecutor {
|
|
|
293
346
|
this.#sessions.set(request.conversationKey, created);
|
|
294
347
|
return created;
|
|
295
348
|
}
|
|
296
|
-
async #ensureInitialized(session, signal) {
|
|
349
|
+
async #ensureInitialized(session, signal, deferSelectedConfig = false) {
|
|
297
350
|
if (session.initialization)
|
|
298
351
|
return await session.initialization;
|
|
299
|
-
const initialization = this.#initialize(session, signal);
|
|
352
|
+
const initialization = this.#initialize(session, signal, false, deferSelectedConfig);
|
|
300
353
|
session.initialization = initialization;
|
|
301
354
|
try {
|
|
302
355
|
await initialization;
|
|
@@ -308,7 +361,7 @@ export class AcpExecutor {
|
|
|
308
361
|
throw error;
|
|
309
362
|
}
|
|
310
363
|
}
|
|
311
|
-
async #initialize(session, signal, probe = false) {
|
|
364
|
+
async #initialize(session, signal, probe = false, deferSelectedConfig = false) {
|
|
312
365
|
const timeout = AbortSignal.timeout(INITIALIZE_TIMEOUT_MS);
|
|
313
366
|
const startupSignal = AbortSignal.any([signal, timeout]);
|
|
314
367
|
startupSignal.throwIfAborted();
|
|
@@ -328,6 +381,7 @@ export class AcpExecutor {
|
|
|
328
381
|
throw new AcpRuntimeError('ACP conversation has no recoverable external Session identity', 'acp_history_only');
|
|
329
382
|
if (stored?.phase === 'history_gap')
|
|
330
383
|
throw new AcpRuntimeError('External Session history cannot be aligned with saved conversation events', 'acp_history_gap');
|
|
384
|
+
session.restoredFromState = !!stored;
|
|
331
385
|
if (stored) {
|
|
332
386
|
session.record = stored;
|
|
333
387
|
session.acpSessionId = stored.sessionId;
|
|
@@ -378,8 +432,22 @@ export class AcpExecutor {
|
|
|
378
432
|
]);
|
|
379
433
|
session.configOptions = restored.configOptions ?? [];
|
|
380
434
|
const restoredModel = currentAcpModel(session.configOptions);
|
|
381
|
-
|
|
382
|
-
|
|
435
|
+
const restoredMode = acpOption(session.configOptions, 'mode')?.currentValue;
|
|
436
|
+
if ((stored.confirmedModel && restoredModel !== stored.confirmedModel) ||
|
|
437
|
+
(stored.confirmedMode && restoredMode !== stored.confirmedMode)) {
|
|
438
|
+
const confirmed = await this.#applyInitialConfig(session, {
|
|
439
|
+
...(stored.confirmedModel ? { model: stored.confirmedModel } : {}),
|
|
440
|
+
...(stored.confirmedMode ? { mode: stored.confirmedMode } : {}),
|
|
441
|
+
}, startupSignal, false,
|
|
442
|
+
// Only an explicit model-only configuration can recover a removed
|
|
443
|
+
// saved mode. Ordinary execution still requires the exact saved pair.
|
|
444
|
+
deferSelectedConfig && !!session.configuration?.model && !session.configuration.mode);
|
|
445
|
+
if ((confirmed.model && currentAcpModel(session.configOptions) !== confirmed.model) ||
|
|
446
|
+
(confirmed.mode &&
|
|
447
|
+
acpOption(session.configOptions, 'mode')?.currentValue !== confirmed.mode))
|
|
448
|
+
throw new AcpRuntimeError('Restored Agent configuration differs from the saved task', 'acp_config_unconfirmed');
|
|
449
|
+
}
|
|
450
|
+
await this.#persistConfirmedConfig(session);
|
|
383
451
|
session.restoring = false;
|
|
384
452
|
if (pending) {
|
|
385
453
|
// The Agent may have progressed beyond Maka's last durable event. Replay
|
|
@@ -436,13 +504,11 @@ export class AcpExecutor {
|
|
|
436
504
|
};
|
|
437
505
|
await this.#state?.write?.(session.conversationKey, established);
|
|
438
506
|
session.record = established;
|
|
439
|
-
|
|
440
|
-
if (createdModel)
|
|
441
|
-
await this.#persistConfirmedModel(session, createdModel);
|
|
507
|
+
await this.#persistConfirmedConfig(session);
|
|
442
508
|
}
|
|
443
|
-
if (!probe) {
|
|
444
|
-
await this.#applyInitialConfig(session, session.configuration?.model
|
|
445
|
-
? {
|
|
509
|
+
if (!probe && !deferSelectedConfig) {
|
|
510
|
+
await this.#applyInitialConfig(session, session.configuration?.model || session.configuration?.mode
|
|
511
|
+
? { ...launch.initialConfig, ...session.configuration }
|
|
446
512
|
: (launch.initialConfig ?? {}), startupSignal);
|
|
447
513
|
}
|
|
448
514
|
}
|
|
@@ -504,60 +570,103 @@ export class AcpExecutor {
|
|
|
504
570
|
await this.#lose(session);
|
|
505
571
|
}
|
|
506
572
|
}
|
|
507
|
-
async #applyInitialConfig(session, values, signal, restoreOnFailure = false) {
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
573
|
+
async #applyInitialConfig(session, values, signal, restoreOnFailure = false, omitUnavailableMode = false) {
|
|
574
|
+
let target = Object.entries(values).filter((entry) => entry[1] !== undefined);
|
|
575
|
+
const before = session.configOptions;
|
|
576
|
+
const selectedModel = target.find(([key]) => key === 'model')?.[1];
|
|
577
|
+
const changingModel = selectedModel !== undefined && acpOption(before, 'model')?.currentValue !== selectedModel;
|
|
578
|
+
const validate = (options, key, value) => {
|
|
579
|
+
const option = acpOption(options, key);
|
|
580
|
+
if (!option)
|
|
511
581
|
throw new AcpRuntimeError(`ACP configuration is unavailable: ${key}`, 'acp_config_unavailable');
|
|
512
|
-
const
|
|
513
|
-
if (!
|
|
582
|
+
const candidates = option.options.flatMap((entry) => 'options' in entry ? entry.options : [entry]);
|
|
583
|
+
if (!candidates.some((entry) => entry.value === value))
|
|
514
584
|
throw new AcpRuntimeError(`ACP configuration value is unavailable: ${key}`, 'acp_config_invalid');
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
585
|
+
};
|
|
586
|
+
// A model change can add, remove or replace the mode option. Validate that
|
|
587
|
+
// dependent option against the Agent's post-model response instead.
|
|
588
|
+
for (const [key, value] of target) {
|
|
589
|
+
if (omitUnavailableMode && key === 'mode')
|
|
518
590
|
continue;
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
591
|
+
if (!changingModel || key === 'model')
|
|
592
|
+
validate(before, key, value);
|
|
593
|
+
}
|
|
594
|
+
const ordered = [
|
|
595
|
+
...target.filter(([key]) => key === 'model'),
|
|
596
|
+
...target.filter(([key]) => key !== 'model'),
|
|
597
|
+
];
|
|
598
|
+
let mutationAttempted = false;
|
|
599
|
+
try {
|
|
600
|
+
for (const [key, value] of ordered) {
|
|
601
|
+
const option = acpOption(session.configOptions, key);
|
|
602
|
+
if (omitUnavailableMode &&
|
|
603
|
+
key === 'mode' &&
|
|
604
|
+
!option?.options
|
|
605
|
+
.flatMap((entry) => ('options' in entry ? entry.options : [entry]))
|
|
606
|
+
.some((entry) => entry.value === value)) {
|
|
607
|
+
target = target.filter(([candidate]) => candidate !== 'mode');
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
// Every remaining value requires confirmation. Outside explicit recovery,
|
|
611
|
+
// a model change cannot waive a requested or saved mode.
|
|
612
|
+
validate(session.configOptions, key, value);
|
|
613
|
+
if (!option)
|
|
614
|
+
throw new Error('Validated ACP option disappeared');
|
|
615
|
+
if (option.currentValue === value)
|
|
616
|
+
continue;
|
|
617
|
+
signal.throwIfAborted();
|
|
618
|
+
mutationAttempted = true;
|
|
522
619
|
const updated = await session.connection.agent.request(methods.agent.session.setConfigOption, { sessionId: session.acpSessionId, configId: option.id, value }, { cancellationSignal: signal });
|
|
523
|
-
const confirmed = updated.configOptions.find((candidate) => candidate.id === option.id);
|
|
524
|
-
if (confirmed?.type !== 'select' || confirmed.currentValue !== value)
|
|
525
|
-
throw new AcpRuntimeError('Agent did not confirm the selected configuration', 'acp_config_unconfirmed');
|
|
526
620
|
session.configOptions = updated.configOptions;
|
|
527
|
-
if (key
|
|
528
|
-
|
|
621
|
+
if (acpOption(updated.configOptions, key)?.currentValue !== value)
|
|
622
|
+
throw new AcpRuntimeError('Agent did not confirm the selected configuration', 'acp_config_unconfirmed');
|
|
529
623
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
624
|
+
if (target.some(([key, value]) => acpOption(session.configOptions, key)?.currentValue !== value))
|
|
625
|
+
throw new AcpRuntimeError('Agent changed another selected configuration', 'acp_config_unconfirmed');
|
|
626
|
+
await this.#persistConfirmedConfig(session);
|
|
627
|
+
return Object.fromEntries(target);
|
|
628
|
+
}
|
|
629
|
+
catch (error) {
|
|
630
|
+
let restored = false;
|
|
631
|
+
if (restoreOnFailure && !session.lost && !signal.aborted) {
|
|
632
|
+
try {
|
|
633
|
+
// A rejected response can still have changed the Agent. Restore the
|
|
634
|
+
// model before its dependent mode, then verify the original snapshot.
|
|
635
|
+
for (const key of ['model', 'mode']) {
|
|
636
|
+
const original = acpOption(before, key);
|
|
637
|
+
const current = acpOption(session.configOptions, key);
|
|
638
|
+
if (!original ||
|
|
639
|
+
!current ||
|
|
640
|
+
(!mutationAttempted && current.currentValue === original.currentValue))
|
|
641
|
+
continue;
|
|
642
|
+
const rollback = await session.connection.agent.request(methods.agent.session.setConfigOption, {
|
|
643
|
+
sessionId: session.acpSessionId,
|
|
644
|
+
configId: current.id,
|
|
645
|
+
value: original.currentValue,
|
|
646
|
+
}, { cancellationSignal: AbortSignal.timeout(5_000) });
|
|
647
|
+
session.configOptions = rollback.configOptions;
|
|
549
648
|
}
|
|
649
|
+
restored = ['model', 'mode'].every((key) => {
|
|
650
|
+
const original = acpOption(before, key);
|
|
651
|
+
return (!original ||
|
|
652
|
+
acpOption(session.configOptions, key)?.currentValue === original.currentValue);
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
catch {
|
|
656
|
+
/* Uncertain configuration remains unavailable. */
|
|
550
657
|
}
|
|
551
|
-
if (!restored)
|
|
552
|
-
await this.#lose(session);
|
|
553
|
-
throw error;
|
|
554
658
|
}
|
|
659
|
+
if (!restored)
|
|
660
|
+
await this.#lose(session);
|
|
661
|
+
throw error;
|
|
555
662
|
}
|
|
556
663
|
}
|
|
557
|
-
async #
|
|
558
|
-
if (!session.record
|
|
664
|
+
async #persistConfirmedConfig(session, configOptions = session.configOptions) {
|
|
665
|
+
if (!session.record)
|
|
666
|
+
return;
|
|
667
|
+
const record = withConfirmedConfiguration(session.record, configOptions);
|
|
668
|
+
if (record === session.record)
|
|
559
669
|
return;
|
|
560
|
-
const record = { ...session.record, confirmedModel: model };
|
|
561
670
|
await this.#state?.write?.(session.conversationKey, record);
|
|
562
671
|
session.record = record;
|
|
563
672
|
}
|
|
@@ -608,8 +717,11 @@ export class AcpExecutor {
|
|
|
608
717
|
return;
|
|
609
718
|
if (update.sessionUpdate === 'config_option_update') {
|
|
610
719
|
// During our mutation, its response (or rollback response) is authoritative.
|
|
611
|
-
if (!session.configuring && !session.lost)
|
|
720
|
+
if (!session.configuring && !session.lost) {
|
|
612
721
|
session.configOptions = update.configOptions;
|
|
722
|
+
// Agent notifications report observed state. Only a confirmed response
|
|
723
|
+
// to our own configuration request may change the durable selection.
|
|
724
|
+
}
|
|
613
725
|
return;
|
|
614
726
|
}
|
|
615
727
|
const active = session.active;
|
|
@@ -751,8 +863,27 @@ export class AcpExecutor {
|
|
|
751
863
|
}
|
|
752
864
|
}
|
|
753
865
|
function currentAcpModel(options) {
|
|
754
|
-
|
|
755
|
-
|
|
866
|
+
return acpOption(options, 'model')?.currentValue;
|
|
867
|
+
}
|
|
868
|
+
function withConfirmedConfiguration(record, options) {
|
|
869
|
+
const model = currentAcpModel(options);
|
|
870
|
+
const mode = acpOption(options, 'mode')?.currentValue;
|
|
871
|
+
const { confirmedModel, confirmedMode, ...base } = record;
|
|
872
|
+
if (confirmedModel === model && confirmedMode === mode)
|
|
873
|
+
return record;
|
|
874
|
+
return {
|
|
875
|
+
...base,
|
|
876
|
+
...(model ? { confirmedModel: model } : {}),
|
|
877
|
+
...(mode ? { confirmedMode: mode } : {}),
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
function isAuthenticationFailure(error) {
|
|
881
|
+
return (error?.code === -32000 ||
|
|
882
|
+
(error instanceof Error &&
|
|
883
|
+
/authentication required|unauthorized/u.test(error.message.toLowerCase())));
|
|
884
|
+
}
|
|
885
|
+
function acpOption(options, key) {
|
|
886
|
+
return options.find((candidate) => candidate.type === 'select' && (candidate.id === key || candidate.category === key));
|
|
756
887
|
}
|
|
757
888
|
function toolTextContent(content) {
|
|
758
889
|
return content
|
|
@@ -769,6 +900,8 @@ function toolTextContent(content) {
|
|
|
769
900
|
* immutable package generation.
|
|
770
901
|
*/
|
|
771
902
|
export class AcpRuntimeService {
|
|
903
|
+
// Shared by every adapter below this ACP runtime Entry, including queued probes.
|
|
904
|
+
#catalogAdmission = new AdmissionLimiter(2);
|
|
772
905
|
constructor(ctx) {
|
|
773
906
|
ctx.provide('acp', this);
|
|
774
907
|
}
|
|
@@ -776,8 +909,11 @@ export class AcpRuntimeService {
|
|
|
776
909
|
const storage = consumer.get('storage');
|
|
777
910
|
if (!storage)
|
|
778
911
|
throw new Error('ACP continuity storage is unavailable');
|
|
912
|
+
const directoryKey = `acp-catalog/${adapter.id}/${createHash('sha256').update(pluginIdentity(consumer).entryId).digest('hex')}`;
|
|
779
913
|
const provider = new AcpExecutor(adapter, config, {
|
|
780
914
|
state: pluginStateStore(storage, adapter.id),
|
|
915
|
+
catalogAdmission: this.#catalogAdmission,
|
|
916
|
+
withCatalogDirectory: (signal, use) => storage.withScratchDirectory(directoryKey, signal, use),
|
|
781
917
|
});
|
|
782
918
|
consumer.effect(() => () => provider.dispose(), `acp.dispose(${JSON.stringify(adapter.id)})`);
|
|
783
919
|
return consumer.executors.register(provider);
|
|
@@ -824,6 +960,8 @@ function decodeContinuity(value) {
|
|
|
824
960
|
(typeof record.pendingTurnId !== 'string' || !record.pendingTurnId)) ||
|
|
825
961
|
(record.confirmedModel !== undefined &&
|
|
826
962
|
(typeof record.confirmedModel !== 'string' || !record.confirmedModel)) ||
|
|
963
|
+
(record.confirmedMode !== undefined &&
|
|
964
|
+
(typeof record.confirmedMode !== 'string' || !record.confirmedMode)) ||
|
|
827
965
|
(record.phase === 'reserved' && record.sessionId !== undefined) ||
|
|
828
966
|
(record.phase !== 'reserved' && !record.sessionId) ||
|
|
829
967
|
(record.phase === 'prompt_pending' && !record.pendingTurnId) ||
|