@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.
Files changed (95) hide show
  1. package/cli/index.js +11 -14
  2. package/lib/agent-cli-provider/adapters/claude.d.ts +2 -2
  3. package/lib/agent-cli-provider/adapters/claude.d.ts.map +1 -1
  4. package/lib/agent-cli-provider/adapters/claude.js +30 -0
  5. package/lib/agent-cli-provider/adapters/claude.js.map +1 -1
  6. package/lib/agent-cli-provider/adapters/codex.d.ts +2 -2
  7. package/lib/agent-cli-provider/adapters/codex.d.ts.map +1 -1
  8. package/lib/agent-cli-provider/adapters/codex.js +37 -0
  9. package/lib/agent-cli-provider/adapters/codex.js.map +1 -1
  10. package/lib/agent-cli-provider/adapters/gemini.d.ts +2 -2
  11. package/lib/agent-cli-provider/adapters/gemini.d.ts.map +1 -1
  12. package/lib/agent-cli-provider/adapters/gemini.js +115 -4
  13. package/lib/agent-cli-provider/adapters/gemini.js.map +1 -1
  14. package/lib/agent-cli-provider/adapters/opencode.d.ts +2 -2
  15. package/lib/agent-cli-provider/adapters/opencode.d.ts.map +1 -1
  16. package/lib/agent-cli-provider/adapters/opencode.js +108 -0
  17. package/lib/agent-cli-provider/adapters/opencode.js.map +1 -1
  18. package/lib/agent-cli-provider/contract-options.d.ts.map +1 -1
  19. package/lib/agent-cli-provider/contract-options.js +12 -0
  20. package/lib/agent-cli-provider/contract-options.js.map +1 -1
  21. package/lib/agent-cli-provider/index.d.ts +2 -2
  22. package/lib/agent-cli-provider/index.d.ts.map +1 -1
  23. package/lib/agent-cli-provider/index.js +2 -1
  24. package/lib/agent-cli-provider/index.js.map +1 -1
  25. package/lib/agent-cli-provider/omp-release.d.ts +15 -0
  26. package/lib/agent-cli-provider/omp-release.d.ts.map +1 -0
  27. package/lib/agent-cli-provider/omp-release.js +50 -0
  28. package/lib/agent-cli-provider/omp-release.js.map +1 -0
  29. package/lib/agent-cli-provider/omp-rpc-protocol.d.ts +38 -0
  30. package/lib/agent-cli-provider/omp-rpc-protocol.d.ts.map +1 -0
  31. package/lib/agent-cli-provider/omp-rpc-protocol.js +267 -0
  32. package/lib/agent-cli-provider/omp-rpc-protocol.js.map +1 -0
  33. package/lib/agent-cli-provider/provider-registry.d.ts +35 -8
  34. package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
  35. package/lib/agent-cli-provider/provider-registry.js +29 -1
  36. package/lib/agent-cli-provider/provider-registry.js.map +1 -1
  37. package/lib/agent-cli-provider/single-agent-runtime.d.ts.map +1 -1
  38. package/lib/agent-cli-provider/single-agent-runtime.js +36 -5
  39. package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -1
  40. package/lib/agent-cli-provider/types.d.ts +30 -3
  41. package/lib/agent-cli-provider/types.d.ts.map +1 -1
  42. package/lib/agent-cli-provider/types.js.map +1 -1
  43. package/lib/cluster/errors.cjs +4 -1
  44. package/lib/cluster/errors.d.ts +2 -0
  45. package/lib/cluster/errors.mjs +2 -0
  46. package/lib/cluster/index.cjs +8 -1
  47. package/lib/cluster/index.d.ts +4 -1
  48. package/lib/cluster/index.mjs +4 -1
  49. package/lib/cluster/json-source.cjs +61 -0
  50. package/lib/cluster/json-source.d.ts +4 -0
  51. package/lib/cluster/json-source.mjs +55 -0
  52. package/lib/cluster/payload-value.cjs +76 -0
  53. package/lib/cluster/payload-value.d.ts +7 -0
  54. package/lib/cluster/payload-value.mjs +72 -0
  55. package/lib/cluster/validators.cjs +21 -0
  56. package/lib/cluster/validators.d.ts +4 -1
  57. package/lib/cluster/validators.mjs +19 -1
  58. package/lib/provider-names.js +10 -0
  59. package/lib/settings.js +6 -3
  60. package/lib/setup-plan.js +5 -5
  61. package/lib/start-cluster.js +3 -3
  62. package/package.json +2 -2
  63. package/src/agent/agent-lifecycle.js +4 -4
  64. package/src/agent/agent-task-executor.js +201 -416
  65. package/src/agent/output-extraction.js +39 -14
  66. package/src/agent/output-reformatter.js +154 -111
  67. package/src/agent/pr-verification.js +4 -1
  68. package/src/agent-cli-provider/adapters/claude.ts +39 -2
  69. package/src/agent-cli-provider/adapters/codex.ts +56 -2
  70. package/src/agent-cli-provider/adapters/gemini.ts +102 -6
  71. package/src/agent-cli-provider/adapters/opencode.ts +85 -2
  72. package/src/agent-cli-provider/contract-options.ts +16 -0
  73. package/src/agent-cli-provider/index.ts +4 -0
  74. package/src/agent-cli-provider/omp-release.ts +63 -0
  75. package/src/agent-cli-provider/omp-rpc-protocol.ts +396 -0
  76. package/src/agent-cli-provider/provider-registry.ts +52 -4
  77. package/src/agent-cli-provider/single-agent-runtime.ts +52 -5
  78. package/src/agent-cli-provider/types.ts +36 -2
  79. package/src/agent-wrapper.js +5 -5
  80. package/src/claude-task-runner.js +5 -3
  81. package/src/cluster/errors.ts +1 -0
  82. package/src/cluster/index.ts +4 -0
  83. package/src/cluster/json-source.ts +71 -0
  84. package/src/cluster/payload-value.ts +84 -0
  85. package/src/cluster/validators.ts +31 -2
  86. package/src/config-validator.js +3 -2
  87. package/src/isolation-manager.js +6 -2
  88. package/src/orchestrator.js +36 -16
  89. package/src/preflight.js +2 -1
  90. package/task-lib/attachable-watcher.js +1 -0
  91. package/task-lib/command-spec-cleanup.js +57 -11
  92. package/task-lib/commands/run.js +1 -0
  93. package/task-lib/provider-session-capture.js +8 -0
  94. package/task-lib/runner.js +9 -6
  95. 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 type { ModelLevel, ProviderAdapter } from './types';
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
- export interface ProviderRegistryEntry {
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: 'npm install -g --ignore-scripts @oh-my-pi/pi-coding-agent',
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.webSearch ?? providerSettings.webSearch;
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.buildCommand(input.context, options),
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.webSearch ?? providerSettings.webSearch;
297
+ const webSearch = baseOptions.structuredOutputRecovery
298
+ ? recoveryWebSearchOption(adapter)
299
+ : (baseOptions.webSearch ?? providerSettings.webSearch);
266
300
  assertWebSearchDeclared(adapter.id, webSearch);
267
- const resolved = {
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 ?? 'claude');
418
+ return getProviderAdapter(configured ?? getDefaultProviderId());
372
419
  }
373
420
 
374
421
  function runtimeProviderSettings(