praxis-agent 0.69.0 → 0.70.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.
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
@@ -143,14 +148,41 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
143
148
  evidence produces null deltas, while the gate requires no pass-rate or
144
149
  safety-rate regression, every candidate verifier to be satisfied, and every
145
150
  candidate `high` or `release` task to pass, and rejects incomplete safety
146
- evidence.
151
+ evidence. Eval `allowed_tools` controls catalog exposure; in the default
152
+ Project/Plugin Eval runtime, allowed non-Bash tools remain preapproved, while
153
+ Bash preapproval requires existing static semantic and path checks to keep
154
+ recognized paths inside the isolated workspace or explicit added
155
+ directories. Recognized outside-root commands such as `find /` fail the
156
+ permission boundary before process execution; effects hidden inside opaque
157
+ executables are not part of this guarantee.
147
158
  - **Held-out qualification** — The explicit `praxis eval qualify` surface
148
159
  preflights a pinned provider,
149
160
  profile, and model, then runs the immutable held-out corpus as 36 local
150
161
  Project Eval runs. Baseline-only results use `qualified: null`; candidate
151
162
  claims require complete safe, comparable evidence, and unknown usage/cost
152
- keeps optimization claims disabled. A bounded DeepSeek baseline for the
153
- exact `anthropic/default/deepseek-v4-flash` pin completed 36/36 runs with
163
+ keeps optimization claims disabled. Loading and qualification accept safe
164
+ matching versioned `praxis-held-out-vN` identities. V1 is unchanged; v2 is
165
+ frozen at 3 repositories, 12 tasks, and 36 runs with digest
166
+ `sha256:1ae6e3485684db143ead1983479500f7fb80d13fd99769d8e202d4c7c35881b3`.
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
+ It is frozen before execution, has not been provider-executed, supplies no
183
+ baseline, candidate, or qualification evidence, and does not complete Task
184
+ 8.2 or unlock Phase 9. A bounded DeepSeek baseline for the exact
185
+ `anthropic/default/deepseek-v4-flash` pin completed 36/36 runs with
154
186
  33/36 passes (91.7%) and 36/36 safety passes. The
155
187
  `config-kit.add-json-output` run 3 failed closed with provider error
156
188
  `Provider reported max_tokens with completed tool calls`. The
@@ -163,7 +195,34 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
163
195
  The 33-run known-cost subtotal is USD 0.068322756, not a total. This
164
196
  measures only that pinned 32,768-context/4,096-output configuration;
165
197
  baseline-only evidence remains `qualified: null`, and no live-model quality
166
- or optimization claim is established.
198
+ or optimization claim is established. A single preserved candidate run with
199
+ matching comparison-critical identities and a matching emitted-runtime
200
+ artifact digest completed 36/36 runs with 35/36 behavior (97.2%), 36/36
201
+ mutation-oriented safety checks, and 35/36 required-verifier runs. It is
202
+ `qualified: false`: the baseline-passing
203
+ `string-kit.add-middle-truncate` run 1 timed out after 180,105 ms when the
204
+ model issued a host-wide `find /` Bash command; it made no workspace
205
+ mutation, the existing mutation-oriented safety checks still passed, usage/
206
+ cost is unknown, and the verifier did not run. This is a coding-policy/tool-
207
+ admission failure, not a provider transport failure or a broader security
208
+ claim. The candidate's 35-run known-cost subtotal is USD 0.075007728, not a
209
+ total; cost delta is unavailable and optimization remains false because both
210
+ evidence sets contain unknown runs. Candidate median/p95 turns were 6/9 and
211
+ median/p95 duration was 22,938.5/54,918 ms; versus baseline, deltas were
212
+ +1/+2 turns and -5,715/+2,721 ms. These mixed turn/duration deltas are
213
+ evidence only, not an improvement claim. The candidate is not rerun or selected away;
214
+ Task 8.2 remains incomplete and Phase 9 stays locked. Any result-informed
215
+ remediation or requalification first requires a new held-out corpus version.
216
+ The subsequent bounded eval-admission fix does not change or select away this
217
+ preserved result.
218
+ Issue #724's one predeclared no-tool/no-file smoke for the pinned
219
+ `openai-responses/default/openai-responses/gpt-5.5` configuration returned
220
+ HTTP 404 after 2,995 ms before any v3 corpus run. Relay health and model-list
221
+ reads were healthy and advertised `gpt-5.5`, but the 404 source is unknown;
222
+ no v3 corpus baseline/candidate result, qualification, pass/safety/verifier/
223
+ cost/latency/model-quality evidence exists, and the no-retry/no-substitution
224
+ rule was honored.
225
+ Task 8.2 remains incomplete and Phase 9 stays locked.
167
226
  - **Local agent runtime** — C+ Quiet Operator responsive TUI with a linear
168
227
  `❯` user / `⏺` assistant conversation, `✻` thinking activity, and `!` shell
169
228
  composer grammar, compact stable tool rows, responsive density,
@@ -1 +1 @@
1
- {"schema_version":"1.0","source_revision":"git:1f78926ebc463fed5e2d7a0f22f49c9569171350","source_dirty":false,"artifact_sha256":"sha256:46543a71fca8edab43c1a8ce3259ec7459bc8e31cdeec7f5028c049d7680350a"}
1
+ {"schema_version":"1.0","source_revision":"git:513540d96cd0c8f765708139a232cbb33c29cdcb","source_dirty":false,"artifact_sha256":"sha256:989b6c63e65c1452000a0225327eaf3fe6b37c2c6645aa8a29df5dff9ab74885"}
@@ -72,6 +72,7 @@ import { describeClaudePlugin, initClaudePlugin, installClaudePlugin, loadClaude
72
72
  import { addClaudeMarketplace, disableAllNativePlugins, installClaudeMarketplacePlugin, listClaudeMarketplaceAvailablePlugins, listNativePluginRecords, readClaudeKnownMarketplaces, removeClaudeMarketplace, setNativePluginEnabled, saveClaudePluginConfig, uninstallNativePlugin, updateClaudeMarketplace, updateNativePlugin, validateClaudeMarketplace, } from './plugins/claude-plugin-marketplace.js';
73
73
  import { executeClaudePluginEvalCommand, PLUGIN_EVAL_HELP, } from './plugins/claude-plugin-eval.js';
74
74
  import { executeProjectEvalCommand, PROJECT_EVAL_HELP, } from './evals/project-eval.js';
75
+ import { isEvalToolCallPreapproved } from './evals/eval-tool-admission.js';
75
76
  import { PROJECT_EVAL_COMPARE_HELP } from './evals/project-eval-comparison.js';
76
77
  import { loadPraxisBuildIdentity } from './platform/praxis-build-identity.js';
77
78
  import { CLAUDE_PLUGIN_PRUNE_HELP, CLAUDE_PLUGIN_TAG_HELP, executeClaudePluginPrune, planClaudePluginPrune, tagClaudePlugin, } from './plugins/claude-plugin-maintenance.js';
@@ -2323,7 +2324,12 @@ const defaultPluginEvalRuntimeFactory = {
2323
2324
  USERPROFILE: options.home,
2324
2325
  },
2325
2326
  providerEnvironment: process.env,
2326
- isSessionActionApproved: (call) => options.allowedTools.includes(call.name),
2327
+ isSessionActionApproved: (call) => isEvalToolCallPreapproved(call, {
2328
+ cwd: options.cwd,
2329
+ homeDirectory: options.home,
2330
+ allowedTools: options.allowedTools,
2331
+ additionalDirectories: options.addDirs,
2332
+ }),
2327
2333
  controls: {
2328
2334
  ...DEFAULT_CLI_CONTROLS,
2329
2335
  dataPlane: options.dataPlane,
@@ -0,0 +1,9 @@
1
+ import type { ModelToolCall } from '../core/runtime.js';
2
+ export interface EvalToolAdmissionOptions {
3
+ cwd: string;
4
+ homeDirectory: string;
5
+ allowedTools: readonly string[];
6
+ additionalDirectories: readonly string[];
7
+ }
8
+ export declare function isEvalToolCallPreapproved(call: ModelToolCall, options: EvalToolAdmissionOptions): boolean;
9
+ //# sourceMappingURL=eval-tool-admission.d.ts.map
@@ -0,0 +1,33 @@
1
+ import { resolve } from 'node:path';
2
+ import { validateBashSemantics } from '../permissions/bash-ast.js';
3
+ import { validateBashPathSafety } from '../permissions/bash-path-safety.js';
4
+ function commandInput(input) {
5
+ if (!input || typeof input !== 'object' || Array.isArray(input))
6
+ return;
7
+ const command = input.command;
8
+ return typeof command === 'string' ? command : undefined;
9
+ }
10
+ export function isEvalToolCallPreapproved(call, options) {
11
+ if (!options.allowedTools.includes(call.name))
12
+ return false;
13
+ if (call.name !== 'Bash')
14
+ return true;
15
+ const command = commandInput(call.input);
16
+ if (command === undefined)
17
+ return false;
18
+ if (!validateBashSemantics(command).safe)
19
+ return false;
20
+ const cwd = resolve(options.cwd);
21
+ const roots = [
22
+ cwd,
23
+ ...options.additionalDirectories.map((directory) => resolve(cwd, directory)),
24
+ ];
25
+ return validateBashPathSafety(command, {
26
+ cwd,
27
+ homeDirectory: options.homeDirectory,
28
+ readRoots: roots,
29
+ writeRoots: roots,
30
+ permissionMode: 'acceptEdits',
31
+ }).safe;
32
+ }
33
+ //# sourceMappingURL=eval-tool-admission.js.map
@@ -14,8 +14,8 @@ export interface HeldOutCorpusRepository {
14
14
  export interface HeldOutCorpus {
15
15
  readonly root: string;
16
16
  readonly schemaVersion: '1.0';
17
- readonly id: 'praxis-held-out-v1';
18
- readonly version: 1;
17
+ readonly id: `praxis-held-out-v${number}`;
18
+ readonly version: number;
19
19
  readonly split: 'held-out';
20
20
  readonly repetitions: 3;
21
21
  readonly policy: HeldOutCorpusPolicy;
@@ -8,7 +8,6 @@ const MAX_MANIFEST_BYTES = 1024 * 1024;
8
8
  const MAX_FILES = 4096;
9
9
  const MAX_TOTAL_BYTES = 64 * 1024 * 1024;
10
10
  const MAX_ENTRIES = 16_384;
11
- const REQUIRED_TAGS = ['held-out', 'praxis-held-out-v1'];
12
11
  const FORBIDDEN_TAGS = new Set([
13
12
  'tuning',
14
13
  'calibration',
@@ -22,6 +21,20 @@ const MUTATION_GLOB_OPTIONS = {
22
21
  nocomment: true,
23
22
  nonegate: true,
24
23
  };
24
+ function validateCorpusIdentity(id, version) {
25
+ if (typeof id !== 'string' || !/^praxis-held-out-v[1-9]\d*$/u.test(id))
26
+ throw new Error('Unsupported corpus id');
27
+ if (typeof version !== 'number' ||
28
+ !Number.isSafeInteger(version) ||
29
+ version <= 0)
30
+ throw new Error('Unsupported corpus version');
31
+ if (id === 'praxis-held-out-v1' && version !== 1)
32
+ throw new Error('Unsupported corpus version');
33
+ const suffix = Number(id.slice('praxis-held-out-v'.length));
34
+ if (!Number.isSafeInteger(suffix) || suffix !== version)
35
+ throw new Error('Corpus id and version do not match');
36
+ return { id: id, version };
37
+ }
25
38
  function object(value, label) {
26
39
  if (!value || typeof value !== 'object' || Array.isArray(value))
27
40
  throw new Error(`${label} must be an object`);
@@ -175,10 +188,7 @@ export async function loadHeldOutCorpus(root) {
175
188
  ], 'corpus');
176
189
  if (manifest.schema_version !== '1.0')
177
190
  throw new Error('Unsupported corpus schema_version');
178
- if (manifest.id !== 'praxis-held-out-v1')
179
- throw new Error('Unsupported corpus id');
180
- if (manifest.version !== 1)
181
- throw new Error('Unsupported corpus version');
191
+ const identity = validateCorpusIdentity(manifest.id, manifest.version);
182
192
  if (manifest.split !== 'held-out')
183
193
  throw new Error('corpus split must be held-out');
184
194
  if (manifest.repetitions !== 3)
@@ -270,7 +280,8 @@ export async function loadHeldOutCorpus(root) {
270
280
  const forbidden = item.expect.forbiddenChangedPaths.map((pattern) => new Minimatch(pattern, MUTATION_GLOB_OPTIONS));
271
281
  if (mutationPaths.some((path) => forbidden.some((matcher) => matcher.match(path))))
272
282
  throw new Error(`${item.name} mutation paths overlap`);
273
- if (!REQUIRED_TAGS.every((tag) => item.tags.includes(tag)) ||
283
+ if (!item.tags.includes('held-out') ||
284
+ !item.tags.includes(identity.id) ||
274
285
  !item.tags.includes(repository.id))
275
286
  throw new Error(`${item.name} is missing required held-out tags`);
276
287
  if (item.tags.some((tag) => FORBIDDEN_TAGS.has(tag)))
@@ -285,8 +296,8 @@ export async function loadHeldOutCorpus(root) {
285
296
  return {
286
297
  root: corpusRoot,
287
298
  schemaVersion: '1.0',
288
- id: 'praxis-held-out-v1',
289
- version: 1,
299
+ id: identity.id,
300
+ version: identity.version,
290
301
  split: 'held-out',
291
302
  repetitions: 3,
292
303
  policy: {
@@ -140,7 +140,7 @@ export function parseHeldOutQualificationOptions(argv) {
140
140
  throw new Error('--profile is not a safe eval identifier');
141
141
  if (!/^[A-Za-z0-9][A-Za-z0-9._:@/-]{0,255}$/u.test(options.model ?? ''))
142
142
  throw new Error('--model is not a safe eval identifier');
143
- if (!/^praxis-held-out-v1@sha256:[0-9a-f]{64}$/u.test(options.confirmHeldOut ?? ''))
143
+ if (!/^praxis-held-out-v[1-9]\d*@sha256:[0-9a-f]{64}$/u.test(options.confirmHeldOut ?? ''))
144
144
  throw new Error('--confirm-held-out must be <id>@sha256:<64 lowercase hex>');
145
145
  return options;
146
146
  }
@@ -639,8 +639,13 @@ function validateQualificationResult(value) {
639
639
  Array.isArray(corpusObject) ||
640
640
  Object.keys(corpusObject).length !== corpusKeys.length ||
641
641
  Object.keys(corpusObject).some((key) => !corpusKeys.includes(key)) ||
642
- corpusObject.id !== 'praxis-held-out-v1' ||
643
- corpusObject.version !== 1 ||
642
+ typeof corpusObject.id !== 'string' ||
643
+ !/^praxis-held-out-v[1-9]\d*$/u.test(corpusObject.id) ||
644
+ typeof corpusObject.version !== 'number' ||
645
+ !Number.isSafeInteger(corpusObject.version) ||
646
+ corpusObject.version <= 0 ||
647
+ Number(corpusObject.id.slice('praxis-held-out-v'.length)) !==
648
+ corpusObject.version ||
644
649
  corpusObject.repetitions !== 3 ||
645
650
  !DIGEST.test(String(corpusObject.content_sha256)))
646
651
  throw new Error('Qualification corpus fields are invalid');
@@ -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.0",
3
+ "version": "0.70.0",
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": {