praxis-agent 0.53.0 → 0.54.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 +29 -6
- package/dist/application/session-service.d.ts +2 -0
- package/dist/application/session-service.js +16 -1
- package/dist/cli-runtime.js +13 -3
- package/dist/native/context.js +26 -8
- package/dist/native/dynamic-context.js +32 -10
- package/dist/providers/codex-subscription.d.ts +1 -7
- package/dist/providers/codex-subscription.js +32 -694
- package/dist/providers/fallback-provider.d.ts +5 -1
- package/dist/providers/fallback-provider.js +62 -4
- package/dist/providers/openai-responses.d.ts +26 -0
- package/dist/providers/openai-responses.js +162 -0
- package/dist/providers/provider-registry.js +22 -0
- package/dist/providers/provider-settings.d.ts +1 -1
- package/dist/providers/provider-settings.js +13 -2
- package/dist/providers/responses-codec.d.ts +31 -0
- package/dist/providers/responses-codec.js +742 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,7 +13,8 @@ Praxis is a local-first, single-user general agent for the command line.
|
|
|
13
13
|
|
|
14
14
|
It provides an interactive or headless agent loop, local tools, permissions,
|
|
15
15
|
sessions, skills, hooks, MCP, plugins, background agents, and provider-neutral
|
|
16
|
-
Anthropic
|
|
16
|
+
Anthropic, OpenAI-compatible Chat Completions, and OpenAI Responses model
|
|
17
|
+
access. Praxis deliberately excludes
|
|
17
18
|
accounts, organizations, billing, managed enterprise policy, remote control,
|
|
18
19
|
IDE surfaces, and telemetry control planes.
|
|
19
20
|
|
|
@@ -27,9 +28,9 @@ sessions, configuration, or compatibility directories.
|
|
|
27
28
|
- macOS or Linux
|
|
28
29
|
- Node.js 24 or newer
|
|
29
30
|
- [`ripgrep`](https://github.com/BurntSushi/ripgrep) (`rg`) for the Grep tool
|
|
30
|
-
- an API key and model ID for an Anthropic
|
|
31
|
-
stable setup), or the explicitly enabled experimental
|
|
32
|
-
subscription integration
|
|
31
|
+
- an API key and model ID for an Anthropic, OpenAI-compatible, or OpenAI
|
|
32
|
+
Responses provider (the stable setup), or the explicitly enabled experimental
|
|
33
|
+
ChatGPT-backed Codex subscription integration
|
|
33
34
|
|
|
34
35
|
Praxis does not use Claude subscription authentication. Claude-shaped message,
|
|
35
36
|
tool, and CLI protocol forms remain supported where they are part of the
|
|
@@ -63,6 +64,20 @@ cd /path/to/project
|
|
|
63
64
|
praxis
|
|
64
65
|
```
|
|
65
66
|
|
|
67
|
+
To use OpenAI's Responses API with an explicit API-key provider:
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
export PRAXIS_PROVIDER="openai-responses"
|
|
71
|
+
export OPENAI_API_KEY="your-api-key"
|
|
72
|
+
export PRAXIS_MODEL="your-responses-model-id"
|
|
73
|
+
|
|
74
|
+
cd /path/to/project
|
|
75
|
+
praxis
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The `openai` provider remains OpenAI-compatible Chat Completions. Provider
|
|
79
|
+
protocols are selected explicitly; model IDs never switch protocols implicitly.
|
|
80
|
+
|
|
66
81
|
Praxis also has an experimental `openai-codex` provider for ChatGPT-backed
|
|
67
82
|
Codex subscriptions. It is separate from OpenAI API-key access, requires
|
|
68
83
|
`experimental.codexSubscription: true`, and stores OAuth credentials in the
|
|
@@ -227,14 +242,22 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
|
|
|
227
242
|
existing default behavior. This is a Praxis permission contract, not a claim
|
|
228
243
|
of verified Claude Code 2.1.208 parity. Explicit concrete `--tools`
|
|
229
244
|
selections load selected tools directly, while
|
|
230
|
-
`--disallowedTools ToolSearch` restores the complete tool list.
|
|
245
|
+
`--disallowedTools ToolSearch` restores the complete tool list. For each
|
|
246
|
+
context assembly, Git status is refreshed from the caller-resolved cwd while
|
|
247
|
+
environment and memory remain lifecycle-stable; collection uses
|
|
248
|
+
`--no-optional-locks`, fails closed on repository/status errors, and bounds
|
|
249
|
+
the rendered status to 2,048 UTF-8 bytes.
|
|
231
250
|
- **Provider-neutral models** — native Provider Registry/Vault routing, API
|
|
232
251
|
adapters, an experimental Codex OAuth adapter, explicit capability checks,
|
|
233
252
|
separate per-attempt connect, byte-idle, and absolute-total timeouts, typed
|
|
234
253
|
recovery for malformed streamed tool arguments without tool execution or
|
|
235
254
|
lost resumability, one default-on bounded Anthropic non-streaming replay for
|
|
236
255
|
eligible stream/idle failures without exposing failed-attempt output, and
|
|
237
|
-
token-only/no-API-dollar accounting for subscription runs.
|
|
256
|
+
token-only/no-API-dollar accounting for subscription runs. For each main user
|
|
257
|
+
turn, failed provider attempts stay buffered; the first successful route,
|
|
258
|
+
whether primary or fallback, is sticky and sealed through that turn's tool
|
|
259
|
+
continuations; incompatible fallback routes fail closed, and each new main
|
|
260
|
+
user turn starts from primary.
|
|
238
261
|
- **Transactional self-update** — `praxis update` verifies the package before
|
|
239
262
|
installing it, rejects concurrent updates, and can roll back after an
|
|
240
263
|
interruption or crash.
|
|
@@ -70,6 +70,8 @@ export interface ClaudeSessionServiceOptions {
|
|
|
70
70
|
enableWorkflows?: boolean;
|
|
71
71
|
providerForModel?: (model: string) => ModelProvider;
|
|
72
72
|
providerForMainModel?: (model: string) => ModelProvider;
|
|
73
|
+
/** Creates one fresh main-turn provider per outer user turn; never used for auxiliary model calls. */
|
|
74
|
+
providerForTurn?: (model?: string) => ModelProvider;
|
|
73
75
|
/** Creates a provider adapter dedicated to Session memory requests so
|
|
74
76
|
* adapter-local cache and retry state are not shared with the foreground. */
|
|
75
77
|
sessionMemoryProviderFactory?: () => ModelProvider;
|
|
@@ -745,7 +745,16 @@ export class ClaudeSessionService {
|
|
|
745
745
|
? (options.extensions?.agent(options.agent) ?? null)
|
|
746
746
|
: null;
|
|
747
747
|
if (configuredAgent && options.provider) {
|
|
748
|
-
|
|
748
|
+
const selectedModel = !options.explicitModel &&
|
|
749
|
+
configuredAgent.model &&
|
|
750
|
+
configuredAgent.model !== 'inherit'
|
|
751
|
+
? configuredAgent.model
|
|
752
|
+
: undefined;
|
|
753
|
+
const selectProvider = options.providerForMainModel ?? options.providerForModel;
|
|
754
|
+
this.activeProvider =
|
|
755
|
+
selectedModel !== undefined && selectProvider
|
|
756
|
+
? selectProvider(selectedModel)
|
|
757
|
+
: this.options.provider;
|
|
749
758
|
}
|
|
750
759
|
}
|
|
751
760
|
nextScheduledPrompt(signal) {
|
|
@@ -5043,6 +5052,12 @@ export class ClaudeSessionService {
|
|
|
5043
5052
|
providerForAgent(agent) {
|
|
5044
5053
|
const inherited = this.provider();
|
|
5045
5054
|
const selectProvider = this.options.providerForMainModel ?? this.options.providerForModel;
|
|
5055
|
+
const effectiveModel = !this.options.explicitModel && agent?.model && agent.model !== 'inherit'
|
|
5056
|
+
? agent.model
|
|
5057
|
+
: inherited.model;
|
|
5058
|
+
if (this.options.providerForTurn) {
|
|
5059
|
+
return this.options.providerForTurn(effectiveModel);
|
|
5060
|
+
}
|
|
5046
5061
|
if (this.options.explicitModel ||
|
|
5047
5062
|
!agent?.model ||
|
|
5048
5063
|
agent.model === 'inherit' ||
|
package/dist/cli-runtime.js
CHANGED
|
@@ -1044,6 +1044,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
|
|
|
1044
1044
|
let provider;
|
|
1045
1045
|
let providerForModel;
|
|
1046
1046
|
let providerForMainModel;
|
|
1047
|
+
let providerForTurn;
|
|
1047
1048
|
let providerBillingMode;
|
|
1048
1049
|
const context = parseContextEnvironment(runtimeEnvironment);
|
|
1049
1050
|
const apiKey = runtimeEnvironment.PRAXIS_API_KEY;
|
|
@@ -1164,17 +1165,25 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
|
|
|
1164
1165
|
providerBillingMode = registry.target.billingMode;
|
|
1165
1166
|
providerForModel = (selectedModel) => registry.create(selectedModel);
|
|
1166
1167
|
const createProvider = providerForModel;
|
|
1167
|
-
|
|
1168
|
+
const createProviderStack = (primaryModel, routeScope) => {
|
|
1168
1169
|
const models = [primaryModel, ...(cli.fallbackModels ?? [])].filter((candidate, index, all) => all.indexOf(candidate) === index);
|
|
1169
1170
|
const providers = models.map((candidate) => createProvider(candidate));
|
|
1170
1171
|
const selected = providers[0];
|
|
1171
1172
|
if (!selected)
|
|
1172
1173
|
throw new Error('A primary model is required');
|
|
1173
1174
|
return providers.length > 1
|
|
1174
|
-
? new FallbackModelProvider({ providers })
|
|
1175
|
+
? new FallbackModelProvider({ providers, routeScope })
|
|
1175
1176
|
: selected;
|
|
1176
1177
|
};
|
|
1177
|
-
|
|
1178
|
+
providerForMainModel = (primaryModel) => createProviderStack(primaryModel, 'completion');
|
|
1179
|
+
const defaultProvider = providerForMainModel(model);
|
|
1180
|
+
provider = defaultProvider;
|
|
1181
|
+
providerForTurn = (turnModel) => {
|
|
1182
|
+
const selectedModel = turnModel ?? model;
|
|
1183
|
+
return selectedModel === undefined
|
|
1184
|
+
? defaultProvider
|
|
1185
|
+
: createProviderStack(selectedModel, 'turn');
|
|
1186
|
+
};
|
|
1178
1187
|
}
|
|
1179
1188
|
catch (error) {
|
|
1180
1189
|
const optionalProviderError = error instanceof ProviderAuthenticationError ||
|
|
@@ -1918,6 +1927,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
|
|
|
1918
1927
|
provider: hostedToolProvider,
|
|
1919
1928
|
...(providerForModel ? { providerForModel } : {}),
|
|
1920
1929
|
...(providerForMainModel ? { providerForMainModel } : {}),
|
|
1930
|
+
...(providerForTurn ? { providerForTurn } : {}),
|
|
1921
1931
|
tools: filteredTools,
|
|
1922
1932
|
toolCapabilityEnvironment: runtimeEnvironment,
|
|
1923
1933
|
...(!experimentalNativeTranscriptWrites ? { mcp: mcpTools } : {}),
|
package/dist/native/context.js
CHANGED
|
@@ -131,7 +131,7 @@ ${sections.join('\n\n')}`,
|
|
|
131
131
|
tailSections.push({
|
|
132
132
|
id: 'relocated-runtime-context',
|
|
133
133
|
placement: 'first-user',
|
|
134
|
-
stability: '
|
|
134
|
+
stability: 'volatile',
|
|
135
135
|
content: renderClaudeDynamicUserContext({
|
|
136
136
|
environment: dynamic.environment,
|
|
137
137
|
...(dynamic.gitStatus ? { gitStatus: dynamic.gitStatus } : {}),
|
|
@@ -143,8 +143,19 @@ ${sections.join('\n\n')}`,
|
|
|
143
143
|
id: 'runtime-context',
|
|
144
144
|
placement: 'system',
|
|
145
145
|
stability: 'session',
|
|
146
|
-
content: renderClaudeDynamicSystemContext(
|
|
146
|
+
content: renderClaudeDynamicSystemContext({
|
|
147
|
+
environment: dynamic.environment,
|
|
148
|
+
...(dynamic.memory ? { memory: dynamic.memory } : {}),
|
|
149
|
+
}),
|
|
147
150
|
});
|
|
151
|
+
if (dynamic.gitStatus) {
|
|
152
|
+
tailSections.push({
|
|
153
|
+
id: 'git-status',
|
|
154
|
+
placement: 'system',
|
|
155
|
+
stability: 'volatile',
|
|
156
|
+
content: dynamic.gitStatus,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
148
159
|
}
|
|
149
160
|
}
|
|
150
161
|
const composition = this.composer.compose({
|
|
@@ -227,15 +238,22 @@ ${sections.join('\n\n')}`,
|
|
|
227
238
|
throw new Error('Dynamic context loader is unavailable');
|
|
228
239
|
if (!snapshot)
|
|
229
240
|
return load(cwd);
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
241
|
+
const current = load(cwd);
|
|
242
|
+
if (!snapshot.stableDynamic) {
|
|
243
|
+
const pending = current.then(({ environment, memory }) => ({
|
|
244
|
+
environment,
|
|
245
|
+
...(memory ? { memory } : {}),
|
|
246
|
+
}));
|
|
247
|
+
snapshot.stableDynamic = pending;
|
|
233
248
|
void pending.catch(() => {
|
|
234
|
-
if (snapshot.
|
|
235
|
-
delete snapshot.
|
|
249
|
+
if (snapshot.stableDynamic === pending)
|
|
250
|
+
delete snapshot.stableDynamic;
|
|
236
251
|
});
|
|
237
252
|
}
|
|
238
|
-
return snapshot.
|
|
253
|
+
return Promise.all([current, snapshot.stableDynamic]).then(([fresh, stable]) => ({
|
|
254
|
+
...stable,
|
|
255
|
+
...(fresh.gitStatus ? { gitStatus: fresh.gitStatus } : {}),
|
|
256
|
+
}));
|
|
239
257
|
}
|
|
240
258
|
loadMcpInstructions(snapshot) {
|
|
241
259
|
const load = this.options.loadMcpInstructions;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { basename } from 'node:path';
|
|
3
3
|
import { platform, release, type } from 'node:os';
|
|
4
|
-
const MAX_GIT_OUTPUT_BYTES =
|
|
4
|
+
const MAX_GIT_OUTPUT_BYTES = 2_048;
|
|
5
|
+
const MAX_GIT_STATUS_BYTES = 2_048;
|
|
5
6
|
const MAX_GIT_ERROR_BYTES = 8 * 1024;
|
|
6
7
|
const GIT_TIMEOUT_MS = 5_000;
|
|
7
8
|
function collectBounded(chunks, chunk, state, limit) {
|
|
@@ -16,6 +17,22 @@ function collectBounded(chunks, chunk, state, limit) {
|
|
|
16
17
|
if (accepted.length < chunk.length)
|
|
17
18
|
state.truncated = true;
|
|
18
19
|
}
|
|
20
|
+
function boundUtf8(value, limit, marker) {
|
|
21
|
+
if (Buffer.byteLength(value, 'utf8') <= limit)
|
|
22
|
+
return value;
|
|
23
|
+
const markerBytes = Buffer.byteLength(marker, 'utf8');
|
|
24
|
+
const prefixLimit = Math.max(0, limit - markerBytes);
|
|
25
|
+
let bytes = 0;
|
|
26
|
+
let prefix = '';
|
|
27
|
+
for (const character of value) {
|
|
28
|
+
const characterBytes = Buffer.byteLength(character, 'utf8');
|
|
29
|
+
if (bytes + characterBytes > prefixLimit)
|
|
30
|
+
break;
|
|
31
|
+
prefix += character;
|
|
32
|
+
bytes += characterBytes;
|
|
33
|
+
}
|
|
34
|
+
return `${prefix}${marker}`;
|
|
35
|
+
}
|
|
19
36
|
function defaultRunGit(cwd, args) {
|
|
20
37
|
return new Promise((resolve, reject) => {
|
|
21
38
|
const child = spawn('git', ['-C', cwd, ...args], {
|
|
@@ -93,9 +110,16 @@ async function renderGitStatus(runGit) {
|
|
|
93
110
|
optionalGit(runGit, ['branch', '--show-current']),
|
|
94
111
|
optionalGit(runGit, ['branch', '--format=%(refname:short)']),
|
|
95
112
|
optionalGit(runGit, ['config', 'user.name']),
|
|
96
|
-
optionalGit(runGit, [
|
|
113
|
+
optionalGit(runGit, [
|
|
114
|
+
'--no-optional-locks',
|
|
115
|
+
'status',
|
|
116
|
+
'--short',
|
|
117
|
+
'--untracked-files=all',
|
|
118
|
+
]),
|
|
97
119
|
optionalGit(runGit, ['log', '-5', '--oneline']),
|
|
98
120
|
]);
|
|
121
|
+
if (!status.available)
|
|
122
|
+
return undefined;
|
|
99
123
|
const branch = branchValue.output || 'HEAD';
|
|
100
124
|
const sections = [
|
|
101
125
|
'# gitStatus',
|
|
@@ -108,18 +132,16 @@ async function renderGitStatus(runGit) {
|
|
|
108
132
|
if (user.available && user.output) {
|
|
109
133
|
sections.push('', `Git user: ${user.output}`);
|
|
110
134
|
}
|
|
111
|
-
const renderedStatus =
|
|
112
|
-
? '
|
|
113
|
-
: status.
|
|
114
|
-
?
|
|
115
|
-
:
|
|
116
|
-
? '... [truncated]'
|
|
117
|
-
: 'Clean';
|
|
135
|
+
const renderedStatus = status.output
|
|
136
|
+
? `${status.output}${status.truncated ? '\n... [truncated]' : ''}`
|
|
137
|
+
: status.truncated
|
|
138
|
+
? '... [truncated]'
|
|
139
|
+
: 'Clean';
|
|
118
140
|
sections.push('', 'Status:', renderedStatus);
|
|
119
141
|
if (commits.available && commits.output) {
|
|
120
142
|
sections.push('', 'Recent commits:', commits.output);
|
|
121
143
|
}
|
|
122
|
-
return sections.join('\n');
|
|
144
|
+
return boundUtf8(sections.join('\n'), MAX_GIT_STATUS_BYTES, '\n... [truncated]');
|
|
123
145
|
}
|
|
124
146
|
export async function loadClaudeDynamicContext(options) {
|
|
125
147
|
const configuredRunGit = options.runGit;
|
|
@@ -14,19 +14,13 @@ export interface CodexSubscriptionProviderOptions {
|
|
|
14
14
|
maxErrorBodyBytes?: number;
|
|
15
15
|
thinking?: ModelThinkingConfig;
|
|
16
16
|
}
|
|
17
|
-
export declare function serializeCodexRequest(request: ModelRequest, model: string, configuredThinking?: ModelThinkingConfig): Record<string, unknown>;
|
|
18
|
-
export declare function parseCodexSseFrame(data: string): ModelStreamEvent[];
|
|
19
17
|
export declare class CodexSubscriptionProvider implements ModelProvider {
|
|
20
18
|
private readonly options;
|
|
21
19
|
readonly model: string;
|
|
22
20
|
readonly capabilities: ModelProvider['capabilities'];
|
|
23
21
|
private readonly fetchImplementation;
|
|
24
|
-
private readonly maxStreamBufferBytes;
|
|
25
|
-
private readonly maxToolArgumentsBytes;
|
|
26
|
-
private readonly maxToolCallsPerResponse;
|
|
27
|
-
private readonly maxToolMetadataBytes;
|
|
28
|
-
private readonly maxReasoningBytes;
|
|
29
22
|
private readonly maxErrorBodyBytes;
|
|
23
|
+
private readonly responsesCodec;
|
|
30
24
|
constructor(options: CodexSubscriptionProviderOptions);
|
|
31
25
|
complete(request: ModelRequest): AsyncIterable<ModelStreamEvent>;
|
|
32
26
|
}
|