@the-open-engine/zeroshot 6.14.0 → 6.16.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 (64) 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/opencode.d.ts.map +1 -1
  15. package/lib/agent-cli-provider/adapters/opencode.js +52 -3
  16. package/lib/agent-cli-provider/adapters/opencode.js.map +1 -1
  17. package/lib/agent-cli-provider/contract-actions.d.ts.map +1 -1
  18. package/lib/agent-cli-provider/contract-actions.js +19 -8
  19. package/lib/agent-cli-provider/contract-actions.js.map +1 -1
  20. package/lib/agent-cli-provider/contract-fallback.d.ts.map +1 -1
  21. package/lib/agent-cli-provider/contract-fallback.js +9 -0
  22. package/lib/agent-cli-provider/contract-fallback.js.map +1 -1
  23. package/lib/agent-cli-provider/contract-options.d.ts.map +1 -1
  24. package/lib/agent-cli-provider/contract-options.js +2 -0
  25. package/lib/agent-cli-provider/contract-options.js.map +1 -1
  26. package/lib/agent-cli-provider/errors.d.ts +7 -0
  27. package/lib/agent-cli-provider/errors.d.ts.map +1 -1
  28. package/lib/agent-cli-provider/errors.js +14 -0
  29. package/lib/agent-cli-provider/errors.js.map +1 -1
  30. package/lib/agent-cli-provider/index.d.ts +1 -1
  31. package/lib/agent-cli-provider/index.d.ts.map +1 -1
  32. package/lib/agent-cli-provider/index.js.map +1 -1
  33. package/lib/agent-cli-provider/provider-registry.d.ts +20 -3
  34. package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
  35. package/lib/agent-cli-provider/provider-registry.js +15 -2
  36. package/lib/agent-cli-provider/provider-registry.js.map +1 -1
  37. package/lib/agent-cli-provider/single-agent-runtime.d.ts +14 -2
  38. package/lib/agent-cli-provider/single-agent-runtime.d.ts.map +1 -1
  39. package/lib/agent-cli-provider/single-agent-runtime.js +97 -22
  40. package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -1
  41. package/lib/agent-cli-provider/types.d.ts +11 -1
  42. package/lib/agent-cli-provider/types.d.ts.map +1 -1
  43. package/lib/agent-cli-provider/types.js.map +1 -1
  44. package/package.json +1 -1
  45. package/src/agent/agent-lifecycle.js +52 -1
  46. package/src/agent/agent-task-executor.js +8 -3
  47. package/src/agent-cli-provider/adapters/codex.ts +23 -2
  48. package/src/agent-cli-provider/adapters/common.ts +14 -0
  49. package/src/agent-cli-provider/adapters/index.ts +16 -2
  50. package/src/agent-cli-provider/adapters/opencode.ts +60 -4
  51. package/src/agent-cli-provider/contract-actions.ts +20 -9
  52. package/src/agent-cli-provider/contract-fallback.ts +9 -0
  53. package/src/agent-cli-provider/contract-options.ts +2 -0
  54. package/src/agent-cli-provider/errors.ts +14 -0
  55. package/src/agent-cli-provider/index.ts +1 -0
  56. package/src/agent-cli-provider/provider-registry.ts +20 -2
  57. package/src/agent-cli-provider/single-agent-runtime.ts +144 -24
  58. package/src/agent-cli-provider/types.ts +12 -1
  59. package/src/claude-task-runner.js +11 -0
  60. package/src/orchestrator.js +8 -3
  61. package/src/providers/index.js +6 -0
  62. package/src/task-run-model-args.js +58 -40
  63. package/src/task-spawn-cleanup-ownership.js +3 -0
  64. package/src/task-startup-error.js +68 -0
@@ -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(
@@ -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,
@@ -23,6 +23,10 @@ import type { ProcessRunner } from './process-runner';
23
23
 
24
24
  function runBuildCommand(request: RequestData): ContractEnvelope {
25
25
  const { adapter, commandSpec, options } = buildCommandSpec(request);
26
+ const webSearch = {
27
+ requested: options.webSearch === true,
28
+ effective: options.webSearch === true && options.cliFeatures?.supportsWebSearch === true,
29
+ };
26
30
  return successEnvelope({
27
31
  command: request.command ?? 'build-command',
28
32
  adapter,
@@ -31,11 +35,13 @@ function runBuildCommand(request: RequestData): ContractEnvelope {
31
35
  evidence: {
32
36
  outputFormat: options.outputFormat ?? null,
33
37
  schemaMode: schemaMode(options),
38
+ configuration: { webSearch },
34
39
  },
35
40
  result: {
36
41
  commandSpec,
37
42
  outputFormat: options.outputFormat ?? null,
38
43
  schemaMode: schemaMode(options),
44
+ configuration: { webSearch },
39
45
  },
40
46
  });
41
47
  }
@@ -43,11 +49,15 @@ function runBuildCommand(request: RequestData): ContractEnvelope {
43
49
  function runProbe(request: RequestData): ContractEnvelope {
44
50
  const adapter = adapterForProvider(request.provider);
45
51
  const helpText = typeof request.raw.helpText === 'string' ? request.raw.helpText : null;
46
- const runtimeProbe = helpText === null ? probeRuntimeProviderCli(adapter.id) : null;
47
- const capabilities =
48
- runtimeProbe === null
49
- ? adapter.detectCliFeatures(helpText)
50
- : runtimeProbe.capabilities;
52
+ const versionText = typeof request.raw.versionText === 'string' ? request.raw.versionText : '';
53
+ const runtimeProbe =
54
+ helpText === null
55
+ ? probeRuntimeProviderCli(adapter.id)
56
+ : probeRuntimeProviderCli(adapter.id, {
57
+ available: true,
58
+ helpText,
59
+ versionText,
60
+ });
51
61
  return successEnvelope({
52
62
  command: request.command ?? 'probe',
53
63
  adapter,
@@ -60,10 +70,11 @@ function runProbe(request: RequestData): ContractEnvelope {
60
70
  },
61
71
  contractVersion: providerExecutableSchemaVersion,
62
72
  adapterVersion: adapter.adapterVersion,
63
- available: runtimeProbe?.available ?? true,
64
- helpText: runtimeProbe?.helpText ?? helpText,
65
- versionText: runtimeProbe?.versionText ?? null,
66
- capabilities,
73
+ available: runtimeProbe.available,
74
+ helpText: runtimeProbe.helpText,
75
+ versionText: runtimeProbe.versionText,
76
+ capabilities: runtimeProbe.capabilities,
77
+ configuration: runtimeProbe.configuration,
67
78
  credentials: adapter.credentialEnvKeys.map((key) => ({
68
79
  key,
69
80
  present: Boolean(request.env[key] ?? process.env[key]),
@@ -6,6 +6,7 @@ import {
6
6
  type ContractErrorObject,
7
7
  type ContractEnvelope,
8
8
  } from './contract-envelope';
9
+ import { UnsupportedProviderCapabilityError } from './errors';
9
10
  import { getString, isRecord, parseJson, unknownToMessage } from './json';
10
11
  import { mergeRedactions, redactString } from './redaction';
11
12
  import type { ProviderAdapter } from './types';
@@ -77,6 +78,14 @@ function errorObject(requestError: ContractRequestError): ContractErrorObject {
77
78
 
78
79
  export function requestErrorFromUnknown(error: unknown): ContractRequestError {
79
80
  if (error instanceof ContractRequestError) return error;
81
+ if (error instanceof UnsupportedProviderCapabilityError) {
82
+ return contractError({
83
+ code: error.code,
84
+ message: error.message,
85
+ exitCode: 4,
86
+ field: 'options.webSearch',
87
+ });
88
+ }
80
89
  return contractError({
81
90
  code: 'internal-error',
82
91
  message: unknownToMessage(error),
@@ -44,6 +44,7 @@ const CLI_FEATURE_FIELDS = [
44
44
  'supportsAddDir',
45
45
  'supportsMcpConfig',
46
46
  'supportsResume',
47
+ 'supportsWebSearch',
47
48
  'supportsBundledRunner',
48
49
  'supportsAcpStdio',
49
50
  'supportsPromptImages',
@@ -210,6 +211,7 @@ function normalizeBuildOptions(value: Record<string, unknown>): BuildProviderCom
210
211
  'continueSession',
211
212
  optionalBooleanValue(value.continueSession, 'options.continueSession')
212
213
  );
214
+ addDefined(result, 'webSearch', optionalBooleanValue(value.webSearch, 'options.webSearch'));
213
215
  addDefined(
214
216
  result,
215
217
  'claudeSettingsFile',
@@ -1,6 +1,20 @@
1
1
  import { getNumber, getRecord, getString, isRecord, unknownToMessage } from './json';
2
2
  import type { ErrorClassification } from './types';
3
3
 
4
+ export class UnsupportedProviderCapabilityError extends Error {
5
+ readonly code = 'unsupported-capability';
6
+ readonly permanent = true;
7
+ readonly provider: string;
8
+ readonly capability: string;
9
+
10
+ constructor(provider: string, capability: string, message: string) {
11
+ super(message);
12
+ this.name = 'UnsupportedProviderCapabilityError';
13
+ this.provider = provider;
14
+ this.capability = capability;
15
+ }
16
+ }
17
+
4
18
  const BASE_RETRYABLE_PATTERNS: readonly RegExp[] = [
5
19
  /rate.?limit/i,
6
20
  /\b429\b/i,
@@ -102,6 +102,7 @@ export type {
102
102
  ToolCallEvent,
103
103
  ToolResultEvent,
104
104
  WarningMetadata,
105
+ WebSearchAttestation,
105
106
  } from './types';
106
107
 
107
108
  import type { AgentCliProviderHelperMetadata } from './types';
@@ -20,6 +20,7 @@ export interface ProviderCapabilities {
20
20
  readonly thinkingMode: ProviderCapabilityState;
21
21
  readonly reasoningEffort: ProviderCapabilityState;
22
22
  readonly sessionResume: ProviderCapabilityState;
23
+ readonly webSearch: ProviderCapabilityState;
23
24
  }
24
25
 
25
26
  interface FixedProviderCommandSpec {
@@ -102,6 +103,7 @@ const STANDARD_CAPABILITIES: Readonly<
102
103
  | 'streamJson'
103
104
  | 'thinkingMode'
104
105
  | 'sessionResume'
106
+ | 'webSearch'
105
107
  >
106
108
  > = {
107
109
  dockerIsolation: true,
@@ -110,6 +112,7 @@ const STANDARD_CAPABILITIES: Readonly<
110
112
  streamJson: true,
111
113
  thinkingMode: true,
112
114
  sessionResume: false,
115
+ webSearch: false,
113
116
  };
114
117
 
115
118
  const CLAUDE_DOCKER_ENV_PASSTHROUGH = [
@@ -203,12 +206,15 @@ export const providerRegistry = [
203
206
  authInstructions: 'codex login',
204
207
  credentialPaths: ['~/.config/codex', '~/.codex'],
205
208
  credentialEnvKeys: codexAdapter.credentialEnvKeys,
206
- settingsFields: [],
209
+ settingsFields: ['webSearch'],
210
+ settingsDefaults: { webSearch: false },
211
+ settingsValidator: (settings): string | null => validateWebSearchSettings('codex', settings),
207
212
  capabilities: {
208
213
  ...STANDARD_CAPABILITIES,
209
214
  jsonSchema: true,
210
215
  reasoningEffort: true,
211
216
  sessionResume: true,
217
+ webSearch: true,
212
218
  },
213
219
  docs: {
214
220
  label: 'Codex',
@@ -326,11 +332,15 @@ export const providerRegistry = [
326
332
  authInstructions: 'opencode auth login',
327
333
  credentialPaths: ['~/.local/share/opencode'],
328
334
  credentialEnvKeys: opencodeAdapter.credentialEnvKeys,
329
- settingsFields: [],
335
+ settingsFields: ['webSearch'],
336
+ settingsDefaults: { webSearch: false },
337
+ settingsValidator: (settings): string | null => validateWebSearchSettings('opencode', settings),
330
338
  capabilities: {
331
339
  ...STANDARD_CAPABILITIES,
332
340
  jsonSchema: 'experimental',
333
341
  reasoningEffort: true,
342
+ sessionResume: true,
343
+ webSearch: true,
334
344
  },
335
345
  docs: {
336
346
  label: 'Opencode',
@@ -472,6 +482,14 @@ export const providerRegistry = [
472
482
  },
473
483
  ] as const satisfies readonly ProviderRegistryEntry[];
474
484
 
485
+ function validateWebSearchSettings(
486
+ provider: 'codex' | 'opencode',
487
+ settings: Record<string, unknown>
488
+ ): string | null {
489
+ if (settings.webSearch === undefined || typeof settings.webSearch === 'boolean') return null;
490
+ return `providerSettings.${provider}.webSearch must be a boolean`;
491
+ }
492
+
475
493
  type RegistryProviderId = (typeof providerRegistry)[number]['id'];
476
494
  type RegistryProviderAlias = (typeof providerRegistry)[number]['aliases'][number];
477
495
 
@@ -1,4 +1,5 @@
1
1
  import { getProviderAdapter } from './adapters';
2
+ import { UnsupportedProviderCapabilityError } from './errors';
2
3
  import { normalizeGatewayBuildOptions, resolveGatewayConfiguration } from './gateway-tools';
3
4
  import { isRecord } from './json';
4
5
  import {
@@ -19,6 +20,7 @@ import type {
19
20
  ProviderId,
20
21
  ResolvedGatewayBuildOptions,
21
22
  ReasoningEffort,
23
+ WebSearchAttestation,
22
24
  } from './types';
23
25
 
24
26
  type UnknownFunction = (...args: readonly unknown[]) => unknown;
@@ -32,6 +34,7 @@ interface RuntimeProviderSettings {
32
34
  readonly defaultLevel?: ModelLevel;
33
35
  readonly levelOverrides: LevelOverrides;
34
36
  readonly gateway?: GatewayBuildOptions;
37
+ readonly webSearch?: boolean;
35
38
  }
36
39
 
37
40
  interface RuntimeCommandContext {
@@ -50,6 +53,9 @@ export interface PreparedSingleAgentProviderCommand {
50
53
  readonly commandSpec: CommandSpec;
51
54
  readonly options: BuildProviderCommandOptions;
52
55
  readonly cliFeatures: CliFeatureOverrides;
56
+ readonly configuration: {
57
+ readonly webSearch: WebSearchAttestation;
58
+ };
53
59
  }
54
60
 
55
61
  export interface RuntimeProviderProbe {
@@ -57,6 +63,15 @@ export interface RuntimeProviderProbe {
57
63
  readonly helpText: string;
58
64
  readonly versionText: string;
59
65
  readonly capabilities: ProviderCliFeatures;
66
+ readonly configuration: {
67
+ readonly webSearch: WebSearchAttestation;
68
+ };
69
+ }
70
+
71
+ interface RuntimeProbeEvidence {
72
+ readonly available?: boolean;
73
+ readonly helpText: string;
74
+ readonly versionText: string;
60
75
  }
61
76
 
62
77
  type MutableModelSpec = {
@@ -91,7 +106,13 @@ export function prepareSingleAgentProviderCommand(
91
106
  adapter.id,
92
107
  baseOptions.cwd ?? process.cwd()
93
108
  );
94
- const cliFeatures = resolveRuntimeCliFeatures(adapter.id, baseOptions.cliFeatures);
109
+ const requestedWebSearch = baseOptions.webSearch ?? providerSettings.webSearch;
110
+ assertWebSearchDeclared(adapter.id, requestedWebSearch);
111
+ const cliFeatures = resolveRuntimeCliFeatures(
112
+ adapter.id,
113
+ baseOptions.cliFeatures,
114
+ requestedWebSearch === true
115
+ );
95
116
  const authEnv = baseOptions.authEnv ?? resolveRuntimeAuthEnv(adapter.id, settings);
96
117
  const options = buildRuntimeOptions(baseOptions, adapter, providerSettings, {
97
118
  cliFeatures,
@@ -101,6 +122,9 @@ export function prepareSingleAgentProviderCommand(
101
122
  adapter,
102
123
  options,
103
124
  cliFeatures,
125
+ configuration: {
126
+ webSearch: webSearchAttestation(options),
127
+ },
104
128
  commandSpec: adapter.buildCommand(input.context, options),
105
129
  };
106
130
  }
@@ -111,22 +135,35 @@ export function detectRuntimeProviderCliFeatures(provider: string): ProviderCliF
111
135
 
112
136
  function resolveRuntimeCliFeatures(
113
137
  provider: ProviderId,
114
- overrides: CliFeatureOverrides | undefined
138
+ overrides: CliFeatureOverrides | undefined,
139
+ webSearchRequested: boolean
115
140
  ): CliFeatureOverrides {
116
141
  if (provider === 'gateway') {
142
+ const detected = detectRuntimeProviderCliFeatures(provider);
117
143
  return {
118
- ...detectRuntimeProviderCliFeatures(provider),
144
+ ...detected,
119
145
  ...overrides,
120
146
  supportsBundledRunner: true,
147
+ supportsWebSearch: false,
121
148
  };
122
149
  }
123
- if (getProviderRegistryEntry(provider).invoke.lane !== 'acp-stdio') {
124
- return overrides ?? detectRuntimeProviderCliFeatures(provider);
150
+ if (getProviderRegistryEntry(provider).invoke.lane === 'acp-stdio') {
151
+ const detected = detectRuntimeProviderCliFeatures(provider);
152
+ if (overrides === undefined) return detected;
153
+ return mergeAcpFailClosedCliFeatures(detected, overrides);
125
154
  }
126
-
155
+ if (overrides === undefined) return detectRuntimeProviderCliFeatures(provider);
156
+ if (!webSearchRequested) return overrides;
127
157
  const detected = detectRuntimeProviderCliFeatures(provider);
128
- if (overrides === undefined) return detected;
129
- return mergeAcpFailClosedCliFeatures(detected, overrides);
158
+ return {
159
+ ...overrides,
160
+ supportsResume:
161
+ 'supportsResume' in detected &&
162
+ detected.supportsResume === true &&
163
+ overrides.supportsResume !== false,
164
+ supportsWebSearch:
165
+ detected.supportsWebSearch === true && overrides.supportsWebSearch !== false,
166
+ };
130
167
  }
131
168
 
132
169
  function mergeAcpFailClosedCliFeatures(
@@ -154,33 +191,61 @@ function mergeAcpFailClosedCliFeatures(
154
191
  };
155
192
  }
156
193
 
157
- export function probeRuntimeProviderCli(provider: string): RuntimeProviderProbe {
194
+ export function probeRuntimeProviderCli(
195
+ provider: string,
196
+ evidence?: RuntimeProbeEvidence
197
+ ): RuntimeProviderProbe {
158
198
  const adapter = getProviderAdapter(provider);
159
199
  if (adapter.id === 'gateway') {
160
200
  return probeGatewayProvider(adapter);
161
201
  }
202
+ const requested = getProviderRegistryEntry(adapter.id).settingsFields.includes('webSearch')
203
+ ? runtimeProviderSettings(loadRuntimeSettings(), adapter.id, process.cwd()).webSearch === true
204
+ : false;
162
205
  const helpCommand = runtimeHelpCommand(adapter.id);
163
- const commandAvailable = booleanResult(commandExistsFn(helpCommand.command));
206
+ const commandAvailable =
207
+ evidence === undefined
208
+ ? booleanResult(commandExistsFn(helpCommand.command))
209
+ : evidence.available !== false;
164
210
  if (!commandAvailable) {
211
+ const capabilities = attestedCliFeatures(adapter, '', '');
165
212
  return {
166
213
  available: false,
167
214
  helpText: '',
168
215
  versionText: '',
169
- capabilities: adapter.detectCliFeatures(''),
216
+ capabilities,
217
+ configuration: {
218
+ webSearch: webSearchAttestation({ webSearch: requested, cliFeatures: capabilities }),
219
+ },
170
220
  };
171
221
  }
172
222
 
173
- const helpText = stringResult(getHelpOutputFn(helpCommand.command, helpCommand.args)).trim();
174
- const versionText = stringResult(
175
- getVersionOutputFn(helpCommand.command, helpCommand.args)
176
- ).trim();
223
+ const helpText =
224
+ evidence?.helpText ??
225
+ stringResult(getHelpOutputFn(helpCommand.command, helpCommand.args)).trim();
226
+ const versionText =
227
+ evidence?.versionText ??
228
+ stringResult(
229
+ getVersionOutputFn(
230
+ helpCommand.command,
231
+ getProviderRegistryEntry(adapter.id).capabilities.webSearch === true
232
+ ? []
233
+ : helpCommand.args
234
+ )
235
+ ).trim();
177
236
  const availabilityProbe = getProviderRegistryEntry(adapter.id).availabilityProbe ?? 'command';
237
+ const capabilities = attestedCliFeatures(adapter, helpText, versionText);
178
238
 
179
239
  return {
180
- available: availabilityProbe === 'help-or-version' ? Boolean(helpText || versionText) : true,
240
+ available:
241
+ evidence?.available ??
242
+ (availabilityProbe === 'help-or-version' ? Boolean(helpText || versionText) : true),
181
243
  helpText,
182
244
  versionText,
183
- capabilities: adapter.detectCliFeatures(helpText),
245
+ capabilities,
246
+ configuration: {
247
+ webSearch: webSearchAttestation({ webSearch: requested, cliFeatures: capabilities }),
248
+ },
184
249
  };
185
250
  }
186
251
 
@@ -197,10 +262,13 @@ function buildRuntimeOptions(
197
262
  providerSettings,
198
263
  modelSpec
199
264
  );
265
+ const webSearch = baseOptions.webSearch ?? providerSettings.webSearch;
266
+ assertWebSearchDeclared(adapter.id, webSearch);
200
267
  const resolved = {
201
268
  ...baseOptions,
202
269
  modelSpec,
203
270
  ...(gateway === undefined ? {} : { gateway }),
271
+ ...(webSearch === undefined ? {} : { webSearch }),
204
272
  cliFeatures: runtime.cliFeatures,
205
273
  };
206
274
  if (baseOptions.jsonSchema && !supportsProviderCapability(adapter.id, 'jsonSchema')) {
@@ -320,16 +388,20 @@ function runtimeProviderSettings(
320
388
  providerSettings.levelOverrides,
321
389
  `settings.providerSettings.${provider}.levelOverrides`
322
390
  );
391
+ const webSearch = optionalBoolean(
392
+ providerSettings.webSearch,
393
+ `settings.providerSettings.${provider}.webSearch`
394
+ );
323
395
  const gateway =
324
396
  provider === 'gateway'
325
397
  ? normalizeGatewayBuildOptions(providerSettings, 'settings.providerSettings.gateway', cwd)
326
398
  : undefined;
327
- if (defaultLevel === undefined) {
328
- return gateway === undefined ? { levelOverrides } : { levelOverrides, gateway };
329
- }
330
- return gateway === undefined
331
- ? { defaultLevel, levelOverrides }
332
- : { defaultLevel, levelOverrides, gateway };
399
+ const base = {
400
+ levelOverrides,
401
+ ...(gateway === undefined ? {} : { gateway }),
402
+ ...(webSearch === undefined ? {} : { webSearch }),
403
+ };
404
+ return defaultLevel === undefined ? base : { ...base, defaultLevel };
333
405
  }
334
406
 
335
407
  function runtimeHelpCommand(provider: ProviderId): CommandParts {
@@ -340,7 +412,7 @@ function runtimeHelpCommand(provider: ProviderId): CommandParts {
340
412
  }
341
413
 
342
414
  function probeGatewayProvider(adapter: ProviderAdapter): RuntimeProviderProbe {
343
- const capabilities = adapter.detectCliFeatures('');
415
+ const capabilities = attestedCliFeatures(adapter, '', '');
344
416
  try {
345
417
  const settings = loadRuntimeSettings();
346
418
  const providerSettings = runtimeProviderSettings(settings, 'gateway', process.cwd());
@@ -354,6 +426,9 @@ function probeGatewayProvider(adapter: ProviderAdapter): RuntimeProviderProbe {
354
426
  helpText: 'Bundled gateway runner',
355
427
  versionText: process.version,
356
428
  capabilities,
429
+ configuration: {
430
+ webSearch: { requested: false, effective: false },
431
+ },
357
432
  };
358
433
  } catch {
359
434
  return {
@@ -361,10 +436,49 @@ function probeGatewayProvider(adapter: ProviderAdapter): RuntimeProviderProbe {
361
436
  helpText: 'Bundled gateway runner',
362
437
  versionText: process.version,
363
438
  capabilities,
439
+ configuration: {
440
+ webSearch: { requested: false, effective: false },
441
+ },
364
442
  };
365
443
  }
366
444
  }
367
445
 
446
+ function attestedCliFeatures(
447
+ adapter: ProviderAdapter,
448
+ helpText: string,
449
+ versionText: string
450
+ ): ProviderCliFeatures {
451
+ const detected = adapter.detectCliFeatures(helpText, versionText);
452
+ return {
453
+ ...detected,
454
+ supportsWebSearch:
455
+ getProviderRegistryEntry(adapter.id).capabilities.webSearch === true &&
456
+ detected.supportsWebSearch === true,
457
+ };
458
+ }
459
+
460
+ function webSearchAttestation(options: {
461
+ readonly webSearch?: boolean;
462
+ readonly cliFeatures?: CliFeatureOverrides;
463
+ }): WebSearchAttestation {
464
+ const requested = options.webSearch === true;
465
+ return {
466
+ requested,
467
+ effective: requested && options.cliFeatures?.supportsWebSearch === true,
468
+ };
469
+ }
470
+
471
+ function assertWebSearchDeclared(provider: ProviderId, requested: boolean | undefined): void {
472
+ if (requested === undefined || getProviderRegistryEntry(provider).capabilities.webSearch === true) {
473
+ return;
474
+ }
475
+ throw new UnsupportedProviderCapabilityError(
476
+ provider,
477
+ 'webSearch',
478
+ `Provider ${provider} does not expose provider-controlled native web search; remove options.webSearch.`
479
+ );
480
+ }
481
+
368
482
  function loadRuntimeSettings(): Record<string, unknown> {
369
483
  if (Object.prototype.hasOwnProperty.call(process.env, LEGACY_ISOLATED_PROVIDER_SETTINGS_ENV)) {
370
484
  throw new Error(
@@ -439,6 +553,12 @@ function addReasoningEffort(result: MutableModelSpec, value: unknown, field: str
439
553
  if (effort !== undefined) result.reasoningEffort = effort;
440
554
  }
441
555
 
556
+ function optionalBoolean(value: unknown, field: string): boolean | undefined {
557
+ if (value === undefined) return undefined;
558
+ if (typeof value === 'boolean') return value;
559
+ throw new Error(`${field} must be a boolean.`);
560
+ }
561
+
442
562
  function optionalModelLevel(value: unknown, field: string): ModelLevel | undefined {
443
563
  if (value === undefined) return undefined;
444
564
  if (value === 'level1' || value === 'level2' || value === 'level3') return value;