maka-agent 0.2.0-dev.31.20260913 → 0.2.0-dev.32.20260914
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/pi-transcript.js +3 -4
- package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
- package/node_modules/@maka/core/dist/agent-graph-schedule.js +15 -4
- package/node_modules/@maka/core/dist/executor-id.js +22 -0
- package/node_modules/@maka/core/dist/external-session.js +18 -0
- package/node_modules/@maka/core/dist/model-call-usage-projection.js +0 -13
- package/node_modules/@maka/core/dist/runtime-event.js +19 -3
- package/node_modules/@maka/core/dist/session-send-projection.js +6 -3
- package/node_modules/@maka/core/dist/session.js +8 -2
- package/node_modules/@maka/core/dist/shell-run-result.js +1 -0
- package/node_modules/@maka/core/dist/shell-run.js +4 -0
- package/node_modules/@maka/core/dist/work-board.js +94 -1
- package/node_modules/@maka/core/package.json +1 -0
- package/node_modules/@maka/eval/dist/fleet-simulation.js +270 -0
- package/node_modules/@maka/eval/dist/fleet-store.js +243 -0
- package/node_modules/@maka/eval/dist/fleet-worker.js +125 -0
- package/node_modules/@maka/eval/dist/fleet.js +329 -0
- package/node_modules/@maka/eval/dist/index.js +3 -0
- package/node_modules/@maka/runtime/dist/agent-run.js +2 -17
- package/node_modules/@maka/runtime/dist/ai-sdk-compaction.js +6 -2
- package/node_modules/@maka/runtime/dist/ai-sdk-message-projection.js +36 -24
- package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +257 -330
- package/node_modules/@maka/runtime/dist/background-task-health-tool.js +104 -0
- package/node_modules/@maka/runtime/dist/history-compaction.js +3 -1
- package/node_modules/@maka/runtime/dist/local-web-fetch.js +1 -1
- package/node_modules/@maka/runtime/dist/model-adapter.js +62 -53
- package/node_modules/@maka/runtime/dist/model-history.js +1 -5
- package/node_modules/@maka/runtime/dist/plugin-executor-backend.js +311 -0
- package/node_modules/@maka/runtime/dist/plugin-executor-service.js +344 -0
- package/node_modules/@maka/runtime/dist/provider-error-classification.js +7 -1
- package/node_modules/@maka/runtime/dist/provider-request-telemetry.js +35 -8
- package/node_modules/@maka/runtime/dist/runtime-event-backfill.js +7 -1
- package/node_modules/@maka/runtime/dist/runtime-event-read-model.js +1 -0
- package/node_modules/@maka/runtime/dist/runtime-invocation-route.js +49 -0
- package/node_modules/@maka/runtime/dist/runtime-kernel.js +72 -150
- package/node_modules/@maka/runtime/dist/runtime-read-model.js +0 -2
- package/node_modules/@maka/runtime/dist/session-event-runtime-mapper.js +3 -0
- package/node_modules/@maka/runtime/dist/session-manager.js +100 -58
- package/node_modules/@maka/runtime/dist/shell-run-manager.js +9 -3
- package/node_modules/@maka/runtime/dist/shell-run-tool-result.js +1 -0
- package/node_modules/@maka/runtime/dist/stream-graph-schedule-reconcile.js +1 -0
- package/node_modules/@maka/runtime/dist/stream-graph-supervisor-tools.js +26 -4
- package/node_modules/@maka/runtime/dist/subagent-tools.js +7 -0
- package/node_modules/@maka/runtime/dist/tool-runtime.js +45 -466
- package/node_modules/@maka/runtime/package.json +3 -0
- package/node_modules/@maka/runtime-host/dist/adapter/session-projector.js +7 -2
- package/node_modules/@maka/runtime-host/dist/client/session-catalog-summary.js +1 -0
- package/node_modules/@maka/runtime-host/dist/protocol/external-session.js +24 -3
- package/node_modules/@maka/runtime-host/dist/protocol/index.js +4 -1
- package/node_modules/@maka/runtime-host/dist/protocol/plugin-platform.js +42 -5
- package/node_modules/@maka/runtime-host/dist/protocol/session-catalog.js +31 -3
- package/node_modules/@maka/runtime-host/dist/protocol/session-continuity.js +6 -0
- package/node_modules/@maka/runtime-host/dist/server/child-agent-composition.js +1 -0
- package/node_modules/@maka/runtime-host/dist/server/execution-artifacts.js +1 -22
- package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +54 -8
- package/node_modules/@maka/runtime-host/dist/server/external-session-coordinator.js +14 -1
- package/node_modules/@maka/runtime-host/dist/server/host-session-availability.js +10 -2
- package/node_modules/@maka/runtime-host/dist/server/plugin-platform-coordinator.js +6 -0
- package/node_modules/@maka/runtime-host/dist/server/plugin-platform.js +6 -0
- package/node_modules/@maka/runtime-host/dist/server/session-catalog-coordinator.js +56 -16
- package/node_modules/@maka/runtime-host/dist/server/session-continuity-coordinator.js +4 -3
- package/node_modules/@maka/runtime-host/dist/server/session-revision-coordinator.js +4 -0
- package/node_modules/@maka/runtime-host/dist/server/web-fetch-tool.js +37 -1
- package/node_modules/@maka/storage/dist/claude-code-session-adapter.js +450 -234
- package/node_modules/@maka/storage/dist/claude-code-transcript-lineage.js +106 -77
- package/node_modules/@maka/storage/dist/legacy-run-header.js +2 -2
- package/node_modules/@maka/storage/dist/model-call-usage-sql.js +1 -1
- package/node_modules/@maka/storage/dist/session-store.js +12 -2
- package/node_modules/@maka/storage/dist/sqlite-long-term-memory-store.js +86 -12
- package/node_modules/@maka/storage/dist/work-board-store.js +40 -1
- package/package.json +1 -1
|
@@ -0,0 +1,311 @@
|
|
|
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 { AsyncEventQueue } from './async-queue.js';
|
|
21
|
+
/** Converts a small plugin executor contract into Maka's existing Run event stream. */
|
|
22
|
+
export class PluginExecutorBackend {
|
|
23
|
+
kind = 'plugin-executor';
|
|
24
|
+
sessionId;
|
|
25
|
+
#cwd;
|
|
26
|
+
#instructions;
|
|
27
|
+
#binding;
|
|
28
|
+
#newId;
|
|
29
|
+
#now;
|
|
30
|
+
#active = new Set();
|
|
31
|
+
#disposed = false;
|
|
32
|
+
constructor(input) {
|
|
33
|
+
this.sessionId = input.sessionId;
|
|
34
|
+
this.#cwd = input.cwd;
|
|
35
|
+
this.#instructions = input.instructions;
|
|
36
|
+
this.#binding = input.binding;
|
|
37
|
+
this.#newId = input.newId ?? randomUUID;
|
|
38
|
+
this.#now = input.now ?? Date.now;
|
|
39
|
+
}
|
|
40
|
+
async *send(input) {
|
|
41
|
+
if (this.#disposed)
|
|
42
|
+
throw new Error('Plugin executor backend is disposed');
|
|
43
|
+
const abort = new AbortController();
|
|
44
|
+
const queue = new AsyncEventQueue();
|
|
45
|
+
const messageId = this.#newId();
|
|
46
|
+
const producer = this.#produce(input, messageId, abort.signal, queue).finally(() => queue.close());
|
|
47
|
+
const active = { abort, settled: producer };
|
|
48
|
+
this.#active.add(active);
|
|
49
|
+
try {
|
|
50
|
+
for await (const event of queue) {
|
|
51
|
+
yield event;
|
|
52
|
+
queue.ackConsumed();
|
|
53
|
+
}
|
|
54
|
+
await producer;
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
queue.noteConsumerDetached();
|
|
58
|
+
abort.abort(new Error('Plugin executor event consumer detached'));
|
|
59
|
+
await producer.catch(() => undefined);
|
|
60
|
+
this.#active.delete(active);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async stop(reason) {
|
|
64
|
+
const active = [...this.#active];
|
|
65
|
+
for (const execution of active)
|
|
66
|
+
execution.abort.abort(new Error(reason));
|
|
67
|
+
await Promise.allSettled(active.map((execution) => execution.settled));
|
|
68
|
+
}
|
|
69
|
+
async respondToSandboxBoundary(_response) {
|
|
70
|
+
throw new Error('Plugin executor does not expose Maka sandbox-boundary requests');
|
|
71
|
+
}
|
|
72
|
+
async respondToUserQuestion(_response) {
|
|
73
|
+
throw new Error('Plugin executor does not expose Maka user-question requests');
|
|
74
|
+
}
|
|
75
|
+
async dispose() {
|
|
76
|
+
if (this.#disposed)
|
|
77
|
+
return;
|
|
78
|
+
this.#disposed = true;
|
|
79
|
+
await this.stop('user_stop');
|
|
80
|
+
}
|
|
81
|
+
async #produce(input, messageId, signal, queue) {
|
|
82
|
+
const turnId = input.turnId;
|
|
83
|
+
let thinkingText = '';
|
|
84
|
+
const toolUseIds = new Map();
|
|
85
|
+
let result;
|
|
86
|
+
let failure;
|
|
87
|
+
let failed = false;
|
|
88
|
+
try {
|
|
89
|
+
result = await this.#binding.execute({
|
|
90
|
+
sessionId: this.sessionId,
|
|
91
|
+
turnId,
|
|
92
|
+
...(input.runId ? { runId: input.runId } : {}),
|
|
93
|
+
conversationKey: this.sessionId,
|
|
94
|
+
text: input.text,
|
|
95
|
+
cwd: this.#cwd,
|
|
96
|
+
...(this.#instructions ? { instructions: this.#instructions } : {}),
|
|
97
|
+
...(input.attachments ? { attachments: input.attachments } : {}),
|
|
98
|
+
...(input.directoryReferences ? { directoryReferences: input.directoryReferences } : {}),
|
|
99
|
+
...(input.quotes ? { quotes: input.quotes } : {}),
|
|
100
|
+
}, {
|
|
101
|
+
signal,
|
|
102
|
+
onEvent: (event) => {
|
|
103
|
+
if (event.type === 'thinking_delta')
|
|
104
|
+
thinkingText += event.text;
|
|
105
|
+
this.#publishOutputEvent(turnId, messageId, event, toolUseIds, queue);
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
failed = true;
|
|
111
|
+
failure = error;
|
|
112
|
+
}
|
|
113
|
+
this.#closeOptionalOutput(turnId, messageId, thinkingText, toolUseIds, queue);
|
|
114
|
+
if (failed) {
|
|
115
|
+
if (signal.aborted)
|
|
116
|
+
this.#publishCancellation(turnId, { status: 'cancelled', source: 'caller' }, queue);
|
|
117
|
+
else
|
|
118
|
+
this.#publishFailure(turnId, failure instanceof Error ? failure.message : 'External executor failed', undefined, false, queue);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (result === undefined) {
|
|
122
|
+
this.#publishFailure(turnId, 'External executor returned no terminal result', undefined, false, queue);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
this.#publishResult(turnId, messageId, result, queue);
|
|
126
|
+
}
|
|
127
|
+
#closeOptionalOutput(turnId, messageId, thinkingText, toolUseIds, queue) {
|
|
128
|
+
if (thinkingText) {
|
|
129
|
+
queue.push({
|
|
130
|
+
type: 'thinking_complete',
|
|
131
|
+
id: this.#newId(),
|
|
132
|
+
turnId,
|
|
133
|
+
ts: this.#now(),
|
|
134
|
+
messageId,
|
|
135
|
+
text: thinkingText,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
for (const toolUseId of toolUseIds.values()) {
|
|
139
|
+
queue.push({
|
|
140
|
+
type: 'tool_result',
|
|
141
|
+
id: this.#newId(),
|
|
142
|
+
turnId,
|
|
143
|
+
ts: this.#now(),
|
|
144
|
+
toolUseId,
|
|
145
|
+
providerExecuted: true,
|
|
146
|
+
isError: true,
|
|
147
|
+
content: { kind: 'text', text: 'External executor ended before reporting a tool result' },
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
#publishResult(turnId, messageId, result, queue) {
|
|
152
|
+
if (result.status === 'completed') {
|
|
153
|
+
queue.push({
|
|
154
|
+
type: 'text_complete',
|
|
155
|
+
id: this.#newId(),
|
|
156
|
+
turnId,
|
|
157
|
+
ts: this.#now(),
|
|
158
|
+
messageId,
|
|
159
|
+
text: result.text,
|
|
160
|
+
});
|
|
161
|
+
queue.push({
|
|
162
|
+
type: 'complete',
|
|
163
|
+
id: this.#newId(),
|
|
164
|
+
turnId,
|
|
165
|
+
ts: this.#now(),
|
|
166
|
+
stopReason: 'end_turn',
|
|
167
|
+
});
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (result.status === 'cancelled') {
|
|
171
|
+
this.#publishCancellation(turnId, result, queue);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
this.#publishFailure(turnId, result.message, result.code, result.recoverable ?? false, queue);
|
|
175
|
+
}
|
|
176
|
+
#publishCancellation(turnId, result, queue) {
|
|
177
|
+
const reason = cancellationEventReason(result);
|
|
178
|
+
queue.push({
|
|
179
|
+
type: 'abort',
|
|
180
|
+
id: this.#newId(),
|
|
181
|
+
turnId,
|
|
182
|
+
ts: this.#now(),
|
|
183
|
+
reason,
|
|
184
|
+
});
|
|
185
|
+
queue.push({
|
|
186
|
+
type: 'complete',
|
|
187
|
+
id: this.#newId(),
|
|
188
|
+
turnId,
|
|
189
|
+
ts: this.#now(),
|
|
190
|
+
stopReason: 'user_stop',
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
#publishOutputEvent(turnId, messageId, event, toolUseIds, queue) {
|
|
194
|
+
if (event.type === 'output_delta') {
|
|
195
|
+
if (!event.text)
|
|
196
|
+
return;
|
|
197
|
+
queue.push({
|
|
198
|
+
type: 'text_delta',
|
|
199
|
+
id: this.#newId(),
|
|
200
|
+
turnId,
|
|
201
|
+
ts: this.#now(),
|
|
202
|
+
messageId,
|
|
203
|
+
text: event.text,
|
|
204
|
+
});
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (event.type === 'thinking_delta') {
|
|
208
|
+
if (!event.text)
|
|
209
|
+
return;
|
|
210
|
+
queue.push({
|
|
211
|
+
type: 'thinking_delta',
|
|
212
|
+
id: this.#newId(),
|
|
213
|
+
turnId,
|
|
214
|
+
ts: this.#now(),
|
|
215
|
+
messageId,
|
|
216
|
+
text: event.text,
|
|
217
|
+
});
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (event.type === 'tool_start') {
|
|
221
|
+
const previousToolUseId = toolUseIds.get(event.toolCallId);
|
|
222
|
+
if (previousToolUseId) {
|
|
223
|
+
queue.push({
|
|
224
|
+
type: 'tool_result',
|
|
225
|
+
id: this.#newId(),
|
|
226
|
+
turnId,
|
|
227
|
+
ts: this.#now(),
|
|
228
|
+
toolUseId: previousToolUseId,
|
|
229
|
+
providerExecuted: true,
|
|
230
|
+
isError: true,
|
|
231
|
+
content: { kind: 'text', text: 'External executor reused an active tool call id' },
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
const toolUseId = this.#newId();
|
|
235
|
+
toolUseIds.set(event.toolCallId, toolUseId);
|
|
236
|
+
queue.push({
|
|
237
|
+
type: 'tool_start',
|
|
238
|
+
id: this.#newId(),
|
|
239
|
+
turnId,
|
|
240
|
+
ts: this.#now(),
|
|
241
|
+
toolUseId,
|
|
242
|
+
toolName: event.name,
|
|
243
|
+
args: event.input ?? {},
|
|
244
|
+
providerExecuted: true,
|
|
245
|
+
stepId: messageId,
|
|
246
|
+
...(event.displayName === undefined ? {} : { displayName: event.displayName }),
|
|
247
|
+
...(event.activityKind === undefined ? {} : { activityKind: event.activityKind }),
|
|
248
|
+
});
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const toolUseId = toolUseIds.get(event.toolCallId);
|
|
252
|
+
if (!toolUseId)
|
|
253
|
+
return;
|
|
254
|
+
if (event.type === 'tool_progress') {
|
|
255
|
+
if (!event.text)
|
|
256
|
+
return;
|
|
257
|
+
queue.push({
|
|
258
|
+
type: 'tool_progress',
|
|
259
|
+
id: this.#newId(),
|
|
260
|
+
turnId,
|
|
261
|
+
ts: this.#now(),
|
|
262
|
+
toolUseId,
|
|
263
|
+
chunk: event.text,
|
|
264
|
+
});
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
toolUseIds.delete(event.toolCallId);
|
|
268
|
+
queue.push({
|
|
269
|
+
type: 'tool_result',
|
|
270
|
+
id: this.#newId(),
|
|
271
|
+
turnId,
|
|
272
|
+
ts: this.#now(),
|
|
273
|
+
toolUseId,
|
|
274
|
+
providerExecuted: true,
|
|
275
|
+
isError: event.isError ?? false,
|
|
276
|
+
content: { kind: 'text', text: event.text },
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
#publishFailure(turnId, message, code, recoverable, queue) {
|
|
280
|
+
queue.push({
|
|
281
|
+
type: 'error',
|
|
282
|
+
id: this.#newId(),
|
|
283
|
+
turnId,
|
|
284
|
+
ts: this.#now(),
|
|
285
|
+
recoverable,
|
|
286
|
+
...(code ? { code, reason: code } : {}),
|
|
287
|
+
message: boundedMessage(message),
|
|
288
|
+
});
|
|
289
|
+
queue.push({
|
|
290
|
+
type: 'complete',
|
|
291
|
+
id: this.#newId(),
|
|
292
|
+
turnId,
|
|
293
|
+
ts: this.#now(),
|
|
294
|
+
stopReason: 'error',
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
function boundedMessage(value) {
|
|
299
|
+
if (value.length <= 8_192)
|
|
300
|
+
return value;
|
|
301
|
+
return `${value.slice(0, 8_191)}…`;
|
|
302
|
+
}
|
|
303
|
+
function cancellationEventReason(result) {
|
|
304
|
+
if (result.reason === 'redirect')
|
|
305
|
+
return 'redirect';
|
|
306
|
+
if (result.reason === 'timeout')
|
|
307
|
+
return 'timeout';
|
|
308
|
+
if (result.source === 'executor_retired')
|
|
309
|
+
return 'crash';
|
|
310
|
+
return 'user_stop';
|
|
311
|
+
}
|
|
@@ -0,0 +1,344 @@
|
|
|
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 { TOOL_ACTIVITY_KINDS } from '@maka/core/events';
|
|
21
|
+
import { isExecutorId } from '@maka/core/executor-id';
|
|
22
|
+
import { Service } from './plugin-kernel.js';
|
|
23
|
+
import { MakaPluginRuntimeError, pluginIdentity, registerPluginContribution, } from './plugin-runtime.js';
|
|
24
|
+
import { PluginScopeRegistry } from './plugin-scope-registry.js';
|
|
25
|
+
class ExecutorRetiredAbort extends Error {
|
|
26
|
+
constructor(executorId) {
|
|
27
|
+
super(`Executor was retired: ${executorId}`);
|
|
28
|
+
this.name = 'ExecutorRetiredAbort';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Scoped black-box execution registry.
|
|
33
|
+
*
|
|
34
|
+
* The service owns only registration, visibility, cancellation, and result
|
|
35
|
+
* validation. Protocol processes, credentials, external conversation ids, and
|
|
36
|
+
* tools remain private to the contributing plugin.
|
|
37
|
+
*/
|
|
38
|
+
export class PluginExecutorService extends Service {
|
|
39
|
+
registry = new PluginScopeRegistry();
|
|
40
|
+
onChanged;
|
|
41
|
+
constructor(ctx, options = {}) {
|
|
42
|
+
super(ctx, 'executors');
|
|
43
|
+
this.onChanged = options.onChanged;
|
|
44
|
+
}
|
|
45
|
+
register(provider) {
|
|
46
|
+
validateProvider(provider);
|
|
47
|
+
const identity = pluginIdentity(this.ctx);
|
|
48
|
+
return registerPluginContribution(this.ctx, `executors.register(${provider.id})`, () => {
|
|
49
|
+
const rootId = identity.scopeId;
|
|
50
|
+
const existing = this.registry.get(rootId, provider.id);
|
|
51
|
+
if (existing && existing.entryId !== identity.entryId) {
|
|
52
|
+
throw new MakaPluginRuntimeError('activation_failed', `Executor is already registered in this scope: ${provider.id}`);
|
|
53
|
+
}
|
|
54
|
+
const capabilities = normalizeCapabilities(provider.capabilities);
|
|
55
|
+
const registeredProvider = Object.freeze({
|
|
56
|
+
id: provider.id,
|
|
57
|
+
...(provider.displayName === undefined ? {} : { displayName: provider.displayName }),
|
|
58
|
+
capabilities,
|
|
59
|
+
execute: provider.execute.bind(provider),
|
|
60
|
+
});
|
|
61
|
+
const entry = {
|
|
62
|
+
...identity,
|
|
63
|
+
provider: registeredProvider,
|
|
64
|
+
token: Symbol(provider.id),
|
|
65
|
+
active: new Set(),
|
|
66
|
+
retired: false,
|
|
67
|
+
};
|
|
68
|
+
return this.registry.publish(rootId, provider.id, entry, {
|
|
69
|
+
...(this.onChanged ? { onChanged: this.onChanged } : {}),
|
|
70
|
+
onRetired: async (retired) => {
|
|
71
|
+
for (const execution of retired.active) {
|
|
72
|
+
execution.abort.abort(new ExecutorRetiredAbort(retired.provider.id));
|
|
73
|
+
}
|
|
74
|
+
await Promise.allSettled([...retired.active].map((execution) => execution.settled));
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
list(sessionId) {
|
|
80
|
+
assertSessionId(sessionId);
|
|
81
|
+
return Object.freeze([...this.registry.visible(sessionId).values()]
|
|
82
|
+
.sort((left, right) => left.provider.id.localeCompare(right.provider.id))
|
|
83
|
+
.map(({ provider, token: _token, active: _active, retired: _retired, ...identity }) => Object.freeze({
|
|
84
|
+
...identity,
|
|
85
|
+
id: provider.id,
|
|
86
|
+
displayName: provider.displayName?.trim() || provider.id,
|
|
87
|
+
capabilities: normalizeCapabilities(provider.capabilities),
|
|
88
|
+
})));
|
|
89
|
+
}
|
|
90
|
+
inspect(rootId) {
|
|
91
|
+
const seen = new Set();
|
|
92
|
+
return Object.freeze([...this.registry.entries(rootId)]
|
|
93
|
+
.filter((entry) => !seen.has(entry) && Boolean(seen.add(entry)))
|
|
94
|
+
.sort((left, right) => left.provider.id.localeCompare(right.provider.id))
|
|
95
|
+
.map(({ provider, token: _token, active: _active, retired: _retired, ...identity }) => Object.freeze({
|
|
96
|
+
...identity,
|
|
97
|
+
id: provider.id,
|
|
98
|
+
displayName: provider.displayName?.trim() || provider.id,
|
|
99
|
+
capabilities: normalizeCapabilities(provider.capabilities),
|
|
100
|
+
})));
|
|
101
|
+
}
|
|
102
|
+
identity(sessionId, executorId) {
|
|
103
|
+
return this.identityForEntry(this.entry(sessionId, executorId));
|
|
104
|
+
}
|
|
105
|
+
bind(sessionId, executorId) {
|
|
106
|
+
const entry = this.entry(sessionId, executorId);
|
|
107
|
+
const identity = this.identityForEntry(entry);
|
|
108
|
+
return Object.freeze({
|
|
109
|
+
identity,
|
|
110
|
+
providerStateIdentity: providerStateIdentity(identity),
|
|
111
|
+
execute: (request, options = {}) => {
|
|
112
|
+
if (request.sessionId !== sessionId) {
|
|
113
|
+
throw new Error('Executor binding cannot cross Session scope');
|
|
114
|
+
}
|
|
115
|
+
return this.executeEntry(entry, request, options);
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
async execute(executorId, request, options = {}) {
|
|
120
|
+
const normalizedRequest = normalizeRequest(request);
|
|
121
|
+
return this.executeEntry(this.entry(normalizedRequest.sessionId, executorId), normalizedRequest, options);
|
|
122
|
+
}
|
|
123
|
+
async executeEntry(entry, request, options) {
|
|
124
|
+
const normalizedRequest = normalizeRequest(request);
|
|
125
|
+
const abort = new AbortController();
|
|
126
|
+
const signal = options.signal ? AbortSignal.any([options.signal, abort.signal]) : abort.signal;
|
|
127
|
+
let settle;
|
|
128
|
+
const settled = new Promise((resolve) => {
|
|
129
|
+
settle = resolve;
|
|
130
|
+
});
|
|
131
|
+
const active = { abort, settled };
|
|
132
|
+
entry.active.add(active);
|
|
133
|
+
try {
|
|
134
|
+
if (entry.retired)
|
|
135
|
+
return cancelledResult(new ExecutorRetiredAbort(entry.provider.id));
|
|
136
|
+
try {
|
|
137
|
+
const result = await entry.provider.execute(normalizedRequest, {
|
|
138
|
+
signal,
|
|
139
|
+
emit: (event) => {
|
|
140
|
+
if (signal.aborted || entry.retired)
|
|
141
|
+
return;
|
|
142
|
+
const normalized = normalizeOutputEvent(event, entry.provider.capabilities);
|
|
143
|
+
try {
|
|
144
|
+
options.onEvent?.(normalized);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// A presentation observer must not change external execution.
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
if (signal.aborted)
|
|
152
|
+
return cancelledResult(signal.reason);
|
|
153
|
+
return normalizeResult(result);
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
if (signal.aborted)
|
|
157
|
+
return cancelledResult(signal.reason);
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
entry.active.delete(active);
|
|
163
|
+
settle();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
entry(sessionId, executorId) {
|
|
167
|
+
assertSessionId(sessionId);
|
|
168
|
+
if (!isExecutorId(executorId))
|
|
169
|
+
throw new TypeError('Executor id is invalid');
|
|
170
|
+
const entry = this.registry.visible(sessionId).get(executorId);
|
|
171
|
+
if (!entry || entry.retired)
|
|
172
|
+
throw new Error(`Executor is unavailable: ${executorId}`);
|
|
173
|
+
return entry;
|
|
174
|
+
}
|
|
175
|
+
identityForEntry(entry) {
|
|
176
|
+
return Object.freeze({
|
|
177
|
+
entryId: entry.entryId,
|
|
178
|
+
scopeId: entry.scopeId,
|
|
179
|
+
extensionId: entry.extensionId,
|
|
180
|
+
generation: entry.generation,
|
|
181
|
+
id: entry.provider.id,
|
|
182
|
+
displayName: entry.provider.displayName?.trim() || entry.provider.id,
|
|
183
|
+
capabilities: normalizeCapabilities(entry.provider.capabilities),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function validateProvider(provider) {
|
|
188
|
+
if (!provider || typeof provider !== 'object')
|
|
189
|
+
throw new TypeError('Executor provider is required');
|
|
190
|
+
if (!isExecutorId(provider.id))
|
|
191
|
+
throw new TypeError('Executor id is invalid');
|
|
192
|
+
if (typeof provider.execute !== 'function') {
|
|
193
|
+
throw new TypeError(`Executor implementation is invalid: ${provider.id}`);
|
|
194
|
+
}
|
|
195
|
+
if (provider.displayName !== undefined &&
|
|
196
|
+
(typeof provider.displayName !== 'string' || !provider.displayName.trim())) {
|
|
197
|
+
throw new TypeError(`Executor display name is invalid: ${provider.id}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function assertSessionId(value) {
|
|
201
|
+
if (!value || /[\0\r\n]/u.test(value))
|
|
202
|
+
throw new TypeError('Session id is invalid');
|
|
203
|
+
}
|
|
204
|
+
function normalizeRequest(request) {
|
|
205
|
+
if (!request || typeof request !== 'object')
|
|
206
|
+
throw new TypeError('Executor request is required');
|
|
207
|
+
assertSessionId(request.sessionId);
|
|
208
|
+
for (const [label, value] of [
|
|
209
|
+
['turnId', request.turnId],
|
|
210
|
+
['conversationKey', request.conversationKey],
|
|
211
|
+
['cwd', request.cwd],
|
|
212
|
+
]) {
|
|
213
|
+
if (!value || /[\0\r\n]/u.test(value))
|
|
214
|
+
throw new TypeError(`Executor ${label} is invalid`);
|
|
215
|
+
}
|
|
216
|
+
if (typeof request.text !== 'string')
|
|
217
|
+
throw new TypeError('Executor request text is invalid');
|
|
218
|
+
if (request.runId !== undefined && (!request.runId || /[\0\r\n]/u.test(request.runId))) {
|
|
219
|
+
throw new TypeError('Executor runId is invalid');
|
|
220
|
+
}
|
|
221
|
+
if (request.instructions !== undefined && typeof request.instructions !== 'string') {
|
|
222
|
+
throw new TypeError('Executor instructions are invalid');
|
|
223
|
+
}
|
|
224
|
+
return Object.freeze({
|
|
225
|
+
...request,
|
|
226
|
+
...(request.attachments ? { attachments: Object.freeze([...request.attachments]) } : {}),
|
|
227
|
+
...(request.directoryReferences
|
|
228
|
+
? { directoryReferences: Object.freeze([...request.directoryReferences]) }
|
|
229
|
+
: {}),
|
|
230
|
+
...(request.quotes ? { quotes: Object.freeze([...request.quotes]) } : {}),
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
function normalizeCapabilities(value) {
|
|
234
|
+
if (value !== undefined &&
|
|
235
|
+
(!value ||
|
|
236
|
+
typeof value !== 'object' ||
|
|
237
|
+
(value.thinking !== undefined && typeof value.thinking !== 'boolean') ||
|
|
238
|
+
(value.toolActivity !== undefined && typeof value.toolActivity !== 'boolean'))) {
|
|
239
|
+
throw new TypeError('Executor capabilities are invalid');
|
|
240
|
+
}
|
|
241
|
+
return Object.freeze({
|
|
242
|
+
thinking: value?.thinking === true,
|
|
243
|
+
toolActivity: value?.toolActivity === true,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
function normalizeOutputEvent(event, capabilities) {
|
|
247
|
+
if (!event || typeof event !== 'object')
|
|
248
|
+
throw new TypeError('Executor output event is invalid');
|
|
249
|
+
if (event.type === 'output_delta' && typeof event.text === 'string') {
|
|
250
|
+
return Object.freeze({ type: event.type, text: event.text });
|
|
251
|
+
}
|
|
252
|
+
if (event.type === 'thinking_delta' &&
|
|
253
|
+
capabilities?.thinking === true &&
|
|
254
|
+
isSafeEventText(event.text)) {
|
|
255
|
+
return Object.freeze({ type: event.type, text: event.text });
|
|
256
|
+
}
|
|
257
|
+
if (event.type === 'tool_start' &&
|
|
258
|
+
capabilities?.toolActivity === true &&
|
|
259
|
+
isSafeEventId(event.toolCallId) &&
|
|
260
|
+
isSafeEventId(event.name) &&
|
|
261
|
+
(event.displayName === undefined || isSafeEventText(event.displayName)) &&
|
|
262
|
+
(event.activityKind === undefined || TOOL_ACTIVITY_KINDS.includes(event.activityKind))) {
|
|
263
|
+
return Object.freeze({
|
|
264
|
+
type: event.type,
|
|
265
|
+
toolCallId: event.toolCallId,
|
|
266
|
+
name: event.name,
|
|
267
|
+
...(event.input === undefined ? {} : { input: structuredClone(event.input) }),
|
|
268
|
+
...(event.displayName === undefined ? {} : { displayName: event.displayName }),
|
|
269
|
+
...(event.activityKind === undefined ? {} : { activityKind: event.activityKind }),
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
if (event.type === 'tool_progress' &&
|
|
273
|
+
capabilities?.toolActivity === true &&
|
|
274
|
+
isSafeEventId(event.toolCallId) &&
|
|
275
|
+
isSafeEventText(event.text)) {
|
|
276
|
+
return Object.freeze({ type: event.type, toolCallId: event.toolCallId, text: event.text });
|
|
277
|
+
}
|
|
278
|
+
if (event.type === 'tool_result' &&
|
|
279
|
+
capabilities?.toolActivity === true &&
|
|
280
|
+
isSafeEventId(event.toolCallId) &&
|
|
281
|
+
isSafeEventText(event.text) &&
|
|
282
|
+
(event.isError === undefined || typeof event.isError === 'boolean')) {
|
|
283
|
+
return Object.freeze({
|
|
284
|
+
type: event.type,
|
|
285
|
+
toolCallId: event.toolCallId,
|
|
286
|
+
text: event.text,
|
|
287
|
+
...(event.isError === undefined ? {} : { isError: event.isError }),
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
throw new TypeError('Executor output event is invalid or undeclared');
|
|
291
|
+
}
|
|
292
|
+
function normalizeResult(result) {
|
|
293
|
+
if (!result || typeof result !== 'object')
|
|
294
|
+
throw new TypeError('Executor result is invalid');
|
|
295
|
+
if (result.status === 'completed' && typeof result.text === 'string') {
|
|
296
|
+
return Object.freeze({ status: result.status, text: result.text });
|
|
297
|
+
}
|
|
298
|
+
if (result.status === 'cancelled' &&
|
|
299
|
+
(result.reason === undefined || typeof result.reason === 'string')) {
|
|
300
|
+
return Object.freeze({
|
|
301
|
+
status: result.status,
|
|
302
|
+
...(result.reason === undefined ? {} : { reason: result.reason }),
|
|
303
|
+
source: 'provider',
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
if (result.status === 'failed' &&
|
|
307
|
+
typeof result.message === 'string' &&
|
|
308
|
+
(result.code === undefined || typeof result.code === 'string') &&
|
|
309
|
+
(result.recoverable === undefined || typeof result.recoverable === 'boolean')) {
|
|
310
|
+
return Object.freeze({
|
|
311
|
+
status: result.status,
|
|
312
|
+
message: result.message,
|
|
313
|
+
...(result.code === undefined ? {} : { code: result.code }),
|
|
314
|
+
...(result.recoverable === undefined ? {} : { recoverable: result.recoverable }),
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
throw new TypeError('Executor result is invalid');
|
|
318
|
+
}
|
|
319
|
+
function providerStateIdentity(identity) {
|
|
320
|
+
return `sha256:${createHash('sha256')
|
|
321
|
+
.update(JSON.stringify([
|
|
322
|
+
'plugin-executor.v1',
|
|
323
|
+
identity.id,
|
|
324
|
+
identity.extensionId,
|
|
325
|
+
identity.entryId,
|
|
326
|
+
identity.generation,
|
|
327
|
+
]))
|
|
328
|
+
.digest('hex')}`;
|
|
329
|
+
}
|
|
330
|
+
function cancelledResult(reason) {
|
|
331
|
+
const source = reason instanceof ExecutorRetiredAbort ? 'executor_retired' : 'caller';
|
|
332
|
+
const message = reason instanceof Error ? reason.message : typeof reason === 'string' ? reason : undefined;
|
|
333
|
+
return Object.freeze({
|
|
334
|
+
status: 'cancelled',
|
|
335
|
+
source,
|
|
336
|
+
...(message ? { reason: message } : {}),
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
function isSafeEventId(value) {
|
|
340
|
+
return (typeof value === 'string' && value.length > 0 && value.length <= 256 && !/[\0\r\n]/u.test(value));
|
|
341
|
+
}
|
|
342
|
+
function isSafeEventText(value) {
|
|
343
|
+
return typeof value === 'string' && value.length <= 8_192 && !/[\0\r]/u.test(value);
|
|
344
|
+
}
|
|
@@ -85,6 +85,7 @@ const PROVIDER_FAILURE_FIELD_MAX_BYTES = 256;
|
|
|
85
85
|
const MAX_SAFE_TIMER_DELAY_MS = 2_147_483_647;
|
|
86
86
|
const OPENAI_RESPONSES_WEBSOCKET_TRANSPORT_ERROR = 'OPENAI_RESPONSES_WEBSOCKET_TRANSPORT_ERROR';
|
|
87
87
|
const RUNTIME_RETRYABLE_ERROR_CODES = new Set([
|
|
88
|
+
'MODEL_STREAM_TIMEOUT',
|
|
88
89
|
OPENAI_RESPONSES_WEBSOCKET_TRANSPORT_ERROR,
|
|
89
90
|
'OPENAI_RESPONSES_CONTINUATION_UNAVAILABLE',
|
|
90
91
|
]);
|
|
@@ -178,7 +179,8 @@ function retryMetadataFromFacts(facts, errorClass = classifyProviderFacts(facts)
|
|
|
178
179
|
return { retryable: false };
|
|
179
180
|
return { retryable: true, retryAfterMs };
|
|
180
181
|
}
|
|
181
|
-
const retryable = errorClass === '
|
|
182
|
+
const retryable = errorClass === 'stream_truncated' ||
|
|
183
|
+
errorClass === 'network' ||
|
|
182
184
|
errorClass === 'provider_unavailable' ||
|
|
183
185
|
status === 408 ||
|
|
184
186
|
status === 409 ||
|
|
@@ -606,6 +608,10 @@ function classifyProviderFacts(facts) {
|
|
|
606
608
|
const normalizedCode = code.toLowerCase();
|
|
607
609
|
if (code === OPENAI_RESPONSES_WEBSOCKET_TRANSPORT_ERROR)
|
|
608
610
|
return 'network';
|
|
611
|
+
if (code === 'MODEL_STREAM_TIMEOUT')
|
|
612
|
+
return 'timeout';
|
|
613
|
+
if (structuredCodes.includes('gateway_stream_terminated'))
|
|
614
|
+
return 'stream_truncated';
|
|
609
615
|
if (PROVIDER_CAPACITY_CODES.has(normalizedCode) ||
|
|
610
616
|
structuredCodes.some((c) => PROVIDER_CAPACITY_CODES.has(c))) {
|
|
611
617
|
return 'provider_capacity';
|