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.
Files changed (71) hide show
  1. package/dist/pi-transcript.js +3 -4
  2. package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
  3. package/node_modules/@maka/core/dist/agent-graph-schedule.js +15 -4
  4. package/node_modules/@maka/core/dist/executor-id.js +22 -0
  5. package/node_modules/@maka/core/dist/external-session.js +18 -0
  6. package/node_modules/@maka/core/dist/model-call-usage-projection.js +0 -13
  7. package/node_modules/@maka/core/dist/runtime-event.js +19 -3
  8. package/node_modules/@maka/core/dist/session-send-projection.js +6 -3
  9. package/node_modules/@maka/core/dist/session.js +8 -2
  10. package/node_modules/@maka/core/dist/shell-run-result.js +1 -0
  11. package/node_modules/@maka/core/dist/shell-run.js +4 -0
  12. package/node_modules/@maka/core/dist/work-board.js +94 -1
  13. package/node_modules/@maka/core/package.json +1 -0
  14. package/node_modules/@maka/eval/dist/fleet-simulation.js +270 -0
  15. package/node_modules/@maka/eval/dist/fleet-store.js +243 -0
  16. package/node_modules/@maka/eval/dist/fleet-worker.js +125 -0
  17. package/node_modules/@maka/eval/dist/fleet.js +329 -0
  18. package/node_modules/@maka/eval/dist/index.js +3 -0
  19. package/node_modules/@maka/runtime/dist/agent-run.js +2 -17
  20. package/node_modules/@maka/runtime/dist/ai-sdk-compaction.js +6 -2
  21. package/node_modules/@maka/runtime/dist/ai-sdk-message-projection.js +36 -24
  22. package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +257 -330
  23. package/node_modules/@maka/runtime/dist/background-task-health-tool.js +104 -0
  24. package/node_modules/@maka/runtime/dist/history-compaction.js +3 -1
  25. package/node_modules/@maka/runtime/dist/local-web-fetch.js +1 -1
  26. package/node_modules/@maka/runtime/dist/model-adapter.js +62 -53
  27. package/node_modules/@maka/runtime/dist/model-history.js +1 -5
  28. package/node_modules/@maka/runtime/dist/plugin-executor-backend.js +311 -0
  29. package/node_modules/@maka/runtime/dist/plugin-executor-service.js +344 -0
  30. package/node_modules/@maka/runtime/dist/provider-error-classification.js +7 -1
  31. package/node_modules/@maka/runtime/dist/provider-request-telemetry.js +35 -8
  32. package/node_modules/@maka/runtime/dist/runtime-event-backfill.js +7 -1
  33. package/node_modules/@maka/runtime/dist/runtime-event-read-model.js +1 -0
  34. package/node_modules/@maka/runtime/dist/runtime-invocation-route.js +49 -0
  35. package/node_modules/@maka/runtime/dist/runtime-kernel.js +72 -150
  36. package/node_modules/@maka/runtime/dist/runtime-read-model.js +0 -2
  37. package/node_modules/@maka/runtime/dist/session-event-runtime-mapper.js +3 -0
  38. package/node_modules/@maka/runtime/dist/session-manager.js +100 -58
  39. package/node_modules/@maka/runtime/dist/shell-run-manager.js +9 -3
  40. package/node_modules/@maka/runtime/dist/shell-run-tool-result.js +1 -0
  41. package/node_modules/@maka/runtime/dist/stream-graph-schedule-reconcile.js +1 -0
  42. package/node_modules/@maka/runtime/dist/stream-graph-supervisor-tools.js +26 -4
  43. package/node_modules/@maka/runtime/dist/subagent-tools.js +7 -0
  44. package/node_modules/@maka/runtime/dist/tool-runtime.js +45 -466
  45. package/node_modules/@maka/runtime/package.json +3 -0
  46. package/node_modules/@maka/runtime-host/dist/adapter/session-projector.js +7 -2
  47. package/node_modules/@maka/runtime-host/dist/client/session-catalog-summary.js +1 -0
  48. package/node_modules/@maka/runtime-host/dist/protocol/external-session.js +24 -3
  49. package/node_modules/@maka/runtime-host/dist/protocol/index.js +4 -1
  50. package/node_modules/@maka/runtime-host/dist/protocol/plugin-platform.js +42 -5
  51. package/node_modules/@maka/runtime-host/dist/protocol/session-catalog.js +31 -3
  52. package/node_modules/@maka/runtime-host/dist/protocol/session-continuity.js +6 -0
  53. package/node_modules/@maka/runtime-host/dist/server/child-agent-composition.js +1 -0
  54. package/node_modules/@maka/runtime-host/dist/server/execution-artifacts.js +1 -22
  55. package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +54 -8
  56. package/node_modules/@maka/runtime-host/dist/server/external-session-coordinator.js +14 -1
  57. package/node_modules/@maka/runtime-host/dist/server/host-session-availability.js +10 -2
  58. package/node_modules/@maka/runtime-host/dist/server/plugin-platform-coordinator.js +6 -0
  59. package/node_modules/@maka/runtime-host/dist/server/plugin-platform.js +6 -0
  60. package/node_modules/@maka/runtime-host/dist/server/session-catalog-coordinator.js +56 -16
  61. package/node_modules/@maka/runtime-host/dist/server/session-continuity-coordinator.js +4 -3
  62. package/node_modules/@maka/runtime-host/dist/server/session-revision-coordinator.js +4 -0
  63. package/node_modules/@maka/runtime-host/dist/server/web-fetch-tool.js +37 -1
  64. package/node_modules/@maka/storage/dist/claude-code-session-adapter.js +450 -234
  65. package/node_modules/@maka/storage/dist/claude-code-transcript-lineage.js +106 -77
  66. package/node_modules/@maka/storage/dist/legacy-run-header.js +2 -2
  67. package/node_modules/@maka/storage/dist/model-call-usage-sql.js +1 -1
  68. package/node_modules/@maka/storage/dist/session-store.js +12 -2
  69. package/node_modules/@maka/storage/dist/sqlite-long-term-memory-store.js +86 -12
  70. package/node_modules/@maka/storage/dist/work-board-store.js +40 -1
  71. 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 === 'network' ||
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';