maka-agent 0.2.0-dev.49.20260924 → 0.2.0-dev.50.20260925
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/LICENSE +35 -0
- package/dist/acp/maka-acp-agent.js +71 -11
- package/dist/acp/session-event-mapper.js +59 -3
- package/dist/acp/session-interactions.js +33 -3
- package/dist/acp/session-mcp.js +160 -11
- package/dist/acp/session-registry.js +1014 -242
- package/dist/acp/turn-observation.js +260 -0
- package/dist/mcp-capability-publication.js +17 -1
- package/dist/pi-transcript-tools.js +8 -3
- package/dist/runtime-host-session-channel.js +71 -47
- package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
- package/node_modules/@maka/core/dist/relative-time.js +9 -1
- package/node_modules/@maka/core/dist/session.js +0 -15
- package/node_modules/@maka/core/dist/tool-quiet-preview.js +31 -0
- package/node_modules/@maka/core/dist/transcript-search.js +0 -1
- package/node_modules/@maka/runtime/dist/conversation-copy.js +7 -1
- package/node_modules/@maka/runtime/dist/runtime-event-backfill.js +0 -1
- package/node_modules/@maka/runtime-host/dist/client/client-capability-channel.js +3 -1
- package/node_modules/@maka/runtime-host/dist/client/connection.js +2 -2
- package/node_modules/@maka/runtime-host/dist/client/session-subscription.js +0 -3
- package/node_modules/@maka/runtime-host/dist/protocol/client-capability.js +19 -1
- package/node_modules/@maka/runtime-host/dist/protocol/index.js +4 -1
- package/node_modules/@maka/runtime-host/dist/protocol/session-continuity.js +1 -2
- package/node_modules/@maka/runtime-host/dist/server/client-capability-coordinator.js +122 -9
- package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +11 -6
- package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +5 -0
- package/node_modules/@maka/runtime-host/dist/server/session-continuity-coordinator.js +0 -16
- package/node_modules/@maka/runtime-host/dist/server/session-transcript-reader.js +3 -38
- package/node_modules/@maka/runtime-host/dist/server/shared-session-transcript.js +0 -3
- package/node_modules/@maka/storage/dist/interaction-store.js +0 -13
- package/node_modules/@maka/storage/dist/runtime-partial-values.js +5 -1
- package/node_modules/@maka/storage/dist/sqlite-core-execution-schema.js +1 -3
- package/node_modules/@maka/storage/dist/usage-screen.js +101 -35
- package/package.json +1 -1
package/LICENSE
CHANGED
|
@@ -574,6 +574,41 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
|
574
574
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
575
575
|
SOFTWARE.
|
|
576
576
|
|
|
577
|
+
virtua dependency patch
|
|
578
|
+
|
|
579
|
+
Source: https://www.npmjs.com/package/virtua/v/0.52.7
|
|
580
|
+
Repository: https://github.com/inokawa/virtua
|
|
581
|
+
Version: 0.52.7
|
|
582
|
+
Dependency patch: patches/virtua+0.52.7.patch
|
|
583
|
+
Copyright (c) 2022 inokawa
|
|
584
|
+
License: MIT
|
|
585
|
+
|
|
586
|
+
Maka redistributes a source patch that applies virtua's scroll correction
|
|
587
|
+
absolutely after the browser clamps the scroll offset. The following MIT
|
|
588
|
+
License applies to that material:
|
|
589
|
+
|
|
590
|
+
MIT License
|
|
591
|
+
|
|
592
|
+
Copyright (c) 2022 inokawa
|
|
593
|
+
|
|
594
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
595
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
596
|
+
in the Software without restriction, including without limitation the rights
|
|
597
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
598
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
599
|
+
furnished to do so, subject to the following conditions:
|
|
600
|
+
|
|
601
|
+
The above copyright notice and this permission notice shall be included in all
|
|
602
|
+
copies or substantial portions of the Software.
|
|
603
|
+
|
|
604
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
605
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
606
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
607
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
608
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
609
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
610
|
+
SOFTWARE.
|
|
611
|
+
|
|
577
612
|
trycua/cua cursor-overlay
|
|
578
613
|
|
|
579
614
|
Source: https://github.com/trycua/cua
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
* specific language governing permissions and limitations
|
|
17
17
|
* under the License.
|
|
18
18
|
*/
|
|
19
|
-
import { agent, methods } from '@agentclientprotocol/sdk';
|
|
19
|
+
import { agent, methods, RequestError, } from '@agentclientprotocol/sdk';
|
|
20
|
+
import { HOST_OPERATION_SPECS } from '@maka/runtime-host/protocol';
|
|
20
21
|
export function createMakaAcpAgent(options) {
|
|
21
22
|
let clientCapabilities = {};
|
|
22
23
|
return agent({ name: 'maka' })
|
|
@@ -24,25 +25,84 @@ export function createMakaAcpAgent(options) {
|
|
|
24
25
|
clientCapabilities = structuredClone(params.clientCapabilities ?? {});
|
|
25
26
|
return {
|
|
26
27
|
protocolVersion: 1,
|
|
27
|
-
agentCapabilities: {
|
|
28
|
+
agentCapabilities: {
|
|
29
|
+
loadSession: true,
|
|
30
|
+
sessionCapabilities: { list: {}, resume: {}, close: {} },
|
|
31
|
+
},
|
|
28
32
|
authMethods: [],
|
|
29
33
|
agentInfo: { name: 'maka', title: 'Maka', version: options.version },
|
|
30
34
|
};
|
|
31
35
|
})
|
|
32
36
|
.onRequest(methods.agent.session.new, ({ params, signal }) => options.sessionRegistry.create(params, signal))
|
|
37
|
+
.onRequest(methods.agent.session.load, ({ params, signal, client }) => options.sessionRegistry.load(params, sessionContext(client, signal, clientCapabilities, true)))
|
|
38
|
+
.onRequest(methods.agent.session.resume, ({ params, signal, client }) => options.sessionRegistry.resume(params, sessionContext(client, signal, clientCapabilities, true)))
|
|
39
|
+
.onRequest('_maka/turn/resume', (value) => {
|
|
40
|
+
try {
|
|
41
|
+
return HOST_OPERATION_SPECS['turn.resume.query'].decodeInput(value);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
throw RequestError.invalidParams({ reason: 'invalid_resume_query' }, 'Invalid Turn resume request');
|
|
45
|
+
}
|
|
46
|
+
}, ({ params, signal, client }) => options.sessionRegistry.resumeTurn(params, sessionContext(client, signal, clientCapabilities, true)))
|
|
47
|
+
.onRequest('_maka/session/copy-source/query', {
|
|
48
|
+
parse: (value) => {
|
|
49
|
+
try {
|
|
50
|
+
return HOST_OPERATION_SPECS['session.turns.query'].decodeInput(value);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
throw RequestError.invalidParams({ reason: 'invalid_copy_source_query' }, 'Invalid Session copy source query');
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
}, ({ params }) => options.sessionRegistry.queryCopySource(params))
|
|
57
|
+
.onRequest('_maka/session/branch/create', {
|
|
58
|
+
parse: (value) => {
|
|
59
|
+
try {
|
|
60
|
+
return HOST_OPERATION_SPECS['session.branch.create'].decodeInput(value);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
throw RequestError.invalidParams({ reason: 'invalid_branch_input' }, 'Invalid Session branch request');
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
}, ({ params }) => options.sessionRegistry.branch(params))
|
|
67
|
+
.onRequest('_maka/session/revision/create', {
|
|
68
|
+
parse: (value) => {
|
|
69
|
+
try {
|
|
70
|
+
return HOST_OPERATION_SPECS['session.revision.create'].decodeInput(value);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
throw RequestError.invalidParams({ reason: 'invalid_revision_input' }, 'Invalid Session revision request');
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
}, ({ params }) => options.sessionRegistry.createRevision(params))
|
|
77
|
+
.onRequest('_maka/session/revision/abandon', {
|
|
78
|
+
parse: (value) => {
|
|
79
|
+
try {
|
|
80
|
+
return HOST_OPERATION_SPECS['session.revision.abandon'].decodeInput(value);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
throw RequestError.invalidParams({ reason: 'invalid_abandon_input' }, 'Invalid Session abandon request');
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
}, ({ params }) => options.sessionRegistry.abandonRevision(params))
|
|
33
87
|
.onRequest(methods.agent.session.list, ({ params }) => options.sessionRegistry.list(params))
|
|
34
88
|
.onRequest(methods.agent.session.setConfigOption, ({ params }) => options.sessionRegistry.setConfigOption(params))
|
|
35
|
-
.onRequest(methods.agent.session.prompt, ({ params, signal, client }) => options.sessionRegistry.prompt(params,
|
|
89
|
+
.onRequest(methods.agent.session.prompt, ({ params, signal, client }) => options.sessionRegistry.prompt(params, sessionContext(client, signal, clientCapabilities)))
|
|
90
|
+
.onNotification(methods.agent.session.cancel, ({ params }) => options.sessionRegistry.cancel(params))
|
|
91
|
+
.onRequest(methods.agent.session.close, ({ params }) => options.sessionRegistry.close(params));
|
|
92
|
+
}
|
|
93
|
+
function sessionContext(client, signal, capabilities, turnStatus = false) {
|
|
94
|
+
return {
|
|
36
95
|
signal,
|
|
37
96
|
notify: (notification) => client.notify(methods.client.session.update, notification),
|
|
38
97
|
interactions: {
|
|
39
|
-
capabilities
|
|
40
|
-
requestPermission: (
|
|
41
|
-
|
|
42
|
-
}),
|
|
43
|
-
createElicitation: (params, cancellationSignal) => client.request(methods.client.elicitation.create, params, { cancellationSignal }),
|
|
98
|
+
capabilities,
|
|
99
|
+
requestPermission: (request, cancellationSignal) => client.request(methods.client.session.requestPermission, request, { cancellationSignal }),
|
|
100
|
+
createElicitation: (request, cancellationSignal) => client.request(methods.client.elicitation.create, request, { cancellationSignal }),
|
|
44
101
|
},
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
102
|
+
...(turnStatus && capabilities._meta?.['_maka/turnStatus'] === true
|
|
103
|
+
? {
|
|
104
|
+
notifyTurnStatus: (status) => client.notify('_maka/turn/status', status),
|
|
105
|
+
}
|
|
106
|
+
: {}),
|
|
107
|
+
};
|
|
48
108
|
}
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { foldRuntimeHostAssistantDelta } from '@maka/runtime-host/adapter';
|
|
20
20
|
import { RequestError, } from '@agentclientprotocol/sdk';
|
|
21
|
+
import { userFacingText } from '@maka/core/session';
|
|
21
22
|
import { whileActive } from './active-promise.js';
|
|
22
23
|
import { AcpToolEventMapper } from './tool-event-mapper.js';
|
|
23
24
|
/** Serializes one ACP prompt's live projection delivery. */
|
|
@@ -67,20 +68,72 @@ export class AcpSessionEventMapper {
|
|
|
67
68
|
return this.acceptTranscriptMessages(turnId, messages);
|
|
68
69
|
}
|
|
69
70
|
/** Apply a bounded authoritative batch; absence from one batch never removes a tool. */
|
|
70
|
-
acceptTranscriptMessages(turnId, messages) {
|
|
71
|
+
acceptTranscriptMessages(turnId, messages, ignoreOlder = false) {
|
|
71
72
|
return this.#enqueue(async () => {
|
|
72
73
|
for (const message of messages) {
|
|
73
74
|
if (message.turnId !== turnId)
|
|
74
75
|
continue;
|
|
75
76
|
if (message.type === 'assistant') {
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
const thinking = message.thinking?.text ?? '';
|
|
78
|
+
if (!ignoreOlder ||
|
|
79
|
+
thinking.startsWith(this.#streams.get(streamKey('thinking', message.id)) ?? ''))
|
|
80
|
+
await this.#acceptText('thinking', message.id, thinking);
|
|
81
|
+
if (!ignoreOlder ||
|
|
82
|
+
message.text.startsWith(this.#streams.get(streamKey('text', message.id)) ?? ''))
|
|
83
|
+
await this.#acceptText('text', message.id, message.text);
|
|
78
84
|
}
|
|
79
85
|
else
|
|
80
86
|
await this.#tools.acceptMessage(message);
|
|
81
87
|
}
|
|
82
88
|
});
|
|
83
89
|
}
|
|
90
|
+
/** Replay one durable row in transcript order for session/load. */
|
|
91
|
+
acceptHistoricalMessage(message) {
|
|
92
|
+
return this.#enqueue(async () => {
|
|
93
|
+
if (message.type === 'user') {
|
|
94
|
+
// ACP v1 has no mid-Turn steering update. Live projection omits these
|
|
95
|
+
// rows, so historical replay must not present them as new user turns.
|
|
96
|
+
if (message.steeringEventId)
|
|
97
|
+
return;
|
|
98
|
+
const attachments = message.attachments ?? [];
|
|
99
|
+
const text = [
|
|
100
|
+
userFacingText(message),
|
|
101
|
+
...attachments.map((attachment) => `[Attachment: ${attachment.name} (${attachment.mimeType}, ${attachment.bytes} bytes)]`),
|
|
102
|
+
]
|
|
103
|
+
.filter(Boolean)
|
|
104
|
+
.join('\n\n');
|
|
105
|
+
if (text) {
|
|
106
|
+
await this.#deliver({
|
|
107
|
+
sessionId: this.#sessionId,
|
|
108
|
+
update: {
|
|
109
|
+
sessionUpdate: 'user_message_chunk',
|
|
110
|
+
content: { type: 'text', text },
|
|
111
|
+
messageId: message.id,
|
|
112
|
+
...(attachments.length || message.origin
|
|
113
|
+
? {
|
|
114
|
+
_meta: {
|
|
115
|
+
...(attachments.length
|
|
116
|
+
? { '_maka/attachments': structuredClone(attachments) }
|
|
117
|
+
: {}),
|
|
118
|
+
...(message.origin
|
|
119
|
+
? { '_maka/origin': structuredClone(message.origin) }
|
|
120
|
+
: {}),
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
: {}),
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
else if (message.type === 'assistant') {
|
|
129
|
+
await this.#acceptText('thinking', message.id, message.thinking?.text ?? '');
|
|
130
|
+
await this.#acceptText('text', message.id, message.text);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
await this.#tools.acceptMessage(message);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
84
137
|
finishTools(turnId, terminalStatus = 'completed') {
|
|
85
138
|
return this.#enqueue(() => this.#tools.finishTools(turnId, terminalStatus));
|
|
86
139
|
}
|
|
@@ -97,6 +150,9 @@ export class AcpSessionEventMapper {
|
|
|
97
150
|
throw this.#failure;
|
|
98
151
|
});
|
|
99
152
|
}
|
|
153
|
+
textForMessage(kind, messageId) {
|
|
154
|
+
return this.#streams.get(streamKey(kind, messageId));
|
|
155
|
+
}
|
|
100
156
|
async #acceptText(kind, hostMessageId, nextText) {
|
|
101
157
|
const key = streamKey(kind, hostMessageId);
|
|
102
158
|
const current = this.#streams.get(key) ?? '';
|
|
@@ -33,6 +33,8 @@ function questionAnswerMaxChars(count) {
|
|
|
33
33
|
/** Connection-local presentation of Host interactions; the Host owns every answer and grant. */
|
|
34
34
|
export class AcpSessionInteractions {
|
|
35
35
|
#options;
|
|
36
|
+
#client;
|
|
37
|
+
#clientLease;
|
|
36
38
|
#lifetime = new AbortController();
|
|
37
39
|
#pending = new Map();
|
|
38
40
|
#resolving = new Map();
|
|
@@ -41,6 +43,34 @@ export class AcpSessionInteractions {
|
|
|
41
43
|
#cancelledInteractions = new Set();
|
|
42
44
|
constructor(options) {
|
|
43
45
|
this.#options = options;
|
|
46
|
+
this.#client = options.client;
|
|
47
|
+
this.#clientLease = { client: options.client, valid: true };
|
|
48
|
+
}
|
|
49
|
+
setClient(client) {
|
|
50
|
+
const lease = {
|
|
51
|
+
client,
|
|
52
|
+
previous: this.#clientLease,
|
|
53
|
+
valid: true,
|
|
54
|
+
};
|
|
55
|
+
this.#clientLease = lease;
|
|
56
|
+
this.#client = client;
|
|
57
|
+
return {
|
|
58
|
+
rollback: () => {
|
|
59
|
+
lease.valid = false;
|
|
60
|
+
if (this.#clientLease !== lease)
|
|
61
|
+
return;
|
|
62
|
+
let previous = lease.previous;
|
|
63
|
+
while (previous && !previous.valid)
|
|
64
|
+
previous = previous.previous;
|
|
65
|
+
if (!previous)
|
|
66
|
+
return;
|
|
67
|
+
this.#clientLease = previous;
|
|
68
|
+
this.#client = previous.client;
|
|
69
|
+
},
|
|
70
|
+
commit: () => {
|
|
71
|
+
lease.previous = undefined;
|
|
72
|
+
},
|
|
73
|
+
};
|
|
44
74
|
}
|
|
45
75
|
pending(snapshot) {
|
|
46
76
|
if (this.#lifetime.signal.aborted) {
|
|
@@ -159,7 +189,7 @@ export class AcpSessionInteractions {
|
|
|
159
189
|
}
|
|
160
190
|
const request = pending.request;
|
|
161
191
|
if ((request.kind === 'question' || request.kind === 'form') &&
|
|
162
|
-
this.#
|
|
192
|
+
this.#client.capabilities.elicitation?.form == null) {
|
|
163
193
|
throw interactionError(pending, 'unsupported_interaction', 'Client requires form elicitation');
|
|
164
194
|
}
|
|
165
195
|
const presented = await whileActive(Promise.resolve().then(() => {
|
|
@@ -170,7 +200,7 @@ export class AcpSessionInteractions {
|
|
|
170
200
|
return;
|
|
171
201
|
let answer;
|
|
172
202
|
if (request.kind === 'question' || request.kind === 'form') {
|
|
173
|
-
const response = await whileActive(this.#
|
|
203
|
+
const response = await whileActive(this.#client.createElicitation(elicitationRequest(pending), signal), signal);
|
|
174
204
|
if (!response.active)
|
|
175
205
|
return;
|
|
176
206
|
if (request.kind === 'question' && response.value.action === 'cancel') {
|
|
@@ -188,7 +218,7 @@ export class AcpSessionInteractions {
|
|
|
188
218
|
}
|
|
189
219
|
else {
|
|
190
220
|
const presentation = permissionPresentation(pending);
|
|
191
|
-
const response = await whileActive(this.#
|
|
221
|
+
const response = await whileActive(this.#client.requestPermission(presentation.request, signal), signal);
|
|
192
222
|
if (!response.active)
|
|
193
223
|
return;
|
|
194
224
|
if (response.value.outcome.outcome === 'cancelled') {
|
package/dist/acp/session-mcp.js
CHANGED
|
@@ -16,12 +16,14 @@
|
|
|
16
16
|
* specific language governing permissions and limitations
|
|
17
17
|
* under the License.
|
|
18
18
|
*/
|
|
19
|
+
import { createHash } from 'node:crypto';
|
|
19
20
|
import { isAbsolute } from 'node:path';
|
|
20
21
|
import { RequestError } from '@agentclientprotocol/sdk';
|
|
21
22
|
import { MCP_CONFIG_VERSION } from '@maka/core/mcp';
|
|
23
|
+
import { stableJsonStringify } from '@maka/core/canonical-json';
|
|
22
24
|
import { McpClientManager } from '@maka/mcp';
|
|
23
25
|
import { normalizeMcpConfig } from '@maka/storage/mcp-config-store';
|
|
24
|
-
import { abortable } from '@maka/runtime-host/client';
|
|
26
|
+
import { RuntimeHostOperationError, abortable, } from '@maka/runtime-host/client';
|
|
25
27
|
import { createMcpCapabilityProvider } from '../mcp-capability-provider.js';
|
|
26
28
|
import { McpCapabilityPublication } from '../mcp-capability-publication.js';
|
|
27
29
|
/** Validates before any process or Host work; never writes a user MCP configuration. */
|
|
@@ -80,6 +82,7 @@ export function createAcpMcpConfig(params) {
|
|
|
80
82
|
export class AcpSessionMcp {
|
|
81
83
|
#sessionId;
|
|
82
84
|
#config;
|
|
85
|
+
#configurationTail = Promise.resolve();
|
|
83
86
|
#manager = new McpClientManager({
|
|
84
87
|
clientName: 'maka-acp',
|
|
85
88
|
excludedStdioEnvironmentKeys: ['MAKA_RUNTIME_HOST_ACCESS_CREDENTIAL'],
|
|
@@ -87,6 +90,11 @@ export class AcpSessionMcp {
|
|
|
87
90
|
#publication;
|
|
88
91
|
#unsubscribeManager;
|
|
89
92
|
#unsubscribeConnection;
|
|
93
|
+
#pendingManager;
|
|
94
|
+
#retainedManagers = new Set();
|
|
95
|
+
#retirementTasks = new Set();
|
|
96
|
+
#publicationRevision = 0;
|
|
97
|
+
#requireIdlePublication = false;
|
|
90
98
|
#availability;
|
|
91
99
|
#prepared = false;
|
|
92
100
|
#closed = false;
|
|
@@ -99,7 +107,7 @@ export class AcpSessionMcp {
|
|
|
99
107
|
connectionIdentity: () => this.#availability?.kind === 'connected'
|
|
100
108
|
? this.#availability.hostEpoch + '\0' + this.#availability.connectionId
|
|
101
109
|
: undefined,
|
|
102
|
-
revision: () => this.#
|
|
110
|
+
revision: () => this.#publicationRevision,
|
|
103
111
|
createProvider: () => createMcpCapabilityProvider(this.#manager, {
|
|
104
112
|
admission: 'mcp',
|
|
105
113
|
onCurrentRegistrationRetired: () => this.#retire(),
|
|
@@ -110,14 +118,21 @@ export class AcpSessionMcp {
|
|
|
110
118
|
offers: () => [],
|
|
111
119
|
currentRegistrationRetired: () => this.#retire(),
|
|
112
120
|
},
|
|
113
|
-
replace: (provider) => connection.replaceClientCapabilities(provider, {
|
|
121
|
+
replace: (provider) => connection.replaceClientCapabilities(provider, {
|
|
122
|
+
sessionId,
|
|
123
|
+
sessionConfigurationId: `sha256:${createHash('sha256').update(stableJsonStringify(this.#config)).digest('hex')}`,
|
|
124
|
+
...(this.#requireIdlePublication ? { requireIdleSession: true } : {}),
|
|
125
|
+
}),
|
|
114
126
|
unregister: () => connection.unregisterClientCapabilities({ sessionId }),
|
|
115
|
-
onState: () =>
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
127
|
+
onState: (state) => {
|
|
128
|
+
if ((state === 'published' || state === 'not_published') &&
|
|
129
|
+
this.#publication.publishedRevision === this.#publicationRevision) {
|
|
130
|
+
this.#requireIdlePublication = false;
|
|
131
|
+
this.#retireRetainedManagers();
|
|
132
|
+
}
|
|
133
|
+
},
|
|
120
134
|
});
|
|
135
|
+
this.#unsubscribeManager = this.#observeManager(this.#manager);
|
|
121
136
|
this.#unsubscribeConnection = connection.subscribeConnectionAvailability((availability) => {
|
|
122
137
|
this.#availability = availability;
|
|
123
138
|
if (availability.kind !== 'connected')
|
|
@@ -150,13 +165,118 @@ export class AcpSessionMcp {
|
|
|
150
165
|
signal?.removeEventListener('abort', cancel);
|
|
151
166
|
}
|
|
152
167
|
}
|
|
168
|
+
get config() {
|
|
169
|
+
return structuredClone(this.#config);
|
|
170
|
+
}
|
|
171
|
+
reconfigure(config, signal, assertCanChange) {
|
|
172
|
+
const task = this.#configurationTail
|
|
173
|
+
.catch(() => undefined)
|
|
174
|
+
.then(async () => {
|
|
175
|
+
this.#assertOpen('mcp.prepare');
|
|
176
|
+
signal?.throwIfAborted();
|
|
177
|
+
if (sameMcpConfig(this.#config, config))
|
|
178
|
+
return this.#settlePublication(signal);
|
|
179
|
+
if (this.#retainedManagers.size > 0)
|
|
180
|
+
await this.#settlePublication(signal);
|
|
181
|
+
await assertCanChange?.();
|
|
182
|
+
signal?.throwIfAborted();
|
|
183
|
+
const previous = this.#manager;
|
|
184
|
+
const previousConfig = this.#config;
|
|
185
|
+
const staged = new McpClientManager({
|
|
186
|
+
clientName: 'maka-acp',
|
|
187
|
+
excludedStdioEnvironmentKeys: ['MAKA_RUNTIME_HOST_ACCESS_CREDENTIAL'],
|
|
188
|
+
});
|
|
189
|
+
this.#pendingManager = staged;
|
|
190
|
+
let swapped = false;
|
|
191
|
+
let previousRevision = this.#publicationRevision;
|
|
192
|
+
try {
|
|
193
|
+
await staged.sync(config);
|
|
194
|
+
signal?.throwIfAborted();
|
|
195
|
+
this.#assertConnected(config, staged);
|
|
196
|
+
previousRevision = this.#publicationRevision;
|
|
197
|
+
this.#unsubscribeManager();
|
|
198
|
+
this.#manager = staged;
|
|
199
|
+
this.#config = config;
|
|
200
|
+
this.#unsubscribeManager = this.#observeManager(staged);
|
|
201
|
+
this.#retainedManagers.add(previous);
|
|
202
|
+
this.#pendingManager = undefined;
|
|
203
|
+
swapped = true;
|
|
204
|
+
this.#requireIdlePublication = true;
|
|
205
|
+
this.#publicationRevision += 1;
|
|
206
|
+
this.#publication.request();
|
|
207
|
+
await this.#settlePublication(signal);
|
|
208
|
+
this.#requireIdlePublication = false;
|
|
209
|
+
this.#retireRetainedManagers();
|
|
210
|
+
await Promise.allSettled([...this.#retirementTasks]);
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
if (swapped && !this.#closed) {
|
|
214
|
+
// Cancellation can win after the Host committed a new provider but
|
|
215
|
+
// before its response arrived. Resolve the in-flight publication
|
|
216
|
+
// before deciding which manager may be released.
|
|
217
|
+
await this.#publication.waitForPending();
|
|
218
|
+
if (this.#publication.publishedRevision === previousRevision &&
|
|
219
|
+
this.#publication.lastError instanceof RuntimeHostOperationError) {
|
|
220
|
+
this.#unsubscribeManager();
|
|
221
|
+
this.#manager = previous;
|
|
222
|
+
this.#config = previousConfig;
|
|
223
|
+
this.#unsubscribeManager = this.#observeManager(previous);
|
|
224
|
+
this.#retainedManagers.delete(previous);
|
|
225
|
+
this.#publicationRevision = previousRevision;
|
|
226
|
+
this.#requireIdlePublication = false;
|
|
227
|
+
this.#publication.request();
|
|
228
|
+
await this.#publication.settle().catch(() => undefined);
|
|
229
|
+
await staged.close().catch(() => undefined);
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
// A committed or unknown outcome may already be serving a Turn.
|
|
233
|
+
// Keep that provider and its manager; the previous manager stays
|
|
234
|
+
// available until a definitive publication or Session close.
|
|
235
|
+
this.#retireRetainedManagers();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
else if (!swapped) {
|
|
239
|
+
this.#pendingManager = undefined;
|
|
240
|
+
await staged.close().catch(() => undefined);
|
|
241
|
+
}
|
|
242
|
+
if (error instanceof RequestError)
|
|
243
|
+
throw error;
|
|
244
|
+
throw mcpUnavailable(this.#sessionId, 'mcp.prepare', 'mcp_reconfiguration_failed');
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
this.#configurationTail = task;
|
|
248
|
+
return task;
|
|
249
|
+
}
|
|
153
250
|
async ready(signal) {
|
|
251
|
+
await abortable(() => this.#configurationTail.catch(() => undefined), signal);
|
|
252
|
+
await this.#settlePublication(signal);
|
|
253
|
+
}
|
|
254
|
+
async #settlePublication(signal) {
|
|
154
255
|
signal?.throwIfAborted();
|
|
155
256
|
this.#assertOpen('mcp.ready');
|
|
156
257
|
const state = await abortable(() => this.#publication.settle(), signal);
|
|
157
258
|
signal?.throwIfAborted();
|
|
158
259
|
this.#assertOpen('mcp.ready');
|
|
159
260
|
if (state !== 'published' && state !== 'not_published') {
|
|
261
|
+
if (this.#publication.lastError instanceof RuntimeHostOperationError &&
|
|
262
|
+
this.#publication.lastError.code === 'session_binding_conflict') {
|
|
263
|
+
throw RequestError.internalError({
|
|
264
|
+
source: 'runtime_host',
|
|
265
|
+
operation: 'mcp.prepare',
|
|
266
|
+
sessionId: this.#sessionId,
|
|
267
|
+
code: 'session_binding_conflict',
|
|
268
|
+
}, 'Session MCP configuration conflicts with an active or frozen provider; restore with the same configuration or close the active attachment');
|
|
269
|
+
}
|
|
270
|
+
if (this.#requireIdlePublication &&
|
|
271
|
+
this.#publication.lastError instanceof RuntimeHostOperationError &&
|
|
272
|
+
this.#publication.lastError.code === 'session_busy') {
|
|
273
|
+
throw RequestError.internalError({
|
|
274
|
+
source: 'runtime_host',
|
|
275
|
+
operation: 'mcp.prepare',
|
|
276
|
+
sessionId: this.#sessionId,
|
|
277
|
+
code: 'session_busy',
|
|
278
|
+
}, 'Cannot replace Session MCP configuration during an active Turn');
|
|
279
|
+
}
|
|
160
280
|
throw mcpUnavailable(this.#sessionId, 'mcp.ready', 'mcp_publication_failed');
|
|
161
281
|
}
|
|
162
282
|
}
|
|
@@ -173,7 +293,12 @@ export class AcpSessionMcp {
|
|
|
173
293
|
this.#closed = true;
|
|
174
294
|
this.#unsubscribeManager();
|
|
175
295
|
this.#unsubscribeConnection();
|
|
176
|
-
const managerClose =
|
|
296
|
+
const managerClose = Promise.allSettled([
|
|
297
|
+
this.#manager.close(),
|
|
298
|
+
this.#pendingManager?.close(),
|
|
299
|
+
...[...this.#retainedManagers].map((manager) => manager.close()),
|
|
300
|
+
...this.#retirementTasks,
|
|
301
|
+
]);
|
|
177
302
|
this.#closeTask = (async () => {
|
|
178
303
|
try {
|
|
179
304
|
try {
|
|
@@ -193,9 +318,30 @@ export class AcpSessionMcp {
|
|
|
193
318
|
})();
|
|
194
319
|
return this.#closeTask;
|
|
195
320
|
}
|
|
196
|
-
#
|
|
321
|
+
#observeManager(manager) {
|
|
322
|
+
return manager.onChange(() => {
|
|
323
|
+
this.#publicationRevision += 1;
|
|
324
|
+
if (this.#prepared && !this.#closed)
|
|
325
|
+
this.#publication.request();
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
#retireRetainedManagers() {
|
|
329
|
+
if (this.#publication.publishedRevision !== this.#publicationRevision)
|
|
330
|
+
return;
|
|
331
|
+
for (const manager of this.#retainedManagers) {
|
|
332
|
+
this.#retainedManagers.delete(manager);
|
|
333
|
+
if (manager === this.#manager)
|
|
334
|
+
continue;
|
|
335
|
+
const task = manager.close().catch((error) => {
|
|
336
|
+
console.error('[acp] Previous Session MCP cleanup failed:', error);
|
|
337
|
+
});
|
|
338
|
+
this.#retirementTasks.add(task);
|
|
339
|
+
void task.finally(() => this.#retirementTasks.delete(task));
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
#assertConnected(config = this.#config, manager = this.#manager) {
|
|
197
343
|
this.#assertOpen('mcp.prepare');
|
|
198
|
-
if (Object.keys(
|
|
344
|
+
if (Object.keys(config.mcpServers).some((serverId) => manager.status(serverId)?.state !== 'connected')) {
|
|
199
345
|
throw mcpUnavailable(this.#sessionId, 'mcp.prepare', 'mcp_not_ready');
|
|
200
346
|
}
|
|
201
347
|
}
|
|
@@ -204,6 +350,9 @@ export class AcpSessionMcp {
|
|
|
204
350
|
throw mcpUnavailable(this.#sessionId, operation, 'mcp_not_ready');
|
|
205
351
|
}
|
|
206
352
|
}
|
|
353
|
+
function sameMcpConfig(left, right) {
|
|
354
|
+
return stableJsonStringify(left) === stableJsonStringify(right);
|
|
355
|
+
}
|
|
207
356
|
function invalidMcpInput(reason) {
|
|
208
357
|
return RequestError.invalidParams({ field: 'mcpServers', reason }, 'Invalid ACP stdio MCP configuration');
|
|
209
358
|
}
|