praxis-agent 0.48.0 → 0.48.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -215,8 +215,9 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
215
215
  disconnect recovery that never replays an already-dispatched call.
216
216
  - **Provider-neutral models** — native Provider Registry/Vault routing, API
217
217
  adapters, an experimental Codex OAuth adapter, explicit capability checks,
218
- per-attempt bounded deadlines, and token-only/no-API-dollar accounting for
219
- subscription runs.
218
+ per-attempt bounded deadlines, typed recovery for malformed streamed tool
219
+ arguments without tool execution or lost resumability, and
220
+ token-only/no-API-dollar accounting for subscription runs.
220
221
  - **Transactional self-update** — `praxis update` verifies the package before
221
222
  installing it, rejects concurrent updates, and can roll back after an
222
223
  interruption or crash.
@@ -301,7 +302,7 @@ normal/low-capability full-frame p95 budgets of `<16.7/<33 ms`.
301
302
  `npm run test:coverage` measures all production code under `src/**` with V8 and
302
303
  enforces global floors of 79% statements, 70% branches, 85% functions, and 81% lines,
303
304
  and rejects any production runtime module with zero covered statements (while allowing
304
- type-only modules). `npm run test:fixtures` executes the 68-behavior native contract; 60 behaviors
305
+ type-only modules). `npm run test:fixtures` executes the 69-behavior native contract; 61 behaviors
305
306
  are qualified and 8 are explicitly excluded. `npm run verify:fixture-contracts`
306
307
  performs the structural check and is part of `npm run check`.
307
308
  `npm run test:core-completion` is retained as a compatibility alias for
@@ -15,7 +15,7 @@ import { projectNativeSessionEntries } from './native-session-projection.js';
15
15
  import { classifyClaudeInterruption, } from '../native/interruption.js';
16
16
  import { findUnresolvedClaudeToolCalls, getClaudeContentBlocks, } from '../native/tool-links.js';
17
17
  import { createClaudeAgentSettingEntry, createClaudeHookAttachmentEntries, createClaudeLastPromptEntry, createClaudeRuleAttachmentEntry, translateProviderEvents, } from '../native/translation.js';
18
- import { AgentRunCancelledError, AgentRuntime, ModelProviderError, } from '../core/runtime.js';
18
+ import { AgentRunCancelledError, AgentRuntime, MALFORMED_TOOL_INPUT_MESSAGE, ModelProviderError, } from '../core/runtime.js';
19
19
  import { resolveToolSchedulingPolicy } from '../core/tool-scheduling-policy.js';
20
20
  import { BackgroundTaskRuntime, } from './background-task-runtime.js';
21
21
  import { backgroundAgentNotificationMarkers, } from './background-agent-manager.js';
@@ -3347,7 +3347,11 @@ export class ClaudeSessionService {
3347
3347
  : {}),
3348
3348
  };
3349
3349
  const unresolvedToolCall = unresolvedToolCalls[0];
3350
- if (unresolvedToolCall && !approveRecovery) {
3350
+ if (unresolvedToolCall &&
3351
+ !unresolvedToolCalls.every((call) => call.inputError?.kind === 'malformed_json' &&
3352
+ call.inputError?.message ===
3353
+ MALFORMED_TOOL_INPUT_MESSAGE) &&
3354
+ !approveRecovery) {
3351
3355
  throw new Error(`Claude session tool call ${unresolvedToolCall.id} requires explicit recovery approval`);
3352
3356
  }
3353
3357
  const recoveryResults = await runtime.recoverToolCalls(unresolvedToolCalls, recoveryRequest);
@@ -43,7 +43,14 @@ export interface ModelToolCall {
43
43
  id: string;
44
44
  name: string;
45
45
  input: Record<string, unknown>;
46
+ inputError?: ModelToolInputError;
46
47
  }
48
+ export declare const MALFORMED_TOOL_INPUT_MESSAGE = "Malformed tool input";
49
+ export interface ModelToolInputError {
50
+ kind: 'malformed_json';
51
+ message: typeof MALFORMED_TOOL_INPUT_MESSAGE;
52
+ }
53
+ export declare function malformedModelToolCall(id: string, name: string): ModelToolCall;
47
54
  export type ModelThinkingMode = 'enabled' | 'adaptive' | 'disabled';
48
55
  export type ModelThinkingBlock = {
49
56
  type: 'thinking';
@@ -1,5 +1,17 @@
1
1
  import { ToolExecutionScheduler } from './tool-execution-scheduler.js';
2
2
  import { resolveToolSchedulingPolicy } from './tool-scheduling-policy.js';
3
+ export const MALFORMED_TOOL_INPUT_MESSAGE = 'Malformed tool input';
4
+ export function malformedModelToolCall(id, name) {
5
+ return {
6
+ id,
7
+ name,
8
+ input: {},
9
+ inputError: {
10
+ kind: 'malformed_json',
11
+ message: MALFORMED_TOOL_INPUT_MESSAGE,
12
+ },
13
+ };
14
+ }
3
15
  const permissionDecisionSources = new WeakMap();
4
16
  const autoModePermissionOutcomes = new WeakMap();
5
17
  export function annotatePermissionDecision(decision, source) {
@@ -516,7 +528,12 @@ export class AgentRuntime {
516
528
  }
517
529
  toolCalls.push(event.call);
518
530
  this.emit(event);
519
- const policy = resolveToolSchedulingPolicy(this.options.tools, event.call);
531
+ const policy = event.call.inputError
532
+ ? {
533
+ concurrency: 'exclusive',
534
+ startAfterAssistant: true,
535
+ }
536
+ : resolveToolSchedulingPolicy(this.options.tools, event.call);
520
537
  toolScheduler.schedule(event.call, request.observer?.toolExecutionStarted
521
538
  ? { ...policy, startAfterAssistant: true }
522
539
  : policy);
@@ -863,6 +880,9 @@ export class AgentRuntime {
863
880
  try {
864
881
  if (emitPresentation)
865
882
  emitProgress(0);
883
+ if (call.inputError) {
884
+ return { content: call.inputError.message, isError: true };
885
+ }
866
886
  const executed = await this.executeTool(call, { ...request, signal }, messages);
867
887
  const unsupportedImages = executed.images?.length && this.provider.capabilities.images !== true;
868
888
  const unsupportedDocuments = executed.documents?.length &&
@@ -1,3 +1,4 @@
1
+ import { MALFORMED_TOOL_INPUT_MESSAGE } from './runtime.js';
1
2
  const terminalReasons = new Set([
2
3
  'end_turn',
3
4
  'tool_use',
@@ -51,11 +52,17 @@ export function isModelContentBlock(value) {
51
52
  return isModelMedia(value);
52
53
  }
53
54
  function isModelToolCall(value) {
55
+ const inputError = isRecord(value) ? value.inputError : undefined;
54
56
  return (isRecord(value) &&
55
- hasOnlyKeys(value, ['id', 'name', 'input']) &&
57
+ hasOnlyKeys(value, ['id', 'name', 'input', 'inputError']) &&
56
58
  isNonEmptyString(value.id) &&
57
59
  isNonEmptyString(value.name) &&
58
- isRecord(value.input));
60
+ isRecord(value.input) &&
61
+ (inputError === undefined ||
62
+ (isRecord(inputError) &&
63
+ hasOnlyKeys(inputError, ['kind', 'message']) &&
64
+ inputError.kind === 'malformed_json' &&
65
+ inputError.message === MALFORMED_TOOL_INPUT_MESSAGE)));
59
66
  }
60
67
  function isModelThinkingBlock(value) {
61
68
  return isRecord(value) && value.type === 'thinking'
@@ -1,4 +1,4 @@
1
- import { ModelProviderError, } from '../core/runtime.js';
1
+ import { ModelProviderError, malformedModelToolCall, } from '../core/runtime.js';
2
2
  import { transportFailureKind } from './provider-errors.js';
3
3
  import { createAnthropicPromptCachePolicyResolver, } from './anthropic-prompt-cache.js';
4
4
  function isRecord(value) {
@@ -119,8 +119,16 @@ function completedToolCall(state, index) {
119
119
  try {
120
120
  input = JSON.parse(pending.partialJson);
121
121
  }
122
- catch (error) {
123
- throw new ModelProviderError(`Provider returned malformed tool arguments for ${pending.name}`, { retryable: false, cause: error });
122
+ catch {
123
+ if (!pending.id || !pending.name) {
124
+ throw new ModelProviderError('Provider returned an invalid tool call', {
125
+ retryable: false,
126
+ });
127
+ }
128
+ return {
129
+ type: 'tool-call',
130
+ call: malformedModelToolCall(pending.id, pending.name),
131
+ };
124
132
  }
125
133
  }
126
134
  if (!pending.id || !pending.name || !isRecord(input)) {
@@ -1,4 +1,4 @@
1
- import { ModelProviderError, } from '../core/runtime.js';
1
+ import { ModelProviderError, malformedModelToolCall, } from '../core/runtime.js';
2
2
  import { CodexOAuthError, } from './codex-oauth.js';
3
3
  export const CODEX_RESPONSES_ENDPOINT = 'https://chatgpt.com/backend-api/codex/responses';
4
4
  const DEFAULT_MAX_STREAM = 1024 * 1024;
@@ -572,7 +572,12 @@ function parseEvent(value, state, limits) {
572
572
  parsed = JSON.parse(call.arguments || '{}');
573
573
  }
574
574
  catch {
575
- throw invalid('Codex provider returned malformed function arguments');
575
+ if (!id || !name)
576
+ throw invalid('Codex provider returned an invalid tool call');
577
+ call.emitted = true;
578
+ state.toolCallEmitted = true;
579
+ events.push({ type: 'tool-call', call: malformedModelToolCall(id, name) });
580
+ return events;
576
581
  }
577
582
  if (!isRecord(parsed))
578
583
  throw invalid('Codex provider returned non-object function arguments');
@@ -1,4 +1,4 @@
1
- import { ModelProviderError, } from '../core/runtime.js';
1
+ import { ModelProviderError, malformedModelToolCall, } from '../core/runtime.js';
2
2
  import { transportFailureKind } from './provider-errors.js';
3
3
  function isRecord(value) {
4
4
  return typeof value === 'object' && value !== null && !Array.isArray(value);
@@ -64,8 +64,16 @@ function completedToolCallEvents(pending) {
64
64
  try {
65
65
  input = JSON.parse(call.arguments || '{}');
66
66
  }
67
- catch (error) {
68
- throw new ModelProviderError(`Provider returned malformed tool arguments for ${call.name}`, { retryable: false, cause: error });
67
+ catch {
68
+ if (!call.id || !call.name) {
69
+ throw new ModelProviderError('Provider returned an invalid tool call', {
70
+ retryable: false,
71
+ });
72
+ }
73
+ return {
74
+ type: 'tool-call',
75
+ call: malformedModelToolCall(call.id, call.name),
76
+ };
69
77
  }
70
78
  if (!isRecord(input) || !call.id || !call.name) {
71
79
  throw new ModelProviderError('Provider returned an invalid tool call', {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "praxis-agent",
3
- "version": "0.48.0",
3
+ "version": "0.48.1",
4
4
  "description": "Local-first, single-user general agent for the command line.",
5
5
  "license": "MIT",
6
6
  "author": "wuqisen",