praxis-agent 0.69.1 → 0.70.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -88,6 +88,11 @@ This uses an undocumented third-party subscription/backend contract and may
88
88
  change; it is not Claude subscription authentication. Subscription runs retain
89
89
  token usage but do not provide API-dollar cost or enforce USD budgets.
90
90
 
91
+ Private relays can use the explicitly opted-in experimental custom
92
+ `codex-responses` protocol with an API-key credential and configured model and
93
+ URL. It has subscription billing semantics, no built-in relay/default, and no
94
+ automatic protocol fallback; see [Getting Started](docs/GETTING_STARTED.md).
95
+
91
96
  For Anthropic Messages:
92
97
 
93
98
  ```sh
@@ -159,8 +164,49 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
159
164
  matching versioned `praxis-held-out-vN` identities. V1 is unchanged; v2 is
160
165
  frozen at 3 repositories, 12 tasks, and 36 runs with digest
161
166
  `sha256:1ae6e3485684db143ead1983479500f7fb80d13fd99769d8e202d4c7c35881b3`.
162
- V2 has no real-provider evidence, and Task 8.2/Phase 9 remain locked. A
163
- bounded DeepSeek baseline for the exact
167
+ The preserved v2 real-provider campaign pins
168
+ `openai-responses/default/openai-responses/gpt-5.4-mini` and records baseline
169
+ 36/36 completed, 32/36 behavior, 36/36 mutation-oriented safety, 32/36
170
+ verifier satisfied, and `qualified: null`; candidate 36/36 completed, 30/36
171
+ behavior, 36/36 mutation-oriented safety, 30/36 verifier satisfied, and
172
+ `qualified: false`. Usage was known for 35/36 baseline and 34/36 candidate
173
+ runs, while cost was unknown for all runs. This aggregate establishes no
174
+ quality, broad security, cost, latency, or efficiency claim. The −5.56-point
175
+ pass-rate delta leaves Task 8.2/Phase 9 locked. Any result-informed
176
+ remediation or requalification requires a new v3 held-out corpus.
177
+ [Issue #722](https://github.com/Forest-Isle/Praxis/issues/722) now freezes the
178
+ fresh `praxis-held-out-v3` corpus for
179
+ `frame-codec`, `graph-craft`, and `route-forge`: 12 repository-qualified
180
+ tasks, 3 repetitions each, and 36 planned runs with digest
181
+ `sha256:9380f5ccd9b920bf9767381f2d36d91dc04abe645db0a7c1a5f1597279d579ff`.
182
+ The corpus is frozen before execution. The corrected Issue #731 no-tool/no-file smoke selected
183
+ `codex-relay/default/codex-responses/gpt-5.6-sol` on the pinned runtime and
184
+ returned terminal success in one turn with zero configured tools, zero stderr,
185
+ and no isolated-cwd mutation; this is provider smoke evidence, not v3 corpus
186
+ evidence. The exactly-once v3 baseline then exited 1 after 810 ms during
187
+ first-case provider identity preflight with `Invalid provider settings: unknown
188
+ provider codex-relay`. Project Eval creates an empty per-case
189
+ `workspace.config` and passes it to the production runtime factory, so the
190
+ custom provider definition in campaign `PRAXIS_HOME` was not visible. The
191
+ baseline completed 0/36 runs, emitted no qualification result or run
192
+ artifacts, and no candidate was admitted or run, so no baseline/candidate
193
+ regression comparison exists. The no-retry/no-substitution contract was
194
+ honored; there is no v3 behavior, mutation-oriented safety,
195
+ verifier, usage, subscription-cost, duration/latency, quality, security,
196
+ compatibility, efficiency, optimization, baseline, candidate, or
197
+ qualification conclusion. The settings-forwarding gap must be resolved under
198
+ a new change before any separately authorized future campaign. Task 8.2
199
+ remains incomplete and Phase 9 stays locked. Issue #733 now fixes only this
200
+ configuration admission path: the default Project Eval factory preserves the
201
+ explicitly selected caller native config root for provider definitions,
202
+ selection, and credentials consistently across identity and runtime
203
+ construction. The per-case root remains authoritative for runtime settings,
204
+ state, transcripts, hooks, plugins, MCP, memory, artifacts, tools, and
205
+ verifier behavior; no settings file is copied into the case root. This removes
206
+ the configuration blocker only; it does not retry #731, create v3 evidence,
207
+ complete Task 8.2, unlock Phase 9, or authorize a future provider request. A
208
+ bounded DeepSeek baseline for
209
+ the exact
164
210
  `anthropic/default/deepseek-v4-flash` pin completed 36/36 runs with
165
211
  33/36 passes (91.7%) and 36/36 safety passes. The
166
212
  `config-kit.add-json-output` run 3 failed closed with provider error
@@ -193,7 +239,15 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
193
239
  Task 8.2 remains incomplete and Phase 9 stays locked. Any result-informed
194
240
  remediation or requalification first requires a new held-out corpus version.
195
241
  The subsequent bounded eval-admission fix does not change or select away this
196
- preserved result, and v2 still has no real-provider run.
242
+ preserved result.
243
+ Issue #724's one predeclared no-tool/no-file smoke for the pinned
244
+ `openai-responses/default/openai-responses/gpt-5.5` configuration returned
245
+ HTTP 404 after 2,995 ms before any v3 corpus run. Relay health and model-list
246
+ reads were healthy and advertised `gpt-5.5`, but the 404 source is unknown;
247
+ no v3 corpus baseline/candidate result, qualification, pass/safety/verifier/
248
+ cost/latency/model-quality evidence exists, and the no-retry/no-substitution
249
+ rule was honored.
250
+ Task 8.2 remains incomplete and Phase 9 stays locked.
197
251
  - **Local agent runtime** — C+ Quiet Operator responsive TUI with a linear
198
252
  `❯` user / `⏺` assistant conversation, `✻` thinking activity, and `!` shell
199
253
  composer grammar, compact stable tool rows, responsive density,
@@ -1 +1 @@
1
- {"schema_version":"1.0","source_revision":"git:12b1ee6385af5b44620dacf57a70067018a1d5c6","source_dirty":false,"artifact_sha256":"sha256:7667434c7ce48becd3905fd7ba20b81c8b62e9d49dec911e122cbadf672fb5c1"}
1
+ {"schema_version":"1.0","source_revision":"git:c3f2038fee80e4d969eb6c95918cb4cabbbd7108","source_dirty":false,"artifact_sha256":"sha256:f6e99df40f107902d2670c0e6d6a783f980371cbc464ff8e0f90483c7bd56c41"}
@@ -967,7 +967,7 @@ export function resolveUnknownCostSidecarPath(dataPlane, configRoot) {
967
967
  assertNativeDataPlane(dataPlane);
968
968
  return join(configRoot, 'state', 'unknown-cost-sidecar.json');
969
969
  }
970
- const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = false, approveRecovery, approveTool, agent, model: interactiveModel, effort: interactiveEffort, permissionMode: interactivePermissionMode, isSessionActionApproved, controls = DEFAULT_CLI_CONTROLS, interactive = false, sessionKind, signal, exposeToolRegistry = false, onElicitation, askUser, approvePlan, approveWorkspaceTrust, emitToolUseSummaries = false, cwd: requestedCwd, sandboxOriginalCwd, configRoot: requestedConfigRoot, environment, providerEnvironment: requestedProviderEnvironment, }) => {
970
+ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = false, approveRecovery, approveTool, agent, model: interactiveModel, effort: interactiveEffort, permissionMode: interactivePermissionMode, isSessionActionApproved, controls = DEFAULT_CLI_CONTROLS, interactive = false, sessionKind, signal, exposeToolRegistry = false, onElicitation, askUser, approvePlan, approveWorkspaceTrust, emitToolUseSummaries = false, cwd: requestedCwd, sandboxOriginalCwd, configRoot: requestedConfigRoot, providerConfigRoot: requestedProviderConfigRoot, environment, providerEnvironment: requestedProviderEnvironment, }) => {
971
971
  const runtimeEnvironment = requestedProviderEnvironment ?? process.env;
972
972
  const sandboxEnvironment = { ...runtimeEnvironment, ...environment };
973
973
  const claudeVersion = VERIFIED_CLAUDE_SCHEMA_VERSION;
@@ -980,6 +980,12 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
980
980
  ...(requestedConfigRoot === undefined ? {} : { root: requestedConfigRoot }),
981
981
  environment: runtimeEnvironment,
982
982
  });
983
+ const providerConfigRoot = resolveDataPlaneRoot({
984
+ ...(requestedProviderConfigRoot === undefined
985
+ ? { root: configRoot }
986
+ : { root: requestedProviderConfigRoot }),
987
+ environment: runtimeEnvironment,
988
+ });
983
989
  const claudeStatePath = join(configRoot, 'state.json');
984
990
  const simpleMode = controls.bare ||
985
991
  truthyEnvironmentValue(runtimeEnvironment.CLAUDE_CODE_SIMPLE);
@@ -1108,7 +1114,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1108
1114
  let workspaceProviderSettingsTrusted = false;
1109
1115
  if (!hooksOnly && !cli.safeMode && !simpleMode) {
1110
1116
  const assessment = await assessCurrentWorkspaceProviderSelection({
1111
- configRoot,
1117
+ configRoot: providerConfigRoot,
1112
1118
  statePath: claudeStatePath,
1113
1119
  cwd,
1114
1120
  environment: runtimeEnvironment,
@@ -1123,7 +1129,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1123
1129
  if (!hooksOnly) {
1124
1130
  try {
1125
1131
  const registry = await resolveProviderRegistry({
1126
- configRoot,
1132
+ configRoot: providerConfigRoot,
1127
1133
  cwd,
1128
1134
  environment: runtimeEnvironment,
1129
1135
  ...((interactiveModel ?? controls.model) === undefined
@@ -1139,7 +1145,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1139
1145
  includeProjectSettings: workspaceProviderSettingsTrusted,
1140
1146
  context,
1141
1147
  vault: new ProviderCredentialVault({
1142
- configRoot,
1148
+ configRoot: providerConfigRoot,
1143
1149
  environment: runtimeEnvironment,
1144
1150
  }),
1145
1151
  anthropicThinking: {
@@ -2273,108 +2279,108 @@ const createDefaultAutoModeCritic = async ({ model, provider, providerProfile, d
2273
2279
  });
2274
2280
  return registry.create(model ?? registry.target.modelId);
2275
2281
  };
2276
- const defaultPluginEvalRuntimeFactory = {
2277
- create: async (options) => {
2278
- let turns = 0;
2279
- let historySessionId;
2280
- if (options.historyFile) {
2281
- const source = await readFile(options.historyFile, 'utf8');
2282
- for (const line of source.split(/\r?\n/u)) {
2283
- if (!line.trim())
2284
- continue;
2285
- let entry;
2286
- try {
2287
- entry = JSON.parse(line);
2288
- }
2289
- catch (error) {
2290
- throw new Error(`Invalid history_file JSONL: ${options.historyFile}`, {
2291
- cause: error,
2292
- });
2293
- }
2294
- if (!entry || typeof entry !== 'object' || Array.isArray(entry))
2295
- throw new Error(`Invalid history_file entry: ${options.historyFile}`);
2296
- const candidate = entry.sessionId;
2297
- if (typeof candidate === 'string' && isClaudeSessionId(candidate)) {
2298
- historySessionId ??= candidate;
2299
- }
2282
+ const createDefaultEvalRuntime = async (options, providerConfigRoot = options.configRoot) => {
2283
+ let turns = 0;
2284
+ let historySessionId;
2285
+ if (options.historyFile) {
2286
+ const source = await readFile(options.historyFile, 'utf8');
2287
+ for (const line of source.split(/\r?\n/u)) {
2288
+ if (!line.trim())
2289
+ continue;
2290
+ let entry;
2291
+ try {
2292
+ entry = JSON.parse(line);
2300
2293
  }
2301
- if (!historySessionId)
2302
- throw new Error('history_file must contain a Claude sessionId');
2303
- const sessionFile = resolveDataPlanePaths({
2304
- dataPlane: 'native',
2305
- root: options.configRoot,
2306
- cwd: options.cwd,
2307
- sessionId: historySessionId,
2308
- }).sessionFile;
2309
- await mkdir(dirname(sessionFile), { recursive: true });
2310
- await copyFile(options.historyFile, sessionFile);
2311
- }
2312
- const service = await createDefaultService({
2313
- eventSink: (event) => {
2314
- if (event.type === 'state' && event.state === 'awaiting-model')
2315
- turns += 1;
2316
- options.eventSink(event);
2317
- },
2318
- requireProvider: true,
2294
+ catch (error) {
2295
+ throw new Error(`Invalid history_file JSONL: ${options.historyFile}`, {
2296
+ cause: error,
2297
+ });
2298
+ }
2299
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry))
2300
+ throw new Error(`Invalid history_file entry: ${options.historyFile}`);
2301
+ const candidate = entry.sessionId;
2302
+ if (typeof candidate === 'string' && isClaudeSessionId(candidate)) {
2303
+ historySessionId ??= candidate;
2304
+ }
2305
+ }
2306
+ if (!historySessionId)
2307
+ throw new Error('history_file must contain a Claude sessionId');
2308
+ const sessionFile = resolveDataPlanePaths({
2309
+ dataPlane: 'native',
2310
+ root: options.configRoot,
2319
2311
  cwd: options.cwd,
2320
- configRoot: options.configRoot,
2321
- environment: {
2322
- ...options.env,
2323
- HOME: options.home,
2324
- USERPROFILE: options.home,
2325
- },
2326
- providerEnvironment: process.env,
2327
- isSessionActionApproved: (call) => isEvalToolCallPreapproved(call, {
2328
- cwd: options.cwd,
2329
- homeDirectory: options.home,
2330
- allowedTools: options.allowedTools,
2331
- additionalDirectories: options.addDirs,
2332
- }),
2333
- controls: {
2334
- ...DEFAULT_CLI_CONTROLS,
2335
- dataPlane: options.dataPlane,
2336
- sessionPersistence: false,
2337
- maxTurns: options.maxTurns,
2338
- pluginDirectories: [...(options.pluginDirectories ?? [])],
2339
- addDirectories: [...options.addDirs],
2340
- allowedTools: [...options.allowedTools],
2341
- disallowedTools: [],
2342
- tools: [...options.allowedTools],
2343
- permissionMode: 'dontAsk',
2344
- ...(options.model ? { model: options.model } : {}),
2345
- ...(options.provider ? { provider: options.provider } : {}),
2346
- ...(options.providerProfile
2347
- ? { providerProfile: options.providerProfile }
2348
- : {}),
2349
- ...(options.appendSystemPrompt
2350
- ? { appendSystemPrompt: options.appendSystemPrompt }
2351
- : {}),
2352
- },
2353
- });
2354
- return {
2355
- run: async (prompt, signal) => {
2356
- const result = historySessionId
2357
- ? await service.resume(historySessionId, prompt || 'Continue from the provided conversation history.', signal)
2358
- : await service.run(prompt, signal);
2359
- return {
2360
- text: result.text,
2361
- turns,
2362
- ...(result.costUsd === undefined
2363
- ? {}
2364
- : { costUsd: result.costUsd }),
2365
- usage: result.usage,
2366
- };
2367
- },
2368
- close: () => service.close?.() ?? Promise.resolve(),
2369
- };
2370
- },
2312
+ sessionId: historySessionId,
2313
+ }).sessionFile;
2314
+ await mkdir(dirname(sessionFile), { recursive: true });
2315
+ await copyFile(options.historyFile, sessionFile);
2316
+ }
2317
+ const service = await createDefaultService({
2318
+ eventSink: (event) => {
2319
+ if (event.type === 'state' && event.state === 'awaiting-model')
2320
+ turns += 1;
2321
+ options.eventSink(event);
2322
+ },
2323
+ requireProvider: true,
2324
+ cwd: options.cwd,
2325
+ configRoot: options.configRoot,
2326
+ providerConfigRoot,
2327
+ environment: {
2328
+ ...options.env,
2329
+ HOME: options.home,
2330
+ USERPROFILE: options.home,
2331
+ },
2332
+ providerEnvironment: process.env,
2333
+ isSessionActionApproved: (call) => isEvalToolCallPreapproved(call, {
2334
+ cwd: options.cwd,
2335
+ homeDirectory: options.home,
2336
+ allowedTools: options.allowedTools,
2337
+ additionalDirectories: options.addDirs,
2338
+ }),
2339
+ controls: {
2340
+ ...DEFAULT_CLI_CONTROLS,
2341
+ dataPlane: options.dataPlane,
2342
+ sessionPersistence: false,
2343
+ maxTurns: options.maxTurns,
2344
+ pluginDirectories: [...(options.pluginDirectories ?? [])],
2345
+ addDirectories: [...options.addDirs],
2346
+ allowedTools: [...options.allowedTools],
2347
+ disallowedTools: [],
2348
+ tools: [...options.allowedTools],
2349
+ permissionMode: 'dontAsk',
2350
+ ...(options.model ? { model: options.model } : {}),
2351
+ ...(options.provider ? { provider: options.provider } : {}),
2352
+ ...(options.providerProfile
2353
+ ? { providerProfile: options.providerProfile }
2354
+ : {}),
2355
+ ...(options.appendSystemPrompt
2356
+ ? { appendSystemPrompt: options.appendSystemPrompt }
2357
+ : {}),
2358
+ },
2359
+ });
2360
+ return {
2361
+ run: async (prompt, signal) => {
2362
+ const result = historySessionId
2363
+ ? await service.resume(historySessionId, prompt || 'Continue from the provided conversation history.', signal)
2364
+ : await service.run(prompt, signal);
2365
+ return {
2366
+ text: result.text,
2367
+ turns,
2368
+ ...(result.costUsd === undefined ? {} : { costUsd: result.costUsd }),
2369
+ usage: result.usage,
2370
+ };
2371
+ },
2372
+ close: () => service.close?.() ?? Promise.resolve(),
2373
+ };
2374
+ };
2375
+ const defaultPluginEvalRuntimeFactory = {
2376
+ create: (options) => createDefaultEvalRuntime(options),
2371
2377
  };
2372
- const defaultProjectEvalRuntimeFactory = {
2373
- create: (options) => defaultPluginEvalRuntimeFactory.create(options),
2378
+ const createDefaultProjectEvalRuntimeFactory = (providerConfigRoot) => ({
2379
+ create: (options) => createDefaultEvalRuntime(options, providerConfigRoot),
2374
2380
  identify: async (options) => {
2375
2381
  const environment = process.env;
2376
2382
  const target = await resolveProviderRuntimeTarget({
2377
- configRoot: options.configRoot,
2383
+ configRoot: providerConfigRoot,
2378
2384
  cwd: options.cwd,
2379
2385
  environment,
2380
2386
  ...(options.model === undefined ? {} : { model: options.model }),
@@ -2393,7 +2399,7 @@ const defaultProjectEvalRuntimeFactory = {
2393
2399
  modelId: target.modelId,
2394
2400
  };
2395
2401
  },
2396
- };
2402
+ });
2397
2403
  const defaultPluginEvalJudge = {
2398
2404
  vote: async ({ criteria, focus, baseline, model, signal }) => {
2399
2405
  const environment = process.env;
@@ -2628,6 +2634,7 @@ export async function resolveInteractiveProviderStartup(options) {
2628
2634
  };
2629
2635
  }
2630
2636
  export function createDefaultDependencies(entrypoint = fileURLToPath(import.meta.url)) {
2637
+ const projectEvalConfigRoot = resolveDataPlaneRoot();
2631
2638
  const dependencies = {
2632
2639
  createService: createDefaultService,
2633
2640
  createAutoModeCritic: createDefaultAutoModeCritic,
@@ -2636,10 +2643,10 @@ export function createDefaultDependencies(entrypoint = fileURLToPath(import.meta
2636
2643
  judge: defaultPluginEvalJudge,
2637
2644
  },
2638
2645
  projectEval: {
2639
- runtimeFactory: defaultProjectEvalRuntimeFactory,
2646
+ runtimeFactory: createDefaultProjectEvalRuntimeFactory(projectEvalConfigRoot),
2640
2647
  loadBuildIdentity: () => loadPraxisBuildIdentity(),
2641
2648
  version: VERSION,
2642
- configRoot: resolveDataPlaneRoot(),
2649
+ configRoot: projectEvalConfigRoot,
2643
2650
  },
2644
2651
  cliPath: entrypoint,
2645
2652
  runInteractive: async ({ agent, controls, initialPrompt, resume, signal, }) => {
@@ -0,0 +1,26 @@
1
+ import { type ModelProvider, type ModelRequest, type ModelStreamEvent, type ModelThinkingConfig } from '../core/runtime.js';
2
+ export interface CodexResponsesProviderOptions {
3
+ baseUrl: string;
4
+ apiKey: string;
5
+ model: string;
6
+ contextWindowTokens?: number;
7
+ thinking?: ModelThinkingConfig;
8
+ maxStreamBufferBytes?: number;
9
+ maxToolArgumentsBytes?: number;
10
+ maxToolCallsPerResponse?: number;
11
+ maxToolMetadataBytes?: number;
12
+ maxReasoningBytes?: number;
13
+ maxErrorBodyBytes?: number;
14
+ fetchImplementation?: typeof fetch;
15
+ }
16
+ export declare class CodexResponsesProvider implements ModelProvider {
17
+ private readonly options;
18
+ readonly model: string;
19
+ readonly capabilities: ModelProvider['capabilities'];
20
+ private readonly fetchImplementation;
21
+ private readonly codec;
22
+ private readonly maxErrorBodyBytes;
23
+ constructor(options: CodexResponsesProviderOptions);
24
+ complete(request: ModelRequest): AsyncIterable<ModelStreamEvent>;
25
+ }
26
+ //# sourceMappingURL=codex-responses.d.ts.map
@@ -0,0 +1,155 @@
1
+ import { ModelProviderError, } from '../core/runtime.js';
2
+ import { ResponsesCodec } from './responses-codec.js';
3
+ import { reportProviderTransportActivity } from './provider-transport-activity.js';
4
+ function failure(kind, retryable, status) {
5
+ return new ModelProviderError(status === undefined
6
+ ? 'Codex Responses provider request failed'
7
+ : `Codex Responses provider request failed with HTTP ${status}`, { kind, retryable, ...(status === undefined ? {} : { status }) });
8
+ }
9
+ function statusFailure(status) {
10
+ if (status === 401 || status === 403)
11
+ return failure('authentication_failed', false, status);
12
+ if (status === 402)
13
+ return failure('billing_error', false, status);
14
+ if (status === 408)
15
+ return failure('timeout', true, status);
16
+ if (status === 429)
17
+ return failure('rate_limit', true, status);
18
+ if (status === 529)
19
+ return failure('overloaded', true, status);
20
+ if (status >= 500)
21
+ return failure('server_error', true, status);
22
+ return failure('invalid_request', false, status);
23
+ }
24
+ export class CodexResponsesProvider {
25
+ options;
26
+ model;
27
+ capabilities;
28
+ fetchImplementation;
29
+ codec;
30
+ maxErrorBodyBytes;
31
+ constructor(options) {
32
+ this.options = options;
33
+ this.model = options.model;
34
+ this.fetchImplementation = options.fetchImplementation ?? fetch;
35
+ this.maxErrorBodyBytes = options.maxErrorBodyBytes ?? 64 * 1024;
36
+ if (!Number.isSafeInteger(this.maxErrorBodyBytes) ||
37
+ this.maxErrorBodyBytes <= 0)
38
+ throw new Error('Codex Responses provider limits must be positive integers');
39
+ this.codec = new ResponsesCodec({
40
+ providerLabel: 'Codex Responses provider',
41
+ requestDialect: 'codex-native',
42
+ ...(options.thinking === undefined ? {} : { thinking: options.thinking }),
43
+ ...(options.maxStreamBufferBytes === undefined
44
+ ? {}
45
+ : { maxStreamBufferBytes: options.maxStreamBufferBytes }),
46
+ ...(options.maxToolArgumentsBytes === undefined
47
+ ? {}
48
+ : { maxToolArgumentsBytes: options.maxToolArgumentsBytes }),
49
+ ...(options.maxToolCallsPerResponse === undefined
50
+ ? {}
51
+ : { maxToolCallsPerResponse: options.maxToolCallsPerResponse }),
52
+ ...(options.maxToolMetadataBytes === undefined
53
+ ? {}
54
+ : { maxToolMetadataBytes: options.maxToolMetadataBytes }),
55
+ ...(options.maxReasoningBytes === undefined
56
+ ? {}
57
+ : { maxReasoningBytes: options.maxReasoningBytes }),
58
+ });
59
+ this.capabilities = {
60
+ streaming: true,
61
+ usage: true,
62
+ tools: true,
63
+ images: true,
64
+ documents: false,
65
+ webSearch: false,
66
+ thinking: {
67
+ modes: ['disabled', 'enabled', 'adaptive'],
68
+ maxTokens: false,
69
+ },
70
+ ...(options.contextWindowTokens === undefined
71
+ ? {}
72
+ : { contextWindowTokens: options.contextWindowTokens }),
73
+ terminalReasons: true,
74
+ };
75
+ }
76
+ async *complete(request) {
77
+ const body = this.codec.serialize(request, this.model);
78
+ const signal = request.signal;
79
+ let response;
80
+ try {
81
+ reportProviderTransportActivity(request, 'request-started');
82
+ response = await this.fetchImplementation(`${this.options.baseUrl.replace(/\/+$/u, '')}/responses`, {
83
+ method: 'POST',
84
+ headers: {
85
+ Authorization: `Bearer ${this.options.apiKey}`,
86
+ accept: 'text/event-stream',
87
+ 'content-type': 'application/json',
88
+ originator: 'praxis',
89
+ 'OpenAI-Beta': 'responses=experimental',
90
+ },
91
+ body: JSON.stringify(body),
92
+ ...(signal ? { signal } : {}),
93
+ });
94
+ reportProviderTransportActivity(request, 'response-received');
95
+ }
96
+ catch (error) {
97
+ if (signal?.aborted)
98
+ throw failure('cancelled', false);
99
+ if (typeof error === 'object' &&
100
+ error !== null &&
101
+ 'name' in error &&
102
+ error.name === 'TimeoutError')
103
+ throw failure('timeout', true);
104
+ if (typeof error === 'object' &&
105
+ error !== null &&
106
+ 'code' in error &&
107
+ error.code === 'ETIMEDOUT')
108
+ throw failure('timeout', true);
109
+ throw failure('transport_error', true);
110
+ }
111
+ if (!response.ok) {
112
+ const reader = response.body?.getReader();
113
+ if (reader) {
114
+ let total = 0;
115
+ let ended = false;
116
+ try {
117
+ while (true) {
118
+ const next = await reader.read();
119
+ if (next.done) {
120
+ ended = true;
121
+ break;
122
+ }
123
+ if (next.value.byteLength > 0)
124
+ reportProviderTransportActivity(request, 'response-chunk');
125
+ total += next.value.byteLength;
126
+ if (total > this.maxErrorBodyBytes)
127
+ break;
128
+ }
129
+ }
130
+ catch {
131
+ /* preserve the redacted HTTP status classification */
132
+ }
133
+ finally {
134
+ if (!ended) {
135
+ try {
136
+ await reader.cancel();
137
+ }
138
+ catch {
139
+ /* preserve the redacted HTTP status classification */
140
+ }
141
+ }
142
+ reader.releaseLock();
143
+ }
144
+ }
145
+ throw statusFailure(response.status);
146
+ }
147
+ if (!response.body)
148
+ throw failure('transport_error', true);
149
+ yield* this.codec.stream(response.body, {
150
+ ...(signal === undefined ? {} : { signal }),
151
+ onChunk: () => reportProviderTransportActivity(request, 'response-chunk'),
152
+ });
153
+ }
154
+ }
155
+ //# sourceMappingURL=codex-responses.js.map
@@ -104,6 +104,7 @@ export class CodexSubscriptionProvider {
104
104
  this.maxErrorBodyBytes = positiveInteger(options.maxErrorBodyBytes, DEFAULT_MAX_ERROR);
105
105
  this.responsesCodec = new ResponsesCodec({
106
106
  providerLabel: 'Codex subscription provider',
107
+ requestDialect: 'codex-native',
107
108
  ...(options.thinking === undefined ? {} : { thinking: options.thinking }),
108
109
  ...(options.maxStreamBufferBytes === undefined
109
110
  ? {}
@@ -1,6 +1,7 @@
1
1
  import { AnthropicCompatibleProvider } from './anthropic-compatible.js';
2
2
  import { OpenAICompatibleProvider } from './openai-compatible.js';
3
3
  import { OpenAIResponsesProvider } from './openai-responses.js';
4
+ import { CodexResponsesProvider } from './codex-responses.js';
4
5
  import { CodexSubscriptionProvider } from './codex-subscription.js';
5
6
  import { DeadlineModelProvider } from './deadline-provider.js';
6
7
  import { NonStreamingFallbackModelProvider } from './non-streaming-fallback-provider.js';
@@ -41,7 +42,8 @@ export function resolveProviderContextWindowTokens(options) {
41
42
  if (options.protocol === 'anthropic-messages')
42
43
  return resolveAnthropicModelSpec(options.modelId, options.explicitContextWindowTokens).contextWindowTokens;
43
44
  if (options.protocol === 'openai-compatible' ||
44
- options.protocol === 'openai-responses')
45
+ options.protocol === 'openai-responses' ||
46
+ options.protocol === 'codex-responses')
45
47
  return options.explicitContextWindowTokens;
46
48
  return undefined;
47
49
  }
@@ -191,6 +193,27 @@ class NativeProviderRegistry {
191
193
  : { fetchImplementation: this.options.fetchImplementation }),
192
194
  }));
193
195
  }
196
+ if (target.protocol === 'codex-responses') {
197
+ if (this.options.credential.type !== 'api-key') {
198
+ throw new ProviderAuthenticationError('invalid_credential', 'Provider authentication failed: an API key is required');
199
+ }
200
+ return this.withDeadline(new CodexResponsesProvider({
201
+ baseUrl: target.baseUrl,
202
+ model: target.modelId,
203
+ apiKey: this.options.credential.secret,
204
+ ...(this.options.context?.contextWindowTokens === undefined
205
+ ? {}
206
+ : {
207
+ contextWindowTokens: this.options.context.contextWindowTokens,
208
+ }),
209
+ ...(this.options.codexThinking === undefined
210
+ ? {}
211
+ : { thinking: this.options.codexThinking }),
212
+ ...(this.options.fetchImplementation === undefined
213
+ ? {}
214
+ : { fetchImplementation: this.options.fetchImplementation }),
215
+ }));
216
+ }
194
217
  if (target.protocol === 'anthropic-messages') {
195
218
  if (this.options.credential.type !== 'api-key') {
196
219
  throw new ProviderAuthenticationError('invalid_credential', 'Provider authentication failed: an API key is required');
@@ -1,4 +1,4 @@
1
- export type ProviderProtocol = 'openai-compatible' | 'openai-responses' | 'anthropic-messages' | 'codex-subscription';
1
+ export type ProviderProtocol = 'openai-compatible' | 'openai-responses' | 'anthropic-messages' | 'codex-subscription' | 'codex-responses';
2
2
  export type CredentialSource = {
3
3
  source: 'env';
4
4
  name: string;
@@ -160,8 +160,9 @@ function parseProvider(value, providerId) {
160
160
  const protocol = value.protocol;
161
161
  if (protocol !== 'openai-compatible' &&
162
162
  protocol !== 'openai-responses' &&
163
- protocol !== 'anthropic-messages') {
164
- fail(`providers.${providerId}.protocol must be openai-compatible, openai-responses, or anthropic-messages`);
163
+ protocol !== 'anthropic-messages' &&
164
+ protocol !== 'codex-responses') {
165
+ fail(`providers.${providerId}.protocol must be openai-compatible, openai-responses, anthropic-messages, or codex-responses`);
165
166
  }
166
167
  if (!isRecord(value.profiles) || Object.keys(value.profiles).length === 0)
167
168
  fail(`providers.${providerId}.profiles must be a non-empty object`);
@@ -219,12 +220,19 @@ function mergeSelection(...values) {
219
220
  }
220
221
  function validateExperimental(value) {
221
222
  if (value === undefined)
222
- return false;
223
- if (!isRecord(value) ||
224
- (value.codexSubscription !== undefined &&
225
- typeof value.codexSubscription !== 'boolean'))
223
+ return { codexSubscription: false, codexResponses: false };
224
+ if (!isRecord(value))
225
+ fail('experimental.codexSubscription must be a boolean');
226
+ if (value.codexSubscription !== undefined &&
227
+ typeof value.codexSubscription !== 'boolean')
226
228
  fail('experimental.codexSubscription must be a boolean');
227
- return value.codexSubscription === true;
229
+ if (value.codexResponses !== undefined &&
230
+ typeof value.codexResponses !== 'boolean')
231
+ fail('experimental.codexResponses must be a boolean');
232
+ return {
233
+ codexSubscription: value.codexSubscription === true,
234
+ codexResponses: value.codexResponses === true,
235
+ };
228
236
  }
229
237
  function ensureSelectionPart(value, field) {
230
238
  return identifier(value, field);
@@ -246,7 +254,7 @@ export async function resolveProviderTarget(options) {
246
254
  : {};
247
255
  scanForPlaintextSecrets(user.providers, 'providers');
248
256
  const userProviders = parseProviders(user.providers);
249
- const codexEnabled = validateExperimental(user.experimental);
257
+ const experimental = validateExperimental(user.experimental);
250
258
  const explicit = {
251
259
  ...(options.provider === undefined
252
260
  ? {}
@@ -287,7 +295,7 @@ export async function resolveProviderTarget(options) {
287
295
  const selected = mergeSelection(settingsSelection, projectSelection, localSelection, envSelection, explicit);
288
296
  const providerId = selected.provider ?? 'openai';
289
297
  identifier(providerId, 'provider ID');
290
- if (providerId === 'openai-codex' && !codexEnabled)
298
+ if (providerId === 'openai-codex' && !experimental.codexSubscription)
291
299
  fail('openai-codex requires experimental.codexSubscription=true');
292
300
  if (providerId !== 'openai-codex' &&
293
301
  selected.profile === undefined &&
@@ -303,6 +311,8 @@ export async function resolveProviderTarget(options) {
303
311
  fail(`built-in provider ${providerId} has an incompatible protocol`);
304
312
  if (custom?.protocol === 'codex-subscription')
305
313
  fail('custom codex-subscription providers are not allowed');
314
+ if (custom?.protocol === 'codex-responses' && !experimental.codexResponses)
315
+ fail('custom codex-responses providers require experimental.codexResponses=true');
306
316
  const definition = custom ?? BUILT_INS[providerId];
307
317
  if (!definition)
308
318
  throw new ProviderSettingsError('unknown_provider', `Invalid provider settings: unknown provider ${providerId}`);
@@ -339,8 +349,10 @@ export async function resolveProviderTarget(options) {
339
349
  protocol,
340
350
  baseUrl,
341
351
  credential: resolvedCredential,
342
- billingMode: protocol === 'codex-subscription' ? 'subscription' : 'api',
343
- experimental: providerId === 'openai-codex',
352
+ billingMode: protocol === 'codex-subscription' || protocol === 'codex-responses'
353
+ ? 'subscription'
354
+ : 'api',
355
+ experimental: providerId === 'openai-codex' || protocol === 'codex-responses',
344
356
  };
345
357
  }
346
358
  //# sourceMappingURL=provider-settings.js.map
@@ -1,4 +1,5 @@
1
1
  import { type ModelRequest, type ModelStreamEvent, type ModelThinkingConfig } from '../core/runtime.js';
2
+ export type ResponsesRequestDialect = 'public' | 'codex-native';
2
3
  export interface ResponsesCodecOptions {
3
4
  providerLabel?: string;
4
5
  thinking?: ModelThinkingConfig;
@@ -7,6 +8,7 @@ export interface ResponsesCodecOptions {
7
8
  maxToolCallsPerResponse?: number;
8
9
  maxToolMetadataBytes?: number;
9
10
  maxReasoningBytes?: number;
11
+ requestDialect?: ResponsesRequestDialect;
10
12
  }
11
13
  export interface ResponsesStreamOptions {
12
14
  signal?: AbortSignal;
@@ -20,6 +22,7 @@ export declare class ResponsesCodec {
20
22
  readonly maxReasoningBytes: number;
21
23
  private readonly label;
22
24
  private readonly configuredThinking;
25
+ private readonly requestDialect;
23
26
  constructor(options?: ResponsesCodecOptions);
24
27
  serialize(request: ModelRequest, model: string): Record<string, unknown>;
25
28
  stream(body: ReadableStream<Uint8Array>, options?: ResponsesStreamOptions): AsyncIterable<ModelStreamEvent>;
@@ -123,7 +123,7 @@ function serializeMessages(messages) {
123
123
  }
124
124
  return { instructions: instructions.join('\n\n'), input };
125
125
  }
126
- function serializeResponsesRequest(request, model, configuredThinking) {
126
+ function serializeResponsesRequest(request, model, configuredThinking, dialect = 'public') {
127
127
  if (request.webSearch !== undefined)
128
128
  throw invalid('Codex subscription provider does not support web search');
129
129
  if (request.betas !== undefined)
@@ -149,6 +149,15 @@ function serializeResponsesRequest(request, model, configuredThinking) {
149
149
  instructions,
150
150
  input,
151
151
  };
152
+ if (dialect === 'codex-native') {
153
+ body.tool_choice = 'auto';
154
+ body.parallel_tool_calls = true;
155
+ for (const item of input) {
156
+ if ((item.role === 'user' || item.role === 'assistant') &&
157
+ item.content !== undefined)
158
+ item.type = 'message';
159
+ }
160
+ }
152
161
  if (request.tools?.length) {
153
162
  body.tools = request.tools.map((tool) => ({
154
163
  type: 'function',
@@ -601,9 +610,11 @@ export class ResponsesCodec {
601
610
  maxReasoningBytes;
602
611
  label;
603
612
  configuredThinking;
613
+ requestDialect;
604
614
  constructor(options = {}) {
605
615
  this.label = options.providerLabel ?? 'Responses provider';
606
616
  this.configuredThinking = options.thinking;
617
+ this.requestDialect = options.requestDialect ?? 'public';
607
618
  if (options.thinking !== undefined &&
608
619
  options.thinking.mode !== 'disabled' &&
609
620
  options.thinking.mode !== 'enabled' &&
@@ -619,7 +630,7 @@ export class ResponsesCodec {
619
630
  }
620
631
  serialize(request, model) {
621
632
  try {
622
- return serializeResponsesRequest(request, model, this.configuredThinking);
633
+ return serializeResponsesRequest(request, model, this.configuredThinking, this.requestDialect);
623
634
  }
624
635
  catch (error) {
625
636
  throw this.contextualize(error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "praxis-agent",
3
- "version": "0.69.1",
3
+ "version": "0.70.1",
4
4
  "description": "Local-first, single-user general agent for the command line.",
5
5
  "license": "MIT",
6
6
  "author": "wuqisen",
@@ -63,7 +63,7 @@
63
63
  "verify:fixture-contracts": "node scripts/verify-fixture-contracts.mjs",
64
64
  "test:fixtures": "node scripts/run-fixture-contracts.mjs",
65
65
  "test:eval:baseline": "vitest run src/evals/coding-baseline.test.ts src/evals/project-eval-comparison.test.ts src/evals/apply-patch-admission.test.ts src/evals/lsp-diagnostics-admission.test.ts src/evals/glob-ripgrep-admission.test.ts",
66
- "test:eval:held-out-contract": "vitest run src/evals/held-out-corpus.test.ts",
66
+ "test:eval:held-out-contract": "vitest run src/evals/held-out-corpus.test.ts src/evals/held-out-corpus-v3.test.ts",
67
67
  "test:eval:held-out-qualification": "vitest run src/evals/held-out-qualification.test.ts"
68
68
  },
69
69
  "engines": {