maka-agent 0.2.0-dev.45.20260921 → 0.2.0-dev.47.20260922
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/DISCLAIMER-WIP +6 -1
- package/README.md +6 -1
- package/dist/acp/maka-acp-agent.js +1 -1
- package/dist/acp/session-mcp.js +212 -0
- package/dist/acp/session-registry.js +52 -8
- package/dist/acp/stdio-server.js +3 -0
- package/dist/cli-core.js +1 -1
- package/dist/mcp-capability-provider.js +6 -1
- package/dist/mcp-capability-publication.js +139 -0
- package/dist/pi-tui-layout.js +46 -17
- package/dist/pi-tui-pickers.js +25 -14
- package/dist/pi-tui-runner.js +24 -15
- package/dist/runtime-host-service-manager.js +26 -2
- package/dist/tui-context-refresh.js +15 -26
- package/dist/tui-mcp-control.js +29 -88
- package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
- package/node_modules/@maka/core/dist/client-capability-grant.js +2 -2
- package/node_modules/@maka/core/dist/model-thinking.js +36 -1
- package/node_modules/@maka/core/dist/provider-registry.js +0 -1
- package/node_modules/@maka/core/dist/refresh-read-coordinator.js +62 -0
- package/node_modules/@maka/core/dist/runtime-policy/connection-catalog-codec.js +4 -0
- package/node_modules/@maka/core/dist/session.js +17 -1
- package/node_modules/@maka/core/dist/settings.js +2 -0
- package/node_modules/@maka/core/package.json +1 -0
- package/node_modules/@maka/runtime/dist/ai-sdk-message-projection.js +15 -60
- package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +18 -0
- package/node_modules/@maka/runtime/dist/apply-patch-profile.js +64 -14
- package/node_modules/@maka/runtime/dist/builtin-tools.js +3 -2
- package/node_modules/@maka/runtime/dist/codex-v4a-patch.js +18 -0
- package/node_modules/@maka/runtime/dist/model-adapter.js +80 -3
- package/node_modules/@maka/runtime/dist/model-runtime.js +6 -0
- package/node_modules/@maka/runtime/dist/openai-apply-patch.js +5 -0
- package/node_modules/@maka/runtime/dist/plugin-executor-backend.js +6 -0
- package/node_modules/@maka/runtime/dist/session-manager.js +15 -2
- package/node_modules/@maka/runtime-host/dist/client/capability-provider-service.js +2 -0
- package/node_modules/@maka/runtime-host/dist/client/client-capability-channel.js +59 -20
- package/node_modules/@maka/runtime-host/dist/client/connection.js +6 -4
- package/node_modules/@maka/runtime-host/dist/client/reconnecting-connection.js +4 -4
- package/node_modules/@maka/runtime-host/dist/protocol/client-capability.js +20 -3
- package/node_modules/@maka/runtime-host/dist/protocol/external-session.js +8 -5
- package/node_modules/@maka/runtime-host/dist/protocol/index.js +11 -1
- package/node_modules/@maka/runtime-host/dist/protocol/session-catalog.js +35 -3
- package/node_modules/@maka/runtime-host/dist/protocol/workhub-coordination.js +1 -5
- package/node_modules/@maka/runtime-host/dist/server/client-capability-coordinator.js +234 -70
- package/node_modules/@maka/runtime-host/dist/server/client-capability-invocation-broker.js +14 -8
- package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +29 -2
- package/node_modules/@maka/runtime-host/dist/server/execution-model-authority.js +3 -0
- package/node_modules/@maka/runtime-host/dist/server/external-session-coordinator.js +29 -7
- package/node_modules/@maka/runtime-host/dist/server/hosted-execution-tool-profile.js +1 -1
- package/node_modules/@maka/runtime-host/dist/server/session-bundle-coordinator.js +4 -1
- package/node_modules/@maka/runtime-host/dist/server/session-catalog-coordinator.js +32 -4
- package/node_modules/@maka/runtime-host/dist/server/session-retirement-coordinator.js +2 -2
- package/package.json +1 -1
package/DISCLAIMER-WIP
CHANGED
|
@@ -11,7 +11,12 @@ policy. For example, releases may have incomplete or un-reviewed licensing
|
|
|
11
11
|
conditions. What follows is a list of issues the project is currently aware of
|
|
12
12
|
(this list is likely to be incomplete):
|
|
13
13
|
|
|
14
|
-
* The
|
|
14
|
+
* The initial-code authorization review remains open at
|
|
15
|
+
https://github.com/apache/maka/issues/3268. Since RC1, the ASF has recorded
|
|
16
|
+
the ICLA of contributor Mingqwqqaq, and the Deep Research implementation
|
|
17
|
+
has been removed. The review still needs to record the authorization basis
|
|
18
|
+
for the retained initial code; these changes do not establish that an SGA
|
|
19
|
+
has been filed.
|
|
15
20
|
|
|
16
21
|
If you are planning to incorporate this work into your product/project, please
|
|
17
22
|
be aware that you will need to conduct a thorough licensing review to determine
|
package/README.md
CHANGED
|
@@ -47,7 +47,12 @@ policy. For example, releases may have incomplete or un-reviewed licensing
|
|
|
47
47
|
conditions. What follows is a list of issues the project is currently aware of
|
|
48
48
|
(this list is likely to be incomplete):
|
|
49
49
|
|
|
50
|
-
* The
|
|
50
|
+
* The initial-code authorization review remains open at
|
|
51
|
+
https://github.com/apache/maka/issues/3268. Since RC1, the ASF has recorded
|
|
52
|
+
the ICLA of contributor Mingqwqqaq, and the Deep Research implementation
|
|
53
|
+
has been removed. The review still needs to record the authorization basis
|
|
54
|
+
for the retained initial code; these changes do not establish that an SGA
|
|
55
|
+
has been filed.
|
|
51
56
|
|
|
52
57
|
If you are planning to incorporate this work into your product/project, please
|
|
53
58
|
be aware that you will need to conduct a thorough licensing review to determine
|
|
@@ -29,7 +29,7 @@ export function createMakaAcpAgent(options) {
|
|
|
29
29
|
agentInfo: { name: 'maka', title: 'Maka', version: options.version },
|
|
30
30
|
};
|
|
31
31
|
})
|
|
32
|
-
.onRequest(methods.agent.session.new, ({ params }) => options.sessionRegistry.create(params))
|
|
32
|
+
.onRequest(methods.agent.session.new, ({ params, signal }) => options.sessionRegistry.create(params, signal))
|
|
33
33
|
.onRequest(methods.agent.session.list, ({ params }) => options.sessionRegistry.list(params))
|
|
34
34
|
.onRequest(methods.agent.session.setConfigOption, ({ params }) => options.sessionRegistry.setConfigOption(params))
|
|
35
35
|
.onRequest(methods.agent.session.prompt, ({ params, signal, client }) => options.sessionRegistry.prompt(params, {
|
|
@@ -0,0 +1,212 @@
|
|
|
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 { isAbsolute } from 'node:path';
|
|
20
|
+
import { RequestError } from '@agentclientprotocol/sdk';
|
|
21
|
+
import { MCP_CONFIG_VERSION } from '@maka/core/mcp';
|
|
22
|
+
import { McpClientManager } from '@maka/mcp';
|
|
23
|
+
import { normalizeMcpConfig } from '@maka/storage/mcp-config-store';
|
|
24
|
+
import { abortable } from '@maka/runtime-host/client';
|
|
25
|
+
import { createMcpCapabilityProvider } from '../mcp-capability-provider.js';
|
|
26
|
+
import { McpCapabilityPublication } from '../mcp-capability-publication.js';
|
|
27
|
+
/** Validates before any process or Host work; never writes a user MCP configuration. */
|
|
28
|
+
export function createAcpMcpConfig(params) {
|
|
29
|
+
const servers = Object.create(null);
|
|
30
|
+
if (!Array.isArray(params.mcpServers))
|
|
31
|
+
throw invalidMcpInput('invalid_servers');
|
|
32
|
+
for (const server of params.mcpServers) {
|
|
33
|
+
if (!server || typeof server !== 'object' || 'type' in server || !('command' in server)) {
|
|
34
|
+
throw invalidMcpInput('unsupported_transport');
|
|
35
|
+
}
|
|
36
|
+
if (typeof server.name !== 'string' || Object.hasOwn(servers, server.name)) {
|
|
37
|
+
throw invalidMcpInput('duplicate_or_invalid_name');
|
|
38
|
+
}
|
|
39
|
+
if (typeof server.command !== 'string' ||
|
|
40
|
+
!isAbsolute(server.command) ||
|
|
41
|
+
server.command.includes('\0')) {
|
|
42
|
+
throw invalidMcpInput('command_must_be_absolute');
|
|
43
|
+
}
|
|
44
|
+
if (!Array.isArray(server.args) ||
|
|
45
|
+
server.args.some((arg) => typeof arg !== 'string' || arg.includes('\0'))) {
|
|
46
|
+
throw invalidMcpInput('invalid_arguments');
|
|
47
|
+
}
|
|
48
|
+
if (!Array.isArray(server.env))
|
|
49
|
+
throw invalidMcpInput('invalid_environment');
|
|
50
|
+
const env = Object.create(null);
|
|
51
|
+
for (const variable of server.env) {
|
|
52
|
+
if (!variable ||
|
|
53
|
+
typeof variable.name !== 'string' ||
|
|
54
|
+
variable.name.length === 0 ||
|
|
55
|
+
variable.name.includes('\0') ||
|
|
56
|
+
variable.name.includes('=') ||
|
|
57
|
+
Object.hasOwn(env, variable.name) ||
|
|
58
|
+
typeof variable.value !== 'string' ||
|
|
59
|
+
variable.value.includes('\0')) {
|
|
60
|
+
throw invalidMcpInput('duplicate_or_invalid_environment');
|
|
61
|
+
}
|
|
62
|
+
env[variable.name] = variable.value;
|
|
63
|
+
}
|
|
64
|
+
servers[server.name] = {
|
|
65
|
+
command: server.command,
|
|
66
|
+
args: server.args,
|
|
67
|
+
env,
|
|
68
|
+
cwd: params.cwd,
|
|
69
|
+
protocol: 'auto',
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
return normalizeMcpConfig({ version: MCP_CONFIG_VERSION, mcpServers: servers });
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
throw invalidMcpInput('invalid_configuration');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** Owns one Session's in-memory MCP processes and publication on the shared Host connection. */
|
|
80
|
+
export class AcpSessionMcp {
|
|
81
|
+
#sessionId;
|
|
82
|
+
#config;
|
|
83
|
+
#manager = new McpClientManager({
|
|
84
|
+
clientName: 'maka-acp',
|
|
85
|
+
excludedStdioEnvironmentKeys: ['MAKA_RUNTIME_HOST_ACCESS_CREDENTIAL'],
|
|
86
|
+
});
|
|
87
|
+
#publication;
|
|
88
|
+
#unsubscribeManager;
|
|
89
|
+
#unsubscribeConnection;
|
|
90
|
+
#availability;
|
|
91
|
+
#prepared = false;
|
|
92
|
+
#closed = false;
|
|
93
|
+
#authoritativelyRetired = false;
|
|
94
|
+
#closeTask;
|
|
95
|
+
constructor(sessionId, config, connection) {
|
|
96
|
+
this.#sessionId = sessionId;
|
|
97
|
+
this.#config = config;
|
|
98
|
+
this.#publication = new McpCapabilityPublication({
|
|
99
|
+
connectionIdentity: () => this.#availability?.kind === 'connected'
|
|
100
|
+
? this.#availability.hostEpoch + '\0' + this.#availability.connectionId
|
|
101
|
+
: undefined,
|
|
102
|
+
revision: () => this.#manager.toolSnapshot().revision,
|
|
103
|
+
createProvider: () => createMcpCapabilityProvider(this.#manager, {
|
|
104
|
+
admission: 'mcp',
|
|
105
|
+
onCurrentRegistrationRetired: () => this.#retire(),
|
|
106
|
+
}) ?? {
|
|
107
|
+
// Empty is still an authoritative Session snapshot. Publish it on a
|
|
108
|
+
// new connection to clear lost Host contracts, and retain retirement
|
|
109
|
+
// notification even while every configured server has no tools.
|
|
110
|
+
offers: () => [],
|
|
111
|
+
currentRegistrationRetired: () => this.#retire(),
|
|
112
|
+
},
|
|
113
|
+
replace: (provider) => connection.replaceClientCapabilities(provider, { sessionId }),
|
|
114
|
+
unregister: () => connection.unregisterClientCapabilities({ sessionId }),
|
|
115
|
+
onState: () => undefined,
|
|
116
|
+
});
|
|
117
|
+
this.#unsubscribeManager = this.#manager.onChange(() => {
|
|
118
|
+
if (this.#prepared && !this.#closed)
|
|
119
|
+
this.#publication.request();
|
|
120
|
+
});
|
|
121
|
+
this.#unsubscribeConnection = connection.subscribeConnectionAvailability((availability) => {
|
|
122
|
+
this.#availability = availability;
|
|
123
|
+
if (availability.kind !== 'connected')
|
|
124
|
+
this.#publication.invalidate();
|
|
125
|
+
if (this.#prepared && !this.#closed)
|
|
126
|
+
this.#publication.request();
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
async prepare(signal) {
|
|
130
|
+
signal?.throwIfAborted();
|
|
131
|
+
const cancel = () => {
|
|
132
|
+
void this.close().catch(() => undefined);
|
|
133
|
+
};
|
|
134
|
+
signal?.addEventListener('abort', cancel, { once: true });
|
|
135
|
+
try {
|
|
136
|
+
await this.#manager.sync(this.#config);
|
|
137
|
+
signal?.throwIfAborted();
|
|
138
|
+
this.#assertConnected();
|
|
139
|
+
this.#prepared = true;
|
|
140
|
+
await this.ready(signal);
|
|
141
|
+
this.#assertConnected();
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
await this.close().catch(() => undefined);
|
|
145
|
+
if (error instanceof RequestError)
|
|
146
|
+
throw error;
|
|
147
|
+
throw mcpUnavailable(this.#sessionId, 'mcp.prepare', 'mcp_preparation_failed');
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
signal?.removeEventListener('abort', cancel);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async ready(signal) {
|
|
154
|
+
signal?.throwIfAborted();
|
|
155
|
+
this.#assertOpen('mcp.ready');
|
|
156
|
+
const state = await abortable(() => this.#publication.settle(), signal);
|
|
157
|
+
signal?.throwIfAborted();
|
|
158
|
+
this.#assertOpen('mcp.ready');
|
|
159
|
+
if (state !== 'published' && state !== 'not_published') {
|
|
160
|
+
throw mcpUnavailable(this.#sessionId, 'mcp.ready', 'mcp_publication_failed');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
close() {
|
|
164
|
+
return this.#close(true);
|
|
165
|
+
}
|
|
166
|
+
#retire() {
|
|
167
|
+
this.#authoritativelyRetired = true;
|
|
168
|
+
return this.#close(false);
|
|
169
|
+
}
|
|
170
|
+
#close(unregister) {
|
|
171
|
+
if (this.#closeTask)
|
|
172
|
+
return this.#closeTask;
|
|
173
|
+
this.#closed = true;
|
|
174
|
+
this.#unsubscribeManager();
|
|
175
|
+
this.#unsubscribeConnection();
|
|
176
|
+
const managerClose = this.#manager.close();
|
|
177
|
+
this.#closeTask = (async () => {
|
|
178
|
+
try {
|
|
179
|
+
try {
|
|
180
|
+
await (unregister ? this.#publication.close() : this.#publication.retire());
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
// Session retirement is authoritative. If it wins the race with a local
|
|
184
|
+
// unregister, the obsolete unregister may reject even though withdrawal
|
|
185
|
+
// has already completed on the Host.
|
|
186
|
+
if (!this.#authoritativelyRetired)
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
finally {
|
|
191
|
+
await managerClose;
|
|
192
|
+
}
|
|
193
|
+
})();
|
|
194
|
+
return this.#closeTask;
|
|
195
|
+
}
|
|
196
|
+
#assertConnected() {
|
|
197
|
+
this.#assertOpen('mcp.prepare');
|
|
198
|
+
if (Object.keys(this.#config.mcpServers).some((serverId) => this.#manager.status(serverId)?.state !== 'connected')) {
|
|
199
|
+
throw mcpUnavailable(this.#sessionId, 'mcp.prepare', 'mcp_not_ready');
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
#assertOpen(operation) {
|
|
203
|
+
if (this.#closed)
|
|
204
|
+
throw mcpUnavailable(this.#sessionId, operation, 'mcp_not_ready');
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function invalidMcpInput(reason) {
|
|
208
|
+
return RequestError.invalidParams({ field: 'mcpServers', reason }, 'Invalid ACP stdio MCP configuration');
|
|
209
|
+
}
|
|
210
|
+
function mcpUnavailable(sessionId, operation, code) {
|
|
211
|
+
return RequestError.internalError({ source: 'adapter', operation, sessionId, code }, 'Session MCP tools are unavailable');
|
|
212
|
+
}
|
|
@@ -28,6 +28,7 @@ import { RuntimeHostSessionUpdateError, getRuntimeHostSession, requireRuntimeHos
|
|
|
28
28
|
import { AcpSessionConfigInputError, createAcpSessionConfigPatch, projectAcpSessionConfigOptions, validateAcpSessionConfigOptionRequest, } from './session-configuration.js';
|
|
29
29
|
import { AcpSessionEventMapper } from './session-event-mapper.js';
|
|
30
30
|
import { mapAcpPromptContent, publishAcpPromptAttachments } from './prompt-content.js';
|
|
31
|
+
import { AcpSessionMcp, createAcpMcpConfig } from './session-mcp.js';
|
|
31
32
|
import { AcpSessionInteractions } from './session-interactions.js';
|
|
32
33
|
const ACP_SESSION_CURSOR_MAX_BYTES = 8 * 1024;
|
|
33
34
|
const ADMISSION_QUERY_MAX_ATTEMPTS = 5;
|
|
@@ -40,6 +41,8 @@ export class AcpSessionRegistry {
|
|
|
40
41
|
#newTurnId;
|
|
41
42
|
#inFlightOperations = new Set();
|
|
42
43
|
#ownedSessionIds = new Set();
|
|
44
|
+
#mcps = new Map();
|
|
45
|
+
#creationAbort = new AbortController();
|
|
43
46
|
#attachmentInteractions = new Map();
|
|
44
47
|
#attachments = new Map();
|
|
45
48
|
#attachmentOpenControllers = new Map();
|
|
@@ -58,10 +61,11 @@ export class AcpSessionRegistry {
|
|
|
58
61
|
this.#newSessionId = options.newSessionId ?? randomUUID;
|
|
59
62
|
this.#newTurnId = options.newTurnId ?? randomUUID;
|
|
60
63
|
}
|
|
61
|
-
async create(params) {
|
|
64
|
+
async create(params, signal) {
|
|
62
65
|
this.#assertOpen('session.create');
|
|
63
66
|
validateNewSessionParams(params);
|
|
64
|
-
|
|
67
|
+
const mcpConfig = createAcpMcpConfig(params);
|
|
68
|
+
return this.#track(this.#create(params, mcpConfig, signal));
|
|
65
69
|
}
|
|
66
70
|
async list(params) {
|
|
67
71
|
this.#assertOpen('session.catalog.query');
|
|
@@ -130,6 +134,7 @@ export class AcpSessionRegistry {
|
|
|
130
134
|
dispose() {
|
|
131
135
|
this.#closing = true;
|
|
132
136
|
this.#connectAbortController?.abort();
|
|
137
|
+
this.#creationAbort.abort();
|
|
133
138
|
this.#disposeTask ??= this.#dispose();
|
|
134
139
|
return this.#disposeTask;
|
|
135
140
|
}
|
|
@@ -214,6 +219,9 @@ export class AcpSessionRegistry {
|
|
|
214
219
|
throw error;
|
|
215
220
|
throw requestErrorFromRuntimeHost(error, 'artifact.ingest');
|
|
216
221
|
}
|
|
222
|
+
if (active.cancelled)
|
|
223
|
+
return { stopReason: await this.#cancelledStopReason(active) };
|
|
224
|
+
await this.#mcps.get(params.sessionId)?.ready(active.projectionAbort.signal);
|
|
217
225
|
if (active.cancelled)
|
|
218
226
|
return { stopReason: await this.#cancelledStopReason(active) };
|
|
219
227
|
active.transcript = attachment.trackPromptTranscript(turnId);
|
|
@@ -591,8 +599,8 @@ export class AcpSessionRegistry {
|
|
|
591
599
|
onInteractionPending: (pending) => {
|
|
592
600
|
if (!interactions.fencesTurn(pending.turnId) &&
|
|
593
601
|
![...(this.#activePrompts.get(sessionId) ?? [])].some((active) => active.turnId === pending.turnId && active.dispatchStarted)) {
|
|
594
|
-
//
|
|
595
|
-
//
|
|
602
|
+
// An idle attachment can observe another client's Turn; it does not
|
|
603
|
+
// transfer that Turn's interaction authority to this ACP client.
|
|
596
604
|
return;
|
|
597
605
|
}
|
|
598
606
|
void interactions.pending(pending);
|
|
@@ -697,6 +705,14 @@ export class AcpSessionRegistry {
|
|
|
697
705
|
closeError = error;
|
|
698
706
|
}
|
|
699
707
|
}
|
|
708
|
+
const mcp = this.#mcps.get(sessionId);
|
|
709
|
+
this.#mcps.delete(sessionId);
|
|
710
|
+
try {
|
|
711
|
+
await mcp?.close();
|
|
712
|
+
}
|
|
713
|
+
catch (error) {
|
|
714
|
+
closeError ??= error;
|
|
715
|
+
}
|
|
700
716
|
await delivery;
|
|
701
717
|
const failedCancellation = cancellation.find((result) => result.status === 'rejected');
|
|
702
718
|
if (failedCancellation)
|
|
@@ -745,11 +761,25 @@ export class AcpSessionRegistry {
|
|
|
745
761
|
if (!this.#ownedSessionIds.has(sessionId))
|
|
746
762
|
throw unknownSessionError();
|
|
747
763
|
}
|
|
748
|
-
async #create(params) {
|
|
764
|
+
async #create(params, mcpConfig, signal) {
|
|
765
|
+
const lifetime = signal
|
|
766
|
+
? AbortSignal.any([signal, this.#creationAbort.signal])
|
|
767
|
+
: this.#creationAbort.signal;
|
|
768
|
+
lifetime.throwIfAborted();
|
|
749
769
|
const connection = await this.#getConnection('session.create');
|
|
750
770
|
const sessionId = this.#newSessionId();
|
|
771
|
+
let mcp;
|
|
772
|
+
if (params.mcpServers.length > 0) {
|
|
773
|
+
mcp = new AcpSessionMcp(sessionId, mcpConfig, connection);
|
|
774
|
+
this.#mcps.set(sessionId, mcp);
|
|
775
|
+
}
|
|
751
776
|
let result;
|
|
777
|
+
let dispatched = false;
|
|
752
778
|
try {
|
|
779
|
+
await mcp?.prepare(lifetime);
|
|
780
|
+
lifetime.throwIfAborted();
|
|
781
|
+
this.#assertOpen('session.create');
|
|
782
|
+
dispatched = true;
|
|
753
783
|
result = await connection.request('session.create', {
|
|
754
784
|
sessionId,
|
|
755
785
|
workspace: { kind: 'host_path', path: params.cwd },
|
|
@@ -757,6 +787,20 @@ export class AcpSessionRegistry {
|
|
|
757
787
|
});
|
|
758
788
|
}
|
|
759
789
|
catch (error) {
|
|
790
|
+
const outcomeUnknown = dispatched &&
|
|
791
|
+
error instanceof RuntimeHostRequestInterruptedError &&
|
|
792
|
+
error.dispatch === 'dispatched';
|
|
793
|
+
if (outcomeUnknown && !this.#closing) {
|
|
794
|
+
// The error returns this ID. Keep its connection-local reservation usable
|
|
795
|
+
// without guessing whether Host committed or resending Session creation.
|
|
796
|
+
this.#ownedSessionIds.add(sessionId);
|
|
797
|
+
}
|
|
798
|
+
else {
|
|
799
|
+
this.#mcps.delete(sessionId);
|
|
800
|
+
await mcp?.close().catch(() => undefined);
|
|
801
|
+
}
|
|
802
|
+
if (error instanceof RequestError)
|
|
803
|
+
throw error;
|
|
760
804
|
throw requestErrorFromRuntimeHost(error, 'session.create', { sessionId });
|
|
761
805
|
}
|
|
762
806
|
// Session creation has committed. Optional presentation failures must not
|
|
@@ -911,6 +955,9 @@ export class AcpSessionRegistry {
|
|
|
911
955
|
}
|
|
912
956
|
}
|
|
913
957
|
await Promise.allSettled(cancellations);
|
|
958
|
+
const mcps = [...this.#mcps.values()];
|
|
959
|
+
this.#mcps.clear();
|
|
960
|
+
await Promise.allSettled(mcps.map((mcp) => mcp.close()));
|
|
914
961
|
await Promise.allSettled([this.#closeOwnedConnection()]);
|
|
915
962
|
await Promise.allSettled([
|
|
916
963
|
...this.#inFlightOperations,
|
|
@@ -995,9 +1042,6 @@ function registryClosedError(operation) {
|
|
|
995
1042
|
}
|
|
996
1043
|
function validateNewSessionParams(params) {
|
|
997
1044
|
assertBoundedAbsoluteCwd(params.cwd);
|
|
998
|
-
if (params.mcpServers.length > 0) {
|
|
999
|
-
throw RequestError.invalidParams({ field: 'mcpServers', reason: 'unsupported' }, 'MCP servers are not supported by this ACP adapter yet');
|
|
1000
|
-
}
|
|
1001
1045
|
if ((params.additionalDirectories?.length ?? 0) > 0) {
|
|
1002
1046
|
throw RequestError.invalidParams({ field: 'additionalDirectories', reason: 'unsupported' }, 'Additional directories are not supported by this ACP adapter yet');
|
|
1003
1047
|
}
|
package/dist/acp/stdio-server.js
CHANGED
|
@@ -40,6 +40,9 @@ export async function runMakaAcpStdioServer(input, dependencies = {}) {
|
|
|
40
40
|
request: connection.request.bind(connection),
|
|
41
41
|
openSessionSubscription: connection.openSessionSubscription.bind(connection),
|
|
42
42
|
openSessionSubscriptionOnce: connection.openSessionSubscriptionOnce.bind(connection),
|
|
43
|
+
replaceClientCapabilities: (provider, options) => connection.replaceClientCapabilities(provider, options),
|
|
44
|
+
unregisterClientCapabilities: (options) => connection.unregisterClientCapabilities(options),
|
|
45
|
+
subscribeConnectionAvailability: (listener) => connection.subscribeConnectionAvailability(listener),
|
|
43
46
|
close: () => context.close(),
|
|
44
47
|
};
|
|
45
48
|
},
|
package/dist/cli-core.js
CHANGED
|
@@ -101,7 +101,7 @@ function helpText(cliCommand) {
|
|
|
101
101
|
'Commands:',
|
|
102
102
|
...[
|
|
103
103
|
['', 'Start the TUI'],
|
|
104
|
-
['--acp', 'Serve ACP v1 over stdio (sessions,
|
|
104
|
+
['--acp', 'Serve ACP v1 over stdio (sessions, tools, permissions, forms, stdio MCP)'],
|
|
105
105
|
['run ...', 'Run one non-interactive model turn'],
|
|
106
106
|
['-p ...', `Alias for ${cliCommand} run`],
|
|
107
107
|
['activate ...', 'Run one Cloud Session activation and emit JSONL'],
|
|
@@ -20,7 +20,7 @@ import { createHash } from 'node:crypto';
|
|
|
20
20
|
import { clientCapabilityEntityId } from '@maka/runtime-host/client-capability-entity-id';
|
|
21
21
|
import { CLIENT_CAPABILITY_MAX_TOOLS, CLIENT_CAPABILITY_MAX_TOOLS_PER_OFFER, decodeClientCapabilityReplaceInput, } from '@maka/runtime-host/protocol';
|
|
22
22
|
const CAPABILITY_VERSION = '0';
|
|
23
|
-
export function createMcpCapabilityProvider(manager) {
|
|
23
|
+
export function createMcpCapabilityProvider(manager, options = {}) {
|
|
24
24
|
const toolSnapshot = manager.toolSnapshot();
|
|
25
25
|
const tools = [...toolSnapshot.tools].sort((left, right) => left.descriptor.serverId.localeCompare(right.descriptor.serverId) ||
|
|
26
26
|
left.descriptor.name.localeCompare(right.descriptor.name));
|
|
@@ -52,6 +52,7 @@ export function createMcpCapabilityProvider(manager) {
|
|
|
52
52
|
version: CAPABILITY_VERSION,
|
|
53
53
|
affinity: 'session',
|
|
54
54
|
hostPathAccess: 'none',
|
|
55
|
+
...(options.admission ? { admission: options.admission } : {}),
|
|
55
56
|
label: servers.size === 1
|
|
56
57
|
? `MCP: ${chunk[0]?.source.descriptor.serverId ?? 'tools'}`.slice(0, 128)
|
|
57
58
|
: `MCP tools (${servers.size} servers)`,
|
|
@@ -64,10 +65,14 @@ export function createMcpCapabilityProvider(manager) {
|
|
|
64
65
|
}
|
|
65
66
|
const canonical = decodeClientCapabilityReplaceInput({
|
|
66
67
|
registrationId: '00000000-0000-4000-8000-000000000000',
|
|
68
|
+
...(options.admission ? { sessionId: 'mcp-manifest-validation' } : {}),
|
|
67
69
|
offers,
|
|
68
70
|
});
|
|
69
71
|
return {
|
|
70
72
|
offers: () => canonical.offers,
|
|
73
|
+
...(options.onCurrentRegistrationRetired
|
|
74
|
+
? { currentRegistrationRetired: options.onCurrentRegistrationRetired }
|
|
75
|
+
: {}),
|
|
71
76
|
call: async (frame, options) => {
|
|
72
77
|
const binding = bindings.get(capabilityBindingKey(frame.offerId, frame.serverId, frame.toolName));
|
|
73
78
|
if (!binding)
|
|
@@ -0,0 +1,139 @@
|
|
|
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
|
+
/** Coalesces MCP snapshots and never commits a publication from an obsolete connection. */
|
|
20
|
+
export class McpCapabilityPublication {
|
|
21
|
+
#options;
|
|
22
|
+
#requested = false;
|
|
23
|
+
#task;
|
|
24
|
+
#closeTask;
|
|
25
|
+
#closed = false;
|
|
26
|
+
#state = 'unavailable';
|
|
27
|
+
#published;
|
|
28
|
+
constructor(options) {
|
|
29
|
+
this.#options = options;
|
|
30
|
+
}
|
|
31
|
+
invalidate() {
|
|
32
|
+
this.#published = undefined;
|
|
33
|
+
}
|
|
34
|
+
request() {
|
|
35
|
+
if (this.#closed)
|
|
36
|
+
return;
|
|
37
|
+
this.#requested = true;
|
|
38
|
+
if (this.#task)
|
|
39
|
+
return;
|
|
40
|
+
this.#task = this.#run().finally(() => {
|
|
41
|
+
this.#task = undefined;
|
|
42
|
+
if (this.#requested && !this.#closed)
|
|
43
|
+
this.request();
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
async settle() {
|
|
47
|
+
this.request();
|
|
48
|
+
while (this.#task)
|
|
49
|
+
await this.#task;
|
|
50
|
+
return this.#closed ? 'unavailable' : this.#state;
|
|
51
|
+
}
|
|
52
|
+
close() {
|
|
53
|
+
this.#closeTask ??= this.#close(true);
|
|
54
|
+
return this.#closeTask;
|
|
55
|
+
}
|
|
56
|
+
/** Stops local publication state after the Host has already retired its current registration. */
|
|
57
|
+
retire() {
|
|
58
|
+
this.#closeTask ??= this.#close(false);
|
|
59
|
+
return this.#closeTask;
|
|
60
|
+
}
|
|
61
|
+
async #close(unregister) {
|
|
62
|
+
this.#closed = true;
|
|
63
|
+
this.#requested = false;
|
|
64
|
+
await this.#task;
|
|
65
|
+
try {
|
|
66
|
+
if (unregister &&
|
|
67
|
+
this.#published?.registered &&
|
|
68
|
+
this.#published.identity === this.#options.connectionIdentity()) {
|
|
69
|
+
await this.#options.unregister();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
this.#published = undefined;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async #run() {
|
|
77
|
+
while (this.#requested && !this.#closed) {
|
|
78
|
+
this.#requested = false;
|
|
79
|
+
await this.#publish();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async #publish() {
|
|
83
|
+
const identity = this.#options.connectionIdentity();
|
|
84
|
+
if (identity === undefined) {
|
|
85
|
+
this.#setState('unavailable');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const revision = this.#options.revision();
|
|
89
|
+
if (this.#published?.identity === identity && this.#published.revision === revision) {
|
|
90
|
+
this.#setState(this.#published.registered ? 'published' : 'not_published');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
let provider;
|
|
94
|
+
this.#setState('publishing');
|
|
95
|
+
try {
|
|
96
|
+
provider = this.#options.createProvider();
|
|
97
|
+
if (provider)
|
|
98
|
+
await this.#options.replace(provider);
|
|
99
|
+
else if (this.#published?.identity === identity && this.#published.registered) {
|
|
100
|
+
await this.#options.unregister();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
try {
|
|
105
|
+
await provider?.close?.();
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
/* Rejected provider cleanup is best effort. */
|
|
109
|
+
}
|
|
110
|
+
if (this.#isCurrent(identity, revision))
|
|
111
|
+
this.#setState('error');
|
|
112
|
+
else if (!this.#closed)
|
|
113
|
+
this.#requested = true;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// Even an obsolete snapshot may have committed on the current connection.
|
|
117
|
+
// Retain that fact so an empty replacement or close can unregister it.
|
|
118
|
+
if (this.#options.connectionIdentity() === identity) {
|
|
119
|
+
this.#published = { identity, revision, registered: provider !== undefined };
|
|
120
|
+
}
|
|
121
|
+
if (!this.#isCurrent(identity, revision)) {
|
|
122
|
+
if (!this.#closed)
|
|
123
|
+
this.#requested = true;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
this.#setState(provider ? 'published' : 'not_published');
|
|
127
|
+
}
|
|
128
|
+
#isCurrent(identity, revision) {
|
|
129
|
+
return (!this.#closed &&
|
|
130
|
+
this.#options.connectionIdentity() === identity &&
|
|
131
|
+
this.#options.revision() === revision);
|
|
132
|
+
}
|
|
133
|
+
#setState(state) {
|
|
134
|
+
if (this.#closed)
|
|
135
|
+
return;
|
|
136
|
+
this.#state = state;
|
|
137
|
+
this.#options.onState(state);
|
|
138
|
+
}
|
|
139
|
+
}
|