maka-agent 0.2.0-dev.47.20260922 → 0.2.0-dev.48.20260923
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/runtime-host-capability-provider-command.js +25 -6
- package/dist/tui-mcp-control.js +37 -24
- 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-errors.js +26 -0
- package/node_modules/@maka/acp-executor-plugin/dist/acp-filesystem.js +103 -0
- package/node_modules/@maka/acp-executor-plugin/dist/acp-process.js +119 -0
- package/node_modules/@maka/acp-executor-plugin/dist/acp-projection.js +146 -0
- package/node_modules/@maka/acp-executor-plugin/dist/index.js +653 -0
- package/node_modules/@maka/acp-executor-plugin/dist/plugin.mjs +24446 -0
- package/node_modules/@maka/acp-executor-plugin/package.json +15 -0
- package/node_modules/@maka/antigravity-acp-plugin/dist/index.js +88 -0
- package/node_modules/@maka/antigravity-acp-plugin/dist/model-presentation.js +53 -0
- package/node_modules/@maka/antigravity-acp-plugin/dist/plugin.mjs +115 -0
- package/node_modules/@maka/antigravity-acp-plugin/package.json +15 -0
- package/node_modules/@maka/core/dist/executor-catalog.js +106 -0
- package/node_modules/@maka/core/dist/mcp.js +4 -1
- package/node_modules/@maka/core/dist/model-metadata.js +28 -1
- package/node_modules/@maka/core/dist/runtime-event-store.js +25 -0
- package/node_modules/@maka/core/dist/runtime-invocation.js +5 -0
- package/node_modules/@maka/core/dist/runtime-policy/credential-vault-codec.js +6 -0
- package/node_modules/@maka/core/dist/runtime-policy/policy-codec.js +21 -0
- package/node_modules/@maka/core/dist/session.js +2 -0
- package/node_modules/@maka/core/dist/settings.js +1 -0
- package/node_modules/@maka/core/dist/tool-ledger-scanner.js +365 -104
- package/node_modules/@maka/core/dist/turn-origin.js +14 -0
- package/node_modules/@maka/core/package.json +2 -0
- package/node_modules/@maka/mcp/dist/credential-coordinator.js +6 -1
- package/node_modules/@maka/mcp/dist/index.js +31 -5
- package/node_modules/@maka/mcp/dist/oauth.js +54 -6
- package/node_modules/@maka/runtime/dist/agent-run-inspect.js +1 -1
- package/node_modules/@maka/runtime/dist/conversation-copy.js +56 -1
- package/node_modules/@maka/runtime/dist/mcp-tools.js +9 -2
- package/node_modules/@maka/runtime/dist/plugin-executor-backend.js +116 -3
- package/node_modules/@maka/runtime/dist/plugin-executor-service.js +212 -16
- package/node_modules/@maka/runtime/dist/runtime-ledger-repair.js +2 -2
- package/node_modules/@maka/runtime/dist/session-event-runtime-mapper.js +3 -0
- package/node_modules/@maka/runtime/dist/session-manager.js +170 -125
- package/node_modules/@maka/runtime/dist/shell-run-manager.js +3 -0
- package/node_modules/@maka/runtime/dist/stream-graph-coordinator.js +29 -4
- package/node_modules/@maka/runtime-host/dist/candidate-entry.js +8 -5
- package/node_modules/@maka/runtime-host/dist/client/index.js +1 -1
- package/node_modules/@maka/runtime-host/dist/client/reconnect-lifecycle.js +8 -1
- package/node_modules/@maka/runtime-host/dist/client/session-catalog-summary.js +6 -1
- package/node_modules/@maka/runtime-host/dist/composition-identity.js +19 -0
- package/node_modules/@maka/runtime-host/dist/protocol/index.js +10 -2
- package/node_modules/@maka/runtime-host/dist/protocol/plugin-platform.js +34 -0
- package/node_modules/@maka/runtime-host/dist/protocol/session-catalog.js +40 -2
- package/node_modules/@maka/runtime-host/dist/server/builtin-external-agent-plugins.js +188 -0
- package/node_modules/@maka/runtime-host/dist/server/candidate.js +12 -7
- package/node_modules/@maka/runtime-host/dist/server/client-capability-recovery.js +44 -46
- package/node_modules/@maka/runtime-host/dist/server/context-coordinator.js +2 -0
- package/node_modules/@maka/runtime-host/dist/server/execution-composition-factory.js +1 -1
- package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +159 -19
- package/node_modules/@maka/runtime-host/dist/server/execution-model-authority.js +3 -0
- package/node_modules/@maka/runtime-host/dist/server/extension-bundle.js +4 -0
- package/node_modules/@maka/runtime-host/dist/server/host-composition-source.js +47 -0
- package/node_modules/@maka/runtime-host/dist/server/host-composition.js +1 -29
- package/node_modules/@maka/runtime-host/dist/server/host-kernel.js +2 -9
- package/node_modules/@maka/runtime-host/dist/server/hosted-execution-recovery.js +75 -9
- package/node_modules/@maka/runtime-host/dist/server/hosted-execution-tool-profile.js +4 -0
- package/node_modules/@maka/runtime-host/dist/server/interaction-coordinator.js +45 -0
- package/node_modules/@maka/runtime-host/dist/server/jev-routing-model.js +157 -0
- package/node_modules/@maka/runtime-host/dist/server/listener-set.js +2 -2
- package/node_modules/@maka/runtime-host/dist/server/message-coordinator.js +4 -0
- package/node_modules/@maka/runtime-host/dist/server/plugin-platform-coordinator.js +11 -1
- package/node_modules/@maka/runtime-host/dist/server/process-lifecycle.js +1 -1
- package/node_modules/@maka/runtime-host/dist/server/process-termination-error.js +27 -0
- package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +91 -12
- package/node_modules/@maka/runtime-host/dist/server/runtime-policy-coordinator.js +2 -0
- package/node_modules/@maka/runtime-host/dist/server/session-catalog-coordinator.js +99 -12
- package/node_modules/@maka/runtime-host/dist/server/session-effect-coordinator.js +12 -6
- package/node_modules/@maka/runtime-host/dist/server/session-transcript-reader.js +33 -0
- package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-coordinator.js +4 -0
- package/node_modules/@maka/runtime-host/dist/server/workhub-result-coordinator.js +173 -0
- package/node_modules/@maka/runtime-host/dist/server/workhub-result-runtime.js +293 -0
- package/node_modules/@maka/runtime-host/package.json +2 -0
- package/node_modules/@maka/storage/dist/agent-run-store-contract.js +8 -0
- package/node_modules/@maka/storage/dist/execution-stores.js +15 -3
- package/node_modules/@maka/storage/dist/mcp-config-store.js +91 -2
- package/node_modules/@maka/storage/dist/operational-state-store.js +15 -1
- package/node_modules/@maka/storage/dist/plan-authority.js +2 -0
- package/node_modules/@maka/storage/dist/plan-store.js +30 -0
- package/node_modules/@maka/storage/dist/runtime-event-persistence.js +1 -0
- package/node_modules/@maka/storage/dist/runtime-policy/credential-vault-document.js +4 -1
- package/node_modules/@maka/storage/dist/runtime-policy/policy-document.js +2 -0
- package/node_modules/@maka/storage/dist/session-store-values.js +16 -4
- package/node_modules/@maka/storage/dist/shell-run-authority.js +1 -0
- package/node_modules/@maka/storage/dist/shell-run-store.js +15 -0
- package/node_modules/@maka/storage/dist/sqlite-runtime-schema.js +74 -1
- package/node_modules/@maka/storage/dist/sqlite-runtime-store.js +724 -84
- package/node_modules/@maka/storage/dist/sqlite-session-metadata-store.js +16 -0
- package/package.json +1 -1
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Licensed to the Apache Software Foundation (ASF) under one
|
|
3
|
+
* or more contributor license agreements. See the NOTICE file
|
|
4
|
+
* distributed with this work for additional information
|
|
5
|
+
* regarding copyright ownership. The ASF licenses this file
|
|
6
|
+
* to you under the Apache License, Version 2.0 (the
|
|
7
|
+
* "License"); you may not use this file except in compliance
|
|
8
|
+
* with the License. You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing,
|
|
13
|
+
* software distributed under the License is distributed on an
|
|
14
|
+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
15
|
+
* KIND, either express or implied. See the License for the
|
|
16
|
+
* specific language governing permissions and limitations
|
|
17
|
+
* under the License.
|
|
18
|
+
*/
|
|
19
|
+
import { createHash } from 'node:crypto';
|
|
20
|
+
import { constants } from 'node:fs';
|
|
21
|
+
import { access, mkdtemp, rm, realpath, stat } from 'node:fs/promises';
|
|
22
|
+
import { tmpdir } from 'node:os';
|
|
23
|
+
import { dirname, isAbsolute, resolve } from 'node:path';
|
|
24
|
+
import { methods, } from '@agentclientprotocol/sdk';
|
|
25
|
+
import { AcpRuntimeError } from './acp-errors.js';
|
|
26
|
+
import { readWorkspaceTextFile, writeWorkspaceTextFile } from './acp-filesystem.js';
|
|
27
|
+
import { createAcpConnection } from './acp-process.js';
|
|
28
|
+
import { activityKind, boundedText, emitText, emitToolOutput, projectToolResult, promptText, summarizeToolContent, toolName, } from './acp-projection.js';
|
|
29
|
+
const CANCEL_TIMEOUT_MS = 15_000;
|
|
30
|
+
const INITIALIZE_TIMEOUT_MS = 30_000;
|
|
31
|
+
export class AcpExecutor {
|
|
32
|
+
id;
|
|
33
|
+
displayName;
|
|
34
|
+
capabilities = Object.freeze({ thinking: true, toolActivity: true });
|
|
35
|
+
#adapter;
|
|
36
|
+
#configured;
|
|
37
|
+
#createConnection;
|
|
38
|
+
#state;
|
|
39
|
+
#sessions = new Map();
|
|
40
|
+
#disposed = false;
|
|
41
|
+
#catalog;
|
|
42
|
+
#discovery;
|
|
43
|
+
constructor(adapter, config, options = {}) {
|
|
44
|
+
this.#adapter = validateAdapter(adapter);
|
|
45
|
+
this.id = adapter.id;
|
|
46
|
+
this.displayName = adapter.displayName;
|
|
47
|
+
this.#configured = validateConfiguredAgent(adapter.configure(config));
|
|
48
|
+
this.#createConnection = options.createConnection ?? createAcpConnection;
|
|
49
|
+
this.#state = options.state;
|
|
50
|
+
}
|
|
51
|
+
async discover(input) {
|
|
52
|
+
if (this.#disposed)
|
|
53
|
+
return this.#catalogEntry('unavailable');
|
|
54
|
+
if (this.#catalog)
|
|
55
|
+
return this.#catalog;
|
|
56
|
+
if (this.#discovery)
|
|
57
|
+
return this.#discovery;
|
|
58
|
+
const probe = async () => {
|
|
59
|
+
// A bounded disposable ACP probe never creates a Maka task or joins the retained-session map.
|
|
60
|
+
const cwd = await mkdtemp(resolve(tmpdir(), 'maka-acp-catalog-'));
|
|
61
|
+
const session = {
|
|
62
|
+
conversationKey: 'catalog-probe',
|
|
63
|
+
cwd,
|
|
64
|
+
configOptions: [],
|
|
65
|
+
lost: false,
|
|
66
|
+
};
|
|
67
|
+
try {
|
|
68
|
+
await this.#initialize(session, input.signal, true);
|
|
69
|
+
const result = this.#catalogEntry('ready', session.configOptions);
|
|
70
|
+
this.#catalog = result;
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
return this.#catalogEntry(error?.code === -32000
|
|
75
|
+
? 'authentication_required'
|
|
76
|
+
: 'unavailable');
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
await this.#disposeSession(session);
|
|
80
|
+
await rm(cwd, { recursive: true, force: true });
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
this.#discovery = probe().finally(() => {
|
|
84
|
+
this.#discovery = undefined;
|
|
85
|
+
});
|
|
86
|
+
return this.#discovery;
|
|
87
|
+
}
|
|
88
|
+
async inspectConversation(input) {
|
|
89
|
+
const session = this.#sessions.get(input.conversationKey);
|
|
90
|
+
if (this.#disposed)
|
|
91
|
+
return this.#catalogEntry('unavailable');
|
|
92
|
+
if (session?.lost || (!session && (await this.#state?.has(input.conversationKey, input.cwd))))
|
|
93
|
+
return this.#catalogEntry('history_only');
|
|
94
|
+
return this.#catalogEntry('ready', session?.configOptions ?? [], input.configuration?.model);
|
|
95
|
+
}
|
|
96
|
+
async configureConversation(input, signal) {
|
|
97
|
+
const session = this.#sessions.get(input.conversationKey);
|
|
98
|
+
if (!session?.connection ||
|
|
99
|
+
session.lost ||
|
|
100
|
+
session.active ||
|
|
101
|
+
session.configuring ||
|
|
102
|
+
!input.configuration?.model)
|
|
103
|
+
throw new Error('ACP model change is unavailable');
|
|
104
|
+
session.configuring = true;
|
|
105
|
+
try {
|
|
106
|
+
await this.#applyInitialConfig(session, { model: input.configuration.model }, signal, true);
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
session.configuring = false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
#catalogEntry(readiness, options = [], selected) {
|
|
113
|
+
const model = options.find((option) => option.type === 'select' && (option.category === 'model' || option.id === 'model'));
|
|
114
|
+
const models = model?.type === 'select'
|
|
115
|
+
? model.options
|
|
116
|
+
.flatMap((entry) => ('options' in entry ? entry.options : [entry]))
|
|
117
|
+
.map((entry) => ({ id: entry.value, name: entry.name }))
|
|
118
|
+
: [];
|
|
119
|
+
return {
|
|
120
|
+
id: this.id,
|
|
121
|
+
displayName: this.displayName,
|
|
122
|
+
readiness,
|
|
123
|
+
...(this.#adapter.describeModels?.(models) ?? { models }),
|
|
124
|
+
...(model?.type === 'select'
|
|
125
|
+
? { currentModel: model.currentValue }
|
|
126
|
+
: selected
|
|
127
|
+
? { currentModel: selected }
|
|
128
|
+
: {}),
|
|
129
|
+
supportsAttachments: false,
|
|
130
|
+
supportsModelChange: model?.type === 'select',
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
async execute(request, context) {
|
|
134
|
+
if (this.#disposed)
|
|
135
|
+
return failure('ACP executor is unavailable', 'acp_unavailable');
|
|
136
|
+
if (request.attachments?.length) {
|
|
137
|
+
return failure(`${this.displayName} ACP supports project files and text only`, 'acp_attachments_unsupported');
|
|
138
|
+
}
|
|
139
|
+
if (request.configuration?.model &&
|
|
140
|
+
request.model &&
|
|
141
|
+
request.model !== this.id &&
|
|
142
|
+
request.configuration.model !== request.model)
|
|
143
|
+
return failure('Conflicting executor models', 'acp_config_invalid');
|
|
144
|
+
const model = request.configuration?.model ?? (request.model === this.id ? undefined : request.model);
|
|
145
|
+
if (model)
|
|
146
|
+
request = { ...request, configuration: { model } };
|
|
147
|
+
let session;
|
|
148
|
+
try {
|
|
149
|
+
session = this.#session(request);
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
return failure(safeErrorMessage(error), errorCode(error));
|
|
153
|
+
}
|
|
154
|
+
if (session.active || session.configuring)
|
|
155
|
+
return failure(`${this.displayName} ACP Session is busy`, 'acp_busy', true);
|
|
156
|
+
const active = { context, tools: new Map(), text: '' };
|
|
157
|
+
session.active = active;
|
|
158
|
+
try {
|
|
159
|
+
await this.#ensureInitialized(session, context.signal);
|
|
160
|
+
context.signal.throwIfAborted();
|
|
161
|
+
if (request.configuration?.model)
|
|
162
|
+
await this.#applyInitialConfig(session, { model: request.configuration.model }, context.signal);
|
|
163
|
+
const prompt = session.connection.agent.request(methods.agent.session.prompt, {
|
|
164
|
+
sessionId: session.acpSessionId,
|
|
165
|
+
prompt: [{ type: 'text', text: promptText(request) }],
|
|
166
|
+
});
|
|
167
|
+
const response = await this.#awaitPrompt(session, prompt, context.signal);
|
|
168
|
+
if (!response)
|
|
169
|
+
return { status: 'cancelled', reason: 'timeout' };
|
|
170
|
+
if (context.signal.aborted || response.stopReason === 'cancelled') {
|
|
171
|
+
return { status: 'cancelled', providerStopReason: response.stopReason };
|
|
172
|
+
}
|
|
173
|
+
if (this.#adapter.executionFailed?.(active.text)) {
|
|
174
|
+
return failure(`${this.displayName} reported an execution failure`, 'acp_prompt_failed');
|
|
175
|
+
}
|
|
176
|
+
if (response.stopReason !== 'end_turn') {
|
|
177
|
+
return failure(`${this.displayName} stopped before completing (${response.stopReason})`, 'acp_prompt_incomplete');
|
|
178
|
+
}
|
|
179
|
+
return { status: 'completed', text: active.text };
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
await this.#lose(session);
|
|
183
|
+
// Retry only when no session/new was attempted. Once continuity is
|
|
184
|
+
// reserved, a lost response may hide an established Agent Session.
|
|
185
|
+
const retryable = !session.acpSessionId &&
|
|
186
|
+
!session.continuityReserved &&
|
|
187
|
+
errorCode(error) !== 'acp_history_only';
|
|
188
|
+
if (retryable && this.#sessions.get(session.conversationKey) === session)
|
|
189
|
+
this.#sessions.delete(session.conversationKey);
|
|
190
|
+
if (context.signal.aborted) {
|
|
191
|
+
if (error === context.signal.reason ||
|
|
192
|
+
(error instanceof DOMException && error.name === 'AbortError'))
|
|
193
|
+
return { status: 'cancelled' };
|
|
194
|
+
return { status: 'cancelled', reason: 'crash', providerStopReason: errorCode(error) };
|
|
195
|
+
}
|
|
196
|
+
return failure(safeErrorMessage(error), errorCode(error), retryable);
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
if (session.active === active)
|
|
200
|
+
session.active = undefined;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async disposeConversation(conversationKey) {
|
|
204
|
+
const session = this.#sessions.get(conversationKey);
|
|
205
|
+
if (session)
|
|
206
|
+
await this.#lose(session);
|
|
207
|
+
}
|
|
208
|
+
async dispose() {
|
|
209
|
+
if (this.#disposed)
|
|
210
|
+
return;
|
|
211
|
+
this.#disposed = true;
|
|
212
|
+
const sessions = [...this.#sessions.values()];
|
|
213
|
+
this.#sessions.clear();
|
|
214
|
+
const settlements = await Promise.allSettled(sessions.map((session) => session.loss ?? this.#disposeSession(session)));
|
|
215
|
+
const failures = settlements.flatMap((settlement) => settlement.status === 'rejected' ? [settlement.reason] : []);
|
|
216
|
+
if (failures.length)
|
|
217
|
+
throw new AggregateError(failures, 'ACP process cleanup failed');
|
|
218
|
+
}
|
|
219
|
+
#session(request) {
|
|
220
|
+
const cwd = resolve(request.cwd);
|
|
221
|
+
const existing = this.#sessions.get(request.conversationKey);
|
|
222
|
+
if (existing) {
|
|
223
|
+
if (existing.cwd !== cwd)
|
|
224
|
+
throw new AcpRuntimeError('ACP conversation cannot change workspace', 'acp_cwd_changed');
|
|
225
|
+
if (existing.lost)
|
|
226
|
+
throw new AcpRuntimeError('ACP conversation is history-only because its external process was lost', 'acp_history_only');
|
|
227
|
+
return existing;
|
|
228
|
+
}
|
|
229
|
+
const created = {
|
|
230
|
+
conversationKey: request.conversationKey,
|
|
231
|
+
...(request.configuration ? { configuration: request.configuration } : {}),
|
|
232
|
+
cwd,
|
|
233
|
+
configOptions: [],
|
|
234
|
+
lost: false,
|
|
235
|
+
};
|
|
236
|
+
this.#sessions.set(request.conversationKey, created);
|
|
237
|
+
return created;
|
|
238
|
+
}
|
|
239
|
+
async #ensureInitialized(session, signal) {
|
|
240
|
+
if (session.initialization)
|
|
241
|
+
return await session.initialization;
|
|
242
|
+
const initialization = this.#initialize(session, signal);
|
|
243
|
+
session.initialization = initialization;
|
|
244
|
+
try {
|
|
245
|
+
await initialization;
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
session.initialization = undefined;
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
async #initialize(session, signal, probe = false) {
|
|
253
|
+
if (!probe && (await this.#state?.has(session.conversationKey, session.cwd))) {
|
|
254
|
+
throw new AcpRuntimeError('ACP conversation is history-only after the Plugin or Host was restarted', 'acp_history_only');
|
|
255
|
+
}
|
|
256
|
+
const timeout = AbortSignal.timeout(INITIALIZE_TIMEOUT_MS);
|
|
257
|
+
const startupSignal = AbortSignal.any([signal, timeout]);
|
|
258
|
+
startupSignal.throwIfAborted();
|
|
259
|
+
const launch = this.#configured.launch;
|
|
260
|
+
const executable = await checkedExecutable(launch.executable);
|
|
261
|
+
await Promise.all((launch.requiredExecutables ?? []).map(checkedExecutable));
|
|
262
|
+
const owner = this.#createConnection({
|
|
263
|
+
...launch,
|
|
264
|
+
executable,
|
|
265
|
+
args: launch.args ?? [],
|
|
266
|
+
cwd: launch.cwd ?? dirname(executable),
|
|
267
|
+
env: launch.env ?? process.env,
|
|
268
|
+
clientName: this.#adapter.clientName ?? `maka-${this.id}`,
|
|
269
|
+
configureClient: (app) => this.#configureClient(session, app),
|
|
270
|
+
});
|
|
271
|
+
session.owner = owner;
|
|
272
|
+
session.connection = owner.connection;
|
|
273
|
+
void owner.failed.catch(() => this.#lose(session)).catch(() => undefined);
|
|
274
|
+
const initialized = await Promise.race([
|
|
275
|
+
owner.connection.agent.request(methods.agent.initialize, {
|
|
276
|
+
protocolVersion: 1,
|
|
277
|
+
clientCapabilities: {
|
|
278
|
+
fs: { readTextFile: !probe, writeTextFile: !probe },
|
|
279
|
+
terminal: false,
|
|
280
|
+
},
|
|
281
|
+
}, { cancellationSignal: startupSignal }),
|
|
282
|
+
owner.failed,
|
|
283
|
+
]);
|
|
284
|
+
if (initialized.protocolVersion !== 1)
|
|
285
|
+
throw new Error('Unsupported ACP protocol version');
|
|
286
|
+
if (!probe) {
|
|
287
|
+
startupSignal.throwIfAborted();
|
|
288
|
+
// Reserve continuity before asking the Agent to create a Session. A failed
|
|
289
|
+
// or interrupted session/new may still have created one remotely.
|
|
290
|
+
await this.#state?.mark(session.conversationKey, session.cwd);
|
|
291
|
+
session.continuityReserved = true;
|
|
292
|
+
}
|
|
293
|
+
const created = await Promise.race([
|
|
294
|
+
owner.connection.agent.request(methods.agent.session.new, { cwd: session.cwd, mcpServers: [] }, { cancellationSignal: startupSignal }),
|
|
295
|
+
owner.failed,
|
|
296
|
+
]);
|
|
297
|
+
session.acpSessionId = created.sessionId;
|
|
298
|
+
session.configOptions = created.configOptions ?? [];
|
|
299
|
+
if (!probe) {
|
|
300
|
+
await this.#applyInitialConfig(session, session.configuration?.model
|
|
301
|
+
? { model: session.configuration.model }
|
|
302
|
+
: (launch.initialConfig ?? {}), startupSignal);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
async #applyInitialConfig(session, values, signal, restoreOnFailure = false) {
|
|
306
|
+
for (const [key, value] of Object.entries(values)) {
|
|
307
|
+
const option = session.configOptions.find((candidate) => candidate.type === 'select' && (candidate.id === key || candidate.category === key));
|
|
308
|
+
if (!option || option.type !== 'select')
|
|
309
|
+
throw new AcpRuntimeError(`ACP configuration is unavailable: ${key}`, 'acp_config_unavailable');
|
|
310
|
+
const options = option.options.flatMap((entry) => 'options' in entry ? entry.options : [entry]);
|
|
311
|
+
if (!options.some((entry) => entry.value === value))
|
|
312
|
+
throw new AcpRuntimeError(`ACP configuration value is unavailable: ${key}`, 'acp_config_invalid');
|
|
313
|
+
if (option.currentValue === value)
|
|
314
|
+
continue;
|
|
315
|
+
signal.throwIfAborted();
|
|
316
|
+
try {
|
|
317
|
+
const updated = await session.connection.agent.request(methods.agent.session.setConfigOption, { sessionId: session.acpSessionId, configId: option.id, value }, { cancellationSignal: signal });
|
|
318
|
+
const confirmed = updated.configOptions.find((candidate) => candidate.id === option.id);
|
|
319
|
+
if (confirmed?.type !== 'select' || confirmed.currentValue !== value)
|
|
320
|
+
throw new AcpRuntimeError('Agent did not confirm the selected configuration', 'acp_config_unconfirmed');
|
|
321
|
+
session.configOptions = updated.configOptions;
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
// A rejected/unconfirmed mutation can have reached the Agent. Restore the
|
|
325
|
+
// previous real ID and require an acknowledgement before permitting retry.
|
|
326
|
+
let restored = false;
|
|
327
|
+
if (restoreOnFailure && !session.lost && !signal.aborted) {
|
|
328
|
+
try {
|
|
329
|
+
const rollback = await session.connection.agent.request(methods.agent.session.setConfigOption, { sessionId: session.acpSessionId, configId: option.id, value: option.currentValue }, { cancellationSignal: AbortSignal.timeout(5_000) });
|
|
330
|
+
const confirmed = rollback.configOptions.find((candidate) => candidate.id === option.id);
|
|
331
|
+
if (confirmed?.type === 'select' &&
|
|
332
|
+
confirmed.currentValue === option.currentValue &&
|
|
333
|
+
!session.lost) {
|
|
334
|
+
session.configOptions = rollback.configOptions;
|
|
335
|
+
restored = true;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
/* Uncertain configuration remains history-only. */
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (!restored)
|
|
343
|
+
await this.#lose(session);
|
|
344
|
+
throw error;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
#configureClient(session, app) {
|
|
349
|
+
app
|
|
350
|
+
.onNotification(methods.client.session.update, ({ params }) => {
|
|
351
|
+
if (params.sessionId === session.acpSessionId)
|
|
352
|
+
this.#acceptUpdate(session, params.update);
|
|
353
|
+
})
|
|
354
|
+
.onRequest(methods.client.fs.readTextFile, async ({ params }) => {
|
|
355
|
+
this.#assertSession(session, params.sessionId);
|
|
356
|
+
return {
|
|
357
|
+
content: await readWorkspaceTextFile(session.cwd, params.path, params.line, params.limit),
|
|
358
|
+
};
|
|
359
|
+
})
|
|
360
|
+
.onRequest(methods.client.fs.writeTextFile, async ({ params }) => {
|
|
361
|
+
this.#assertSession(session, params.sessionId);
|
|
362
|
+
await writeWorkspaceTextFile(session.cwd, params.path, params.content);
|
|
363
|
+
return {};
|
|
364
|
+
})
|
|
365
|
+
.onRequest(methods.client.session.requestPermission, ({ params }) => this.#requestPermission(session, params));
|
|
366
|
+
}
|
|
367
|
+
async #requestPermission(session, request) {
|
|
368
|
+
this.#assertSession(session, request.sessionId);
|
|
369
|
+
const active = session.active;
|
|
370
|
+
if (!active)
|
|
371
|
+
return { outcome: { outcome: 'cancelled' } };
|
|
372
|
+
const outcome = await active.context.requestPermission({
|
|
373
|
+
kind: this.#adapter.permissionKind?.(request) ?? 'permission',
|
|
374
|
+
toolCallId: request.toolCall.toolCallId,
|
|
375
|
+
title: request.toolCall.title || `${this.displayName} requests permission`,
|
|
376
|
+
options: request.options.map((option) => ({ optionId: option.optionId, name: option.name })),
|
|
377
|
+
});
|
|
378
|
+
return { outcome };
|
|
379
|
+
}
|
|
380
|
+
#acceptUpdate(session, update) {
|
|
381
|
+
if (update.sessionUpdate === 'config_option_update') {
|
|
382
|
+
// During our mutation, its response (or rollback response) is authoritative.
|
|
383
|
+
if (!session.configuring && !session.lost)
|
|
384
|
+
session.configOptions = update.configOptions;
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
const active = session.active;
|
|
388
|
+
if (!active)
|
|
389
|
+
return;
|
|
390
|
+
if (update.sessionUpdate === 'agent_message_chunk' && update.content.type === 'text') {
|
|
391
|
+
active.text += update.content.text;
|
|
392
|
+
emitText(active.context, 'output_delta', update.content.text);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
if (update.sessionUpdate === 'agent_thought_chunk' && update.content.type === 'text') {
|
|
396
|
+
emitText(active.context, 'thinking_delta', update.content.text);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
if (update.sessionUpdate === 'tool_call')
|
|
400
|
+
this.#acceptTool(active, update);
|
|
401
|
+
if (update.sessionUpdate === 'tool_call_update')
|
|
402
|
+
this.#acceptTool(active, update);
|
|
403
|
+
}
|
|
404
|
+
#acceptTool(active, update) {
|
|
405
|
+
const snapshot = active.tools.get(update.toolCallId) ?? {
|
|
406
|
+
id: update.toolCallId,
|
|
407
|
+
title: 'External tool',
|
|
408
|
+
content: [],
|
|
409
|
+
output: '',
|
|
410
|
+
outputText: '',
|
|
411
|
+
started: false,
|
|
412
|
+
terminal: false,
|
|
413
|
+
};
|
|
414
|
+
if ('title' in update && update.title != null)
|
|
415
|
+
snapshot.title = update.title;
|
|
416
|
+
if (update.name != null)
|
|
417
|
+
snapshot.name = update.name;
|
|
418
|
+
if (update.kind != null)
|
|
419
|
+
snapshot.kind = update.kind;
|
|
420
|
+
if (update.status != null)
|
|
421
|
+
snapshot.status = update.status;
|
|
422
|
+
if (update.content != null)
|
|
423
|
+
snapshot.content = [...update.content];
|
|
424
|
+
if (update.rawInput !== undefined)
|
|
425
|
+
snapshot.rawInput = update.rawInput;
|
|
426
|
+
if (update.rawOutput !== undefined)
|
|
427
|
+
snapshot.rawOutput = update.rawOutput;
|
|
428
|
+
active.tools.set(snapshot.id, snapshot);
|
|
429
|
+
if (!snapshot.started) {
|
|
430
|
+
active.context.emit({
|
|
431
|
+
type: 'tool_start',
|
|
432
|
+
toolCallId: snapshot.id,
|
|
433
|
+
name: toolName(snapshot.name ?? snapshot.kind ?? 'external_tool'),
|
|
434
|
+
displayName: boundedText(snapshot.title),
|
|
435
|
+
input: snapshot.rawInput ?? {},
|
|
436
|
+
activityKind: activityKind(snapshot.kind),
|
|
437
|
+
});
|
|
438
|
+
snapshot.started = true;
|
|
439
|
+
}
|
|
440
|
+
if (snapshot.status !== 'completed' && snapshot.status !== 'failed') {
|
|
441
|
+
const output = summarizeToolContent(snapshot.content);
|
|
442
|
+
if (output.startsWith(snapshot.output) && output.length > snapshot.output.length) {
|
|
443
|
+
emitToolOutput(active.context, snapshot.id, output.slice(snapshot.output.length));
|
|
444
|
+
snapshot.output = output;
|
|
445
|
+
snapshot.outputText = toolTextContent(snapshot.content);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
if (!snapshot.terminal && (snapshot.status === 'completed' || snapshot.status === 'failed')) {
|
|
449
|
+
if (snapshot.content.some((item) => item.type === 'diff')) {
|
|
450
|
+
const text = toolTextContent(snapshot.content);
|
|
451
|
+
const remaining = text.startsWith(snapshot.outputText)
|
|
452
|
+
? text.slice(snapshot.outputText.length)
|
|
453
|
+
: text;
|
|
454
|
+
if (remaining)
|
|
455
|
+
emitToolOutput(active.context, snapshot.id, remaining);
|
|
456
|
+
}
|
|
457
|
+
active.context.emit({
|
|
458
|
+
type: 'tool_result',
|
|
459
|
+
toolCallId: snapshot.id,
|
|
460
|
+
content: projectToolResult(snapshot.content, snapshot.rawOutput),
|
|
461
|
+
...(snapshot.status === 'failed' ? { isError: true } : {}),
|
|
462
|
+
});
|
|
463
|
+
snapshot.terminal = true;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
async #awaitPrompt(session, prompt, signal) {
|
|
467
|
+
const running = Promise.race([prompt, session.owner.failed]);
|
|
468
|
+
if (!signal.aborted) {
|
|
469
|
+
let onAbort;
|
|
470
|
+
const aborted = new Promise((resolveAbort) => {
|
|
471
|
+
onAbort = resolveAbort;
|
|
472
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
473
|
+
});
|
|
474
|
+
try {
|
|
475
|
+
await Promise.race([running.then(() => undefined), aborted]);
|
|
476
|
+
}
|
|
477
|
+
finally {
|
|
478
|
+
signal.removeEventListener('abort', onAbort);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (!signal.aborted)
|
|
482
|
+
return await running;
|
|
483
|
+
let timeout;
|
|
484
|
+
// Cancellation notification and settlement share one deadline; a blocked stdin cannot hang stop.
|
|
485
|
+
const result = await Promise.race([
|
|
486
|
+
(async () => {
|
|
487
|
+
await session.connection?.agent
|
|
488
|
+
.notify(methods.agent.session.cancel, { sessionId: session.acpSessionId })
|
|
489
|
+
.catch(() => undefined);
|
|
490
|
+
return { response: await running };
|
|
491
|
+
})(),
|
|
492
|
+
new Promise((resolveTimeout) => {
|
|
493
|
+
timeout = setTimeout(() => resolveTimeout(undefined), CANCEL_TIMEOUT_MS);
|
|
494
|
+
}),
|
|
495
|
+
]).finally(() => clearTimeout(timeout));
|
|
496
|
+
if (!result)
|
|
497
|
+
await this.#lose(session);
|
|
498
|
+
return result?.response;
|
|
499
|
+
}
|
|
500
|
+
#assertSession(session, sessionId) {
|
|
501
|
+
if (!session.acpSessionId || session.acpSessionId !== sessionId)
|
|
502
|
+
throw new Error('Unknown ACP Session');
|
|
503
|
+
}
|
|
504
|
+
async #lose(session) {
|
|
505
|
+
if (!session.loss) {
|
|
506
|
+
session.lost = true;
|
|
507
|
+
session.loss = this.#disposeSession(session);
|
|
508
|
+
}
|
|
509
|
+
await session.loss;
|
|
510
|
+
}
|
|
511
|
+
async #disposeSession(session) {
|
|
512
|
+
const owner = session.owner;
|
|
513
|
+
session.owner = undefined;
|
|
514
|
+
session.connection = undefined;
|
|
515
|
+
if (owner)
|
|
516
|
+
await owner.dispose();
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
function toolTextContent(content) {
|
|
520
|
+
return content
|
|
521
|
+
.flatMap((item) => item.type === 'content' && item.content.type === 'text' ? [item.content.text] : [])
|
|
522
|
+
.join('\n');
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Shared ACP registration surface.
|
|
526
|
+
*
|
|
527
|
+
* Installed Plugin packages are self-contained bundles, so this class must not
|
|
528
|
+
* rely on `instanceof Service` across package generations. The adapter passes
|
|
529
|
+
* its own Context explicitly; that preserves the child Entry identity used by
|
|
530
|
+
* PluginExecutorService even when the ACP runtime was loaded from another
|
|
531
|
+
* immutable package generation.
|
|
532
|
+
*/
|
|
533
|
+
export class AcpRuntimeService {
|
|
534
|
+
constructor(ctx) {
|
|
535
|
+
ctx.provide('acp', this);
|
|
536
|
+
}
|
|
537
|
+
register(consumer, adapter, config) {
|
|
538
|
+
const storage = consumer.get('storage');
|
|
539
|
+
if (!storage)
|
|
540
|
+
throw new Error('ACP continuity storage is unavailable');
|
|
541
|
+
const provider = new AcpExecutor(adapter, config, {
|
|
542
|
+
state: pluginStateStore(storage, adapter.id),
|
|
543
|
+
});
|
|
544
|
+
consumer.effect(() => () => provider.dispose(), `acp.dispose(${JSON.stringify(adapter.id)})`);
|
|
545
|
+
return consumer.executors.register(provider);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
function pluginStateStore(storage, executorId) {
|
|
549
|
+
const key = (conversationKey) => `acp/${executorId}/${createHash('sha256').update(conversationKey).digest('hex')}`;
|
|
550
|
+
return {
|
|
551
|
+
async has(conversationKey) {
|
|
552
|
+
const value = (await storage.get(key(conversationKey)))
|
|
553
|
+
.value;
|
|
554
|
+
return value?.version === 1;
|
|
555
|
+
},
|
|
556
|
+
async mark(conversationKey, cwd) {
|
|
557
|
+
await storage.set(key(conversationKey), { version: 1, cwd });
|
|
558
|
+
},
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
function validateAdapter(adapter) {
|
|
562
|
+
if (!adapter ||
|
|
563
|
+
typeof adapter !== 'object' ||
|
|
564
|
+
typeof adapter.id !== 'string' ||
|
|
565
|
+
!/^[a-z][a-z0-9]*(?:[-_.][a-z0-9]+)*$/u.test(adapter.id) ||
|
|
566
|
+
typeof adapter.displayName !== 'string' ||
|
|
567
|
+
!adapter.displayName.trim() ||
|
|
568
|
+
(adapter.clientName !== undefined &&
|
|
569
|
+
(typeof adapter.clientName !== 'string' || !adapter.clientName.trim())) ||
|
|
570
|
+
typeof adapter.configure !== 'function') {
|
|
571
|
+
throw new TypeError('Invalid ACP Agent adapter');
|
|
572
|
+
}
|
|
573
|
+
return adapter;
|
|
574
|
+
}
|
|
575
|
+
function validateConfiguredAgent(value) {
|
|
576
|
+
if (!value || typeof value !== 'object' || !value.launch || typeof value.launch !== 'object')
|
|
577
|
+
throw new TypeError('Invalid ACP Agent configuration');
|
|
578
|
+
if (value.launch.args !== undefined && !Array.isArray(value.launch.args))
|
|
579
|
+
throw new TypeError('ACP launch arguments are invalid');
|
|
580
|
+
if (value.launch.requiredExecutables !== undefined &&
|
|
581
|
+
!Array.isArray(value.launch.requiredExecutables))
|
|
582
|
+
throw new TypeError('ACP required executables are invalid');
|
|
583
|
+
const executable = absolutePath(value.launch.executable, 'executable');
|
|
584
|
+
const args = value.launch.args?.map((argument) => {
|
|
585
|
+
if (typeof argument !== 'string' || /[\0\r\n]/u.test(argument))
|
|
586
|
+
throw new TypeError('ACP launch argument is invalid');
|
|
587
|
+
return argument;
|
|
588
|
+
});
|
|
589
|
+
const requiredExecutables = value.launch.requiredExecutables?.map((path) => absolutePath(path, 'required executable'));
|
|
590
|
+
const cwd = value.launch.cwd === undefined ? undefined : absolutePath(value.launch.cwd, 'cwd');
|
|
591
|
+
const initialConfig = validateInitialConfig(value.launch.initialConfig);
|
|
592
|
+
return Object.freeze({
|
|
593
|
+
launch: Object.freeze({
|
|
594
|
+
...value.launch,
|
|
595
|
+
executable,
|
|
596
|
+
...(args ? { args: Object.freeze(args) } : {}),
|
|
597
|
+
...(requiredExecutables ? { requiredExecutables: Object.freeze(requiredExecutables) } : {}),
|
|
598
|
+
...(cwd ? { cwd } : {}),
|
|
599
|
+
...(value.launch.env ? { env: Object.freeze({ ...value.launch.env }) } : {}),
|
|
600
|
+
...(initialConfig ? { initialConfig } : {}),
|
|
601
|
+
}),
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
function validateInitialConfig(value) {
|
|
605
|
+
if (value === undefined)
|
|
606
|
+
return undefined;
|
|
607
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
608
|
+
throw new TypeError('ACP initial configuration is invalid');
|
|
609
|
+
const entries = Object.entries(value);
|
|
610
|
+
if (entries.length > 64 ||
|
|
611
|
+
entries.some(([key, item]) => !/^[a-z][a-z0-9]*(?:[-_.][a-z0-9]+)*$/u.test(key) ||
|
|
612
|
+
typeof item !== 'string' ||
|
|
613
|
+
/[\0\r\n]/u.test(item)))
|
|
614
|
+
throw new TypeError('ACP initial configuration is invalid');
|
|
615
|
+
return Object.freeze(Object.fromEntries(entries));
|
|
616
|
+
}
|
|
617
|
+
function absolutePath(value, label) {
|
|
618
|
+
if (typeof value !== 'string' || !isAbsolute(value) || /[\0\r\n]/u.test(value))
|
|
619
|
+
throw new TypeError(`ACP ${label} must be an absolute path`);
|
|
620
|
+
return resolve(value);
|
|
621
|
+
}
|
|
622
|
+
async function checkedExecutable(path) {
|
|
623
|
+
const resolved = await realpath(path);
|
|
624
|
+
if (!(await stat(resolved)).isFile())
|
|
625
|
+
throw new Error('ACP executable is not a file');
|
|
626
|
+
await access(resolved, constants.X_OK);
|
|
627
|
+
return resolved;
|
|
628
|
+
}
|
|
629
|
+
function failure(message, code, recoverable = false) {
|
|
630
|
+
return { status: 'failed', message, code, recoverable };
|
|
631
|
+
}
|
|
632
|
+
function errorCode(error) {
|
|
633
|
+
if (error instanceof AcpRuntimeError)
|
|
634
|
+
return error.code;
|
|
635
|
+
if (error instanceof DOMException && error.name === 'TimeoutError')
|
|
636
|
+
return 'acp_initialize_timed_out';
|
|
637
|
+
return 'acp_execution_failed';
|
|
638
|
+
}
|
|
639
|
+
function safeErrorMessage(error) {
|
|
640
|
+
if (error instanceof AcpRuntimeError)
|
|
641
|
+
return error.message;
|
|
642
|
+
return 'ACP execution failed';
|
|
643
|
+
}
|
|
644
|
+
const host = Object.freeze({
|
|
645
|
+
apply(ctx) {
|
|
646
|
+
new AcpRuntimeService(ctx);
|
|
647
|
+
},
|
|
648
|
+
});
|
|
649
|
+
export default Object.freeze({
|
|
650
|
+
packageId: 'acp-executor',
|
|
651
|
+
contributions: Object.freeze([{ id: 'acp', kind: 'service' }]),
|
|
652
|
+
host,
|
|
653
|
+
});
|