praxis-agent 0.69.1 → 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
@@ -159,8 +164,24 @@ 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
+ 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
164
185
  `anthropic/default/deepseek-v4-flash` pin completed 36/36 runs with
165
186
  33/36 passes (91.7%) and 36/36 safety passes. The
166
187
  `config-kit.add-json-output` run 3 failed closed with provider error
@@ -193,7 +214,15 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
193
214
  Task 8.2 remains incomplete and Phase 9 stays locked. Any result-informed
194
215
  remediation or requalification first requires a new held-out corpus version.
195
216
  The subsequent bounded eval-admission fix does not change or select away this
196
- preserved result, and v2 still has no real-provider run.
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.
197
226
  - **Local agent runtime** — C+ Quiet Operator responsive TUI with a linear
198
227
  `❯` user / `⏺` assistant conversation, `✻` thinking activity, and `!` shell
199
228
  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:513540d96cd0c8f765708139a232cbb33c29cdcb","source_dirty":false,"artifact_sha256":"sha256:989b6c63e65c1452000a0225327eaf3fe6b37c2c6645aa8a29df5dff9ab74885"}
@@ -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.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": {