@the-open-engine/zeroshot 6.22.0 → 6.24.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 (73) hide show
  1. package/docker/zeroshot-cluster/Dockerfile +7 -0
  2. package/lib/agent-cli-provider/adapters/omp.d.ts +3 -1
  3. package/lib/agent-cli-provider/adapters/omp.d.ts.map +1 -1
  4. package/lib/agent-cli-provider/adapters/omp.js +252 -269
  5. package/lib/agent-cli-provider/adapters/omp.js.map +1 -1
  6. package/lib/agent-cli-provider/contract-invoke.d.ts.map +1 -1
  7. package/lib/agent-cli-provider/contract-invoke.js +68 -14
  8. package/lib/agent-cli-provider/contract-invoke.js.map +1 -1
  9. package/lib/agent-cli-provider/contract-options.d.ts.map +1 -1
  10. package/lib/agent-cli-provider/contract-options.js +5 -3
  11. package/lib/agent-cli-provider/contract-options.js.map +1 -1
  12. package/lib/agent-cli-provider/index.d.ts +6 -0
  13. package/lib/agent-cli-provider/index.d.ts.map +1 -1
  14. package/lib/agent-cli-provider/index.js +12 -1
  15. package/lib/agent-cli-provider/index.js.map +1 -1
  16. package/lib/agent-cli-provider/omp-release.d.ts +6 -1
  17. package/lib/agent-cli-provider/omp-release.d.ts.map +1 -1
  18. package/lib/agent-cli-provider/omp-release.js +41 -2
  19. package/lib/agent-cli-provider/omp-release.js.map +1 -1
  20. package/lib/agent-cli-provider/omp-rpc-bounds.d.ts +7 -0
  21. package/lib/agent-cli-provider/omp-rpc-bounds.d.ts.map +1 -0
  22. package/lib/agent-cli-provider/omp-rpc-bounds.js +27 -0
  23. package/lib/agent-cli-provider/omp-rpc-bounds.js.map +1 -0
  24. package/lib/agent-cli-provider/omp-rpc-driver.d.ts +40 -0
  25. package/lib/agent-cli-provider/omp-rpc-driver.d.ts.map +1 -0
  26. package/lib/agent-cli-provider/omp-rpc-driver.js +494 -0
  27. package/lib/agent-cli-provider/omp-rpc-driver.js.map +1 -0
  28. package/lib/agent-cli-provider/omp-rpc-events.d.ts +28 -0
  29. package/lib/agent-cli-provider/omp-rpc-events.d.ts.map +1 -0
  30. package/lib/agent-cli-provider/omp-rpc-events.js +264 -0
  31. package/lib/agent-cli-provider/omp-rpc-events.js.map +1 -0
  32. package/lib/agent-cli-provider/omp-rpc-protocol.d.ts +1 -1
  33. package/lib/agent-cli-provider/omp-rpc-protocol.d.ts.map +1 -1
  34. package/lib/agent-cli-provider/omp-rpc-protocol.js +8 -3
  35. package/lib/agent-cli-provider/omp-rpc-protocol.js.map +1 -1
  36. package/lib/agent-cli-provider/omp-rpc-session.d.ts +17 -0
  37. package/lib/agent-cli-provider/omp-rpc-session.d.ts.map +1 -0
  38. package/lib/agent-cli-provider/omp-rpc-session.js +6 -0
  39. package/lib/agent-cli-provider/omp-rpc-session.js.map +1 -0
  40. package/lib/agent-cli-provider/provider-registry.d.ts +33 -17
  41. package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
  42. package/lib/agent-cli-provider/provider-registry.js +63 -11
  43. package/lib/agent-cli-provider/provider-registry.js.map +1 -1
  44. package/lib/agent-cli-provider/types.d.ts +13 -11
  45. package/lib/agent-cli-provider/types.d.ts.map +1 -1
  46. package/lib/agent-cli-provider/types.js.map +1 -1
  47. package/lib/docker-config.js +122 -5
  48. package/package.json +2 -1
  49. package/src/agent/agent-lifecycle.js +12 -1
  50. package/src/agent/provider-session.js +1 -0
  51. package/src/agent-cli-provider/adapters/omp.ts +276 -329
  52. package/src/agent-cli-provider/contract-invoke.ts +86 -15
  53. package/src/agent-cli-provider/contract-options.ts +5 -3
  54. package/src/agent-cli-provider/index.ts +13 -0
  55. package/src/agent-cli-provider/omp-release.ts +50 -1
  56. package/src/agent-cli-provider/omp-rpc-bounds.ts +28 -0
  57. package/src/agent-cli-provider/omp-rpc-driver.ts +611 -0
  58. package/src/agent-cli-provider/omp-rpc-events.ts +318 -0
  59. package/src/agent-cli-provider/omp-rpc-protocol.ts +8 -3
  60. package/src/agent-cli-provider/omp-rpc-session.ts +20 -0
  61. package/src/agent-cli-provider/provider-registry.ts +118 -23
  62. package/src/agent-cli-provider/types.ts +14 -11
  63. package/src/command-cleanup-ownership.js +307 -0
  64. package/src/isolation-manager.js +535 -89
  65. package/src/omp-config-overlay.js +96 -0
  66. package/src/orchestrator.js +31 -6
  67. package/src/preflight.js +50 -5
  68. package/src/watcher-prompt-channel.js +209 -0
  69. package/task-lib/command-spec-cleanup.js +1 -262
  70. package/task-lib/provider-helper-runtime.js +6 -0
  71. package/task-lib/rpc-watcher.js +186 -0
  72. package/task-lib/runner.js +40 -6
  73. package/task-lib/watcher-output-runtime.js +32 -0
@@ -1,17 +1,11 @@
1
- import { appendJsonSchemaPrompt } from '../schema';
2
1
  import { contractError } from '../contract-errors';
2
+ import { UnsupportedProviderCapabilityError } from '../errors';
3
+ import { appendJsonSchemaPrompt } from '../schema';
4
+ import { isRecord, unknownToMessage } from '../json';
5
+ import { OMP_REMEDIATION, OMP_SUPPORTED_VERSION } from '../omp-release';
6
+ import { parseNormalizedOmpRpcEventLine } from '../omp-rpc-events';
3
7
  import {
4
- getArray,
5
- getBoolean,
6
- getNumber,
7
- getOptionalString,
8
- getRecord,
9
- getString,
10
- isRecord,
11
- tryParseJson,
12
- unknownToMessage,
13
- } from '../json';
14
- import {
8
+ InvalidProviderModelError,
15
9
  type BuildProviderCommandOptions,
16
10
  type CommandSpec,
17
11
  type ErrorClassification,
@@ -20,8 +14,8 @@ import {
20
14
  type ModelCatalogEntry,
21
15
  type ModelLevel,
22
16
  type OmpCliFeatures,
23
- type OutputEvent,
24
17
  type ProviderAdapter,
18
+ type ProviderParseResult,
25
19
  type ProviderParserState,
26
20
  type ResolvedModelSpec,
27
21
  type WarningMetadata,
@@ -35,62 +29,176 @@ import {
35
29
  warning,
36
30
  } from './common';
37
31
 
32
+ interface OmpConfigOverlay {
33
+ readonly dir: string;
34
+ readonly file: string;
35
+ }
36
+
37
+ type UnknownFunction = (...args: unknown[]) => unknown;
38
+
39
+ function isUnknownFunction(value: unknown): value is UnknownFunction {
40
+ return typeof value === 'function';
41
+ }
42
+
43
+ function createOmpConfigOverlay(): OmpConfigOverlay {
44
+ const overlayModule: unknown = require('../../../src/omp-config-overlay');
45
+ if (!isRecord(overlayModule) || !isUnknownFunction(overlayModule.createOmpConfigOverlay)) {
46
+ throw new Error('src/omp-config-overlay must export createOmpConfigOverlay.');
47
+ }
48
+ const overlay = overlayModule.createOmpConfigOverlay();
49
+ if (
50
+ !isRecord(overlay) ||
51
+ typeof overlay.dir !== 'string' ||
52
+ typeof overlay.file !== 'string'
53
+ ) {
54
+ throw new Error('createOmpConfigOverlay() must return { dir, file } strings.');
55
+ }
56
+ return { dir: overlay.dir, file: overlay.file };
57
+ }
58
+
38
59
  const MODEL_CATALOG: Readonly<Record<string, ModelCatalogEntry>> = {};
39
60
 
40
61
  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' },
62
+ level1: { rank: 1, model: '@smol' },
63
+ level2: { rank: 2, model: '@default' },
64
+ level3: { rank: 3, model: '@slow' },
44
65
  };
45
66
 
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 {
67
+ // Verified against tagged v17.2.1 source: docs/environment-variables.md ("Model/provider
68
+ // authentication", "GitHub/Copilot tokens", "Auth broker", web-search credentials, Bedrock,
69
+ // Azure, Vertex sections) plus docs/providers.md's provider/env-var map. This is the full
70
+ // official host credential inventory OMP itself resolves credentials from — distinct from
71
+ // (and broader than) any narrower automatic Docker env-passthrough allowlist — because OMP
72
+ // routes to 70+ downstream model/search providers and every one of their credential env vars
73
+ // must be detected and redacted from Zeroshot logs/output, not just the handful Zeroshot's
74
+ // own cluster config commonly sets.
75
+ const CREDENTIAL_ENV_KEYS: readonly string[] = [
76
+ 'AIMLAPI_API_KEY',
77
+ 'AI_GATEWAY_API_KEY',
78
+ 'ALIBABA_CODING_PLAN_API_KEY',
79
+ 'ALIBABA_TOKEN_PLAN_API_KEY',
80
+ 'ANTHROPIC_API_KEY',
81
+ 'ANTHROPIC_FOUNDRY_API_KEY',
82
+ 'ANTHROPIC_OAUTH_TOKEN',
83
+ 'ANTHROPIC_SEARCH_API_KEY',
84
+ 'AWS_ACCESS_KEY_ID',
85
+ 'AWS_BEARER_TOKEN_BEDROCK',
86
+ 'AWS_SECRET_ACCESS_KEY',
87
+ 'AZURE_OPENAI_API_KEY',
88
+ 'BAILIAN_TOKEN_PLAN_API_KEY',
89
+ 'BRAVE_API_KEY',
90
+ 'CEREBRAS_API_KEY',
91
+ 'CLAUDE_CODE_CLIENT_CERT',
92
+ 'CLAUDE_CODE_CLIENT_KEY',
93
+ 'CLOUDFLARE_AI_GATEWAY_API_KEY',
94
+ 'COPILOT_GITHUB_TOKEN',
95
+ 'CURSOR_ACCESS_TOKEN',
96
+ 'DEEPSEEK_API_KEY',
97
+ 'EXA_API_KEY',
98
+ 'FIREPASS_API_KEY',
99
+ 'FIREWORKS_API_KEY',
100
+ 'GEMINI_API_KEY',
101
+ 'GH_TOKEN',
102
+ 'GITHUB_TOKEN',
103
+ 'GITLAB_TOKEN',
104
+ 'GOOGLE_API_KEY',
105
+ 'GOOGLE_APPLICATION_CREDENTIALS',
106
+ 'GOOGLE_CLOUD_API_KEY',
107
+ 'GROQ_API_KEY',
108
+ 'HF_TOKEN',
109
+ 'HUGGINGFACE_HUB_TOKEN',
110
+ 'JINA_API_KEY',
111
+ 'KAGI_API_KEY',
112
+ 'KILO_API_KEY',
113
+ 'KIMI_SEARCH_API_KEY',
114
+ 'LITELLM_API_KEY',
115
+ 'LLAMA_CPP_API_KEY',
116
+ 'LM_STUDIO_API_KEY',
117
+ 'MINIMAX_API_KEY',
118
+ 'MINIMAX_CODE_API_KEY',
119
+ 'MINIMAX_CODE_CN_API_KEY',
120
+ 'MISTRAL_API_KEY',
121
+ 'MOONSHOT_API_KEY',
122
+ 'MOONSHOT_SEARCH_API_KEY',
123
+ 'NANO_GPT_API_KEY',
124
+ 'NOVITA_API_KEY',
125
+ 'NVIDIA_API_KEY',
126
+ 'OLLAMA_API_KEY',
127
+ 'OLLAMA_CLOUD_API_KEY',
128
+ 'OMP_AUTH_BROKER_TOKEN',
129
+ 'OPENAI_API_KEY',
130
+ 'OPENCODE_API_KEY',
131
+ 'OPENROUTER_API_KEY',
132
+ 'PARALLEL_API_KEY',
133
+ 'PERPLEXITY_API_KEY',
134
+ 'PERPLEXITY_COOKIES',
135
+ 'QIANFAN_API_KEY',
136
+ 'QWEN_OAUTH_TOKEN',
137
+ 'QWEN_PORTAL_API_KEY',
138
+ 'SEARXNG_BASIC_PASSWORD',
139
+ 'SEARXNG_TOKEN',
140
+ 'SILICONFLOW_API_KEY',
141
+ 'SILICONFLOW_CN_API_KEY',
142
+ 'SMITHERY_API_KEY',
143
+ 'SYNTHETIC_API_KEY',
144
+ 'TAVILY_API_KEY',
145
+ 'TOGETHER_API_KEY',
146
+ 'UMANS_AI_CODING_PLAN_API_KEY',
147
+ 'VENICE_API_KEY',
148
+ 'VLLM_API_KEY',
149
+ 'WAFER_SERVERLESS_API_KEY',
150
+ 'XAI_API_KEY',
151
+ 'XAI_OAUTH_TOKEN',
152
+ 'XIAOMI_API_KEY',
153
+ 'XIAOMI_TOKEN_PLAN_AMS_API_KEY',
154
+ 'XIAOMI_TOKEN_PLAN_CN_API_KEY',
155
+ 'XIAOMI_TOKEN_PLAN_SGP_API_KEY',
156
+ 'ZAI_API_KEY',
157
+ 'ZENMUX_API_KEY',
158
+ 'ZHIPU_API_KEY',
159
+ ];
160
+
161
+ const VERSION_TOKEN_PATTERN = /(?<![\w.])17\.2\.1(?![\w.])/;
162
+ const MODEL_ID_PATTERN = /^@?[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/u;
163
+
164
+ function detectCliFeatures(helpText?: string | null, versionText?: string | null): OmpCliFeatures {
59
165
  const help = helpText ?? '';
60
- const unknown = !help;
166
+ const version = (versionText ?? '').trim();
61
167
  return {
62
168
  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,
169
+ versionMatches: VERSION_TOKEN_PATTERN.test(version),
170
+ supportsRpcMode: /(^|\s)rpc(\s|$)/m.test(help),
171
+ supportsConfig: /--config\b/.test(help),
172
+ supportsModel: /--model\b/.test(help),
173
+ supportsThinking: /--thinking\b/.test(help),
174
+ supportsApprovalMode: /--approval-mode\b/.test(help),
175
+ supportsNoTitle: /--no-title\b/.test(help),
176
+ supportsNoSession: /--no-session\b/.test(help),
177
+ supportsSessionDir: /--session-dir\b/.test(help),
178
+ supportsResume: /--resume\b/.test(help),
179
+ unknown: !help,
74
180
  };
75
181
  }
76
182
 
77
183
  function assertRequiredOmpFeatures(options: BuildProviderCommandOptions): void {
78
184
  const features = optionFeatures(options);
79
185
  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'],
186
+ [features.versionMatches, `exact OMP version ${OMP_SUPPORTED_VERSION}`],
187
+ [features.supportsRpcMode, '"rpc" mode'],
188
+ [features.supportsConfig, '--config'],
189
+ [features.supportsModel, '--model'],
190
+ [features.supportsApprovalMode, '--approval-mode'],
191
+ [features.supportsNoTitle, '--no-title'],
192
+ [features.supportsNoSession, '--no-session'],
84
193
  ];
85
- const missing = required.filter(([supported]) => supported === false).map(([, flag]) => flag);
194
+ const missing = required.filter(([supported]) => supported === false).map(([, label]) => label);
86
195
  if (missing.length === 0) return;
87
196
  throw contractError({
88
197
  code: 'unsupported-provider-cli',
89
198
  exitCode: 2,
90
199
  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.',
200
+ `omp CLI is missing required evidence: ${missing.join(', ')}. ${OMP_REMEDIATION} ` +
201
+ 'Zeroshot will not silently fall back to Pi or another provider.',
94
202
  });
95
203
  }
96
204
 
@@ -103,286 +211,78 @@ function failClosedUnsupportedSessionControl(options: BuildProviderCommandOption
103
211
  field,
104
212
  exitCode: 2,
105
213
  message:
106
- 'OMP CLI does not support resume/continue session control; fail closed and start a fresh run instead.',
214
+ 'OMP RPC lane runs sessionless (--no-session) in this slice; resume/continue session control is capability-gated off (sessionResume: false).',
107
215
  });
108
216
  }
109
217
 
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');
218
+ function rejectMcpConfig(options: BuildProviderCommandOptions): void {
219
+ if (!options.mcpConfig || options.mcpConfig.length === 0) return;
220
+ throw new UnsupportedProviderCapabilityError(
221
+ 'omp',
222
+ 'mcpServers',
223
+ "OMP does not accept Zeroshot's --mcp-config surface; OMP's own discovered MCP/web tools remain governed by its own harness policy, not translated from Claude/Copilot MCP envelopes."
224
+ );
116
225
  }
117
226
 
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
- }
227
+ function resolveModelSelector(options: BuildProviderCommandOptions): string {
228
+ const model = options.modelSpec?.model;
229
+ if (typeof model === 'string' && model.length > 0) return model;
230
+ throw contractError({
231
+ code: 'unsupported-provider-cli',
232
+ exitCode: 2,
233
+ message: 'omp requires a resolved --model selector; none was resolved for this run.',
234
+ });
126
235
  }
127
236
 
128
237
  function collectWarnings(options: BuildProviderCommandOptions): WarningMetadata[] {
129
- const features = optionFeatures(options);
130
- const warnings: WarningMetadata[] = [];
238
+ if (!options.jsonSchema) return [];
239
+ return [
240
+ warning(
241
+ 'omp',
242
+ 'omp-jsonschema',
243
+ 'OMP CLI does not support provider-native JSON schema; appending schema instructions to the prompt and validating locally.'
244
+ ),
245
+ ];
246
+ }
131
247
 
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;
248
+ /** Prompt text sent over the RPC `prompt` command — never part of argv for the rpc-stdio lane. */
249
+ export function buildOmpPrompt(context: string, options: BuildProviderCommandOptions = {}): string {
250
+ return options.jsonSchema ? appendJsonSchemaPrompt(context, options.jsonSchema) : context;
160
251
  }
161
252
 
162
- function buildCommand(context: string, options: BuildProviderCommandOptions = {}): CommandSpec {
253
+ function buildCommand(_context: string, options: BuildProviderCommandOptions = {}): CommandSpec {
163
254
  assertRequiredOmpFeatures(options);
164
255
  failClosedUnsupportedSessionControl(options);
165
- const finalContext = options.jsonSchema
166
- ? appendJsonSchemaPrompt(context, options.jsonSchema)
167
- : context;
168
- const args: string[] = ['--mode', 'json', '-p'];
256
+ rejectMcpConfig(options);
257
+ const modelSelector = resolveModelSelector(options);
258
+ const warnings = collectWarnings(options);
259
+ const overlay = createOmpConfigOverlay();
169
260
 
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);
261
+ const args: string[] = ['--mode', 'rpc', '--no-session', '--model', modelSelector];
262
+ if (options.modelSpec?.reasoningEffort) {
263
+ args.push('--thinking', options.modelSpec.reasoningEffort);
264
+ }
265
+ args.push('--approval-mode', 'yolo', '--no-title', '--config', overlay.file);
175
266
 
176
267
  return commandSpec({
177
268
  binary: 'omp',
178
269
  args,
179
270
  env: {},
180
271
  ...(options.cwd === undefined ? {} : { cwd: options.cwd }),
181
- warnings: collectWarnings(options),
272
+ cleanup: [overlay.dir],
273
+ cleanupMetadata: [
274
+ { kind: 'temp-directory', provider: 'omp', path: overlay.dir, reason: 'isolated-config' },
275
+ ],
276
+ warnings,
182
277
  });
183
278
  }
184
279
 
185
280
  function createOmpState(): ProviderParserState {
186
- return {
187
- ...createParserState('omp'),
188
- lastAssistantText: '',
189
- lastAssistantThinking: '',
190
- };
281
+ return createParserState('omp');
191
282
  }
192
283
 
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;
284
+ function parseEvent(line: string, state: ProviderParserState): ProviderParseResult {
285
+ return parseNormalizedOmpRpcEventLine(line, state);
386
286
  }
387
287
 
388
288
  function resolveModelSpec(level: ModelLevel, overrides?: LevelOverrides): ResolvedModelSpec {
@@ -397,42 +297,89 @@ function resolveModelSpec(level: ModelLevel, overrides?: LevelOverrides): Resolv
397
297
 
398
298
  function validateModelId(modelId: string | null | undefined): string | null | undefined {
399
299
  if (modelId === undefined || modelId === null) return modelId;
400
- if (typeof modelId !== 'string') {
401
- throw new Error(`Invalid model "${unknownToMessage(modelId)}" for provider "omp".`);
300
+ if (typeof modelId !== 'string' || !MODEL_ID_PATTERN.test(modelId)) {
301
+ throw new InvalidProviderModelError(
302
+ `Invalid model "${unknownToMessage(modelId)}" for provider "omp": must match ${MODEL_ID_PATTERN.source}.`
303
+ );
402
304
  }
403
305
  return modelId;
404
306
  }
405
307
 
308
+ const OMP_RETRYABLE_PATTERNS: readonly RegExp[] = [
309
+ /\brate(?:[_ -]?limit| limited)\b/i,
310
+ /\bquota\b/i,
311
+ /\bresource[_ -]?exhausted\b/i,
312
+ /\btemporar(?:y|ily)\b/i,
313
+ /\boverloaded\b/i,
314
+ /\bservice unavailable\b/i,
315
+ ];
316
+
317
+ const OMP_PERMANENT_PATTERNS: readonly RegExp[] = [
318
+ // Driver stopReason tokens (see omp-rpc-driver.ts) that always indicate a permanent,
319
+ // non-retryable failure: bad binary/protocol/version/limits/selector/config, malformed or
320
+ // over-bound frames, and missing auth/model evidence.
321
+ /\bunsupported-protocol\b/i,
322
+ /\bunsupported-limits\b/i,
323
+ /\bunsupported-provider-cli\b/i,
324
+ /\bunsupported-ui-method\b/i,
325
+ /\bunsupported-capability\b/i,
326
+ /\bunsafe-config\b/i,
327
+ /\bmalformed-response\b/i,
328
+ /\bmalformed-extension-ui-request\b/i,
329
+ /\bmalformed-host-tool-call\b/i,
330
+ /\bmalformed-host-uri-request\b/i,
331
+ /\bextension-error\b/i,
332
+ /\blocal-only-prompt\b/i,
333
+ /\blifetime-request-id-exceeded\b/i,
334
+ /\bpending-request-exceeded\b/i,
335
+ /\bspawn-hook-failed\b/i,
336
+ // Frame decoder codes (omp-rpc-protocol.ts OmpRpcProtocolError.code values) — every one of
337
+ // these means the wire traffic itself is invalid or out-of-bounds, never a transient condition.
338
+ /\bphysical-frame-too-large\b/i,
339
+ /\bmalformed-physical-frame\b/i,
340
+ /\binvalid-chunk-metadata\b/i,
341
+ /\binvalid-chunk-data\b/i,
342
+ /\bchunk-sequence-must-start-at-zero\b/i,
343
+ /\bchunk-sequence-mismatch\b/i,
344
+ /\bchunk-sequence-exceeds-declared-length\b/i,
345
+ /\bchunk-sequence-length-mismatch\b/i,
346
+ /\bmalformed-json-in-reassembled-frame\b/i,
347
+ /\bnon-object-reassembled-frame\b/i,
348
+ /\binvalid-utf8-in-reassembled-frame\b/i,
349
+ /\binflight-reassembly-bytes-exceeded\b/i,
350
+ /\binterleaved-chunk-sequence\b/i,
351
+ /\binterrupted-chunk-sequence\b/i,
352
+ /\bincomplete-physical-frame\b/i,
353
+ /\bincomplete-chunk-sequence\b/i,
354
+ /\boutbound-frame-too-large\b/i,
355
+ /\bpre-negotiation-rpc-chunk\b/i,
356
+ /\b[\w-]+-bound-exceeded\b/i,
357
+ // Auth/model absence and permission/usage errors surfaced verbatim in OMP output.
358
+ /\bno available authenticated\b/i,
359
+ /\bno keyless model\b/i,
360
+ /\bno valid authentication\b/i,
361
+ /\bmissing api key\b/i,
362
+ /\brun\s*\/login\b/i,
363
+ /\bunknown option\b/i,
364
+ /\bfailed to load\b/i,
365
+ /\bcannot find module\b/i,
366
+ /\bno such file or directory\b/i,
367
+ ];
368
+
406
369
  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
- );
370
+ const message = unknownToMessage(error);
371
+ if (/\b(cancelled|canceled)\b/i.test(message)) {
372
+ return { retryable: false, kind: 'cancelled' };
373
+ }
374
+ return classifyBaseProviderError(error, OMP_RETRYABLE_PATTERNS, OMP_PERMANENT_PATTERNS);
428
375
  }
429
376
 
430
377
  export const ompAdapter: ProviderAdapter = {
431
378
  id: 'omp',
432
- displayName: 'OMP',
379
+ displayName: 'OMP (Oh My Pi)',
433
380
  binary: 'omp',
434
- adapterVersion: '1',
435
- credentialEnvKeys: [],
381
+ adapterVersion: '2',
382
+ credentialEnvKeys: CREDENTIAL_ENV_KEYS,
436
383
  modelCatalog: MODEL_CATALOG,
437
384
  levelMapping: LEVEL_MAPPING,
438
385
  defaultLevel: 'level2',