praxis-agent 0.55.2 → 0.55.3

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.
@@ -1,4 +1,4 @@
1
- import type { ModelDocument, ModelDocumentMediaType, ModelImage, ModelImageMediaType, ModelUsage, RuntimeEventSink } from '../core/runtime.js';
1
+ import type { ModelDocument, ModelDocumentMediaType, ModelImage, ModelImageMediaType, ModelTerminalReason, ModelUsage, RuntimeEventSink } from '../core/runtime.js';
2
2
  export type CliInputFormat = 'text' | 'stream-json';
3
3
  export type CliOutputFormat = 'text' | 'json' | 'stream-json';
4
4
  export type CliPermissionMode = 'acceptEdits' | 'auto' | 'bypassPermissions' | 'manual' | 'dontAsk' | 'plan' | 'default';
@@ -225,8 +225,25 @@ export interface ProtocolResult {
225
225
  */
226
226
  modelCostUsd?: Readonly<Record<string, number>>;
227
227
  }
228
- export declare function createSuccessResult(result: ProtocolResult, info: CliRuntimeInfo, startedAt: number, modelTurns: number): Record<string, unknown>;
229
- export declare function createErrorResult(message: string, sessionId: string, startedAt: number, modelTurns: number): Record<string, unknown>;
228
+ export interface ProtocolSuccessProjectionContext {
229
+ localCommand?: boolean;
230
+ stopReason?: ModelTerminalReason;
231
+ ttftMs?: number;
232
+ ttftStreamMs?: number;
233
+ timeToRequestMs?: number;
234
+ }
235
+ export interface ProtocolErrorProjectionContext {
236
+ providerApiError?: boolean;
237
+ apiErrorStatus?: number | null;
238
+ }
239
+ export interface ProtocolTimingProjection {
240
+ ttftMs?: number;
241
+ ttftStreamMs?: number;
242
+ timeToRequestMs?: number;
243
+ }
244
+ export declare function projectProtocolTimings(startedAt: number, requestAt?: number, outputAt?: number): ProtocolTimingProjection;
245
+ export declare function createSuccessResult(result: ProtocolResult, info: CliRuntimeInfo, startedAt: number, modelTurns: number, context?: ProtocolSuccessProjectionContext): Record<string, unknown>;
246
+ export declare function createErrorResult(message: string, sessionId: string, startedAt: number, modelTurns: number, context?: ProtocolErrorProjectionContext): Record<string, unknown>;
230
247
  /**
231
248
  * Returns the argument text of a headless `/color` prompt, or undefined when
232
249
  * the prompt is not a bare `/color` command. A trailing space matches with an
@@ -259,6 +276,11 @@ export declare class StreamJsonOutput {
259
276
  private activeThinkingIndex;
260
277
  private nextContentIndex;
261
278
  private modelTurns;
279
+ private projectionStartedAt;
280
+ private firstRequestAt;
281
+ private firstOutputAt;
282
+ private turnHasRealContent;
283
+ private pendingFailureMessage;
262
284
  private compacting;
263
285
  private sessionState;
264
286
  private readonly emitSessionStateEvents;
@@ -272,7 +294,7 @@ export declare class StreamJsonOutput {
272
294
  private readonly includePartialMessages;
273
295
  private readonly includeHookEvents;
274
296
  private write;
275
- init(): void;
297
+ init(startedAt?: number): void;
276
298
  replayUser(message: StreamUserMessage['message']): void;
277
299
  /**
278
300
  * Emits the synthetic assistant message for a provider-free local command
@@ -285,13 +307,14 @@ export declare class StreamJsonOutput {
285
307
  request: Record<string, unknown>;
286
308
  }): void;
287
309
  readonly sink: RuntimeEventSink;
288
- result(result: ProtocolResult, startedAt: number): void;
310
+ result(result: ProtocolResult, startedAt: number, context?: ProtocolSuccessProjectionContext): void;
289
311
  promptSuggestion(suggestion: string): void;
290
- error(message: string, startedAt: number): void;
312
+ error(message: string, startedAt: number, context?: ProtocolErrorProjectionContext): void;
291
313
  private onEvent;
292
314
  private writeSessionState;
293
315
  private writeTerminalIdle;
294
316
  private ensureTurn;
317
+ private observeOutput;
295
318
  private discardTurn;
296
319
  private startTurn;
297
320
  private writePartialMessageStart;
@@ -1,32 +1,74 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- export function createSuccessResult(result, info, startedAt, modelTurns) {
3
- const duration = Date.now() - startedAt;
4
- const modelUsage = result.modelUsage ?? { [info.model]: result.usage };
5
- const usage = {
6
- input_tokens: result.usage.inputTokens,
7
- output_tokens: result.usage.outputTokens,
8
- ...(result.usage.cacheReadInputTokens === undefined
2
+ export function projectProtocolTimings(startedAt, requestAt, outputAt) {
3
+ return {
4
+ ...(requestAt === undefined
9
5
  ? {}
10
- : { cache_read_input_tokens: result.usage.cacheReadInputTokens }),
11
- ...(result.usage.cacheCreationInputTokens === undefined
6
+ : { timeToRequestMs: requestAt - startedAt }),
7
+ ...(outputAt === undefined
12
8
  ? {}
13
9
  : {
14
- cache_creation_input_tokens: result.usage.cacheCreationInputTokens,
10
+ ttftMs: outputAt - startedAt,
11
+ ttftStreamMs: outputAt - startedAt,
15
12
  }),
16
13
  };
14
+ }
15
+ function protocolUsage(usage) {
16
+ return {
17
+ input_tokens: usage.inputTokens,
18
+ cache_creation_input_tokens: usage.cacheCreationInputTokens ?? 0,
19
+ cache_read_input_tokens: usage.cacheReadInputTokens ?? 0,
20
+ output_tokens: usage.outputTokens,
21
+ server_tool_use: {
22
+ web_search_requests: usage.webSearchRequests ?? 0,
23
+ web_fetch_requests: 0,
24
+ },
25
+ service_tier: 'standard',
26
+ cache_creation: {
27
+ ephemeral_1h_input_tokens: 0,
28
+ ephemeral_5m_input_tokens: 0,
29
+ },
30
+ inference_geo: '',
31
+ iterations: [],
32
+ speed: 'standard',
33
+ };
34
+ }
35
+ function normalizeApiError(message, status) {
36
+ const body = message.replace(/^API Error:\s*\d+\s*/u, '');
37
+ return `API Error: ${status} ${body}`;
38
+ }
39
+ export function createSuccessResult(result, info, startedAt, modelTurns, context = {}) {
40
+ const duration = Date.now() - startedAt;
41
+ const localCommand = context.localCommand === true;
42
+ const modelUsage = result.modelUsage ?? (localCommand ? {} : { [info.model]: result.usage });
43
+ const usage = protocolUsage(result.usage);
17
44
  return {
18
45
  type: 'result',
19
46
  subtype: 'success',
20
47
  is_error: false,
21
48
  duration_ms: duration,
22
- duration_api_ms: result.durationApiMs === undefined
23
- ? null
24
- : Math.round(result.durationApiMs),
25
- num_turns: modelTurns,
49
+ ...(localCommand
50
+ ? {}
51
+ : {
52
+ api_error_status: null,
53
+ ...(context.ttftMs === undefined ? {} : { ttft_ms: context.ttftMs }),
54
+ ...(context.ttftStreamMs === undefined
55
+ ? {}
56
+ : { ttft_stream_ms: context.ttftStreamMs }),
57
+ ...(context.timeToRequestMs === undefined
58
+ ? {}
59
+ : { time_to_request_ms: context.timeToRequestMs }),
60
+ }),
61
+ duration_api_ms: localCommand
62
+ ? 0
63
+ : result.durationApiMs === undefined
64
+ ? 0
65
+ : Math.round(result.durationApiMs),
66
+ num_turns: localCommand ? 0 : modelTurns,
26
67
  result: result.text,
27
- stop_reason: null,
68
+ stop_reason: localCommand ? null : (context.stopReason ?? null),
69
+ ...(localCommand ? {} : { terminal_reason: 'completed' }),
28
70
  session_id: result.sessionId,
29
- total_cost_usd: result.costUsd ?? null,
71
+ total_cost_usd: result.costUsd ?? 0,
30
72
  usage,
31
73
  modelUsage: Object.fromEntries(Object.entries(modelUsage).map(([model, modelUsage]) => [
32
74
  model,
@@ -35,11 +77,9 @@ export function createSuccessResult(result, info, startedAt, modelTurns) {
35
77
  outputTokens: modelUsage.outputTokens,
36
78
  cacheReadInputTokens: modelUsage.cacheReadInputTokens ?? 0,
37
79
  cacheCreationInputTokens: modelUsage.cacheCreationInputTokens ?? 0,
80
+ webSearchRequests: modelUsage.webSearchRequests ?? 0,
38
81
  costUSD: result.modelCostUsd?.[model] ??
39
- (model === info.model ? (result.costUsd ?? null) : null),
40
- ...(modelUsage.webSearchRequests === undefined
41
- ? {}
42
- : { webSearchRequests: modelUsage.webSearchRequests }),
82
+ (model === info.model ? (result.costUsd ?? 0) : 0),
43
83
  contextWindow: modelUsage.contextWindow ??
44
84
  (model === info.model ? (info.contextWindowTokens ?? 0) : 0),
45
85
  maxOutputTokens: modelUsage.maxOutputTokens ??
@@ -63,24 +103,45 @@ function errorResultSubtype(message) {
63
103
  return 'error_max_structured_output_retries';
64
104
  return 'error_during_execution';
65
105
  }
66
- export function createErrorResult(message, sessionId, startedAt, modelTurns) {
106
+ export function createErrorResult(message, sessionId, startedAt, modelTurns, context = {}) {
67
107
  const duration = Date.now() - startedAt;
108
+ const providerApiError = context.providerApiError === true;
109
+ const apiErrorStatus = context.apiErrorStatus ?? null;
110
+ const normalizedMessage = providerApiError && typeof context.apiErrorStatus === 'number'
111
+ ? normalizeApiError(message, context.apiErrorStatus)
112
+ : message;
113
+ if (providerApiError) {
114
+ return {
115
+ type: 'result',
116
+ subtype: 'success',
117
+ is_error: true,
118
+ api_error_status: apiErrorStatus,
119
+ duration_ms: duration,
120
+ duration_api_ms: 0,
121
+ num_turns: modelTurns,
122
+ result: normalizedMessage,
123
+ stop_reason: 'stop_sequence',
124
+ terminal_reason: 'api_error',
125
+ session_id: sessionId,
126
+ total_cost_usd: 0,
127
+ usage: protocolUsage({ inputTokens: 0, outputTokens: 0 }),
128
+ modelUsage: {},
129
+ permission_denials: [],
130
+ fast_mode_state: 'off',
131
+ uuid: randomUUID(),
132
+ };
133
+ }
68
134
  return {
69
135
  type: 'result',
70
136
  subtype: errorResultSubtype(message),
71
137
  is_error: true,
72
138
  duration_ms: duration,
73
- duration_api_ms: null,
139
+ duration_api_ms: 0,
74
140
  num_turns: modelTurns,
75
141
  stop_reason: null,
76
142
  session_id: sessionId,
77
- total_cost_usd: null,
78
- usage: {
79
- input_tokens: 0,
80
- output_tokens: 0,
81
- cache_creation_input_tokens: 0,
82
- cache_read_input_tokens: 0,
83
- },
143
+ total_cost_usd: 0,
144
+ usage: protocolUsage({ inputTokens: 0, outputTokens: 0 }),
84
145
  modelUsage: {},
85
146
  permission_denials: [],
86
147
  errors: [message],
@@ -1710,6 +1771,11 @@ export class StreamJsonOutput {
1710
1771
  activeThinkingIndex;
1711
1772
  nextContentIndex = 0;
1712
1773
  modelTurns = 0;
1774
+ projectionStartedAt;
1775
+ firstRequestAt;
1776
+ firstOutputAt;
1777
+ turnHasRealContent = false;
1778
+ pendingFailureMessage;
1713
1779
  compacting = false;
1714
1780
  sessionState;
1715
1781
  emitSessionStateEvents;
@@ -1732,7 +1798,11 @@ export class StreamJsonOutput {
1732
1798
  session_id: value.session_id ?? this.sessionId,
1733
1799
  });
1734
1800
  }
1735
- init() {
1801
+ init(startedAt = Date.now()) {
1802
+ this.projectionStartedAt = startedAt;
1803
+ this.firstRequestAt = undefined;
1804
+ this.firstOutputAt = undefined;
1805
+ this.pendingFailureMessage = undefined;
1736
1806
  this.writeSessionState('running');
1737
1807
  this.write({
1738
1808
  type: 'system',
@@ -1783,9 +1853,21 @@ export class StreamJsonOutput {
1783
1853
  this.write({ type: 'control_request', ...request });
1784
1854
  }
1785
1855
  sink = (event) => this.onEvent(event);
1786
- result(result, startedAt) {
1856
+ result(result, startedAt, context = {}) {
1787
1857
  this.finishTurn();
1788
- this.write(createSuccessResult(result, this.info, startedAt, this.modelTurns));
1858
+ const requestAt = this.firstRequestAt;
1859
+ const outputAt = this.firstOutputAt;
1860
+ const stopReason = context.stopReason ?? this.turnTerminalReason;
1861
+ const timing = projectProtocolTimings(this.projectionStartedAt ?? startedAt, requestAt, outputAt);
1862
+ this.write(createSuccessResult(result, this.info, startedAt, this.modelTurns, {
1863
+ ...context,
1864
+ ...(context.localCommand
1865
+ ? {}
1866
+ : {
1867
+ ...(stopReason === undefined ? {} : { stopReason }),
1868
+ ...timing,
1869
+ }),
1870
+ }));
1789
1871
  this.writeTerminalIdle();
1790
1872
  this.modelTurns = 0;
1791
1873
  }
@@ -1797,9 +1879,15 @@ export class StreamJsonOutput {
1797
1879
  session_id: this.sessionId,
1798
1880
  });
1799
1881
  }
1800
- error(message, startedAt) {
1882
+ error(message, startedAt, context = {}) {
1883
+ if (this.pendingFailureMessage !== undefined) {
1884
+ this.turnText =
1885
+ context.providerApiError && typeof context.apiErrorStatus === 'number'
1886
+ ? normalizeApiError(message, context.apiErrorStatus)
1887
+ : message;
1888
+ }
1801
1889
  this.finishTurn();
1802
- this.write(createErrorResult(message, this.sessionId, startedAt, this.modelTurns));
1890
+ this.write(createErrorResult(message, this.sessionId, startedAt, this.modelTurns, context));
1803
1891
  this.writeTerminalIdle();
1804
1892
  this.modelTurns = 0;
1805
1893
  }
@@ -1841,7 +1929,9 @@ export class StreamJsonOutput {
1841
1929
  event.state === 'completed' ||
1842
1930
  event.state === 'cancelled' ||
1843
1931
  event.state === 'failed') {
1844
- this.flushAssistant();
1932
+ if (event.state !== 'failed' ||
1933
+ this.pendingFailureMessage === undefined)
1934
+ this.flushAssistant();
1845
1935
  if (event.state === 'awaiting-permission')
1846
1936
  this.writeSessionState('requires_action');
1847
1937
  }
@@ -1849,12 +1939,14 @@ export class StreamJsonOutput {
1849
1939
  }
1850
1940
  if (event.type === 'text-delta') {
1851
1941
  this.ensureTurn();
1942
+ this.observeOutput();
1852
1943
  this.turnText += event.delta;
1853
1944
  this.partialText(event.delta);
1854
1945
  return;
1855
1946
  }
1856
1947
  if (event.type === 'thinking-start') {
1857
1948
  this.ensureTurn();
1949
+ this.observeOutput();
1858
1950
  if (this.includePartialMessages) {
1859
1951
  if (this.activeThinkingIndex !== undefined) {
1860
1952
  throw new Error('Thinking content blocks cannot overlap');
@@ -1885,6 +1977,7 @@ export class StreamJsonOutput {
1885
1977
  if (event.type === 'thinking-delta' ||
1886
1978
  event.type === 'thinking-signature-delta') {
1887
1979
  this.ensureTurn();
1980
+ this.observeOutput();
1888
1981
  if (this.includePartialMessages) {
1889
1982
  if (this.activeThinkingIndex === undefined) {
1890
1983
  throw new Error('Thinking delta arrived without an active block');
@@ -1906,6 +1999,7 @@ export class StreamJsonOutput {
1906
1999
  }
1907
2000
  if (event.type === 'thinking-stop') {
1908
2001
  this.ensureTurn();
2002
+ this.observeOutput();
1909
2003
  this.turnThinking.push(event.block);
1910
2004
  if (this.includePartialMessages) {
1911
2005
  if (this.activeThinkingIndex === undefined) {
@@ -1930,6 +2024,7 @@ export class StreamJsonOutput {
1930
2024
  }
1931
2025
  if (event.type === 'tool-call') {
1932
2026
  this.ensureTurn();
2027
+ this.observeOutput();
1933
2028
  this.turnCalls.push(event.call);
1934
2029
  if (this.includePartialMessages) {
1935
2030
  const index = this.nextContentIndex++;
@@ -2165,13 +2260,16 @@ export class StreamJsonOutput {
2165
2260
  }
2166
2261
  if (event.type === 'failed') {
2167
2262
  this.ensureTurn();
2168
- if (this.turnText.length === 0 &&
2263
+ if (!this.turnHasRealContent &&
2264
+ this.turnText.length === 0 &&
2169
2265
  this.turnThinking.length === 0 &&
2170
2266
  this.turnCalls.length === 0) {
2171
- this.turnText = event.message;
2172
- this.partialText(event.message);
2267
+ this.pendingFailureMessage = event.message;
2268
+ this.assistantFlushed = false;
2269
+ }
2270
+ else {
2271
+ this.flushAssistant();
2173
2272
  }
2174
- this.flushAssistant();
2175
2273
  }
2176
2274
  }
2177
2275
  writeSessionState(state) {
@@ -2204,6 +2302,11 @@ export class StreamJsonOutput {
2204
2302
  if (!this.turnActive)
2205
2303
  this.startTurn();
2206
2304
  }
2305
+ observeOutput() {
2306
+ this.turnHasRealContent = true;
2307
+ if (this.firstOutputAt === undefined)
2308
+ this.firstOutputAt = Date.now();
2309
+ }
2207
2310
  discardTurn(reason) {
2208
2311
  this.write({
2209
2312
  type: 'system',
@@ -2225,6 +2328,8 @@ export class StreamJsonOutput {
2225
2328
  this.nextContentIndex = 0;
2226
2329
  this.partialEvents = [];
2227
2330
  this.pendingPartialStop = undefined;
2331
+ this.turnHasRealContent = false;
2332
+ this.pendingFailureMessage = undefined;
2228
2333
  }
2229
2334
  startTurn() {
2230
2335
  if (this.turnActive)
@@ -2243,6 +2348,9 @@ export class StreamJsonOutput {
2243
2348
  this.nextContentIndex = 0;
2244
2349
  this.partialEvents = [];
2245
2350
  this.pendingPartialStop = undefined;
2351
+ this.turnHasRealContent = false;
2352
+ if (this.firstRequestAt === undefined)
2353
+ this.firstRequestAt = Date.now();
2246
2354
  this.modelTurns += 1;
2247
2355
  if (this.includePartialMessages) {
2248
2356
  this.write({
@@ -19,7 +19,7 @@ import { isSessionId } from './core/session.js';
19
19
  import { assertNativeDataPlane, resolveDataPlane, resolveDataPlaneRoot, resolveDataPlanePaths, } from './persistence/data-plane.js';
20
20
  import { loadNativeContextResources, loadNativeSettings, loadNativeSharedResources, } from './persistence/native-resources.js';
21
21
  import { resolveProjectIdentity } from './platform/project-identity.js';
22
- import { AgentRunCancelledError, } from './core/runtime.js';
22
+ import { AgentRunCancelledError, ModelProviderError, } from './core/runtime.js';
23
23
  import { persistTuiPermissionUpdates } from './cli/tui/permission-settings.js';
24
24
  import { loadClaudeReleaseNotes } from './cli/tui/release-notes.js';
25
25
  import { canonicalClaudeCostModelName, formatCostSummary, } from './cli/tui/cost-summary.js';
@@ -65,7 +65,7 @@ import { WorkspaceContext } from './application/session-worktree.js';
65
65
  import { launchTmuxWorktree } from './platform/tmux-worktree.js';
66
66
  import { claudeSandboxRuntime } from './sandbox/claude-sandbox-runtime.js';
67
67
  import { nativeSandboxTempDirectory, loadClaudeSandboxSettings, } from './sandbox/claude-sandbox-settings.js';
68
- import { createErrorResult, createSuccessResult, isHeadlessCostCommand, matchHeadlessColorCommand, parseCliInvocation, readStreamJsonMessages, StreamJsonOutput, } from './cli/protocol.js';
68
+ import { createErrorResult, createSuccessResult, isHeadlessCostCommand, matchHeadlessColorCommand, parseCliInvocation, projectProtocolTimings, readStreamJsonMessages, StreamJsonOutput, } from './cli/protocol.js';
69
69
  import { executeProviderAuthCommand } from './cli/provider-auth-command.js';
70
70
  import { describeClaudePlugin, initClaudePlugin, installClaudePlugin, loadClaudePlugins, readPluginRegistry, setClaudePluginEnabled, uninstallClaudePlugin, updateClaudePlugin, validateClaudePlugin, } from './plugins/claude-plugin-runtime.js';
71
71
  import { addClaudeMarketplace, disableAllNativePlugins, installClaudeMarketplacePlugin, listClaudeMarketplaceAvailablePlugins, listNativePluginRecords, readClaudeKnownMarketplaces, removeClaudeMarketplace, setNativePluginEnabled, saveClaudePluginConfig, uninstallNativePlugin, updateClaudeMarketplace, updateNativePlugin, validateClaudeMarketplace, } from './plugins/claude-plugin-marketplace.js';
@@ -5412,6 +5412,9 @@ async function execute(argv, io, dependencies, signal) {
5412
5412
  }
5413
5413
  let streamOutput;
5414
5414
  let jsonModelTurns = 0;
5415
+ let jsonRequestAt;
5416
+ let jsonOutputAt;
5417
+ let jsonTerminalReason;
5415
5418
  const pendingEvents = [];
5416
5419
  let streamIterator;
5417
5420
  let firstStreamMessage;
@@ -5615,10 +5618,10 @@ async function execute(argv, io, dependencies, signal) {
5615
5618
  output.init();
5616
5619
  output.sink({ type: 'text-delta', delta: text });
5617
5620
  output.sink({ type: 'usage', usage: result.usage });
5618
- output.result(result, startedAt);
5621
+ output.result(result, startedAt, { localCommand: true });
5619
5622
  }
5620
5623
  else if (outputFormat === 'json' || invocation.legacyJson) {
5621
- writeJson(io, createSuccessResult(result, info, startedAt, 0));
5624
+ writeJson(io, createSuccessResult(result, info, startedAt, 0, { localCommand: true }));
5622
5625
  }
5623
5626
  else {
5624
5627
  io.stdout(`${text}\n`);
@@ -5663,6 +5666,20 @@ async function execute(argv, io, dependencies, signal) {
5663
5666
  ? (event) => {
5664
5667
  if (event.type === 'state' && event.state === 'awaiting-model') {
5665
5668
  jsonModelTurns += 1;
5669
+ if (jsonRequestAt === undefined)
5670
+ jsonRequestAt = Date.now();
5671
+ }
5672
+ if (event.type === 'terminal') {
5673
+ jsonTerminalReason = event.reason;
5674
+ }
5675
+ if ((event.type === 'text-delta' ||
5676
+ event.type === 'thinking-start' ||
5677
+ event.type === 'thinking-delta' ||
5678
+ event.type === 'thinking-signature-delta' ||
5679
+ event.type === 'thinking-stop' ||
5680
+ event.type === 'tool-call') &&
5681
+ jsonOutputAt === undefined) {
5682
+ jsonOutputAt = Date.now();
5666
5683
  }
5667
5684
  if (event.type === 'warning') {
5668
5685
  io.stderr(`Warning: ${redactSensitiveText(event.message, sensitiveEnvironmentValues(process.env))}\n`);
@@ -5816,8 +5833,12 @@ async function execute(argv, io, dependencies, signal) {
5816
5833
  currentTurnAbort = streamIterator ? turnAbort : undefined;
5817
5834
  const runSignal = streamIterator ? turnAbort.signal : signal;
5818
5835
  await ensureCostBaseline(activeSessionId);
5836
+ jsonModelTurns = 0;
5837
+ jsonRequestAt = undefined;
5838
+ jsonOutputAt = undefined;
5839
+ jsonTerminalReason = undefined;
5819
5840
  if (streamOutput) {
5820
- streamOutput.init();
5841
+ streamOutput.init(startedAt);
5821
5842
  if (isFirstTurn) {
5822
5843
  for (const event of pendingEvents)
5823
5844
  streamOutput.sink(event);
@@ -5902,10 +5923,20 @@ async function execute(argv, io, dependencies, signal) {
5902
5923
  throw error;
5903
5924
  }
5904
5925
  const message = redactSensitiveText(error instanceof Error ? error.message : String(error), sensitiveEnvironmentValues(process.env));
5926
+ const providerApiError = error instanceof ModelProviderError &&
5927
+ (error.status !== undefined || error.kind === 'api_error');
5928
+ const errorContext = providerApiError
5929
+ ? {
5930
+ providerApiError: true,
5931
+ apiErrorStatus: error instanceof ModelProviderError
5932
+ ? (error.status ?? null)
5933
+ : null,
5934
+ }
5935
+ : {};
5905
5936
  if (streamOutput)
5906
- streamOutput.error(message, startedAt);
5937
+ streamOutput.error(message, startedAt, errorContext);
5907
5938
  else if (outputFormat === 'json') {
5908
- writeJson(io, createErrorResult(message, activeSessionId, startedAt, jsonModelTurns));
5939
+ writeJson(io, createErrorResult(message, activeSessionId, startedAt, jsonModelTurns, errorContext));
5909
5940
  }
5910
5941
  else {
5911
5942
  if (currentTurnAbort === turnAbort)
@@ -5925,11 +5956,17 @@ async function execute(argv, io, dependencies, signal) {
5925
5956
  if (localCommand) {
5926
5957
  streamOutput.syntheticAssistant(result.text);
5927
5958
  }
5928
- streamOutput.result(result, startedAt);
5959
+ streamOutput.result(result, startedAt, { localCommand });
5929
5960
  }
5930
5961
  else if (outputFormat === 'json') {
5931
5962
  const resultRuntimeInfo = service.runtimeInfo?.() ?? runtimeInfo;
5932
- writeJson(io, createSuccessResult(result, resultRuntimeInfo, startedAt, localCommand ? 0 : Math.max(1, jsonModelTurns)));
5963
+ writeJson(io, createSuccessResult(result, resultRuntimeInfo, startedAt, localCommand ? 0 : Math.max(1, jsonModelTurns), {
5964
+ localCommand,
5965
+ ...(jsonTerminalReason === undefined
5966
+ ? {}
5967
+ : { stopReason: jsonTerminalReason }),
5968
+ ...projectProtocolTimings(startedAt, jsonRequestAt, jsonOutputAt),
5969
+ }));
5933
5970
  }
5934
5971
  else if (outputFormat !== 'text')
5935
5972
  writeJson(io, { type: 'result', ...result });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "praxis-agent",
3
- "version": "0.55.2",
3
+ "version": "0.55.3",
4
4
  "description": "Local-first, single-user general agent for the command line.",
5
5
  "license": "MIT",
6
6
  "author": "wuqisen",