@the-open-engine/zeroshot 6.17.0 → 6.19.0
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/cli/index.js +11 -14
- package/lib/agent-cli-provider/adapters/claude.d.ts +2 -2
- package/lib/agent-cli-provider/adapters/claude.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/claude.js +30 -0
- package/lib/agent-cli-provider/adapters/claude.js.map +1 -1
- package/lib/agent-cli-provider/adapters/codex.d.ts +2 -2
- package/lib/agent-cli-provider/adapters/codex.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/codex.js +37 -0
- package/lib/agent-cli-provider/adapters/codex.js.map +1 -1
- package/lib/agent-cli-provider/adapters/gemini.d.ts +2 -2
- package/lib/agent-cli-provider/adapters/gemini.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/gemini.js +115 -4
- package/lib/agent-cli-provider/adapters/gemini.js.map +1 -1
- package/lib/agent-cli-provider/adapters/opencode.d.ts +2 -2
- package/lib/agent-cli-provider/adapters/opencode.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/opencode.js +108 -0
- package/lib/agent-cli-provider/adapters/opencode.js.map +1 -1
- package/lib/agent-cli-provider/contract-options.d.ts.map +1 -1
- package/lib/agent-cli-provider/contract-options.js +12 -0
- package/lib/agent-cli-provider/contract-options.js.map +1 -1
- package/lib/agent-cli-provider/index.d.ts +2 -2
- package/lib/agent-cli-provider/index.d.ts.map +1 -1
- package/lib/agent-cli-provider/index.js +2 -1
- package/lib/agent-cli-provider/index.js.map +1 -1
- package/lib/agent-cli-provider/omp-release.d.ts +15 -0
- package/lib/agent-cli-provider/omp-release.d.ts.map +1 -0
- package/lib/agent-cli-provider/omp-release.js +50 -0
- package/lib/agent-cli-provider/omp-release.js.map +1 -0
- package/lib/agent-cli-provider/omp-rpc-protocol.d.ts +38 -0
- package/lib/agent-cli-provider/omp-rpc-protocol.d.ts.map +1 -0
- package/lib/agent-cli-provider/omp-rpc-protocol.js +267 -0
- package/lib/agent-cli-provider/omp-rpc-protocol.js.map +1 -0
- package/lib/agent-cli-provider/provider-registry.d.ts +35 -8
- package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
- package/lib/agent-cli-provider/provider-registry.js +29 -1
- package/lib/agent-cli-provider/provider-registry.js.map +1 -1
- package/lib/agent-cli-provider/single-agent-runtime.d.ts.map +1 -1
- package/lib/agent-cli-provider/single-agent-runtime.js +36 -5
- package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -1
- package/lib/agent-cli-provider/types.d.ts +30 -3
- package/lib/agent-cli-provider/types.d.ts.map +1 -1
- package/lib/agent-cli-provider/types.js.map +1 -1
- package/lib/cluster/errors.cjs +4 -1
- package/lib/cluster/errors.d.ts +2 -0
- package/lib/cluster/errors.mjs +2 -0
- package/lib/cluster/index.cjs +8 -1
- package/lib/cluster/index.d.ts +4 -1
- package/lib/cluster/index.mjs +4 -1
- package/lib/cluster/json-source.cjs +61 -0
- package/lib/cluster/json-source.d.ts +4 -0
- package/lib/cluster/json-source.mjs +55 -0
- package/lib/cluster/payload-value.cjs +76 -0
- package/lib/cluster/payload-value.d.ts +7 -0
- package/lib/cluster/payload-value.mjs +72 -0
- package/lib/cluster/validators.cjs +21 -0
- package/lib/cluster/validators.d.ts +4 -1
- package/lib/cluster/validators.mjs +19 -1
- package/lib/provider-names.js +10 -0
- package/lib/settings.js +6 -3
- package/lib/setup-plan.js +5 -5
- package/lib/start-cluster.js +3 -3
- package/package.json +2 -2
- package/src/agent/agent-lifecycle.js +4 -4
- package/src/agent/agent-task-executor.js +201 -416
- package/src/agent/output-extraction.js +39 -14
- package/src/agent/output-reformatter.js +154 -111
- package/src/agent/pr-verification.js +4 -1
- package/src/agent-cli-provider/adapters/claude.ts +39 -2
- package/src/agent-cli-provider/adapters/codex.ts +56 -2
- package/src/agent-cli-provider/adapters/gemini.ts +102 -6
- package/src/agent-cli-provider/adapters/opencode.ts +85 -2
- package/src/agent-cli-provider/contract-options.ts +16 -0
- package/src/agent-cli-provider/index.ts +4 -0
- package/src/agent-cli-provider/omp-release.ts +63 -0
- package/src/agent-cli-provider/omp-rpc-protocol.ts +396 -0
- package/src/agent-cli-provider/provider-registry.ts +52 -4
- package/src/agent-cli-provider/single-agent-runtime.ts +52 -5
- package/src/agent-cli-provider/types.ts +36 -2
- package/src/agent-wrapper.js +5 -5
- package/src/claude-task-runner.js +5 -3
- package/src/cluster/errors.ts +1 -0
- package/src/cluster/index.ts +4 -0
- package/src/cluster/json-source.ts +71 -0
- package/src/cluster/payload-value.ts +84 -0
- package/src/cluster/validators.ts +31 -2
- package/src/config-validator.js +3 -2
- package/src/isolation-manager.js +6 -2
- package/src/orchestrator.js +36 -16
- package/src/preflight.js +2 -1
- package/task-lib/attachable-watcher.js +1 -0
- package/task-lib/command-spec-cleanup.js +57 -11
- package/task-lib/commands/run.js +1 -0
- package/task-lib/provider-session-capture.js +8 -0
- package/task-lib/runner.js +9 -6
- package/task-lib/watcher.js +1 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import { getNumber, getString, isRecord } from './json';
|
|
2
|
+
import { OMP_SUPPORTED_VERSION } from './omp-release';
|
|
3
|
+
|
|
4
|
+
// Purely for traceability/doc; no runtime coupling to omp-release's spawn/install concerns.
|
|
5
|
+
export const OMP_RPC_CONTRACT_RELEASE = OMP_SUPPORTED_VERSION;
|
|
6
|
+
|
|
7
|
+
export interface OmpRpcDecoderLimits {
|
|
8
|
+
readonly maxPhysicalFrameBytes: number;
|
|
9
|
+
readonly maxReassembledFrameBytes: number;
|
|
10
|
+
readonly maxConcurrentReassemblies: number;
|
|
11
|
+
readonly maxChunksPerFrame: number;
|
|
12
|
+
readonly maxInflightReassemblyBytes: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Verified against the pinned OMP release's rpc-frame.ts: MAX_RPC_FRAME_BYTES=1MiB, MAX_RPC_REASSEMBLED_BYTES=64MiB,
|
|
16
|
+
// RPC_CHUNK_PAYLOAD_BYTES=256KiB -> ceil(64MiB/256KiB)=256 chunks max. The real decoder tracks exactly one
|
|
17
|
+
// pending sequence at a time, so maxConcurrentReassemblies/maxInflightReassemblyBytes mirror that ceiling.
|
|
18
|
+
export const DEFAULT_OMP_RPC_DECODER_LIMITS: OmpRpcDecoderLimits = {
|
|
19
|
+
maxPhysicalFrameBytes: 1024 * 1024,
|
|
20
|
+
maxReassembledFrameBytes: 64 * 1024 * 1024,
|
|
21
|
+
maxConcurrentReassemblies: 1,
|
|
22
|
+
maxChunksPerFrame: 256,
|
|
23
|
+
maxInflightReassemblyBytes: 64 * 1024 * 1024,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export interface OmpRpcInboundFrame {
|
|
27
|
+
readonly type: string;
|
|
28
|
+
readonly [key: string]: unknown;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface OmpRpcCommand {
|
|
32
|
+
readonly id?: string;
|
|
33
|
+
readonly type: string;
|
|
34
|
+
readonly [key: string]: unknown;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class OmpRpcProtocolError extends Error {
|
|
38
|
+
readonly code: string;
|
|
39
|
+
|
|
40
|
+
constructor(code: string, message: string) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.name = 'OmpRpcProtocolError';
|
|
43
|
+
this.code = code;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const KNOWN_PRE_NEGOTIATION_FRAME_TYPES: ReadonlySet<string> = new Set([
|
|
48
|
+
'ready',
|
|
49
|
+
'available_commands_update',
|
|
50
|
+
'response',
|
|
51
|
+
'extension_error',
|
|
52
|
+
'agent_start',
|
|
53
|
+
'agent_end',
|
|
54
|
+
'turn_start',
|
|
55
|
+
'turn_end',
|
|
56
|
+
'message_start',
|
|
57
|
+
'message_update',
|
|
58
|
+
'message_end',
|
|
59
|
+
'tool_execution_start',
|
|
60
|
+
'tool_execution_update',
|
|
61
|
+
'tool_execution_end',
|
|
62
|
+
'auto_compaction_start',
|
|
63
|
+
'auto_compaction_end',
|
|
64
|
+
'auto_retry_start',
|
|
65
|
+
'auto_retry_end',
|
|
66
|
+
'ttsr_triggered',
|
|
67
|
+
'todo_reminder',
|
|
68
|
+
'todo_auto_clear',
|
|
69
|
+
'extension_ui_request',
|
|
70
|
+
'host_tool_call',
|
|
71
|
+
'host_tool_cancel',
|
|
72
|
+
'host_uri_request',
|
|
73
|
+
'host_uri_cancel',
|
|
74
|
+
'prompt_result',
|
|
75
|
+
'command_output',
|
|
76
|
+
'session_info_update',
|
|
77
|
+
'config_update',
|
|
78
|
+
'subagent_lifecycle',
|
|
79
|
+
'subagent_progress',
|
|
80
|
+
'subagent_event',
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
export function classifyOmpRpcFrameType(
|
|
84
|
+
type: string
|
|
85
|
+
): 'known-pre-negotiation' | 'v2-only' | 'unknown' {
|
|
86
|
+
if (type === 'rpc_chunk') return 'v2-only';
|
|
87
|
+
if (KNOWN_PRE_NEGOTIATION_FRAME_TYPES.has(type)) return 'known-pre-negotiation';
|
|
88
|
+
return 'unknown';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function assertNoPreNegotiationRpcChunk(frameType: string, negotiatedV2: boolean): void {
|
|
92
|
+
if (frameType === 'rpc_chunk' && !negotiatedV2) {
|
|
93
|
+
throw new OmpRpcProtocolError(
|
|
94
|
+
'pre-negotiation-rpc-chunk',
|
|
95
|
+
'rpc_chunk frame received before protocol v2 negotiation succeeded.'
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const STRICT_BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
101
|
+
|
|
102
|
+
function decodeStrictBase64(data: unknown): Buffer {
|
|
103
|
+
if (typeof data !== 'string' || data.length === 0 || !STRICT_BASE64_PATTERN.test(data)) {
|
|
104
|
+
throw new OmpRpcProtocolError(
|
|
105
|
+
'invalid-chunk-data',
|
|
106
|
+
"rpc_chunk 'data' must be a non-empty, canonically-padded base64 string."
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
const bytes = Buffer.from(data, 'base64');
|
|
110
|
+
if (bytes.toString('base64') !== data) {
|
|
111
|
+
throw new OmpRpcProtocolError(
|
|
112
|
+
'invalid-chunk-data',
|
|
113
|
+
"rpc_chunk 'data' failed a base64 round-trip check (non-canonical encoding)."
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
return bytes;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function decodeStrictUtf8(bytes: Buffer): string {
|
|
120
|
+
try {
|
|
121
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
122
|
+
} catch {
|
|
123
|
+
throw new OmpRpcProtocolError(
|
|
124
|
+
'invalid-utf8-in-reassembled-frame',
|
|
125
|
+
'Reassembled rpc_chunk sequence is not valid UTF-8.'
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function toPhysicalFrame(record: Record<string, unknown>): OmpRpcInboundFrame {
|
|
131
|
+
const type = getString(record, 'type');
|
|
132
|
+
if (type === null) {
|
|
133
|
+
throw new OmpRpcProtocolError(
|
|
134
|
+
'malformed-physical-frame',
|
|
135
|
+
"Physical frame is missing a string 'type' field."
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return { ...record, type };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function toReassembledFrame(record: Record<string, unknown>, chunkId: string): OmpRpcInboundFrame {
|
|
142
|
+
const type = getString(record, 'type');
|
|
143
|
+
if (type === null) {
|
|
144
|
+
throw new OmpRpcProtocolError(
|
|
145
|
+
'malformed-json-in-reassembled-frame',
|
|
146
|
+
`rpc_chunk sequence "${chunkId}" reassembled into an object missing a string 'type' field.`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
return { ...record, type };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
interface ChunkMetadata {
|
|
153
|
+
readonly chunkId: string;
|
|
154
|
+
readonly index: number;
|
|
155
|
+
readonly count: number;
|
|
156
|
+
readonly byteLength: number;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function readChunkMetadata(
|
|
160
|
+
parsed: Record<string, unknown>,
|
|
161
|
+
limits: OmpRpcDecoderLimits
|
|
162
|
+
): ChunkMetadata {
|
|
163
|
+
const chunkId = getString(parsed, 'chunkId');
|
|
164
|
+
if (chunkId === null || chunkId.length === 0 || chunkId.length > 128) {
|
|
165
|
+
throw new OmpRpcProtocolError(
|
|
166
|
+
'invalid-chunk-metadata',
|
|
167
|
+
"rpc_chunk 'chunkId' must be a non-empty string of at most 128 characters."
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const index = getNumber(parsed, 'index');
|
|
172
|
+
const count = getNumber(parsed, 'count');
|
|
173
|
+
const byteLength = getNumber(parsed, 'byteLength');
|
|
174
|
+
if (
|
|
175
|
+
index === null ||
|
|
176
|
+
!Number.isSafeInteger(index) ||
|
|
177
|
+
index < 0 ||
|
|
178
|
+
count === null ||
|
|
179
|
+
!Number.isSafeInteger(count) ||
|
|
180
|
+
count < 2 ||
|
|
181
|
+
count > limits.maxChunksPerFrame ||
|
|
182
|
+
index >= count ||
|
|
183
|
+
byteLength === null ||
|
|
184
|
+
!Number.isSafeInteger(byteLength) ||
|
|
185
|
+
byteLength <= 0 ||
|
|
186
|
+
byteLength > limits.maxReassembledFrameBytes
|
|
187
|
+
) {
|
|
188
|
+
throw new OmpRpcProtocolError(
|
|
189
|
+
'invalid-chunk-metadata',
|
|
190
|
+
"rpc_chunk has invalid 'index'/'count'/'byteLength' metadata."
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return { chunkId, index, count, byteLength };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
interface PendingReassembly {
|
|
198
|
+
readonly chunkId: string;
|
|
199
|
+
readonly count: number;
|
|
200
|
+
readonly byteLength: number;
|
|
201
|
+
nextIndex: number;
|
|
202
|
+
readonly chunks: Buffer[];
|
|
203
|
+
receivedBytes: number;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export class OmpRpcFrameDecoder {
|
|
207
|
+
private readonly limits: OmpRpcDecoderLimits;
|
|
208
|
+
private buffer: Buffer;
|
|
209
|
+
private finished: boolean;
|
|
210
|
+
private readonly pendingReassemblies: Map<string, PendingReassembly>;
|
|
211
|
+
private inflightReassemblyBytes: number;
|
|
212
|
+
|
|
213
|
+
constructor(limits: OmpRpcDecoderLimits) {
|
|
214
|
+
this.limits = limits;
|
|
215
|
+
this.buffer = Buffer.alloc(0);
|
|
216
|
+
this.finished = false;
|
|
217
|
+
this.pendingReassemblies = new Map();
|
|
218
|
+
this.inflightReassemblyBytes = 0;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
push(chunk: Uint8Array): readonly OmpRpcInboundFrame[] {
|
|
222
|
+
if (this.finished) {
|
|
223
|
+
throw new OmpRpcProtocolError('decoder-finished', 'push() called after finish().');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
this.buffer = Buffer.concat([this.buffer, Buffer.from(chunk)]);
|
|
227
|
+
const frames: OmpRpcInboundFrame[] = [];
|
|
228
|
+
|
|
229
|
+
for (;;) {
|
|
230
|
+
const newlineIndex = this.buffer.indexOf(0x0a);
|
|
231
|
+
if (newlineIndex === -1) {
|
|
232
|
+
if (this.buffer.byteLength > this.limits.maxPhysicalFrameBytes) {
|
|
233
|
+
throw new OmpRpcProtocolError(
|
|
234
|
+
'physical-frame-too-large',
|
|
235
|
+
`Buffered physical frame exceeds the ${this.limits.maxPhysicalFrameBytes}-byte limit without a terminating newline.`
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const lineBytes = this.buffer.subarray(0, newlineIndex);
|
|
242
|
+
this.buffer = this.buffer.subarray(newlineIndex + 1);
|
|
243
|
+
if (lineBytes.byteLength + 1 > this.limits.maxPhysicalFrameBytes) {
|
|
244
|
+
throw new OmpRpcProtocolError(
|
|
245
|
+
'physical-frame-too-large',
|
|
246
|
+
`Physical frame of ${lineBytes.byteLength + 1} bytes exceeds the ${this.limits.maxPhysicalFrameBytes}-byte limit.`
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const frame = this.consumeLine(lineBytes);
|
|
251
|
+
if (frame !== null) frames.push(frame);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return frames;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
finish(): void {
|
|
258
|
+
this.finished = true;
|
|
259
|
+
if (this.buffer.byteLength > 0) {
|
|
260
|
+
throw new OmpRpcProtocolError(
|
|
261
|
+
'incomplete-physical-frame',
|
|
262
|
+
'Stream ended with an unterminated physical frame (no trailing newline).'
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
if (this.pendingReassemblies.size > 0) {
|
|
266
|
+
throw new OmpRpcProtocolError(
|
|
267
|
+
'incomplete-chunk-sequence',
|
|
268
|
+
'Stream ended with an incomplete rpc_chunk sequence still pending.'
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private consumeLine(lineBytes: Buffer): OmpRpcInboundFrame | null {
|
|
274
|
+
let value: unknown;
|
|
275
|
+
try {
|
|
276
|
+
value = JSON.parse(lineBytes.toString('utf8'));
|
|
277
|
+
} catch {
|
|
278
|
+
throw new OmpRpcProtocolError('malformed-physical-frame', 'Physical frame is not valid JSON.');
|
|
279
|
+
}
|
|
280
|
+
if (!isRecord(value)) {
|
|
281
|
+
throw new OmpRpcProtocolError(
|
|
282
|
+
'malformed-physical-frame',
|
|
283
|
+
'Physical frame must be a JSON object.'
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const type = getString(value, 'type');
|
|
288
|
+
if (type !== 'rpc_chunk') {
|
|
289
|
+
if (this.pendingReassemblies.size > 0) {
|
|
290
|
+
throw new OmpRpcProtocolError(
|
|
291
|
+
'interrupted-chunk-sequence',
|
|
292
|
+
'A non-chunk frame arrived while an rpc_chunk sequence was pending.'
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
return toPhysicalFrame(value);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return this.consumeChunk(value);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private consumeChunk(parsed: Record<string, unknown>): OmpRpcInboundFrame | null {
|
|
302
|
+
const metadata = readChunkMetadata(parsed, this.limits);
|
|
303
|
+
const bytes = decodeStrictBase64(parsed.data);
|
|
304
|
+
|
|
305
|
+
let pending = this.pendingReassemblies.get(metadata.chunkId);
|
|
306
|
+
if (pending === undefined) {
|
|
307
|
+
if (metadata.index !== 0) {
|
|
308
|
+
throw new OmpRpcProtocolError(
|
|
309
|
+
'chunk-sequence-must-start-at-zero',
|
|
310
|
+
`rpc_chunk sequence "${metadata.chunkId}" must begin at index 0.`
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
if (this.pendingReassemblies.size >= this.limits.maxConcurrentReassemblies) {
|
|
314
|
+
throw new OmpRpcProtocolError(
|
|
315
|
+
'interleaved-chunk-sequence',
|
|
316
|
+
`Starting rpc_chunk sequence "${metadata.chunkId}" would exceed the concurrent-reassembly limit of ${this.limits.maxConcurrentReassemblies}.`
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
pending = {
|
|
320
|
+
chunkId: metadata.chunkId,
|
|
321
|
+
count: metadata.count,
|
|
322
|
+
byteLength: metadata.byteLength,
|
|
323
|
+
nextIndex: 0,
|
|
324
|
+
chunks: [],
|
|
325
|
+
receivedBytes: 0,
|
|
326
|
+
};
|
|
327
|
+
this.pendingReassemblies.set(metadata.chunkId, pending);
|
|
328
|
+
} else if (
|
|
329
|
+
pending.count !== metadata.count ||
|
|
330
|
+
pending.byteLength !== metadata.byteLength ||
|
|
331
|
+
pending.nextIndex !== metadata.index
|
|
332
|
+
) {
|
|
333
|
+
throw new OmpRpcProtocolError(
|
|
334
|
+
'chunk-sequence-mismatch',
|
|
335
|
+
`rpc_chunk sequence "${metadata.chunkId}" metadata or ordering does not match the tracked sequence.`
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
pending.chunks.push(bytes);
|
|
340
|
+
pending.receivedBytes += bytes.byteLength;
|
|
341
|
+
this.inflightReassemblyBytes += bytes.byteLength;
|
|
342
|
+
if (this.inflightReassemblyBytes > this.limits.maxInflightReassemblyBytes) {
|
|
343
|
+
throw new OmpRpcProtocolError(
|
|
344
|
+
'inflight-reassembly-bytes-exceeded',
|
|
345
|
+
`Total in-flight rpc_chunk reassembly bytes exceed the ${this.limits.maxInflightReassemblyBytes}-byte limit.`
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
if (pending.receivedBytes > pending.byteLength) {
|
|
349
|
+
throw new OmpRpcProtocolError(
|
|
350
|
+
'chunk-sequence-exceeds-declared-length',
|
|
351
|
+
`rpc_chunk sequence "${metadata.chunkId}" received more bytes than its declared byteLength.`
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
pending.nextIndex += 1;
|
|
355
|
+
if (pending.nextIndex < pending.count) return null;
|
|
356
|
+
|
|
357
|
+
this.pendingReassemblies.delete(metadata.chunkId);
|
|
358
|
+
this.inflightReassemblyBytes -= pending.receivedBytes;
|
|
359
|
+
if (pending.receivedBytes !== pending.byteLength) {
|
|
360
|
+
throw new OmpRpcProtocolError(
|
|
361
|
+
'chunk-sequence-length-mismatch',
|
|
362
|
+
`rpc_chunk sequence "${metadata.chunkId}" completed with ${pending.receivedBytes} bytes but declared ${pending.byteLength}.`
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const decodedText = decodeStrictUtf8(Buffer.concat(pending.chunks));
|
|
367
|
+
let reassembled: unknown;
|
|
368
|
+
try {
|
|
369
|
+
reassembled = JSON.parse(decodedText);
|
|
370
|
+
} catch {
|
|
371
|
+
throw new OmpRpcProtocolError(
|
|
372
|
+
'malformed-json-in-reassembled-frame',
|
|
373
|
+
`rpc_chunk sequence "${metadata.chunkId}" reassembled into invalid JSON.`
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
if (!isRecord(reassembled)) {
|
|
377
|
+
throw new OmpRpcProtocolError(
|
|
378
|
+
'non-object-reassembled-frame',
|
|
379
|
+
`rpc_chunk sequence "${metadata.chunkId}" reassembled into a non-object JSON value.`
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
return toReassembledFrame(reassembled, metadata.chunkId);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export function encodeOmpRpcCommand(command: OmpRpcCommand, maxFrameBytes: number): Buffer {
|
|
387
|
+
const line = `${JSON.stringify(command)}\n`;
|
|
388
|
+
const byteLength = Buffer.byteLength(line, 'utf8');
|
|
389
|
+
if (byteLength > maxFrameBytes) {
|
|
390
|
+
throw new OmpRpcProtocolError(
|
|
391
|
+
'outbound-frame-too-large',
|
|
392
|
+
`Outbound command of ${byteLength} bytes exceeds the ${maxFrameBytes}-byte limit.`
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
return Buffer.from(line, 'utf8');
|
|
396
|
+
}
|
|
@@ -8,7 +8,8 @@ import { opencodeAdapter } from './adapters/opencode';
|
|
|
8
8
|
import { ompAdapter } from './adapters/omp';
|
|
9
9
|
import { piAdapter } from './adapters/pi';
|
|
10
10
|
import { resolveClaudeCommand } from './claude-command';
|
|
11
|
-
import
|
|
11
|
+
import { OMP_INSTALL_COMMAND } from './omp-release';
|
|
12
|
+
import type { ModelLevel, ProviderAdapter, StructuredOutputRecoveryAdapter } from './types';
|
|
12
13
|
|
|
13
14
|
export type ProviderCapabilityState = boolean | 'experimental';
|
|
14
15
|
|
|
@@ -69,8 +70,9 @@ export interface ProviderDockerMetadata {
|
|
|
69
70
|
readonly install?: string;
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
|
|
73
|
+
interface ProviderRegistryEntryBase {
|
|
73
74
|
readonly id: string;
|
|
75
|
+
readonly default: boolean;
|
|
74
76
|
readonly aliases: readonly string[];
|
|
75
77
|
readonly displayName: string;
|
|
76
78
|
readonly binary: string;
|
|
@@ -84,7 +86,6 @@ export interface ProviderRegistryEntry {
|
|
|
84
86
|
readonly settingsDefaults?: Readonly<Record<string, unknown>>;
|
|
85
87
|
readonly settingsValidator?: (settings: Record<string, unknown>) => string | null;
|
|
86
88
|
readonly availabilityProbe?: 'command' | 'help-or-version';
|
|
87
|
-
readonly capabilities: ProviderCapabilities;
|
|
88
89
|
readonly docs: ProviderDocsMetadata;
|
|
89
90
|
readonly docker: ProviderDockerMetadata;
|
|
90
91
|
readonly defaultLevels: Readonly<{
|
|
@@ -92,9 +93,26 @@ export interface ProviderRegistryEntry {
|
|
|
92
93
|
readonly default: ModelLevel;
|
|
93
94
|
readonly max: ModelLevel;
|
|
94
95
|
}>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface StructuredOutputProviderRegistryEntry extends ProviderRegistryEntryBase {
|
|
99
|
+
readonly capabilities: Omit<ProviderCapabilities, 'jsonSchema'> & {
|
|
100
|
+
readonly jsonSchema: true | 'experimental';
|
|
101
|
+
};
|
|
102
|
+
readonly adapter: StructuredOutputRecoveryAdapter;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface UnstructuredOutputProviderRegistryEntry extends ProviderRegistryEntryBase {
|
|
106
|
+
readonly capabilities: Omit<ProviderCapabilities, 'jsonSchema'> & {
|
|
107
|
+
readonly jsonSchema: false;
|
|
108
|
+
};
|
|
95
109
|
readonly adapter: ProviderAdapter;
|
|
96
110
|
}
|
|
97
111
|
|
|
112
|
+
export type ProviderRegistryEntry =
|
|
113
|
+
| StructuredOutputProviderRegistryEntry
|
|
114
|
+
| UnstructuredOutputProviderRegistryEntry;
|
|
115
|
+
|
|
98
116
|
const STANDARD_CAPABILITIES: Readonly<
|
|
99
117
|
Pick<
|
|
100
118
|
ProviderCapabilities,
|
|
@@ -160,6 +178,7 @@ const kiroAdapter = createAcpAdapter({
|
|
|
160
178
|
export const providerRegistry = [
|
|
161
179
|
{
|
|
162
180
|
id: 'claude',
|
|
181
|
+
default: true,
|
|
163
182
|
aliases: ['anthropic'],
|
|
164
183
|
displayName: 'Claude',
|
|
165
184
|
binary: 'claude',
|
|
@@ -198,6 +217,7 @@ export const providerRegistry = [
|
|
|
198
217
|
},
|
|
199
218
|
{
|
|
200
219
|
id: 'codex',
|
|
220
|
+
default: false,
|
|
201
221
|
aliases: ['openai'],
|
|
202
222
|
displayName: 'Codex',
|
|
203
223
|
binary: 'codex',
|
|
@@ -239,6 +259,7 @@ export const providerRegistry = [
|
|
|
239
259
|
},
|
|
240
260
|
{
|
|
241
261
|
id: 'gateway',
|
|
262
|
+
default: false,
|
|
242
263
|
aliases: [],
|
|
243
264
|
displayName: 'Gateway',
|
|
244
265
|
binary: 'node',
|
|
@@ -287,6 +308,7 @@ export const providerRegistry = [
|
|
|
287
308
|
},
|
|
288
309
|
{
|
|
289
310
|
id: 'gemini',
|
|
311
|
+
default: false,
|
|
290
312
|
aliases: ['google'],
|
|
291
313
|
displayName: 'Gemini',
|
|
292
314
|
binary: 'gemini',
|
|
@@ -324,6 +346,7 @@ export const providerRegistry = [
|
|
|
324
346
|
},
|
|
325
347
|
{
|
|
326
348
|
id: 'opencode',
|
|
349
|
+
default: false,
|
|
327
350
|
aliases: [],
|
|
328
351
|
displayName: 'Opencode',
|
|
329
352
|
binary: 'opencode',
|
|
@@ -364,6 +387,7 @@ export const providerRegistry = [
|
|
|
364
387
|
},
|
|
365
388
|
{
|
|
366
389
|
id: 'pi',
|
|
390
|
+
default: false,
|
|
367
391
|
aliases: [],
|
|
368
392
|
displayName: 'Pi',
|
|
369
393
|
binary: 'pi',
|
|
@@ -403,12 +427,13 @@ export const providerRegistry = [
|
|
|
403
427
|
},
|
|
404
428
|
{
|
|
405
429
|
id: 'omp',
|
|
430
|
+
default: false,
|
|
406
431
|
aliases: [],
|
|
407
432
|
displayName: 'OMP',
|
|
408
433
|
binary: 'omp',
|
|
409
434
|
command: { kind: 'fixed', command: 'omp', args: [] },
|
|
410
435
|
invoke: SPAWN_INVOKE,
|
|
411
|
-
installInstructions:
|
|
436
|
+
installInstructions: OMP_INSTALL_COMMAND,
|
|
412
437
|
authInstructions: 'omp\n/login',
|
|
413
438
|
credentialPaths: ['~/.omp'],
|
|
414
439
|
credentialEnvKeys: ompAdapter.credentialEnvKeys,
|
|
@@ -441,6 +466,7 @@ export const providerRegistry = [
|
|
|
441
466
|
},
|
|
442
467
|
{
|
|
443
468
|
id: 'kiro',
|
|
469
|
+
default: false,
|
|
444
470
|
aliases: [],
|
|
445
471
|
displayName: 'Kiro',
|
|
446
472
|
binary: 'kiro-cli',
|
|
@@ -478,6 +504,7 @@ export const providerRegistry = [
|
|
|
478
504
|
},
|
|
479
505
|
{
|
|
480
506
|
id: 'copilot',
|
|
507
|
+
default: false,
|
|
481
508
|
aliases: [],
|
|
482
509
|
displayName: 'Copilot',
|
|
483
510
|
binary: 'copilot',
|
|
@@ -549,6 +576,23 @@ export const providerAliasMap: Readonly<Record<string, RegistryProviderId>> = Ob
|
|
|
549
576
|
}, {})
|
|
550
577
|
);
|
|
551
578
|
|
|
579
|
+
export function assertExactlyOneDefaultProvider<T extends { id: string; default: boolean }>(
|
|
580
|
+
entries: readonly T[]
|
|
581
|
+
): T['id'] {
|
|
582
|
+
const defaults = entries.filter((e) => e.default);
|
|
583
|
+
const [onlyDefault, ...rest] = defaults;
|
|
584
|
+
if (!onlyDefault || rest.length > 0) {
|
|
585
|
+
throw new Error(
|
|
586
|
+
`Provider registry must declare exactly one default provider; found ${defaults.length}${defaults.length ? ' (' + defaults.map((e) => e.id).join(', ') + ')' : ''}`
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
return onlyDefault.id;
|
|
590
|
+
}
|
|
591
|
+
const DEFAULT_PROVIDER_ID = assertExactlyOneDefaultProvider(providerRegistry);
|
|
592
|
+
export function getDefaultProviderId(): RegistryProviderId {
|
|
593
|
+
return DEFAULT_PROVIDER_ID;
|
|
594
|
+
}
|
|
595
|
+
|
|
552
596
|
export function normalizeProviderName(name: string): RegistryProviderId | string {
|
|
553
597
|
const normalized = name.toLowerCase();
|
|
554
598
|
return providerAliasMap[normalized] ?? name;
|
|
@@ -590,3 +634,7 @@ export function supportsProviderCapability(
|
|
|
590
634
|
): boolean {
|
|
591
635
|
return getProviderRegistryEntry(name).capabilities[capability] === true;
|
|
592
636
|
}
|
|
637
|
+
|
|
638
|
+
export function supportsProviderOutputReformatting(name: string): boolean {
|
|
639
|
+
return getProviderRegistryEntry(name).capabilities.jsonSchema !== false;
|
|
640
|
+
}
|
|
@@ -3,9 +3,11 @@ import { UnsupportedProviderCapabilityError } from './errors';
|
|
|
3
3
|
import { normalizeGatewayBuildOptions, resolveGatewayConfiguration } from './gateway-tools';
|
|
4
4
|
import { isRecord } from './json';
|
|
5
5
|
import {
|
|
6
|
+
getDefaultProviderId,
|
|
6
7
|
getProviderRegistryEntry,
|
|
7
8
|
resolveProviderCommand,
|
|
8
9
|
supportsProviderCapability,
|
|
10
|
+
supportsProviderOutputReformatting,
|
|
9
11
|
} from './provider-registry';
|
|
10
12
|
import type {
|
|
11
13
|
BuildProviderCommandOptions,
|
|
@@ -16,6 +18,7 @@ import type {
|
|
|
16
18
|
ModelLevel,
|
|
17
19
|
ModelSpec,
|
|
18
20
|
ProviderAdapter,
|
|
21
|
+
StructuredOutputRecoveryAdapter,
|
|
19
22
|
ProviderCliFeatures,
|
|
20
23
|
ProviderId,
|
|
21
24
|
ResolvedGatewayBuildOptions,
|
|
@@ -106,7 +109,9 @@ export function prepareSingleAgentProviderCommand(
|
|
|
106
109
|
adapter.id,
|
|
107
110
|
baseOptions.cwd ?? process.cwd()
|
|
108
111
|
);
|
|
109
|
-
const requestedWebSearch = baseOptions.
|
|
112
|
+
const requestedWebSearch = baseOptions.structuredOutputRecovery
|
|
113
|
+
? recoveryWebSearchOption(adapter)
|
|
114
|
+
: (baseOptions.webSearch ?? providerSettings.webSearch);
|
|
110
115
|
assertWebSearchDeclared(adapter.id, requestedWebSearch);
|
|
111
116
|
const cliFeatures = resolveRuntimeCliFeatures(
|
|
112
117
|
adapter.id,
|
|
@@ -125,10 +130,37 @@ export function prepareSingleAgentProviderCommand(
|
|
|
125
130
|
configuration: {
|
|
126
131
|
webSearch: webSearchAttestation(options),
|
|
127
132
|
},
|
|
128
|
-
commandSpec: adapter
|
|
133
|
+
commandSpec: buildRuntimeCommand(adapter, input.context, options),
|
|
129
134
|
};
|
|
130
135
|
}
|
|
131
136
|
|
|
137
|
+
function buildRuntimeCommand(
|
|
138
|
+
adapter: ProviderAdapter,
|
|
139
|
+
context: string,
|
|
140
|
+
options: BuildProviderCommandOptions
|
|
141
|
+
): CommandSpec {
|
|
142
|
+
if (options.structuredOutputRecovery !== true) {
|
|
143
|
+
return adapter.buildCommand(context, options);
|
|
144
|
+
}
|
|
145
|
+
if (!supportsProviderOutputReformatting(adapter.id)) {
|
|
146
|
+
throw new UnsupportedProviderCapabilityError(
|
|
147
|
+
adapter.id,
|
|
148
|
+
'structuredOutputRecovery',
|
|
149
|
+
`Provider ${adapter.id} does not advertise structured-output recovery.`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
const recoveryAdapter = adapter as ProviderAdapter &
|
|
153
|
+
Partial<StructuredOutputRecoveryAdapter>;
|
|
154
|
+
if (typeof recoveryAdapter.buildStructuredOutputRecoveryCommand !== 'function') {
|
|
155
|
+
throw new UnsupportedProviderCapabilityError(
|
|
156
|
+
adapter.id,
|
|
157
|
+
'structuredOutputRecovery',
|
|
158
|
+
`Provider ${adapter.id} advertises structured output without a recovery adapter. Upgrade Zeroshot before retrying.`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
return recoveryAdapter.buildStructuredOutputRecoveryCommand(context, options);
|
|
162
|
+
}
|
|
163
|
+
|
|
132
164
|
export function detectRuntimeProviderCliFeatures(provider: string): ProviderCliFeatures {
|
|
133
165
|
return probeRuntimeProviderCli(provider).capabilities;
|
|
134
166
|
}
|
|
@@ -262,15 +294,26 @@ function buildRuntimeOptions(
|
|
|
262
294
|
providerSettings,
|
|
263
295
|
modelSpec
|
|
264
296
|
);
|
|
265
|
-
const webSearch = baseOptions.
|
|
297
|
+
const webSearch = baseOptions.structuredOutputRecovery
|
|
298
|
+
? recoveryWebSearchOption(adapter)
|
|
299
|
+
: (baseOptions.webSearch ?? providerSettings.webSearch);
|
|
266
300
|
assertWebSearchDeclared(adapter.id, webSearch);
|
|
267
|
-
const
|
|
301
|
+
const baseResolved = {
|
|
268
302
|
...baseOptions,
|
|
269
303
|
modelSpec,
|
|
270
304
|
...(gateway === undefined ? {} : { gateway }),
|
|
271
305
|
...(webSearch === undefined ? {} : { webSearch }),
|
|
272
306
|
cliFeatures: runtime.cliFeatures,
|
|
273
307
|
};
|
|
308
|
+
const resolved = { ...baseResolved };
|
|
309
|
+
if (baseOptions.structuredOutputRecovery) {
|
|
310
|
+
delete resolved.resumeSessionId;
|
|
311
|
+
delete resolved.continueSession;
|
|
312
|
+
delete resolved.mcpConfig;
|
|
313
|
+
resolved.autoApprove = false;
|
|
314
|
+
if (recoveryWebSearchOption(adapter) === undefined) delete resolved.webSearch;
|
|
315
|
+
else resolved.webSearch = false;
|
|
316
|
+
}
|
|
274
317
|
if (baseOptions.jsonSchema && !supportsProviderCapability(adapter.id, 'jsonSchema')) {
|
|
275
318
|
if (!shouldIncludeAuthEnv(baseOptions, runtime.authEnv)) {
|
|
276
319
|
return { ...resolved, strictSchema: false };
|
|
@@ -281,6 +324,10 @@ function buildRuntimeOptions(
|
|
|
281
324
|
return { ...resolved, authEnv: runtime.authEnv };
|
|
282
325
|
}
|
|
283
326
|
|
|
327
|
+
function recoveryWebSearchOption(adapter: ProviderAdapter): false | undefined {
|
|
328
|
+
return getProviderRegistryEntry(adapter.id).capabilities.webSearch === false ? undefined : false;
|
|
329
|
+
}
|
|
330
|
+
|
|
284
331
|
function resolveRuntimeGatewayOptions(
|
|
285
332
|
provider: ProviderId,
|
|
286
333
|
baseOptions: BuildProviderCommandOptions,
|
|
@@ -368,7 +415,7 @@ function adapterForRuntimeInput(
|
|
|
368
415
|
): ProviderAdapter {
|
|
369
416
|
const configured =
|
|
370
417
|
provider ?? optionalString(settings.defaultProvider, 'settings.defaultProvider');
|
|
371
|
-
return getProviderAdapter(configured ??
|
|
418
|
+
return getProviderAdapter(configured ?? getDefaultProviderId());
|
|
372
419
|
}
|
|
373
420
|
|
|
374
421
|
function runtimeProviderSettings(
|