maka-agent 0.2.0-dev.42.20260918 → 0.2.0-dev.43.20260919
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/acp/maka-acp-agent.js +17 -6
- package/dist/acp/session-event-mapper.js +6 -0
- package/dist/acp/session-interactions.js +520 -0
- package/dist/acp/session-registry.js +80 -9
- package/dist/acp/tool-event-mapper.js +60 -2
- package/dist/pi-tui-runner.js +57 -6
- package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
- package/node_modules/@maka/core/dist/connection-usage.js +27 -0
- package/node_modules/@maka/core/dist/external-session.js +4 -3
- package/node_modules/@maka/core/dist/provider-auth.js +9 -1
- package/node_modules/@maka/core/dist/skill-locations.js +29 -0
- package/node_modules/@maka/core/package.json +2 -0
- package/node_modules/@maka/runtime/dist/commandcode-cli-language-model.js +27 -3
- package/node_modules/@maka/runtime/dist/connection-usage.js +232 -0
- package/node_modules/@maka/runtime/dist/skills-discovery.js +17 -23
- package/node_modules/@maka/runtime/package.json +1 -0
- package/node_modules/@maka/runtime-host/dist/protocol/connection-effects.js +132 -1
- package/node_modules/@maka/runtime-host/dist/protocol/index.js +7 -3
- package/node_modules/@maka/runtime-host/dist/protocol/operations.js +1 -0
- package/node_modules/@maka/runtime-host/dist/server/connection-effect-coordinator.js +57 -1
- package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +4 -3
- package/node_modules/@maka/storage/dist/codex-session-adapter.js +26 -28
- package/node_modules/@maka/storage/dist/runtime-policy/coordinator.js +34 -1
- package/node_modules/@maka/storage/dist/runtime-policy-stores.js +2 -0
- package/node_modules/@maka/storage/dist/session-store.js +5 -2
- package/package.json +1 -1
|
@@ -18,19 +18,30 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { agent, methods } from '@agentclientprotocol/sdk';
|
|
20
20
|
export function createMakaAcpAgent(options) {
|
|
21
|
+
let clientCapabilities = {};
|
|
21
22
|
return agent({ name: 'maka' })
|
|
22
|
-
.onRequest(methods.agent.initialize, () =>
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
.onRequest(methods.agent.initialize, ({ params }) => {
|
|
24
|
+
clientCapabilities = structuredClone(params.clientCapabilities ?? {});
|
|
25
|
+
return {
|
|
26
|
+
protocolVersion: 1,
|
|
27
|
+
agentCapabilities: { sessionCapabilities: { list: {}, close: {} } },
|
|
28
|
+
authMethods: [],
|
|
29
|
+
agentInfo: { name: 'maka', title: 'Maka', version: options.version },
|
|
30
|
+
};
|
|
31
|
+
})
|
|
28
32
|
.onRequest(methods.agent.session.new, ({ params }) => options.sessionRegistry.create(params))
|
|
29
33
|
.onRequest(methods.agent.session.list, ({ params }) => options.sessionRegistry.list(params))
|
|
30
34
|
.onRequest(methods.agent.session.setConfigOption, ({ params }) => options.sessionRegistry.setConfigOption(params))
|
|
31
35
|
.onRequest(methods.agent.session.prompt, ({ params, signal, client }) => options.sessionRegistry.prompt(params, {
|
|
32
36
|
signal,
|
|
33
37
|
notify: (notification) => client.notify(methods.client.session.update, notification),
|
|
38
|
+
interactions: {
|
|
39
|
+
capabilities: clientCapabilities,
|
|
40
|
+
requestPermission: (params, cancellationSignal) => client.request(methods.client.session.requestPermission, params, {
|
|
41
|
+
cancellationSignal,
|
|
42
|
+
}),
|
|
43
|
+
createElicitation: (params, cancellationSignal) => client.request(methods.client.elicitation.create, params, { cancellationSignal }),
|
|
44
|
+
},
|
|
34
45
|
}))
|
|
35
46
|
.onNotification(methods.agent.session.cancel, ({ params }) => options.sessionRegistry.cancel(params))
|
|
36
47
|
.onRequest(methods.agent.session.close, ({ params }) => options.sessionRegistry.close(params));
|
|
@@ -84,6 +84,12 @@ export class AcpSessionEventMapper {
|
|
|
84
84
|
finishTools(turnId, terminalStatus = 'completed') {
|
|
85
85
|
return this.#enqueue(() => this.#tools.finishTools(turnId, terminalStatus));
|
|
86
86
|
}
|
|
87
|
+
pendingInteraction(pending) {
|
|
88
|
+
return this.#enqueue(() => this.#tools.pendingInteraction(pending));
|
|
89
|
+
}
|
|
90
|
+
resolvedInteraction(resolved, pending) {
|
|
91
|
+
return this.#enqueue(() => this.#tools.resolvedInteraction(resolved, pending));
|
|
92
|
+
}
|
|
87
93
|
/** Waits until every notification already accepted by this mapper has settled. */
|
|
88
94
|
flush() {
|
|
89
95
|
return this.#tail.then(() => {
|
|
@@ -0,0 +1,520 @@
|
|
|
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 { randomUUID } from 'node:crypto';
|
|
20
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
21
|
+
import { RequestError, } from '@agentclientprotocol/sdk';
|
|
22
|
+
import { decodeInteractionAnswer, isInteractionAnswerValidForRequest, INTERACTION_ANSWER_MAX_BYTES, INTERACTION_ANSWER_SERIALIZED_MAX_BYTES, } from '@maka/core/interaction';
|
|
23
|
+
import { RuntimeHostOperationError } from '@maka/runtime-host/client';
|
|
24
|
+
import { INTERACTION_MAX_PENDING_PER_SESSION, } from '@maka/runtime-host/protocol';
|
|
25
|
+
import { whileActive } from './active-promise.js';
|
|
26
|
+
// JSON Schema maxLength counts Unicode characters, while Core limits UTF-8
|
|
27
|
+
// bytes per answer and for the whole answer object. A character can occupy
|
|
28
|
+
// four UTF-8 bytes or six bytes when JSON-escaped (e.g. a control character).
|
|
29
|
+
function questionAnswerMaxChars(count) {
|
|
30
|
+
const envelopeBytes = Buffer.byteLength(JSON.stringify({ kind: 'question', answers: Array(count).fill('') }));
|
|
31
|
+
return Math.min(Math.floor(INTERACTION_ANSWER_MAX_BYTES / 4), Math.floor((INTERACTION_ANSWER_SERIALIZED_MAX_BYTES - envelopeBytes) / (count * 6)));
|
|
32
|
+
}
|
|
33
|
+
/** Connection-local presentation of Host interactions; the Host owns every answer and grant. */
|
|
34
|
+
export class AcpSessionInteractions {
|
|
35
|
+
#options;
|
|
36
|
+
#lifetime = new AbortController();
|
|
37
|
+
#pending = new Map();
|
|
38
|
+
#resolving = new Map();
|
|
39
|
+
#published = new Set();
|
|
40
|
+
#cancelledTurns = new Set();
|
|
41
|
+
#cancelledInteractions = new Set();
|
|
42
|
+
constructor(options) {
|
|
43
|
+
this.#options = options;
|
|
44
|
+
}
|
|
45
|
+
pending(snapshot) {
|
|
46
|
+
if (this.#lifetime.signal.aborted) {
|
|
47
|
+
return Promise.resolve();
|
|
48
|
+
}
|
|
49
|
+
if (this.#cancelledTurns.has(snapshot.turnId) ||
|
|
50
|
+
this.#cancelledInteractions.has(snapshot.interactionId)) {
|
|
51
|
+
this.#cancelledInteractions.add(snapshot.interactionId);
|
|
52
|
+
return Promise.resolve();
|
|
53
|
+
}
|
|
54
|
+
const existing = this.#pending.get(snapshot.interactionId);
|
|
55
|
+
if (existing)
|
|
56
|
+
return existing.task;
|
|
57
|
+
if (snapshot.sessionId !== this.#options.sessionId) {
|
|
58
|
+
this.#fail(snapshot, interactionError(snapshot, 'invalid_interaction', 'Wrong Session'));
|
|
59
|
+
return Promise.resolve();
|
|
60
|
+
}
|
|
61
|
+
if (this.#pending.size >= INTERACTION_MAX_PENDING_PER_SESSION) {
|
|
62
|
+
this.#fail(snapshot, interactionError(snapshot, 'interaction_capacity_exceeded'));
|
|
63
|
+
return Promise.resolve();
|
|
64
|
+
}
|
|
65
|
+
const entry = {
|
|
66
|
+
snapshot,
|
|
67
|
+
cancellation: new AbortController(),
|
|
68
|
+
task: Promise.resolve(),
|
|
69
|
+
};
|
|
70
|
+
this.#pending.set(snapshot.interactionId, entry);
|
|
71
|
+
entry.task = this.#present(entry)
|
|
72
|
+
.catch((error) => {
|
|
73
|
+
if (!entry.cancellation.signal.aborted)
|
|
74
|
+
this.#fail(snapshot, error);
|
|
75
|
+
})
|
|
76
|
+
.finally(() => {
|
|
77
|
+
if (this.#pending.get(snapshot.interactionId) === entry) {
|
|
78
|
+
this.#pending.delete(snapshot.interactionId);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
return entry.task;
|
|
82
|
+
}
|
|
83
|
+
resolved(pending) {
|
|
84
|
+
if (pending.sessionId !== this.#options.sessionId) {
|
|
85
|
+
this.#fail(pending, interactionError(pending, 'invalid_interaction', 'Wrong Session'));
|
|
86
|
+
return Promise.resolve();
|
|
87
|
+
}
|
|
88
|
+
this.#retire(pending.interactionId);
|
|
89
|
+
if (this.#lifetime.signal.aborted || this.#published.has(pending.interactionId)) {
|
|
90
|
+
this.#retireCancelledInteraction(pending);
|
|
91
|
+
return Promise.resolve();
|
|
92
|
+
}
|
|
93
|
+
const existing = this.#resolving.get(pending.interactionId);
|
|
94
|
+
if (existing)
|
|
95
|
+
return existing;
|
|
96
|
+
if (this.#resolving.size >= INTERACTION_MAX_PENDING_PER_SESSION) {
|
|
97
|
+
this.#fail(pending, interactionError(pending, 'interaction_capacity_exceeded'));
|
|
98
|
+
this.#retireCancelledInteraction(pending);
|
|
99
|
+
return Promise.resolve();
|
|
100
|
+
}
|
|
101
|
+
const task = this.#readResolved(pending)
|
|
102
|
+
.catch((error) => this.#fail(pending, error))
|
|
103
|
+
.finally(() => {
|
|
104
|
+
if (this.#resolving.get(pending.interactionId) === task) {
|
|
105
|
+
this.#resolving.delete(pending.interactionId);
|
|
106
|
+
}
|
|
107
|
+
this.#retireCancelledInteraction(pending);
|
|
108
|
+
});
|
|
109
|
+
this.#resolving.set(pending.interactionId, task);
|
|
110
|
+
return task;
|
|
111
|
+
}
|
|
112
|
+
cancelTurn(turnId) {
|
|
113
|
+
this.#cancelledTurns.add(turnId);
|
|
114
|
+
for (const entry of this.#pending.values()) {
|
|
115
|
+
if (entry.snapshot.turnId !== turnId)
|
|
116
|
+
continue;
|
|
117
|
+
this.#cancelledInteractions.add(entry.snapshot.interactionId);
|
|
118
|
+
this.#retire(entry.snapshot.interactionId);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
fencesTurn(turnId) {
|
|
122
|
+
return this.#cancelledTurns.has(turnId);
|
|
123
|
+
}
|
|
124
|
+
/** Local prompt settlement cannot prove the Host Turn is terminal after a failed Stop. */
|
|
125
|
+
settleTurn(turnId) {
|
|
126
|
+
for (const entry of this.#pending.values()) {
|
|
127
|
+
if (entry.snapshot.turnId === turnId)
|
|
128
|
+
this.#retire(entry.snapshot.interactionId);
|
|
129
|
+
}
|
|
130
|
+
// Keep cancelled Turns fenced until an authoritative terminal observation
|
|
131
|
+
// or attachment closure. A new interaction ID may arrive after Stop fails.
|
|
132
|
+
}
|
|
133
|
+
terminalTurn(turnId) {
|
|
134
|
+
this.#cancelledTurns.delete(turnId);
|
|
135
|
+
}
|
|
136
|
+
close() {
|
|
137
|
+
if (this.#lifetime.signal.aborted)
|
|
138
|
+
return;
|
|
139
|
+
this.#lifetime.abort();
|
|
140
|
+
for (const id of this.#pending.keys())
|
|
141
|
+
this.#retire(id);
|
|
142
|
+
this.#resolving.clear();
|
|
143
|
+
this.#published.clear();
|
|
144
|
+
this.#cancelledTurns.clear();
|
|
145
|
+
this.#cancelledInteractions.clear();
|
|
146
|
+
}
|
|
147
|
+
async #present(entry) {
|
|
148
|
+
const pending = entry.snapshot;
|
|
149
|
+
const signal = entry.cancellation.signal;
|
|
150
|
+
// A replayed pending snapshot can outlive an answer. Re-query the existing
|
|
151
|
+
// authority before opening another dialog rather than retaining an unbounded history.
|
|
152
|
+
const queried = await whileActive(this.#query(pending), signal);
|
|
153
|
+
if (!queried.active)
|
|
154
|
+
return;
|
|
155
|
+
assertSameInteraction(pending, queried.value);
|
|
156
|
+
if (queried.value.status !== 'pending') {
|
|
157
|
+
await this.#publishResolved(queried.value, pending, signal);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const request = pending.request;
|
|
161
|
+
if ((request.kind === 'question' || request.kind === 'form') &&
|
|
162
|
+
this.#options.client.capabilities.elicitation?.form == null) {
|
|
163
|
+
throw interactionError(pending, 'unsupported_interaction', 'Client requires form elicitation');
|
|
164
|
+
}
|
|
165
|
+
const presented = await whileActive(Promise.resolve().then(() => {
|
|
166
|
+
if (!signal.aborted)
|
|
167
|
+
return this.#options.onPending(pending);
|
|
168
|
+
}), signal);
|
|
169
|
+
if (!presented.active)
|
|
170
|
+
return;
|
|
171
|
+
let answer;
|
|
172
|
+
if (request.kind === 'question' || request.kind === 'form') {
|
|
173
|
+
const response = await whileActive(this.#options.client.createElicitation(elicitationRequest(pending), signal), signal);
|
|
174
|
+
if (!response.active)
|
|
175
|
+
return;
|
|
176
|
+
if (request.kind === 'question' && response.value.action === 'cancel') {
|
|
177
|
+
this.#cancelPendingTurn(pending);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
answer = elicitationAnswer(pending, response.value);
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
if (error instanceof RequestError)
|
|
185
|
+
throw error;
|
|
186
|
+
throw interactionError(pending, 'invalid_interaction_answer');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
const presentation = permissionPresentation(pending);
|
|
191
|
+
const response = await whileActive(this.#options.client.requestPermission(presentation.request, signal), signal);
|
|
192
|
+
if (!response.active)
|
|
193
|
+
return;
|
|
194
|
+
if (response.value.outcome.outcome === 'cancelled') {
|
|
195
|
+
this.#cancelPendingTurn(pending);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const optionId = response.value.outcome.optionId;
|
|
199
|
+
const selected = presentation.answers.get(optionId);
|
|
200
|
+
if (!selected) {
|
|
201
|
+
throw interactionError(pending, 'invalid_interaction_answer', 'Unknown permission option');
|
|
202
|
+
}
|
|
203
|
+
answer = selected;
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
answer = decodeInteractionAnswer(answer);
|
|
207
|
+
if (!isInteractionAnswerValidForRequest(pending.request, answer))
|
|
208
|
+
throw new Error();
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
throw interactionError(pending, 'invalid_interaction_answer');
|
|
212
|
+
}
|
|
213
|
+
if (signal.aborted)
|
|
214
|
+
return;
|
|
215
|
+
try {
|
|
216
|
+
const answered = await whileActive(this.#options.connection.request('interaction.answer', {
|
|
217
|
+
sessionId: pending.sessionId,
|
|
218
|
+
interactionId: pending.interactionId,
|
|
219
|
+
answer,
|
|
220
|
+
}), signal);
|
|
221
|
+
if (answered.active)
|
|
222
|
+
await this.#publishResolved(answered.value, pending, signal);
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
if (!(error instanceof RuntimeHostOperationError) || error.code !== 'already_resolved') {
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
228
|
+
await this.#readResolved(pending, signal);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
#query(pending) {
|
|
232
|
+
return this.#options.connection.request('interaction.query', {
|
|
233
|
+
sessionId: pending.sessionId,
|
|
234
|
+
interactionId: pending.interactionId,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
async #readResolved(pending, signal = this.#lifetime.signal) {
|
|
238
|
+
const active = AbortSignal.any([signal, this.#lifetime.signal]);
|
|
239
|
+
const queried = await whileActive(this.#query(pending), active);
|
|
240
|
+
if (!queried.active)
|
|
241
|
+
return;
|
|
242
|
+
assertSameInteraction(pending, queried.value);
|
|
243
|
+
if (queried.value.status !== 'pending') {
|
|
244
|
+
await this.#publishResolved(queried.value, pending, active);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
async #publishResolved(resolved, pending, signal = this.#lifetime.signal) {
|
|
248
|
+
const active = AbortSignal.any([signal, this.#lifetime.signal]);
|
|
249
|
+
if (active.aborted || this.#published.has(pending.interactionId))
|
|
250
|
+
return;
|
|
251
|
+
assertSameInteraction(pending, resolved);
|
|
252
|
+
this.#published.add(pending.interactionId);
|
|
253
|
+
// Keep replay deduplication bounded for a long-lived attachment. IDs older
|
|
254
|
+
// than this window may be projected again only if the Host replays a
|
|
255
|
+
// pathologically old resolved event.
|
|
256
|
+
if (this.#published.size > INTERACTION_MAX_PENDING_PER_SESSION) {
|
|
257
|
+
const oldest = this.#published.values().next();
|
|
258
|
+
if (!oldest.done)
|
|
259
|
+
this.#published.delete(oldest.value);
|
|
260
|
+
}
|
|
261
|
+
if (resolved.status === 'answered')
|
|
262
|
+
this.#options.onAnswered(resolved, pending);
|
|
263
|
+
await whileActive(Promise.resolve().then(() => {
|
|
264
|
+
if (!active.aborted)
|
|
265
|
+
return this.#options.onResolved(resolved, pending);
|
|
266
|
+
}), active);
|
|
267
|
+
}
|
|
268
|
+
#retire(interactionId) {
|
|
269
|
+
const entry = this.#pending.get(interactionId);
|
|
270
|
+
this.#pending.delete(interactionId);
|
|
271
|
+
entry?.cancellation.abort();
|
|
272
|
+
}
|
|
273
|
+
#retireCancelledInteraction(pending) {
|
|
274
|
+
this.#cancelledInteractions.delete(pending.interactionId);
|
|
275
|
+
}
|
|
276
|
+
#cancelPendingTurn(pending) {
|
|
277
|
+
this.cancelTurn(pending.turnId);
|
|
278
|
+
this.#options.onCancelled(pending);
|
|
279
|
+
}
|
|
280
|
+
#fail(pending, error) {
|
|
281
|
+
if (this.#lifetime.signal.aborted)
|
|
282
|
+
return;
|
|
283
|
+
let failure;
|
|
284
|
+
if (error instanceof RuntimeHostOperationError) {
|
|
285
|
+
failure = RequestError.internalError({ source: 'runtime_host', operation: error.operation, code: error.code }, 'Runtime Host interaction failed');
|
|
286
|
+
}
|
|
287
|
+
else if (error instanceof RequestError && error.code !== -32601) {
|
|
288
|
+
failure = error;
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
failure = interactionError(pending, error instanceof RequestError ? 'unsupported_interaction' : 'interaction_failed');
|
|
292
|
+
}
|
|
293
|
+
this.#options.onFailure(pending, failure);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function elicitationRequest(pending) {
|
|
297
|
+
const request = pending.request;
|
|
298
|
+
if (request.kind !== 'question' && request.kind !== 'form')
|
|
299
|
+
throw new Error('Not a form');
|
|
300
|
+
const properties = Object.create(null);
|
|
301
|
+
const required = [];
|
|
302
|
+
if (request.kind === 'question') {
|
|
303
|
+
const maxChars = questionAnswerMaxChars(request.questions.length);
|
|
304
|
+
request.questions.forEach((question, index) => {
|
|
305
|
+
properties[`q${index}`] = {
|
|
306
|
+
type: 'string',
|
|
307
|
+
title: question.question,
|
|
308
|
+
description: [
|
|
309
|
+
...question.options.map((option) => option.description ? `${option.label}: ${option.description}` : option.label),
|
|
310
|
+
'Enter an option or your own answer. Leave empty to skip this question.',
|
|
311
|
+
`Limit: ${maxChars} characters (${INTERACTION_ANSWER_MAX_BYTES} UTF-8 bytes per answer).`,
|
|
312
|
+
].join('\n'),
|
|
313
|
+
maxLength: maxChars,
|
|
314
|
+
};
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
for (const field of request.fields) {
|
|
319
|
+
properties[field.name] = formProperty(field);
|
|
320
|
+
if (field.required)
|
|
321
|
+
required.push(field.name);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return {
|
|
325
|
+
sessionId: pending.sessionId,
|
|
326
|
+
toolCallId: request.toolUseId,
|
|
327
|
+
mode: 'form',
|
|
328
|
+
message: request.kind === 'question'
|
|
329
|
+
? 'Please answer the following questions.'
|
|
330
|
+
: `${request.requester.name}${request.requester.source ? ` (${request.requester.source})` : ''}: ${request.message}`,
|
|
331
|
+
requestedSchema: { type: 'object', properties, required },
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
function formProperty(field) {
|
|
335
|
+
const base = {
|
|
336
|
+
title: field.label,
|
|
337
|
+
...(field.description === undefined ? {} : { description: field.description }),
|
|
338
|
+
...(field.default === undefined ? {} : { default: structuredClone(field.default) }),
|
|
339
|
+
};
|
|
340
|
+
switch (field.kind) {
|
|
341
|
+
case 'string':
|
|
342
|
+
return {
|
|
343
|
+
...base,
|
|
344
|
+
type: 'string',
|
|
345
|
+
...(field.minLength === undefined ? {} : { minLength: field.minLength }),
|
|
346
|
+
...(field.maxLength === undefined ? {} : { maxLength: field.maxLength }),
|
|
347
|
+
...(field.format === undefined ? {} : { format: field.format }),
|
|
348
|
+
};
|
|
349
|
+
case 'number':
|
|
350
|
+
return {
|
|
351
|
+
...base,
|
|
352
|
+
type: 'number',
|
|
353
|
+
...(field.minimum === undefined ? {} : { minimum: field.minimum }),
|
|
354
|
+
...(field.maximum === undefined ? {} : { maximum: field.maximum }),
|
|
355
|
+
};
|
|
356
|
+
case 'integer':
|
|
357
|
+
return {
|
|
358
|
+
...base,
|
|
359
|
+
type: 'integer',
|
|
360
|
+
minimum: Math.max(Number.MIN_SAFE_INTEGER, Math.ceil(field.minimum ?? Number.MIN_SAFE_INTEGER)),
|
|
361
|
+
maximum: Math.min(Number.MAX_SAFE_INTEGER, Math.floor(field.maximum ?? Number.MAX_SAFE_INTEGER)),
|
|
362
|
+
};
|
|
363
|
+
case 'boolean':
|
|
364
|
+
return { ...base, type: 'boolean' };
|
|
365
|
+
case 'single_select':
|
|
366
|
+
return {
|
|
367
|
+
...base,
|
|
368
|
+
type: 'string',
|
|
369
|
+
oneOf: field.options.map((option) => ({ const: option.value, title: option.label })),
|
|
370
|
+
};
|
|
371
|
+
case 'multi_select':
|
|
372
|
+
return {
|
|
373
|
+
...base,
|
|
374
|
+
type: 'array',
|
|
375
|
+
items: {
|
|
376
|
+
anyOf: field.options.map((option) => ({ const: option.value, title: option.label })),
|
|
377
|
+
},
|
|
378
|
+
...(field.minItems === undefined ? {} : { minItems: field.minItems }),
|
|
379
|
+
...(field.maxItems === undefined ? {} : { maxItems: field.maxItems }),
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
function elicitationAnswer(pending, response) {
|
|
384
|
+
const request = pending.request;
|
|
385
|
+
const action = response.action;
|
|
386
|
+
if (action !== 'accept' && action !== 'decline' && action !== 'cancel') {
|
|
387
|
+
throw interactionError(pending, 'invalid_interaction_answer', 'Unknown elicitation action');
|
|
388
|
+
}
|
|
389
|
+
if (request.kind === 'form') {
|
|
390
|
+
return decodeInteractionAnswer(action === 'accept'
|
|
391
|
+
? { kind: 'form', action, values: response.content ?? {} }
|
|
392
|
+
: { kind: 'form', action });
|
|
393
|
+
}
|
|
394
|
+
if (request.kind !== 'question')
|
|
395
|
+
throw new Error('Not a question');
|
|
396
|
+
if (response.action === 'cancel') {
|
|
397
|
+
throw interactionError(pending, 'invalid_interaction_answer', 'Question cancellation escaped');
|
|
398
|
+
}
|
|
399
|
+
if (response.action === 'decline') {
|
|
400
|
+
return { kind: 'question', answers: request.questions.map(() => null) };
|
|
401
|
+
}
|
|
402
|
+
if (response.action !== 'accept') {
|
|
403
|
+
throw interactionError(pending, 'invalid_interaction_answer', 'Unknown elicitation action');
|
|
404
|
+
}
|
|
405
|
+
const content = response.content ?? {};
|
|
406
|
+
if (typeof content !== 'object' ||
|
|
407
|
+
Array.isArray(content) ||
|
|
408
|
+
Object.keys(content).some((key) => !request.questions.some((_, index) => key === `q${index}`))) {
|
|
409
|
+
throw interactionError(pending, 'invalid_interaction_answer', 'Invalid question fields');
|
|
410
|
+
}
|
|
411
|
+
return {
|
|
412
|
+
kind: 'question',
|
|
413
|
+
answers: request.questions.map((_, index) => {
|
|
414
|
+
const value = content[`q${index}`];
|
|
415
|
+
if (value === undefined)
|
|
416
|
+
return null;
|
|
417
|
+
if (typeof value !== 'string') {
|
|
418
|
+
throw interactionError(pending, 'invalid_interaction_answer', 'Question answer is not text');
|
|
419
|
+
}
|
|
420
|
+
return value.trim() || null;
|
|
421
|
+
}),
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
function permissionPresentation(pending) {
|
|
425
|
+
const request = pending.request;
|
|
426
|
+
if (request.kind !== 'permission' &&
|
|
427
|
+
request.kind !== 'sandbox_boundary' &&
|
|
428
|
+
request.kind !== 'client_capability') {
|
|
429
|
+
throw new Error('Not a permission request');
|
|
430
|
+
}
|
|
431
|
+
const allow = randomUUID();
|
|
432
|
+
const deny = randomUUID();
|
|
433
|
+
if (request.kind === 'permission') {
|
|
434
|
+
const canRemember = request.prompt.kind === 'tool_permission' && request.prompt.rememberForTurnAllowed;
|
|
435
|
+
const options = [
|
|
436
|
+
{ optionId: allow, name: 'Allow once', kind: 'allow_once' },
|
|
437
|
+
];
|
|
438
|
+
const answers = new Map([
|
|
439
|
+
[allow, { kind: 'permission', decision: 'allow', rememberForTurn: false }],
|
|
440
|
+
]);
|
|
441
|
+
if (canRemember) {
|
|
442
|
+
const remember = randomUUID();
|
|
443
|
+
options.push({ optionId: remember, name: 'Allow for this Turn', kind: 'allow_always' });
|
|
444
|
+
answers.set(remember, { kind: 'permission', decision: 'allow', rememberForTurn: true });
|
|
445
|
+
}
|
|
446
|
+
options.push({ optionId: deny, name: 'Reject', kind: 'reject_once' });
|
|
447
|
+
answers.set(deny, { kind: 'permission', decision: 'deny', rememberForTurn: false });
|
|
448
|
+
return {
|
|
449
|
+
request: {
|
|
450
|
+
sessionId: pending.sessionId,
|
|
451
|
+
toolCall: {
|
|
452
|
+
toolCallId: request.toolUseId,
|
|
453
|
+
title: `Authorize ${request.prompt.toolName}`,
|
|
454
|
+
status: 'pending',
|
|
455
|
+
content: [
|
|
456
|
+
{
|
|
457
|
+
type: 'content',
|
|
458
|
+
content: {
|
|
459
|
+
type: 'text',
|
|
460
|
+
text: `Review this exact permission request:\n${JSON.stringify(request.prompt, null, 2)}`,
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
],
|
|
464
|
+
},
|
|
465
|
+
options,
|
|
466
|
+
},
|
|
467
|
+
answers,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
const boundary = request.kind === 'sandbox_boundary';
|
|
471
|
+
return {
|
|
472
|
+
request: {
|
|
473
|
+
sessionId: pending.sessionId,
|
|
474
|
+
toolCall: {
|
|
475
|
+
toolCallId: boundary ? pending.interactionId : request.toolUseId,
|
|
476
|
+
title: boundary
|
|
477
|
+
? 'Expand this Session’s sandbox boundary'
|
|
478
|
+
: 'Authorize a Session capability',
|
|
479
|
+
status: 'pending',
|
|
480
|
+
content: [
|
|
481
|
+
{
|
|
482
|
+
type: 'content',
|
|
483
|
+
content: {
|
|
484
|
+
type: 'text',
|
|
485
|
+
text: boundary
|
|
486
|
+
? `${request.justification}\n\nApply only this requested expansion to this Session’s boundary:\n${JSON.stringify(request.expansion, null, 2)}`
|
|
487
|
+
: `Grant only the following exact capability target for this Session:\n${JSON.stringify(request.target, null, 2)}`,
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
],
|
|
491
|
+
},
|
|
492
|
+
options: [
|
|
493
|
+
{
|
|
494
|
+
optionId: allow,
|
|
495
|
+
name: boundary
|
|
496
|
+
? 'Apply this expansion to this Session'
|
|
497
|
+
: 'Allow this scope for this Session',
|
|
498
|
+
kind: 'allow_always',
|
|
499
|
+
},
|
|
500
|
+
{ optionId: deny, name: 'Reject', kind: 'reject_once' },
|
|
501
|
+
],
|
|
502
|
+
},
|
|
503
|
+
answers: new Map([
|
|
504
|
+
[allow, { kind: request.kind, decision: 'allow' }],
|
|
505
|
+
[deny, { kind: request.kind, decision: 'deny' }],
|
|
506
|
+
]),
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
function assertSameInteraction(pending, actual) {
|
|
510
|
+
if (pending.interactionId !== actual.interactionId ||
|
|
511
|
+
pending.sessionId !== actual.sessionId ||
|
|
512
|
+
pending.turnId !== actual.turnId ||
|
|
513
|
+
pending.runId !== actual.runId ||
|
|
514
|
+
!isDeepStrictEqual(pending.request, actual.request)) {
|
|
515
|
+
throw interactionError(pending, 'invalid_interaction', 'Host interaction identity changed');
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
function interactionError(pending, code, detail) {
|
|
519
|
+
return RequestError.internalError({ source: 'adapter', code, kind: pending.request.kind, interactionId: pending.interactionId }, detail ? `ACP interaction failed: ${detail}` : 'ACP interaction failed');
|
|
520
|
+
}
|