praxis-agent 0.53.1 → 0.55.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
@@ -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/OpenAI-compatible model access. Praxis deliberately excludes
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 or OpenAI-compatible provider (the
31
- stable setup), or the explicitly enabled experimental ChatGPT-backed Codex
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
@@ -238,7 +253,16 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
238
253
  recovery for malformed streamed tool arguments without tool execution or
239
254
  lost resumability, one default-on bounded Anthropic non-streaming replay for
240
255
  eligible stream/idle failures without exposing failed-attempt output, and
241
- token-only/no-API-dollar accounting for subscription runs.
256
+ token-only/no-API-dollar accounting for subscription runs. Each main user
257
+ Turn and independent auxiliary Agent, Workflow, Team, recovery, or memory
258
+ Turn receives its own provider client. Session-memory requests reuse a
259
+ completion-scoped client but restart routing from primary for each request;
260
+ auto-mode critic and eval-judge requests remain independently constructed
261
+ one-shot clients. Failed attempts stay buffered, and the first successful
262
+ route, whether primary or fallback, stays sticky only through that logical
263
+ Turn's tool continuations; incompatible routes fail closed, and the next
264
+ independent Turn starts from primary. Recovery may persist only an optional
265
+ selected model, never provider route or wire state.
242
266
  - **Transactional self-update** — `praxis update` verifies the package before
243
267
  installing it, rejects concurrent updates, and can roll back after an
244
268
  interruption or crash.
@@ -7,6 +7,8 @@ export interface NativeSidechainMetadata {
7
7
  readonly spawnDepth: number;
8
8
  readonly cwd: string;
9
9
  readonly promptId: string;
10
+ /** Provider-neutral selected model identifier for recovery. */
11
+ readonly model?: string;
10
12
  readonly name?: string;
11
13
  readonly permissionMode?: NativeSidechainPermissionMode;
12
14
  readonly isolation?: 'worktree';
@@ -19,6 +19,7 @@ const requiredKeys = [
19
19
  'promptId',
20
20
  ];
21
21
  const optionalKeys = [
22
+ 'model',
22
23
  'name',
23
24
  'permissionMode',
24
25
  'isolation',
@@ -74,6 +75,8 @@ function validateMetadata(value) {
74
75
  throw new Error('native sidechain metadata cwd is invalid');
75
76
  if (!nonBlank(record.promptId))
76
77
  throw new Error('native sidechain metadata promptId is invalid');
78
+ if (record.model !== undefined && !nonBlank(record.model))
79
+ throw new Error('native sidechain metadata model is invalid');
77
80
  if (record.name !== undefined && !nonBlank(record.name))
78
81
  throw new Error('native sidechain metadata name is invalid');
79
82
  if (record.permissionMode !== undefined &&
@@ -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
- this.activeProvider = this.providerForAgent(configuredAgent);
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) {
@@ -1108,6 +1117,9 @@ export class ClaudeSessionService {
1108
1117
  ...(this.options.providerForModel
1109
1118
  ? { providerForModel: this.options.providerForModel }
1110
1119
  : {}),
1120
+ ...(this.options.providerForTurn
1121
+ ? { providerForTurn: this.options.providerForTurn }
1122
+ : {}),
1111
1123
  baseTools: wrappedBase,
1112
1124
  ...(this.options.deferMcpTools === undefined
1113
1125
  ? {}
@@ -2849,6 +2861,9 @@ export class ClaudeSessionService {
2849
2861
  ...(this.options.providerForModel
2850
2862
  ? { providerForModel: this.options.providerForModel }
2851
2863
  : {}),
2864
+ ...(this.options.providerForTurn
2865
+ ? { providerForTurn: this.options.providerForTurn }
2866
+ : {}),
2852
2867
  baseTools,
2853
2868
  ...(this.options.deferMcpTools === undefined
2854
2869
  ? {}
@@ -5043,6 +5058,12 @@ export class ClaudeSessionService {
5043
5058
  providerForAgent(agent) {
5044
5059
  const inherited = this.provider();
5045
5060
  const selectProvider = this.options.providerForMainModel ?? this.options.providerForModel;
5061
+ const effectiveModel = !this.options.explicitModel && agent?.model && agent.model !== 'inherit'
5062
+ ? agent.model
5063
+ : inherited.model;
5064
+ if (this.options.providerForTurn) {
5065
+ return this.options.providerForTurn(effectiveModel);
5066
+ }
5046
5067
  if (this.options.explicitModel ||
5047
5068
  !agent?.model ||
5048
5069
  agent.model === 'inherit' ||
@@ -76,6 +76,7 @@ export interface ClaudeSubagentExecutorOptions {
76
76
  maxCalls?: number;
77
77
  maxOutputBytes?: number;
78
78
  providerForModel?: (model: string) => ModelProvider;
79
+ providerForTurn?: (model?: string) => ModelProvider;
79
80
  toolNames?: readonly string[];
80
81
  backgroundTaskNotifications?: (waitForRunning: boolean) => Promise<string[]>;
81
82
  notificationDelivered?: (notification: {
@@ -117,6 +118,7 @@ export declare class ClaudeSubagentExecutor {
117
118
  sendBackgroundMessage(agentId: string, message: string, summary: string | undefined, toolUseId: string): string;
118
119
  stopAllBackgroundTasks(): readonly string[];
119
120
  private cwd;
121
+ private providerForTurn;
120
122
  private agentDefinition;
121
123
  private resolveAgentInput;
122
124
  registry(sessionId: string, depth: number, promptIdForCall: (callId: string) => string | null, parentAgentId?: string): ToolRegistry;
@@ -586,6 +586,15 @@ export class ClaudeSubagentExecutor {
586
586
  cwd() {
587
587
  return this.options.cwdProvider?.() ?? this.options.cwd;
588
588
  }
589
+ providerForTurn(model) {
590
+ if (this.options.providerForTurn) {
591
+ return this.options.providerForTurn(model);
592
+ }
593
+ if (model !== undefined) {
594
+ return this.options.providerForModel?.(model) ?? this.options.provider;
595
+ }
596
+ return this.options.provider;
597
+ }
589
598
  agentDefinition(input) {
590
599
  return this.options.extensions?.agent(input.subagentType) ?? null;
591
600
  }
@@ -753,7 +762,9 @@ export class ClaudeSubagentExecutor {
753
762
  !this.options.extensions?.agent(input.subagentType)) {
754
763
  throw new Error(`Unknown Claude agent ${input.subagentType}`);
755
764
  }
756
- if (input.model && !this.options.providerForModel) {
765
+ if (input.model &&
766
+ !this.options.providerForModel &&
767
+ !this.options.providerForTurn) {
757
768
  throw new Error('Agent model overrides are unavailable for this provider');
758
769
  }
759
770
  if (input.permissionMode &&
@@ -850,6 +861,7 @@ export class ClaudeSubagentExecutor {
850
861
  ...(input.permissionMode
851
862
  ? { permissionMode: input.permissionMode }
852
863
  : {}),
864
+ ...(input.model ? { model: input.model } : {}),
853
865
  ...(input.isolation ? { isolation: input.isolation } : {}),
854
866
  ...(parentAgentId ? { parentAgentId } : {}),
855
867
  ...(initialIsolation ? { worktreePath: initialIsolation.cwd } : {}),
@@ -863,9 +875,7 @@ export class ClaudeSubagentExecutor {
863
875
  catch (error) {
864
876
  return settleInitialSetupFailure(error, initialIsolation);
865
877
  }
866
- const provider = input.model
867
- ? (this.options.providerForModel?.(input.model) ?? this.options.provider)
868
- : this.options.provider;
878
+ const provider = this.providerForTurn(input.model);
869
879
  const backgroundRun = this.createBackgroundAgentRun({
870
880
  input,
871
881
  parentCwd,
@@ -874,11 +884,14 @@ export class ClaudeSubagentExecutor {
874
884
  ...(initialIsolation ? { initialIsolation } : {}),
875
885
  createIsolation: () => this.createAgentWorktree(paths.praxisRoot, sessionId, agentId, parentCwd),
876
886
  execute: async (cwd, message, signal, continuation) => {
887
+ const turnProvider = continuation
888
+ ? this.providerForTurn(input.model)
889
+ : provider;
877
890
  const run = (lease) => this.runSidechain({
878
891
  ...lease,
879
892
  sessionId,
880
893
  input,
881
- provider,
894
+ provider: turnProvider,
882
895
  agentId,
883
896
  spawnDepth,
884
897
  promptId,
@@ -1346,13 +1359,12 @@ export class ClaudeSubagentExecutor {
1346
1359
  !this.options.extensions?.agent(options.agentType)) {
1347
1360
  throw new Error(`Unknown Claude agent ${options.agentType}`);
1348
1361
  }
1349
- if (options.model && !this.options.providerForModel) {
1362
+ if (options.model &&
1363
+ !this.options.providerForModel &&
1364
+ !this.options.providerForTurn) {
1350
1365
  throw new Error('Workflow agent model overrides are unavailable for this provider');
1351
1366
  }
1352
- const provider = options.model
1353
- ? (this.options.providerForModel?.(options.model) ??
1354
- this.options.provider)
1355
- : this.options.provider;
1367
+ const provider = this.providerForTurn(options.model);
1356
1368
  const input = {
1357
1369
  description: options.label ?? 'Workflow agent',
1358
1370
  prompt: options.prompt,
@@ -1395,6 +1407,7 @@ export class ClaudeSubagentExecutor {
1395
1407
  spawnDepth: 1,
1396
1408
  cwd: agentCwd,
1397
1409
  promptId: options.promptId,
1410
+ ...(input.model ? { model: input.model } : {}),
1398
1411
  ...(options.isolation ? { isolation: options.isolation } : {}),
1399
1412
  ...(isolation ? { worktreePath: isolation.cwd } : {}),
1400
1413
  });
@@ -1716,11 +1729,11 @@ export class ClaudeSubagentExecutor {
1716
1729
  prompt,
1717
1730
  subagentType: agentType,
1718
1731
  ...(name ? { name } : {}),
1732
+ ...(metadata?.model ? { model: metadata.model } : {}),
1719
1733
  ...(permissionMode ? { permissionMode } : {}),
1720
1734
  ...(isolation ? { isolation } : {}),
1721
1735
  runInBackground: true,
1722
1736
  };
1723
- const provider = this.options.provider;
1724
1737
  const recoveredPromptId = metadata?.promptId ?? randomUUID();
1725
1738
  const backgroundRun = this.createBackgroundAgentRun({
1726
1739
  input,
@@ -1731,6 +1744,9 @@ export class ClaudeSubagentExecutor {
1731
1744
  ...(restoredIsolation ? { initialIsolation: restoredIsolation } : {}),
1732
1745
  createIsolation: () => this.createAgentWorktree(paths.praxisRoot, sessionId, agentId, parentCwd),
1733
1746
  execute: async (cwd, message, signal, continuation) => {
1747
+ // Recovery never restores provider-native route state. Each recovered
1748
+ // execution, including every later follow-up, gets a fresh turn.
1749
+ const provider = this.providerForTurn(input.model);
1734
1750
  const run = (lease) => this.runSidechain({
1735
1751
  ...lease,
1736
1752
  sessionId,
@@ -1757,7 +1773,7 @@ export class ClaudeSubagentExecutor {
1757
1773
  prompt,
1758
1774
  toolUseId,
1759
1775
  outputFile: sidechainPaths.transcriptFile,
1760
- resolvedModel: provider.model ?? 'praxis/provider',
1776
+ resolvedModel: metadata?.model ?? this.options.provider.model ?? 'praxis/provider',
1761
1777
  lifecycle: backgroundRun.lifecycle,
1762
1778
  run: backgroundRun.run,
1763
1779
  markBackground: backgroundRun.markBackground,
@@ -14,6 +14,7 @@ export interface ClaudeTeamAgentRuntimeOptions {
14
14
  readonly hooks?: ClaudeHookRunner;
15
15
  readonly contextAssembler?: ContextAssembler;
16
16
  readonly providerForModel?: (model: string) => ModelProvider;
17
+ readonly providerForTurn?: (model?: string) => ModelProvider;
17
18
  readonly permissionResolverForMode?: (mode: AgentPermissionMode) => PermissionResolver;
18
19
  readonly eventSink?: RuntimeEventSink;
19
20
  readonly approveTool?: (call: ModelToolCall, originalCall?: ModelToolCall, decision?: PermissionDecision) => PermissionApproval | Promise<PermissionApproval>;
@@ -79,6 +79,9 @@ export class ClaudeTeamAgentRuntime {
79
79
  ...(this.options.providerForModel
80
80
  ? { providerForModel: this.options.providerForModel }
81
81
  : {}),
82
+ ...(this.options.providerForTurn
83
+ ? { providerForTurn: this.options.providerForTurn }
84
+ : {}),
82
85
  ...(this.options.permissionResolverForMode
83
86
  ? {
84
87
  permissionResolverForMode: this.options.permissionResolverForMode,
@@ -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
- providerForMainModel = (primaryModel) => {
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
- provider = providerForMainModel(model);
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 ||
@@ -1468,9 +1477,11 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1468
1477
  });
1469
1478
  if (memoryDirectory)
1470
1479
  await mkdir(memoryDirectory, { recursive: true });
1471
- const projectMemoryProviderFactory = providerForMainModel && model
1472
- ? () => providerForMainModel(model)
1473
- : undefined;
1480
+ const projectMemoryProviderFactory = providerForTurn
1481
+ ? () => providerForTurn()
1482
+ : providerForMainModel && model
1483
+ ? () => providerForMainModel(model)
1484
+ : undefined;
1474
1485
  const projectMemoryRecall = projectMemoryPolicy.recall &&
1475
1486
  memoryDirectory &&
1476
1487
  projectMemoryProviderFactory
@@ -1902,6 +1913,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1902
1913
  ...(hooks ? { hooks } : {}),
1903
1914
  ...(contextAssembler ? { contextAssembler } : {}),
1904
1915
  ...(providerForModel ? { providerForModel } : {}),
1916
+ ...(providerForTurn ? { providerForTurn } : {}),
1905
1917
  permissionResolverForMode,
1906
1918
  eventSink: runtimeEventSink,
1907
1919
  }),
@@ -1918,6 +1930,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1918
1930
  provider: hostedToolProvider,
1919
1931
  ...(providerForModel ? { providerForModel } : {}),
1920
1932
  ...(providerForMainModel ? { providerForMainModel } : {}),
1933
+ ...(providerForTurn ? { providerForTurn } : {}),
1921
1934
  tools: filteredTools,
1922
1935
  toolCapabilityEnvironment: runtimeEnvironment,
1923
1936
  ...(!experimentalNativeTranscriptWrites ? { mcp: mcpTools } : {}),
@@ -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
  }