@the-open-engine/zeroshot 6.15.0 → 6.17.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 (69) hide show
  1. package/README.md +10 -7
  2. package/cli/commands/providers.js +1 -0
  3. package/cli/index.js +45 -4
  4. package/lib/agent-cli-provider/adapters/codex.d.ts.map +1 -1
  5. package/lib/agent-cli-provider/adapters/codex.js +14 -2
  6. package/lib/agent-cli-provider/adapters/codex.js.map +1 -1
  7. package/lib/agent-cli-provider/adapters/common.d.ts +1 -0
  8. package/lib/agent-cli-provider/adapters/common.d.ts.map +1 -1
  9. package/lib/agent-cli-provider/adapters/common.js +14 -0
  10. package/lib/agent-cli-provider/adapters/common.js.map +1 -1
  11. package/lib/agent-cli-provider/adapters/index.d.ts.map +1 -1
  12. package/lib/agent-cli-provider/adapters/index.js +6 -1
  13. package/lib/agent-cli-provider/adapters/index.js.map +1 -1
  14. package/lib/agent-cli-provider/adapters/omp.d.ts +3 -0
  15. package/lib/agent-cli-provider/adapters/omp.d.ts.map +1 -0
  16. package/lib/agent-cli-provider/adapters/omp.js +356 -0
  17. package/lib/agent-cli-provider/adapters/omp.js.map +1 -0
  18. package/lib/agent-cli-provider/adapters/opencode.d.ts.map +1 -1
  19. package/lib/agent-cli-provider/adapters/opencode.js +52 -3
  20. package/lib/agent-cli-provider/adapters/opencode.js.map +1 -1
  21. package/lib/agent-cli-provider/contract-actions.d.ts.map +1 -1
  22. package/lib/agent-cli-provider/contract-actions.js +19 -8
  23. package/lib/agent-cli-provider/contract-actions.js.map +1 -1
  24. package/lib/agent-cli-provider/contract-fallback.d.ts.map +1 -1
  25. package/lib/agent-cli-provider/contract-fallback.js +9 -0
  26. package/lib/agent-cli-provider/contract-fallback.js.map +1 -1
  27. package/lib/agent-cli-provider/contract-options.d.ts.map +1 -1
  28. package/lib/agent-cli-provider/contract-options.js +7 -0
  29. package/lib/agent-cli-provider/contract-options.js.map +1 -1
  30. package/lib/agent-cli-provider/errors.d.ts +7 -0
  31. package/lib/agent-cli-provider/errors.d.ts.map +1 -1
  32. package/lib/agent-cli-provider/errors.js +14 -0
  33. package/lib/agent-cli-provider/errors.js.map +1 -1
  34. package/lib/agent-cli-provider/index.d.ts +1 -1
  35. package/lib/agent-cli-provider/index.d.ts.map +1 -1
  36. package/lib/agent-cli-provider/index.js.map +1 -1
  37. package/lib/agent-cli-provider/provider-registry.d.ts +66 -3
  38. package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
  39. package/lib/agent-cli-provider/provider-registry.js +54 -2
  40. package/lib/agent-cli-provider/provider-registry.js.map +1 -1
  41. package/lib/agent-cli-provider/single-agent-runtime.d.ts +14 -2
  42. package/lib/agent-cli-provider/single-agent-runtime.d.ts.map +1 -1
  43. package/lib/agent-cli-provider/single-agent-runtime.js +97 -22
  44. package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -1
  45. package/lib/agent-cli-provider/types.d.ts +30 -2
  46. package/lib/agent-cli-provider/types.d.ts.map +1 -1
  47. package/lib/agent-cli-provider/types.js.map +1 -1
  48. package/package.json +1 -1
  49. package/src/agent/agent-lifecycle.js +52 -1
  50. package/src/agent/agent-task-executor.js +8 -3
  51. package/src/agent-cli-provider/adapters/codex.ts +23 -2
  52. package/src/agent-cli-provider/adapters/common.ts +14 -0
  53. package/src/agent-cli-provider/adapters/index.ts +16 -2
  54. package/src/agent-cli-provider/adapters/omp.ts +448 -0
  55. package/src/agent-cli-provider/adapters/opencode.ts +60 -4
  56. package/src/agent-cli-provider/contract-actions.ts +20 -9
  57. package/src/agent-cli-provider/contract-fallback.ts +9 -0
  58. package/src/agent-cli-provider/contract-options.ts +7 -0
  59. package/src/agent-cli-provider/errors.ts +14 -0
  60. package/src/agent-cli-provider/index.ts +2 -0
  61. package/src/agent-cli-provider/provider-registry.ts +59 -2
  62. package/src/agent-cli-provider/single-agent-runtime.ts +144 -24
  63. package/src/agent-cli-provider/types.ts +32 -1
  64. package/src/claude-task-runner.js +11 -0
  65. package/src/orchestrator.js +8 -3
  66. package/src/providers/index.js +6 -0
  67. package/src/task-run-model-args.js +58 -40
  68. package/src/task-spawn-cleanup-ownership.js +3 -0
  69. package/src/task-startup-error.js +68 -0
@@ -1,3 +1,4 @@
1
+ import { UnsupportedProviderCapabilityError } from '../errors';
1
2
  import { getString, isRecord, tryParseJson } from '../json';
2
3
  import { appendJsonSchemaPrompt, writeStrictOutputSchemaFile } from '../schema';
3
4
  import {
@@ -17,6 +18,7 @@ import {
17
18
  classifyBaseProviderError,
18
19
  commandSpec,
19
20
  createParserState,
21
+ isCliVersionAtLeast,
20
22
  optionFeatures,
21
23
  resolveModelSpecWithConfig,
22
24
  validateModelIdFromCatalog,
@@ -46,19 +48,25 @@ function supports(help: string, pattern: RegExp): boolean {
46
48
  return help ? pattern.test(help) : true;
47
49
  }
48
50
 
49
- function detectCliFeatures(helpText?: string | null): CodexCliFeatures {
51
+ function detectCliFeatures(
52
+ helpText?: string | null,
53
+ versionText?: string | null
54
+ ): CodexCliFeatures {
50
55
  const help = helpText ?? '';
51
56
  const unknown = !help;
57
+ const supportsConfigOverride = supports(help, /--config\b/);
52
58
  return {
53
59
  provider: 'codex',
54
60
  supportsJson: supports(help, /--json\b/),
55
61
  supportsOutputSchema: supports(help, /--output-schema\b/),
56
62
  supportsAutoApprove: supports(help, /--dangerously-bypass-approvals-and-sandbox\b/),
57
63
  supportsCwd: supports(help, /\s-C\b/) || supports(help, /--cwd\b/),
58
- supportsConfigOverride: supports(help, /--config\b/),
64
+ supportsConfigOverride,
59
65
  supportsModel: supports(help, /\s-m\b/) || supports(help, /--model\b/),
60
66
  supportsSkipGitRepoCheck: supports(help, /--skip-git-repo-check\b/),
61
67
  supportsResume: supports(help, /\bresume\b/),
68
+ supportsWebSearch:
69
+ !unknown && supportsConfigOverride && isCliVersionAtLeast(versionText, '0.146.0'),
62
70
  unknown,
63
71
  };
64
72
  }
@@ -178,6 +186,18 @@ function collectWarnings(options: BuildProviderCommandOptions): WarningMetadata[
178
186
  return warnings;
179
187
  }
180
188
 
189
+ function addWebSearchArgs(args: string[], options: BuildProviderCommandOptions): void {
190
+ if (options.webSearch !== true) return;
191
+ if (optionFeatures(options).supportsWebSearch !== true) {
192
+ throw new UnsupportedProviderCapabilityError(
193
+ 'codex',
194
+ 'webSearch',
195
+ 'Codex web search requires nonempty `codex exec --help` support for `--config` and a parseable Codex CLI version >= 0.146.0. Update @openai/codex or set providerSettings.codex.webSearch to false.'
196
+ );
197
+ }
198
+ args.push('--config', 'web_search="live"');
199
+ }
200
+
181
201
  function buildCommand(context: string, options: BuildProviderCommandOptions = {}): CommandSpec {
182
202
  if (options.resumeSessionId && optionFeatures(options).supportsResume === false) {
183
203
  throw new Error(
@@ -190,6 +210,7 @@ function buildCommand(context: string, options: BuildProviderCommandOptions = {}
190
210
  options.resumeSessionId && optionFeatures(options).supportsResume !== false
191
211
  ? options.resumeSessionId
192
212
  : null;
213
+ addWebSearchArgs(args, options);
193
214
  if (resumeSessionId) {
194
215
  args.push('resume');
195
216
  }
@@ -123,3 +123,17 @@ export function optionFeatures(
123
123
  ): CliFeatureOverrides {
124
124
  return options?.cliFeatures ?? {};
125
125
  }
126
+
127
+ export function isCliVersionAtLeast(versionText: string | null | undefined, floor: string): boolean {
128
+ const actualMatch = /(?:^|[^\w.])v?(\d+)\.(\d+)\.(\d+)(?![\w.-])/.exec(
129
+ versionText ?? ''
130
+ );
131
+ const floorMatch = /^(\d+)\.(\d+)\.(\d+)$/.exec(floor);
132
+ if (!actualMatch || !floorMatch) return false;
133
+ for (let index = 1; index <= 3; index += 1) {
134
+ const actual = Number(actualMatch[index]);
135
+ const minimum = Number(floorMatch[index]);
136
+ if (actual !== minimum) return actual > minimum;
137
+ }
138
+ return true;
139
+ }
@@ -1,5 +1,11 @@
1
1
  import { stripTimestampPrefix } from '../log-prefix';
2
- import { getProviderRegistryEntry, normalizeProviderName, providerIds } from '../provider-registry';
2
+ import { UnsupportedProviderCapabilityError } from '../errors';
3
+ import {
4
+ getProviderRegistryEntry,
5
+ normalizeProviderName,
6
+ providerIds,
7
+ supportsProviderCapability,
8
+ } from '../provider-registry';
3
9
  import type {
4
10
  BuildProviderCommandOptions,
5
11
  CommandSpec,
@@ -52,7 +58,15 @@ export function buildProviderCommand(
52
58
  context: string,
53
59
  options?: BuildProviderCommandOptions
54
60
  ): CommandSpec {
55
- return getProviderAdapter(providerName).buildCommand(context, options);
61
+ const adapter = getProviderAdapter(providerName);
62
+ if (options?.webSearch !== undefined && !supportsProviderCapability(adapter.id, 'webSearch')) {
63
+ throw new UnsupportedProviderCapabilityError(
64
+ adapter.id,
65
+ 'webSearch',
66
+ `Provider ${adapter.id} does not expose provider-controlled native web search; remove options.webSearch.`
67
+ );
68
+ }
69
+ return adapter.buildCommand(context, options);
56
70
  }
57
71
 
58
72
  export function parseProviderChunk(
@@ -0,0 +1,448 @@
1
+ import { appendJsonSchemaPrompt } from '../schema';
2
+ import { contractError } from '../contract-errors';
3
+ import {
4
+ getArray,
5
+ getBoolean,
6
+ getNumber,
7
+ getOptionalString,
8
+ getRecord,
9
+ getString,
10
+ isRecord,
11
+ tryParseJson,
12
+ unknownToMessage,
13
+ } from '../json';
14
+ import {
15
+ type BuildProviderCommandOptions,
16
+ type CommandSpec,
17
+ type ErrorClassification,
18
+ type LevelModelSpec,
19
+ type LevelOverrides,
20
+ type ModelCatalogEntry,
21
+ type ModelLevel,
22
+ type OmpCliFeatures,
23
+ type OutputEvent,
24
+ type ProviderAdapter,
25
+ type ProviderParserState,
26
+ type ResolvedModelSpec,
27
+ type WarningMetadata,
28
+ } from '../types';
29
+ import {
30
+ classifyBaseProviderError,
31
+ commandSpec,
32
+ createParserState,
33
+ optionFeatures,
34
+ resolveModelSpecWithConfig,
35
+ warning,
36
+ } from './common';
37
+
38
+ const MODEL_CATALOG: Readonly<Record<string, ModelCatalogEntry>> = {};
39
+
40
+ const LEVEL_MAPPING: Readonly<Record<ModelLevel, LevelModelSpec>> = {
41
+ level1: { rank: 1, model: null, reasoningEffort: 'low' },
42
+ level2: { rank: 2, model: null, reasoningEffort: 'medium' },
43
+ level3: { rank: 3, model: null, reasoningEffort: 'high' },
44
+ };
45
+
46
+ const IGNORED_EVENT_TYPES = new Set([
47
+ 'session',
48
+ 'agent_start',
49
+ 'agent_end',
50
+ 'turn_start',
51
+ 'queue_update',
52
+ 'compaction_start',
53
+ 'compaction_end',
54
+ 'auto_retry_start',
55
+ 'auto_retry_end',
56
+ ]);
57
+
58
+ function detectCliFeatures(helpText?: string | null): OmpCliFeatures {
59
+ const help = helpText ?? '';
60
+ const unknown = !help;
61
+ return {
62
+ provider: 'omp',
63
+ supportsModeJson: unknown ? true : /--mode\b/.test(help) && /\bjson\b/.test(help),
64
+ supportsPrint: unknown ? true : /-p\b/.test(help) || /--print\b/.test(help),
65
+ supportsCwd: unknown ? true : /--cwd\b/.test(help),
66
+ supportsAutoApprove: unknown ? true : /--auto-approve\b/.test(help),
67
+ supportsModel: unknown ? true : /--model\b/.test(help),
68
+ supportsThinking: unknown ? true : /--thinking\b/.test(help),
69
+ supportsNoExtensions: unknown ? true : /--no-extensions\b/.test(help),
70
+ supportsNoSkills: unknown ? true : /--no-skills\b/.test(help),
71
+ supportsNoRules: unknown ? true : /--no-rules\b/.test(help),
72
+ supportsNoTitle: unknown ? true : /--no-title\b/.test(help),
73
+ unknown,
74
+ };
75
+ }
76
+
77
+ function assertRequiredOmpFeatures(options: BuildProviderCommandOptions): void {
78
+ const features = optionFeatures(options);
79
+ const required: ReadonlyArray<readonly [boolean | undefined, string]> = [
80
+ [features.supportsModeJson, '--mode json'],
81
+ [features.supportsPrint, '-p/--print'],
82
+ [features.supportsCwd, '--cwd'],
83
+ [features.supportsAutoApprove, '--auto-approve'],
84
+ ];
85
+ const missing = required.filter(([supported]) => supported === false).map(([, flag]) => flag);
86
+ if (missing.length === 0) return;
87
+ throw contractError({
88
+ code: 'unsupported-provider-cli',
89
+ exitCode: 2,
90
+ message:
91
+ 'omp CLI is missing required non-interactive flag(s): ' +
92
+ missing.join(', ') +
93
+ '. Update/install @oh-my-pi/pi-coding-agent; Zeroshot will not silently fall back to Pi or another provider.',
94
+ });
95
+ }
96
+
97
+ function failClosedUnsupportedSessionControl(options: BuildProviderCommandOptions): void {
98
+ const hasResumeSessionId = options.resumeSessionId !== undefined;
99
+ if (!hasResumeSessionId && !options.continueSession) return;
100
+ const field = hasResumeSessionId ? 'options.resumeSessionId' : 'options.continueSession';
101
+ throw contractError({
102
+ code: 'invalid-field',
103
+ field,
104
+ exitCode: 2,
105
+ message:
106
+ 'OMP CLI does not support resume/continue session control; fail closed and start a fresh run instead.',
107
+ });
108
+ }
109
+
110
+ function addOptionalFlags(args: string[], options: BuildProviderCommandOptions): void {
111
+ const features = optionFeatures(options);
112
+ if (features.supportsNoExtensions !== false) args.push('--no-extensions');
113
+ if (features.supportsNoSkills !== false) args.push('--no-skills');
114
+ if (features.supportsNoRules !== false) args.push('--no-rules');
115
+ if (features.supportsNoTitle !== false) args.push('--no-title');
116
+ }
117
+
118
+ function addModelArgs(args: string[], options: BuildProviderCommandOptions): void {
119
+ const features = optionFeatures(options);
120
+ if (options.modelSpec?.model && features.supportsModel !== false) {
121
+ args.push('--model', options.modelSpec.model);
122
+ }
123
+ if (options.modelSpec?.reasoningEffort && features.supportsThinking !== false) {
124
+ args.push('--thinking', options.modelSpec.reasoningEffort);
125
+ }
126
+ }
127
+
128
+ function collectWarnings(options: BuildProviderCommandOptions): WarningMetadata[] {
129
+ const features = optionFeatures(options);
130
+ const warnings: WarningMetadata[] = [];
131
+
132
+ if (options.jsonSchema) {
133
+ warnings.push(
134
+ warning(
135
+ 'omp',
136
+ 'omp-jsonschema',
137
+ 'OMP CLI does not support provider-native JSON schema; appending schema instructions to the prompt.'
138
+ )
139
+ );
140
+ }
141
+ if (options.modelSpec?.model && features.supportsModel === false) {
142
+ warnings.push(
143
+ warning(
144
+ 'omp',
145
+ 'omp-model-unsupported',
146
+ "OMP CLI does not advertise --model; continuing with OMP's own configured model."
147
+ )
148
+ );
149
+ }
150
+ if (options.modelSpec?.reasoningEffort && features.supportsThinking === false) {
151
+ warnings.push(
152
+ warning(
153
+ 'omp',
154
+ 'omp-thinking-unsupported',
155
+ 'OMP CLI does not advertise --thinking; continuing without a reasoning-effort override.'
156
+ )
157
+ );
158
+ }
159
+ return warnings;
160
+ }
161
+
162
+ function buildCommand(context: string, options: BuildProviderCommandOptions = {}): CommandSpec {
163
+ assertRequiredOmpFeatures(options);
164
+ failClosedUnsupportedSessionControl(options);
165
+ const finalContext = options.jsonSchema
166
+ ? appendJsonSchemaPrompt(context, options.jsonSchema)
167
+ : context;
168
+ const args: string[] = ['--mode', 'json', '-p'];
169
+
170
+ if (options.cwd) args.push('--cwd', options.cwd);
171
+ args.push('--auto-approve');
172
+ addOptionalFlags(args, options);
173
+ addModelArgs(args, options);
174
+ args.push(finalContext);
175
+
176
+ return commandSpec({
177
+ binary: 'omp',
178
+ args,
179
+ env: {},
180
+ ...(options.cwd === undefined ? {} : { cwd: options.cwd }),
181
+ warnings: collectWarnings(options),
182
+ });
183
+ }
184
+
185
+ function createOmpState(): ProviderParserState {
186
+ return {
187
+ ...createParserState('omp'),
188
+ lastAssistantText: '',
189
+ lastAssistantThinking: '',
190
+ };
191
+ }
192
+
193
+ function assistantSnapshot(message: Record<string, unknown>): { text: string; thinking: string } {
194
+ if (getString(message, 'role') !== 'assistant') return { text: '', thinking: '' };
195
+
196
+ let text = '';
197
+ let thinking = '';
198
+ for (const item of getArray(message, 'content')) {
199
+ if (!isRecord(item)) continue;
200
+ const type = getString(item, 'type');
201
+ if (type === 'text') {
202
+ text += getString(item, 'text') ?? '';
203
+ } else if (type === 'thinking') {
204
+ thinking += getString(item, 'thinking') ?? '';
205
+ }
206
+ }
207
+
208
+ return { text, thinking };
209
+ }
210
+
211
+ function snapshotDelta(previous: string, current: string): string | null {
212
+ if (!current) return null;
213
+ if (previous === current) return null;
214
+ if (current.startsWith(previous)) return current.slice(previous.length) || null;
215
+ return current;
216
+ }
217
+
218
+ function emitAssistantSnapshot(
219
+ message: Record<string, unknown>,
220
+ state: ProviderParserState
221
+ ): readonly OutputEvent[] {
222
+ const snapshot = assistantSnapshot(message);
223
+ const events: OutputEvent[] = [];
224
+
225
+ const textDelta = snapshotDelta(state.lastAssistantText ?? '', snapshot.text);
226
+ if (textDelta) events.push({ type: 'text', text: textDelta });
227
+ state.lastAssistantText = snapshot.text;
228
+
229
+ const thinkingDelta = snapshotDelta(state.lastAssistantThinking ?? '', snapshot.thinking);
230
+ if (thinkingDelta) events.push({ type: 'thinking', text: thinkingDelta });
231
+ state.lastAssistantThinking = snapshot.thinking;
232
+
233
+ return events;
234
+ }
235
+
236
+ function parseAssistantEvent(
237
+ event: Record<string, unknown>,
238
+ state: ProviderParserState
239
+ ): OutputEvent | null {
240
+ const type = getString(event, 'type');
241
+ if (type === 'text_delta') {
242
+ const delta = getString(event, 'delta');
243
+ if (!delta) return null;
244
+ state.lastAssistantText += delta;
245
+ return { type: 'text', text: delta };
246
+ }
247
+ if (type === 'thinking_delta') {
248
+ const delta = getString(event, 'delta');
249
+ if (!delta) return null;
250
+ state.lastAssistantThinking += delta;
251
+ return { type: 'thinking', text: delta };
252
+ }
253
+ return null;
254
+ }
255
+
256
+ function parseToolExecutionStart(
257
+ event: Record<string, unknown>,
258
+ state: ProviderParserState
259
+ ): OutputEvent {
260
+ const toolId = getOptionalString(event, 'toolCallId');
261
+ state.lastToolId = toolId;
262
+ return {
263
+ type: 'tool_call',
264
+ toolName: getOptionalString(event, 'toolName'),
265
+ toolId,
266
+ input: event.args ?? {},
267
+ };
268
+ }
269
+
270
+ function parseToolExecutionUpdate(
271
+ event: Record<string, unknown>,
272
+ state: ProviderParserState
273
+ ): OutputEvent | null {
274
+ const toolId = getOptionalString(event, 'toolCallId') ?? state.lastToolId;
275
+ if (toolId !== undefined) state.lastToolId = toolId;
276
+ if (!Object.prototype.hasOwnProperty.call(event, 'partialResult')) return null;
277
+ return {
278
+ type: 'tool_result',
279
+ toolId,
280
+ content: event.partialResult,
281
+ isError: false,
282
+ };
283
+ }
284
+
285
+ function parseToolExecutionEnd(
286
+ event: Record<string, unknown>,
287
+ state: ProviderParserState
288
+ ): OutputEvent {
289
+ const toolId = getOptionalString(event, 'toolCallId') ?? state.lastToolId;
290
+ return {
291
+ type: 'tool_result',
292
+ toolId,
293
+ content: event.result ?? '',
294
+ isError: getBoolean(event, 'isError') ?? false,
295
+ };
296
+ }
297
+
298
+ function parseTurnEnd(
299
+ event: Record<string, unknown>,
300
+ state: ProviderParserState
301
+ ): readonly OutputEvent[] {
302
+ const message = getRecord(event, 'message');
303
+ const events: OutputEvent[] = [];
304
+ if (message !== null) {
305
+ events.push(...emitAssistantSnapshot(message, state));
306
+ }
307
+
308
+ const usage = message ? getRecord(message, 'usage') ?? {} : {};
309
+ const stopReason = message ? getString(message, 'stopReason') : null;
310
+ const errorMessage = message ? getString(message, 'errorMessage') : null;
311
+ const snapshot = message ? assistantSnapshot(message) : { text: '', thinking: '' };
312
+ const success = stopReason !== 'error' && stopReason !== 'aborted' && !errorMessage;
313
+ events.push({
314
+ type: 'result',
315
+ success,
316
+ result: success ? snapshot.text || null : null,
317
+ error: success ? null : (errorMessage ?? stopReason ?? 'OMP turn failed'),
318
+ inputTokens: getNumber(usage, 'input') ?? 0,
319
+ outputTokens: getNumber(usage, 'output') ?? 0,
320
+ cacheReadInputTokens: getNumber(usage, 'cacheRead') ?? 0,
321
+ cacheCreationInputTokens: getNumber(usage, 'cacheWrite') ?? 0,
322
+ cost: getRecord(usage, 'cost') ?? null,
323
+ modelUsage: usage,
324
+ });
325
+ return events;
326
+ }
327
+
328
+ function parseMessageEvent(
329
+ type: string,
330
+ event: Record<string, unknown>,
331
+ state: ProviderParserState
332
+ ): readonly OutputEvent[] | OutputEvent | null | undefined {
333
+ if (type === 'message_start') {
334
+ const message = getRecord(event, 'message');
335
+ if (message !== null && getString(message, 'role') === 'assistant') {
336
+ state.lastAssistantText = '';
337
+ state.lastAssistantThinking = '';
338
+ }
339
+ return null;
340
+ }
341
+
342
+ if (type === 'message_update') {
343
+ const assistantMessageEvent = getRecord(event, 'assistantMessageEvent');
344
+ if (assistantMessageEvent !== null) {
345
+ const assistantEvent = parseAssistantEvent(assistantMessageEvent, state);
346
+ if (assistantEvent !== null) return assistantEvent;
347
+ }
348
+ const message = getRecord(event, 'message');
349
+ return message === null ? null : emitAssistantSnapshot(message, state);
350
+ }
351
+
352
+ if (type !== 'message_end') return undefined;
353
+ const message = getRecord(event, 'message');
354
+ return message === null ? null : emitAssistantSnapshot(message, state);
355
+ }
356
+
357
+ function parseToolEvent(
358
+ type: string,
359
+ event: Record<string, unknown>,
360
+ state: ProviderParserState
361
+ ): OutputEvent | null | undefined {
362
+ if (type === 'tool_execution_start') return parseToolExecutionStart(event, state);
363
+ if (type === 'tool_execution_update') return parseToolExecutionUpdate(event, state);
364
+ if (type === 'tool_execution_end') return parseToolExecutionEnd(event, state);
365
+ return undefined;
366
+ }
367
+
368
+ function parseEvent(
369
+ line: string,
370
+ state: ProviderParserState
371
+ ): readonly OutputEvent[] | OutputEvent | null {
372
+ const parsed = tryParseJson(line);
373
+ if (!isRecord(parsed)) return null;
374
+
375
+ const type = getString(parsed, 'type');
376
+ if (type === null || IGNORED_EVENT_TYPES.has(type)) return null;
377
+
378
+ const messageEvent = parseMessageEvent(type, parsed, state);
379
+ if (messageEvent !== undefined) return messageEvent;
380
+
381
+ const toolEvent = parseToolEvent(type, parsed, state);
382
+ if (toolEvent !== undefined) return toolEvent;
383
+
384
+ if (type === 'turn_end') return parseTurnEnd(parsed, state);
385
+ return null;
386
+ }
387
+
388
+ function resolveModelSpec(level: ModelLevel, overrides?: LevelOverrides): ResolvedModelSpec {
389
+ return resolveModelSpecWithConfig({
390
+ mapping: LEVEL_MAPPING,
391
+ defaultLevel: 'level2',
392
+ level,
393
+ overrides,
394
+ validateModelId,
395
+ });
396
+ }
397
+
398
+ function validateModelId(modelId: string | null | undefined): string | null | undefined {
399
+ if (modelId === undefined || modelId === null) return modelId;
400
+ if (typeof modelId !== 'string') {
401
+ throw new Error(`Invalid model "${unknownToMessage(modelId)}" for provider "omp".`);
402
+ }
403
+ return modelId;
404
+ }
405
+
406
+ function classifyError(error: unknown): ErrorClassification {
407
+ return classifyBaseProviderError(
408
+ error,
409
+ [
410
+ /\brate(?:[_ -]?limit| limited)\b/i,
411
+ /\bquota\b/i,
412
+ /\bresource[_ -]?exhausted\b/i,
413
+ /\btemporar(?:y|ily)\b/i,
414
+ /\boverloaded\b/i,
415
+ /\bservice unavailable\b/i,
416
+ ],
417
+ [
418
+ /\b(cancelled|canceled|aborted|interrupted)\b/i,
419
+ /\brun\s*\/login\b/i,
420
+ /\bmissing api key\b/i,
421
+ /\bno valid authentication\b/i,
422
+ /\bunknown option\b/i,
423
+ /\bfailed to load\b/i,
424
+ /\bcannot find module\b/i,
425
+ /\bno such file or directory\b/i,
426
+ ]
427
+ );
428
+ }
429
+
430
+ export const ompAdapter: ProviderAdapter = {
431
+ id: 'omp',
432
+ displayName: 'OMP',
433
+ binary: 'omp',
434
+ adapterVersion: '1',
435
+ credentialEnvKeys: [],
436
+ modelCatalog: MODEL_CATALOG,
437
+ levelMapping: LEVEL_MAPPING,
438
+ defaultLevel: 'level2',
439
+ defaultMaxLevel: 'level3',
440
+ defaultMinLevel: 'level1',
441
+ detectCliFeatures,
442
+ buildCommand,
443
+ parseEvent,
444
+ createParserState: createOmpState,
445
+ resolveModelSpec,
446
+ validateModelId,
447
+ classifyError,
448
+ };
@@ -1,3 +1,5 @@
1
+ import { contractError } from '../contract-errors';
2
+ import { UnsupportedProviderCapabilityError } from '../errors';
1
3
  import { appendJsonSchemaPrompt } from '../schema';
2
4
  import {
3
5
  getNumber,
@@ -20,8 +22,8 @@ import {
20
22
  classifyBaseProviderError,
21
23
  commandSpec,
22
24
  createParserState,
25
+ isCliVersionAtLeast,
23
26
  optionFeatures,
24
- unsupportedSessionControlWarnings,
25
27
  warning,
26
28
  } from './common';
27
29
  import {
@@ -32,7 +34,10 @@ import {
32
34
  validateModelId,
33
35
  } from './opencode-models';
34
36
 
35
- function detectCliFeatures(helpText?: string | null): OpencodeCliFeatures {
37
+ function detectCliFeatures(
38
+ helpText?: string | null,
39
+ versionText?: string | null
40
+ ): OpencodeCliFeatures {
36
41
  const help = helpText ?? '';
37
42
  const unknown = !help;
38
43
  return {
@@ -43,6 +48,8 @@ function detectCliFeatures(helpText?: string | null): OpencodeCliFeatures {
43
48
  supportsDir: unknown ? false : /--dir\b/.test(help),
44
49
  supportsCwd: unknown ? false : /--cwd\b/.test(help),
45
50
  supportsAutoApprove: false,
51
+ supportsResume: !unknown && /--session\b/.test(help) && /--continue\b/.test(help),
52
+ supportsWebSearch: isCliVersionAtLeast(versionText, '1.0.137'),
46
53
  unknown,
47
54
  };
48
55
  }
@@ -76,7 +83,7 @@ function addOpencodeOptionalArgs(args: string[], options: BuildProviderCommandOp
76
83
 
77
84
  function collectOpencodeWarnings(options: BuildProviderCommandOptions): WarningMetadata[] {
78
85
  const features = optionFeatures(options);
79
- const warnings: WarningMetadata[] = unsupportedSessionControlWarnings('opencode', options);
86
+ const warnings: WarningMetadata[] = [];
80
87
  if (options.modelSpec?.reasoningEffort && features.supportsVariant === false) {
81
88
  warnings.push(
82
89
  warning(
@@ -89,19 +96,67 @@ function collectOpencodeWarnings(options: BuildProviderCommandOptions): WarningM
89
96
  return warnings;
90
97
  }
91
98
 
99
+ function addSessionArgs(args: string[], options: BuildProviderCommandOptions): void {
100
+ const hasResumeSessionId = options.resumeSessionId !== undefined;
101
+ if (!hasResumeSessionId && !options.continueSession) return;
102
+ const field = hasResumeSessionId ? 'options.resumeSessionId' : 'options.continueSession';
103
+ if (hasResumeSessionId && options.resumeSessionId?.trim().length === 0) {
104
+ throw contractError({
105
+ code: 'invalid-field',
106
+ field,
107
+ exitCode: 2,
108
+ message: 'options.resumeSessionId must be a non-empty OpenCode session ID.',
109
+ });
110
+ }
111
+ if (optionFeatures(options).supportsResume === false) {
112
+ throw contractError({
113
+ code: 'invalid-field',
114
+ field,
115
+ exitCode: 2,
116
+ message:
117
+ 'OpenCode CLI cannot safely run continuation context because this installation lacks run --session/--continue.',
118
+ });
119
+ }
120
+ if (options.resumeSessionId) {
121
+ args.push('--session', options.resumeSessionId);
122
+ } else {
123
+ args.push('--continue');
124
+ }
125
+ }
126
+
127
+ function webSearchEnv(options: BuildProviderCommandOptions): Readonly<Record<string, string>> {
128
+ if (options.webSearch !== true) return {};
129
+ if (optionFeatures(options).supportsWebSearch !== true) {
130
+ throw new UnsupportedProviderCapabilityError(
131
+ 'opencode',
132
+ 'webSearch',
133
+ 'OpenCode web search requires a parseable OpenCode CLI version >= 1.0.137. Update OpenCode or set providerSettings.opencode.webSearch to false.'
134
+ );
135
+ }
136
+ return { OPENCODE_ENABLE_EXA: '1' };
137
+ }
138
+
139
+ function extractSessionId(line: string): string | null {
140
+ const event = tryParseJson(line.trim());
141
+ if (!isRecord(event)) return null;
142
+ const sessionId = getString(event, 'sessionID') ?? getString(event, 'sessionId');
143
+ return sessionId?.trim() || null;
144
+ }
145
+
92
146
  function buildCommand(context: string, options: BuildProviderCommandOptions = {}): CommandSpec {
93
147
  const finalContext = options.jsonSchema
94
148
  ? appendJsonSchemaPrompt(context, options.jsonSchema)
95
149
  : context;
96
150
  const args: string[] = ['run'];
97
151
  addOpencodeOptionalArgs(args, options);
152
+ addSessionArgs(args, options);
98
153
 
99
154
  args.push(finalContext);
100
155
 
101
156
  return commandSpec({
102
157
  binary: 'opencode',
103
158
  args,
104
- env: {},
159
+ env: webSearchEnv(options),
105
160
  ...(options.cwd === undefined ? {} : { cwd: options.cwd }),
106
161
  warnings: collectOpencodeWarnings(options),
107
162
  });
@@ -226,6 +281,7 @@ export const opencodeAdapter: ProviderAdapter = {
226
281
  defaultMinLevel: 'level1',
227
282
  detectCliFeatures,
228
283
  buildCommand,
284
+ extractSessionId,
229
285
  parseEvent,
230
286
  createParserState: () => createParserState('opencode'),
231
287
  resolveModelSpec,