praxis-agent 0.55.2 → 0.56.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 +3 -2
- package/dist/application/session-service.js +53 -14
- package/dist/application/subagent-service.js +1 -0
- package/dist/cli/interactive.js +2 -2
- package/dist/cli/process-signal.d.ts +3 -0
- package/dist/cli/process-signal.js +5 -0
- package/dist/cli/protocol.d.ts +30 -6
- package/dist/cli/protocol.js +166 -39
- package/dist/cli-runtime.js +57 -10
- package/dist/cli.js +4 -2
- package/dist/evals/eval-contract.js +9 -1
- package/dist/mcp/praxis-mcp-server.js +13 -1
- package/dist/permissions/claude-permission-resolver.js +147 -4
- package/dist/platform/atomic-write.js +2 -0
- package/dist/tools/apply-patch.d.ts +25 -0
- package/dist/tools/apply-patch.js +128 -0
- package/dist/tools/claude-interactive-tools.js +8 -0
- package/dist/tools/local-tools.d.ts +1 -0
- package/dist/tools/local-tools.js +102 -1
- package/dist/tools/team-member-tools.js +5 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -172,8 +172,9 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
|
|
|
172
172
|
deterministic resize-aware URL/form elicitation rendering, and measured
|
|
173
173
|
context budgets; print mode,
|
|
174
174
|
structured JSON/JSONL, context compaction, tool loops, and bounded execution.
|
|
175
|
-
- **Built-in tools** — read, write, edit,
|
|
176
|
-
|
|
175
|
+
- **Built-in tools** — read, write, edit, `ApplyPatch` for bounded ordered exact
|
|
176
|
+
multi-file replacements, glob, search, shell, notebook, PDF, image, web,
|
|
177
|
+
scheduled prompts, workflows, and worktrees.
|
|
177
178
|
- **Shell lifecycle** — foreground Bash allows up to 10 minutes and carries a
|
|
178
179
|
validated final working directory across calls in the same session without
|
|
179
180
|
leaking state across sessions or overriding an explicit `/cd`.
|
|
@@ -51,6 +51,7 @@ import { completeMeteredModelRequest } from './metered-model-completion.js';
|
|
|
51
51
|
import { SessionMemoryController, SessionMemoryStateError, SessionMemoryStore, } from './session-memory.js';
|
|
52
52
|
import { FilteredToolRegistry } from '../tools/filtered-tool-registry.js';
|
|
53
53
|
import { DeferredToolCatalog } from '../tools/deferred-tool-catalog.js';
|
|
54
|
+
import { parseApplyPatchInput } from '../tools/apply-patch.js';
|
|
54
55
|
import { ClaudeCapabilityToolRegistry, resolveClaudeToolCapabilities, } from '../tools/claude-capabilities.js';
|
|
55
56
|
import { generateToolUseSummary } from './tool-use-summary.js';
|
|
56
57
|
import { ClaudeUserMessageToolRegistry, } from '../tools/claude-user-message.js';
|
|
@@ -88,6 +89,21 @@ function mainAgentToolNames(tools, agent) {
|
|
|
88
89
|
.map(({ name }) => name)
|
|
89
90
|
.filter((name) => (!requested || requested.has(name)) && !disallowed.has(name));
|
|
90
91
|
}
|
|
92
|
+
function mutationPaths(call) {
|
|
93
|
+
if (call.name === 'ApplyPatch')
|
|
94
|
+
return parseApplyPatchInput(call.input).map((edit) => edit.file_path);
|
|
95
|
+
if (call.name === 'Write' || call.name === 'Edit') {
|
|
96
|
+
return typeof call.input.file_path === 'string'
|
|
97
|
+
? [call.input.file_path]
|
|
98
|
+
: [];
|
|
99
|
+
}
|
|
100
|
+
if (call.name === 'NotebookEdit') {
|
|
101
|
+
return typeof call.input.notebook_path === 'string'
|
|
102
|
+
? [call.input.notebook_path]
|
|
103
|
+
: [];
|
|
104
|
+
}
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
91
107
|
function isSessionCandidateError(error) {
|
|
92
108
|
return (error instanceof NativeTranscriptIndexCandidateError ||
|
|
93
109
|
['ENOENT', 'ENOTDIR', 'ELOOP'].includes(error.code ?? ''));
|
|
@@ -1168,6 +1184,7 @@ export class ClaudeSessionService {
|
|
|
1168
1184
|
'Read',
|
|
1169
1185
|
'LSP',
|
|
1170
1186
|
'Edit',
|
|
1187
|
+
'ApplyPatch',
|
|
1171
1188
|
'Write',
|
|
1172
1189
|
'NotebookEdit',
|
|
1173
1190
|
'WebFetch',
|
|
@@ -2869,16 +2886,18 @@ export class ClaudeSessionService {
|
|
|
2869
2886
|
}),
|
|
2870
2887
|
prepare: (call, context) => interactiveMessageTools.prepare(call, context),
|
|
2871
2888
|
execute: async (call, context) => {
|
|
2872
|
-
const
|
|
2873
|
-
|
|
2874
|
-
: call.name === 'NotebookEdit'
|
|
2875
|
-
? call.input.notebook_path
|
|
2876
|
-
: undefined;
|
|
2877
|
-
if (typeof path !== 'string') {
|
|
2889
|
+
const paths = mutationPaths(call);
|
|
2890
|
+
if (paths.length === 0) {
|
|
2878
2891
|
return interactiveMessageTools.execute(call, context);
|
|
2879
2892
|
}
|
|
2880
|
-
|
|
2881
|
-
|
|
2893
|
+
const historyPaths = call.name === 'Write' ||
|
|
2894
|
+
call.name === 'Edit' ||
|
|
2895
|
+
call.name === 'ApplyPatch'
|
|
2896
|
+
? (await Promise.all(paths.map(async (path) => (await this.options.interactiveTools?.isPlanFile(sessionId, path))
|
|
2897
|
+
? null
|
|
2898
|
+
: path))).filter((path) => path !== null)
|
|
2899
|
+
: paths;
|
|
2900
|
+
if (historyPaths.length === 0) {
|
|
2882
2901
|
return interactiveMessageTools.execute(call, context);
|
|
2883
2902
|
}
|
|
2884
2903
|
const snapshotMessageId = currentPromptId ??
|
|
@@ -2888,21 +2907,31 @@ export class ClaudeSessionService {
|
|
|
2888
2907
|
if (!snapshotMessageId || !assistantMessageId) {
|
|
2889
2908
|
throw new Error('Claude file history could not link tool call');
|
|
2890
2909
|
}
|
|
2891
|
-
const
|
|
2910
|
+
const preparedMutations = [];
|
|
2911
|
+
try {
|
|
2912
|
+
for (const path of [...new Set(historyPaths)])
|
|
2913
|
+
preparedMutations.push(await fileHistory.prepareMutation(projectionSnapshot().entries, snapshotMessageId, path));
|
|
2914
|
+
}
|
|
2915
|
+
catch (error) {
|
|
2916
|
+
await Promise.all(preparedMutations.map((mutation) => mutation.rollback()));
|
|
2917
|
+
throw error;
|
|
2918
|
+
}
|
|
2892
2919
|
let result;
|
|
2893
2920
|
try {
|
|
2894
2921
|
result = await interactiveMessageTools.execute(call, context);
|
|
2895
2922
|
}
|
|
2896
2923
|
catch (error) {
|
|
2897
|
-
await
|
|
2924
|
+
await Promise.all(preparedMutations.map((mutation) => mutation.rollback()));
|
|
2898
2925
|
throw error;
|
|
2899
2926
|
}
|
|
2900
2927
|
if (result.isError) {
|
|
2901
|
-
await
|
|
2928
|
+
await Promise.all(preparedMutations.map((mutation) => mutation.rollback()));
|
|
2902
2929
|
return result;
|
|
2903
2930
|
}
|
|
2904
|
-
const
|
|
2905
|
-
|
|
2931
|
+
const entries = preparedMutations
|
|
2932
|
+
.map((mutation) => mutation.commit(assistantMessageId))
|
|
2933
|
+
.filter((entry) => entry !== null);
|
|
2934
|
+
for (const entry of entries) {
|
|
2906
2935
|
await persistence.commit({
|
|
2907
2936
|
kind: 'messages',
|
|
2908
2937
|
input: {
|
|
@@ -3105,7 +3134,9 @@ export class ClaudeSessionService {
|
|
|
3105
3134
|
if (call.name === 'Read') {
|
|
3106
3135
|
turnMemory.recordRead(path);
|
|
3107
3136
|
}
|
|
3108
|
-
else if (call.name === 'Write' ||
|
|
3137
|
+
else if (call.name === 'Write' ||
|
|
3138
|
+
call.name === 'Edit' ||
|
|
3139
|
+
call.name === 'ApplyPatch') {
|
|
3109
3140
|
projectMemoryMaintained = true;
|
|
3110
3141
|
}
|
|
3111
3142
|
}
|
|
@@ -3117,6 +3148,14 @@ export class ClaudeSessionService {
|
|
|
3117
3148
|
}
|
|
3118
3149
|
}
|
|
3119
3150
|
}
|
|
3151
|
+
else if (call.name === 'ApplyPatch') {
|
|
3152
|
+
for (const accessedPath of toolResult.accessedPaths ?? []) {
|
|
3153
|
+
const resolvedAccessedPath = resolve(this.activeCwd(), accessedPath);
|
|
3154
|
+
if (isPathWithin(this.options.projectMemoryDirectory, resolvedAccessedPath)) {
|
|
3155
|
+
projectMemoryMaintained = true;
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3120
3159
|
}
|
|
3121
3160
|
if (toolResult.isError ||
|
|
3122
3161
|
call.name !== 'Read' ||
|
package/dist/cli/interactive.js
CHANGED
|
@@ -1885,7 +1885,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
1885
1885
|
}
|
|
1886
1886
|
break;
|
|
1887
1887
|
case 'tool-call':
|
|
1888
|
-
if (['Edit', 'Write', 'NotebookEdit'].includes(event.call.name))
|
|
1888
|
+
if (['Edit', 'Write', 'ApplyPatch', 'NotebookEdit'].includes(event.call.name))
|
|
1889
1889
|
turnMutatedFilesRef.current = true;
|
|
1890
1890
|
permissionCallsRef.current.set(event.call.id, event.call);
|
|
1891
1891
|
append({
|
|
@@ -4055,7 +4055,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
4055
4055
|
}
|
|
4056
4056
|
if (permission.kind === 'tool' &&
|
|
4057
4057
|
selected.action === 'allow-session-edits') {
|
|
4058
|
-
for (const rule of ['Write', 'Edit', 'NotebookEdit']) {
|
|
4058
|
+
for (const rule of ['Write', 'Edit', 'ApplyPatch', 'NotebookEdit']) {
|
|
4059
4059
|
if (!immediatePermissionRulesRef.current.includes(rule))
|
|
4060
4060
|
immediatePermissionRulesRef.current.push(rule);
|
|
4061
4061
|
}
|
package/dist/cli/protocol.d.ts
CHANGED
|
@@ -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
|
|
229
|
-
|
|
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;
|
|
@@ -300,6 +323,7 @@ export declare class StreamJsonOutput {
|
|
|
300
323
|
private finishPartial;
|
|
301
324
|
private finishPartialTail;
|
|
302
325
|
private flushAssistant;
|
|
326
|
+
private flushPartialOnly;
|
|
303
327
|
private finishTurn;
|
|
304
328
|
}
|
|
305
329
|
//# sourceMappingURL=protocol.d.ts.map
|
package/dist/cli/protocol.js
CHANGED
|
@@ -1,32 +1,74 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
export function
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
: {
|
|
11
|
-
...(
|
|
6
|
+
: { timeToRequestMs: requestAt - startedAt }),
|
|
7
|
+
...(outputAt === undefined
|
|
12
8
|
? {}
|
|
13
9
|
: {
|
|
14
|
-
|
|
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
|
-
|
|
23
|
-
?
|
|
24
|
-
:
|
|
25
|
-
|
|
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 ??
|
|
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 ??
|
|
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:
|
|
139
|
+
duration_api_ms: 0,
|
|
74
140
|
num_turns: modelTurns,
|
|
75
141
|
stop_reason: null,
|
|
76
142
|
session_id: sessionId,
|
|
77
|
-
total_cost_usd:
|
|
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
|
-
|
|
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,16 @@ export class StreamJsonOutput {
|
|
|
1841
1929
|
event.state === 'completed' ||
|
|
1842
1930
|
event.state === 'cancelled' ||
|
|
1843
1931
|
event.state === 'failed') {
|
|
1844
|
-
|
|
1932
|
+
const shouldFlush = event.state === 'cancelled'
|
|
1933
|
+
? this.turnText.length > 0 ||
|
|
1934
|
+
this.turnThinking.length > 0 ||
|
|
1935
|
+
this.turnCalls.length > 0
|
|
1936
|
+
: event.state !== 'failed' ||
|
|
1937
|
+
this.pendingFailureMessage === undefined;
|
|
1938
|
+
if (shouldFlush)
|
|
1939
|
+
this.flushAssistant();
|
|
1940
|
+
else if (event.state === 'cancelled')
|
|
1941
|
+
this.flushPartialOnly();
|
|
1845
1942
|
if (event.state === 'awaiting-permission')
|
|
1846
1943
|
this.writeSessionState('requires_action');
|
|
1847
1944
|
}
|
|
@@ -1849,12 +1946,14 @@ export class StreamJsonOutput {
|
|
|
1849
1946
|
}
|
|
1850
1947
|
if (event.type === 'text-delta') {
|
|
1851
1948
|
this.ensureTurn();
|
|
1949
|
+
this.observeOutput();
|
|
1852
1950
|
this.turnText += event.delta;
|
|
1853
1951
|
this.partialText(event.delta);
|
|
1854
1952
|
return;
|
|
1855
1953
|
}
|
|
1856
1954
|
if (event.type === 'thinking-start') {
|
|
1857
1955
|
this.ensureTurn();
|
|
1956
|
+
this.observeOutput();
|
|
1858
1957
|
if (this.includePartialMessages) {
|
|
1859
1958
|
if (this.activeThinkingIndex !== undefined) {
|
|
1860
1959
|
throw new Error('Thinking content blocks cannot overlap');
|
|
@@ -1885,6 +1984,7 @@ export class StreamJsonOutput {
|
|
|
1885
1984
|
if (event.type === 'thinking-delta' ||
|
|
1886
1985
|
event.type === 'thinking-signature-delta') {
|
|
1887
1986
|
this.ensureTurn();
|
|
1987
|
+
this.observeOutput();
|
|
1888
1988
|
if (this.includePartialMessages) {
|
|
1889
1989
|
if (this.activeThinkingIndex === undefined) {
|
|
1890
1990
|
throw new Error('Thinking delta arrived without an active block');
|
|
@@ -1906,6 +2006,7 @@ export class StreamJsonOutput {
|
|
|
1906
2006
|
}
|
|
1907
2007
|
if (event.type === 'thinking-stop') {
|
|
1908
2008
|
this.ensureTurn();
|
|
2009
|
+
this.observeOutput();
|
|
1909
2010
|
this.turnThinking.push(event.block);
|
|
1910
2011
|
if (this.includePartialMessages) {
|
|
1911
2012
|
if (this.activeThinkingIndex === undefined) {
|
|
@@ -1930,6 +2031,7 @@ export class StreamJsonOutput {
|
|
|
1930
2031
|
}
|
|
1931
2032
|
if (event.type === 'tool-call') {
|
|
1932
2033
|
this.ensureTurn();
|
|
2034
|
+
this.observeOutput();
|
|
1933
2035
|
this.turnCalls.push(event.call);
|
|
1934
2036
|
if (this.includePartialMessages) {
|
|
1935
2037
|
const index = this.nextContentIndex++;
|
|
@@ -2165,13 +2267,16 @@ export class StreamJsonOutput {
|
|
|
2165
2267
|
}
|
|
2166
2268
|
if (event.type === 'failed') {
|
|
2167
2269
|
this.ensureTurn();
|
|
2168
|
-
if (this.
|
|
2270
|
+
if (!this.turnHasRealContent &&
|
|
2271
|
+
this.turnText.length === 0 &&
|
|
2169
2272
|
this.turnThinking.length === 0 &&
|
|
2170
2273
|
this.turnCalls.length === 0) {
|
|
2171
|
-
this.
|
|
2172
|
-
this.
|
|
2274
|
+
this.pendingFailureMessage = event.message;
|
|
2275
|
+
this.assistantFlushed = false;
|
|
2276
|
+
}
|
|
2277
|
+
else {
|
|
2278
|
+
this.flushAssistant();
|
|
2173
2279
|
}
|
|
2174
|
-
this.flushAssistant();
|
|
2175
2280
|
}
|
|
2176
2281
|
}
|
|
2177
2282
|
writeSessionState(state) {
|
|
@@ -2204,6 +2309,11 @@ export class StreamJsonOutput {
|
|
|
2204
2309
|
if (!this.turnActive)
|
|
2205
2310
|
this.startTurn();
|
|
2206
2311
|
}
|
|
2312
|
+
observeOutput() {
|
|
2313
|
+
this.turnHasRealContent = true;
|
|
2314
|
+
if (this.firstOutputAt === undefined)
|
|
2315
|
+
this.firstOutputAt = Date.now();
|
|
2316
|
+
}
|
|
2207
2317
|
discardTurn(reason) {
|
|
2208
2318
|
this.write({
|
|
2209
2319
|
type: 'system',
|
|
@@ -2225,6 +2335,8 @@ export class StreamJsonOutput {
|
|
|
2225
2335
|
this.nextContentIndex = 0;
|
|
2226
2336
|
this.partialEvents = [];
|
|
2227
2337
|
this.pendingPartialStop = undefined;
|
|
2338
|
+
this.turnHasRealContent = false;
|
|
2339
|
+
this.pendingFailureMessage = undefined;
|
|
2228
2340
|
}
|
|
2229
2341
|
startTurn() {
|
|
2230
2342
|
if (this.turnActive)
|
|
@@ -2243,6 +2355,9 @@ export class StreamJsonOutput {
|
|
|
2243
2355
|
this.nextContentIndex = 0;
|
|
2244
2356
|
this.partialEvents = [];
|
|
2245
2357
|
this.pendingPartialStop = undefined;
|
|
2358
|
+
this.turnHasRealContent = false;
|
|
2359
|
+
if (this.firstRequestAt === undefined)
|
|
2360
|
+
this.firstRequestAt = Date.now();
|
|
2246
2361
|
this.modelTurns += 1;
|
|
2247
2362
|
if (this.includePartialMessages) {
|
|
2248
2363
|
this.write({
|
|
@@ -2384,6 +2499,18 @@ export class StreamJsonOutput {
|
|
|
2384
2499
|
});
|
|
2385
2500
|
this.finishPartialTail();
|
|
2386
2501
|
}
|
|
2502
|
+
flushPartialOnly() {
|
|
2503
|
+
if (!this.includePartialMessages ||
|
|
2504
|
+
this.assistantFlushed ||
|
|
2505
|
+
this.partialEvents.length === 0)
|
|
2506
|
+
return;
|
|
2507
|
+
this.assistantFlushed = true;
|
|
2508
|
+
this.writePartialMessageStart();
|
|
2509
|
+
for (const event of this.partialEvents)
|
|
2510
|
+
this.write(event);
|
|
2511
|
+
this.partialEvents = [];
|
|
2512
|
+
this.pendingPartialStop = undefined;
|
|
2513
|
+
}
|
|
2387
2514
|
finishTurn() {
|
|
2388
2515
|
this.flushAssistant();
|
|
2389
2516
|
this.turnActive = false;
|