@parall/claude-agent 1.50.1 → 1.52.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/dist/dispatch.d.ts +23 -16
- package/dist/dispatch.d.ts.map +1 -1
- package/dist/dispatch.js +297 -182
- package/dist/input-lifecycle.d.ts +39 -0
- package/dist/input-lifecycle.d.ts.map +1 -0
- package/dist/input-lifecycle.js +97 -0
- package/dist/output-parser.d.ts +37 -2
- package/dist/output-parser.d.ts.map +1 -1
- package/dist/output-parser.js +94 -3
- package/dist/turn-outcome.d.ts +10 -0
- package/dist/turn-outcome.d.ts.map +1 -0
- package/dist/turn-outcome.js +173 -0
- package/package.json +4 -4
- package/src/dispatch.ts +316 -189
- package/src/input-lifecycle.ts +137 -0
- package/src/output-parser.ts +137 -5
- package/src/turn-outcome.ts +177 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { DispatchInputLifecycle, GatewayLogger, RuntimeInputState } from '@parall/agent-core';
|
|
2
|
+
export type ClaudeInputDelivery = {
|
|
3
|
+
deliveryKey: string;
|
|
4
|
+
commandUuid: string;
|
|
5
|
+
lifecycle?: DispatchInputLifecycle;
|
|
6
|
+
injected: boolean;
|
|
7
|
+
drained: boolean;
|
|
8
|
+
terminal?: 'completed' | 'failed' | 'settled';
|
|
9
|
+
reportedState?: RuntimeInputState;
|
|
10
|
+
resultFailed: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* The turn settled as usage_limit: the lane-level deferred complete will
|
|
13
|
+
* re-deliver this input at retry_at without burning redrive budget. A
|
|
14
|
+
* failed-input report here would trigger the immediate failed-input
|
|
15
|
+
* redrive straight back into the still-choked LLM
|
|
16
|
+
* (agent-turn-outcome-design.md §6).
|
|
17
|
+
*/
|
|
18
|
+
suppressFailReport?: boolean;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Per-process registry that maps Parall WorkItem batches to Claude stdin
|
|
22
|
+
* UUIDs. It owns lifecycle transition rules; process/stdout orchestration
|
|
23
|
+
* stays in ClaudeCodeAdapter.
|
|
24
|
+
*/
|
|
25
|
+
export declare class ClaudeInputRegistry {
|
|
26
|
+
private readonly byKey;
|
|
27
|
+
private readonly byCommand;
|
|
28
|
+
getByKey(deliveryKey: string): ClaudeInputDelivery | undefined;
|
|
29
|
+
getByCommand(commandUuid: string): ClaudeInputDelivery | undefined;
|
|
30
|
+
values(): IterableIterator<ClaudeInputDelivery>;
|
|
31
|
+
hasPendingInjections(): boolean;
|
|
32
|
+
register(deliveryKey: string, lifecycle: DispatchInputLifecycle | undefined, injected: boolean): ClaudeInputDelivery;
|
|
33
|
+
remove(delivery: ClaudeInputDelivery): void;
|
|
34
|
+
apply(delivery: ClaudeInputDelivery, state: 'queued' | 'started' | 'completed' | 'cancelled' | 'discarded'): Promise<void>;
|
|
35
|
+
fail(delivery: ClaudeInputDelivery): Promise<void>;
|
|
36
|
+
failBestEffort(delivery: ClaudeInputDelivery, log?: GatewayLogger): Promise<void>;
|
|
37
|
+
failAllBestEffort(log?: GatewayLogger): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=input-lifecycle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"input-lifecycle.d.ts","sourceRoot":"","sources":["../src/input-lifecycle.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAEnG,MAAM,MAAM,mBAAmB,GAAG;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,sBAAsB,CAAC;IACnC,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,WAAW,GAAG,QAAQ,GAAG,SAAS,CAAC;IAC9C,aAAa,CAAC,EAAE,iBAAiB,CAAC;IAClC,YAAY,EAAE,OAAO,CAAC;IACtB;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B,CAAC;AAEF;;;;GAIG;AACH,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA0C;IAChE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA0C;IAEpE,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS;IAI9D,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS;IAIlE,MAAM,IAAI,gBAAgB,CAAC,mBAAmB,CAAC;IAI/C,oBAAoB,IAAI,OAAO;IAI/B,QAAQ,CACN,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,sBAAsB,GAAG,SAAS,EAC7C,QAAQ,EAAE,OAAO,GAChB,mBAAmB;IAiBtB,MAAM,CAAC,QAAQ,EAAE,mBAAmB,GAAG,IAAI;IASrC,KAAK,CACT,QAAQ,EAAE,mBAAmB,EAC7B,KAAK,EAAE,QAAQ,GAAG,SAAS,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,GACpE,OAAO,CAAC,IAAI,CAAC;IAsBV,IAAI,CAAC,QAAQ,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBlD,cAAc,CAAC,QAAQ,EAAE,mBAAmB,EAAE,GAAG,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAUjF,iBAAiB,CAAC,GAAG,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;CAO5D"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
/**
|
|
3
|
+
* Per-process registry that maps Parall WorkItem batches to Claude stdin
|
|
4
|
+
* UUIDs. It owns lifecycle transition rules; process/stdout orchestration
|
|
5
|
+
* stays in ClaudeCodeAdapter.
|
|
6
|
+
*/
|
|
7
|
+
export class ClaudeInputRegistry {
|
|
8
|
+
byKey = new Map();
|
|
9
|
+
byCommand = new Map();
|
|
10
|
+
getByKey(deliveryKey) {
|
|
11
|
+
return this.byKey.get(deliveryKey);
|
|
12
|
+
}
|
|
13
|
+
getByCommand(commandUuid) {
|
|
14
|
+
return this.byCommand.get(commandUuid);
|
|
15
|
+
}
|
|
16
|
+
values() {
|
|
17
|
+
return this.byKey.values();
|
|
18
|
+
}
|
|
19
|
+
hasPendingInjections() {
|
|
20
|
+
return [...this.byKey.values()].some((delivery) => delivery.injected && !delivery.drained);
|
|
21
|
+
}
|
|
22
|
+
register(deliveryKey, lifecycle, injected) {
|
|
23
|
+
if (this.byKey.has(deliveryKey)) {
|
|
24
|
+
throw new Error(`duplicate Claude delivery key ${deliveryKey}`);
|
|
25
|
+
}
|
|
26
|
+
const delivery = {
|
|
27
|
+
deliveryKey,
|
|
28
|
+
commandUuid: randomUUID(),
|
|
29
|
+
lifecycle,
|
|
30
|
+
injected,
|
|
31
|
+
drained: !injected,
|
|
32
|
+
resultFailed: false,
|
|
33
|
+
};
|
|
34
|
+
this.byKey.set(deliveryKey, delivery);
|
|
35
|
+
this.byCommand.set(delivery.commandUuid, delivery);
|
|
36
|
+
return delivery;
|
|
37
|
+
}
|
|
38
|
+
remove(delivery) {
|
|
39
|
+
if (this.byKey.get(delivery.deliveryKey) === delivery) {
|
|
40
|
+
this.byKey.delete(delivery.deliveryKey);
|
|
41
|
+
}
|
|
42
|
+
if (this.byCommand.get(delivery.commandUuid) === delivery) {
|
|
43
|
+
this.byCommand.delete(delivery.commandUuid);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async apply(delivery, state) {
|
|
47
|
+
if (delivery.terminal || state === 'queued')
|
|
48
|
+
return;
|
|
49
|
+
if (state === 'started') {
|
|
50
|
+
if (delivery.reportedState)
|
|
51
|
+
return;
|
|
52
|
+
await delivery.lifecycle?.update('started');
|
|
53
|
+
delivery.reportedState = 'started';
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (state === 'cancelled' || state === 'discarded' || delivery.resultFailed) {
|
|
57
|
+
await this.fail(delivery);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (delivery.reportedState !== 'completed') {
|
|
61
|
+
await delivery.lifecycle?.update('completed');
|
|
62
|
+
delivery.reportedState = 'completed';
|
|
63
|
+
}
|
|
64
|
+
delivery.terminal = 'completed';
|
|
65
|
+
}
|
|
66
|
+
async fail(delivery) {
|
|
67
|
+
if (delivery.terminal)
|
|
68
|
+
return;
|
|
69
|
+
if (delivery.suppressFailReport) {
|
|
70
|
+
delivery.terminal = 'settled';
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
if (delivery.reportedState !== 'failed') {
|
|
75
|
+
const result = await delivery.lifecycle?.update('failed');
|
|
76
|
+
delivery.reportedState = 'failed';
|
|
77
|
+
delivery.terminal = result?.retry === false ? 'settled' : 'failed';
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
delivery.terminal ??= 'failed';
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async failBestEffort(delivery, log) {
|
|
85
|
+
try {
|
|
86
|
+
await this.fail(delivery);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
log?.warn?.(`failed to report Claude input ${delivery.commandUuid} as failed: ${String(err)}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async failAllBestEffort(log) {
|
|
93
|
+
await Promise.all([...this.byKey.values()]
|
|
94
|
+
.filter((delivery) => !delivery.terminal)
|
|
95
|
+
.map((delivery) => this.failBestEffort(delivery, log)));
|
|
96
|
+
}
|
|
97
|
+
}
|
package/dist/output-parser.d.ts
CHANGED
|
@@ -1,13 +1,48 @@
|
|
|
1
1
|
import type { Readable } from 'node:stream';
|
|
2
2
|
import type { RuntimeEvent } from '@parall/agent-core';
|
|
3
|
+
/**
|
|
4
|
+
* Structured snapshot of the `result` frame's LLM-layer discriminators and
|
|
5
|
+
* accounting — the raw material for turn-outcome classification
|
|
6
|
+
* (agent-turn-outcome-design.md §4.1). Every field is optional: older CLIs
|
|
7
|
+
* omit some, and classification degrades gracefully.
|
|
8
|
+
*/
|
|
9
|
+
export type ClaudeResultMeta = {
|
|
10
|
+
subtype?: string;
|
|
11
|
+
terminalReason?: string;
|
|
12
|
+
apiErrorStatus?: number;
|
|
13
|
+
stopReason?: string;
|
|
14
|
+
/** The frame's `result` text — synthetic failure notices land here. */
|
|
15
|
+
resultText?: string;
|
|
16
|
+
isError: boolean;
|
|
17
|
+
numTurns?: number;
|
|
18
|
+
durationMs?: number;
|
|
19
|
+
durationApiMs?: number;
|
|
20
|
+
totalCostUsd?: number;
|
|
21
|
+
inputTokens?: number;
|
|
22
|
+
outputTokens?: number;
|
|
23
|
+
cacheReadTokens?: number;
|
|
24
|
+
cacheCreationTokens?: number;
|
|
25
|
+
/** First model key of `modelUsage` — the model the CLI actually used. */
|
|
26
|
+
model?: string;
|
|
27
|
+
};
|
|
3
28
|
export type ClaudeParsedEvent = RuntimeEvent | {
|
|
4
|
-
type: '
|
|
5
|
-
sessionId
|
|
29
|
+
type: 'runtime_init';
|
|
30
|
+
sessionId?: string;
|
|
31
|
+
capabilities: string[];
|
|
32
|
+
} | {
|
|
33
|
+
type: 'command_lifecycle';
|
|
34
|
+
commandUuid: string;
|
|
35
|
+
state: 'queued' | 'started' | 'completed' | 'cancelled' | 'discarded';
|
|
36
|
+
} | {
|
|
37
|
+
type: 'assistant_error';
|
|
38
|
+
message: string;
|
|
6
39
|
} | {
|
|
7
40
|
type: 'turn_end';
|
|
8
41
|
sessionId?: string;
|
|
42
|
+
userMessageUuid?: string;
|
|
9
43
|
isError: boolean;
|
|
10
44
|
numTurns?: number;
|
|
45
|
+
resultMeta?: ClaudeResultMeta;
|
|
11
46
|
};
|
|
12
47
|
export declare function parseClaudeStreamJson(readable: Readable): AsyncGenerator<ClaudeParsedEvent>;
|
|
13
48
|
//# sourceMappingURL=output-parser.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"output-parser.d.ts","sourceRoot":"","sources":["../src/output-parser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD,MAAM,MAAM,iBAAiB,GACzB,YAAY,GACZ;IAAE,IAAI,EAAE,
|
|
1
|
+
{"version":3,"file":"output-parser.d.ts","sourceRoot":"","sources":["../src/output-parser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,yEAAyE;IACzE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GACzB,YAAY,GACZ;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,EAAE,CAAA;CAAE,GACpE;IACE,IAAI,EAAE,mBAAmB,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,QAAQ,GAAG,SAAS,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,CAAC;CACvE,GAID;IAAE,IAAI,EAAE,iBAAiB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAqB5C;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,gBAAgB,CAAC;CAC/B,CAAC;AAgFN,wBAAuB,qBAAqB,CAC1C,QAAQ,EAAE,QAAQ,GACjB,cAAc,CAAC,iBAAiB,CAAC,CA4KnC"}
|
package/dist/output-parser.js
CHANGED
|
@@ -93,13 +93,46 @@ export async function* parseClaudeStreamJson(readable) {
|
|
|
93
93
|
const eventRecord = event;
|
|
94
94
|
if (eventRecord.type === 'system' && eventRecord.subtype === 'init') {
|
|
95
95
|
const sessionId = asTrimmedString(eventRecord.session_id);
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
const capabilities = Array.isArray(eventRecord.capabilities)
|
|
97
|
+
? eventRecord.capabilities
|
|
98
|
+
.map((capability) => asTrimmedString(capability))
|
|
99
|
+
.filter((capability) => Boolean(capability))
|
|
100
|
+
: [];
|
|
101
|
+
yield {
|
|
102
|
+
type: 'runtime_init',
|
|
103
|
+
...(sessionId ? { sessionId } : {}),
|
|
104
|
+
capabilities,
|
|
105
|
+
};
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (eventRecord.type === 'command_lifecycle') {
|
|
109
|
+
const commandUuid = asTrimmedString(eventRecord.command_uuid);
|
|
110
|
+
const state = asTrimmedString(eventRecord.state);
|
|
111
|
+
if (commandUuid &&
|
|
112
|
+
(state === 'queued' ||
|
|
113
|
+
state === 'started' ||
|
|
114
|
+
state === 'completed' ||
|
|
115
|
+
state === 'cancelled' ||
|
|
116
|
+
state === 'discarded')) {
|
|
117
|
+
yield { type: 'command_lifecycle', commandUuid, state };
|
|
98
118
|
}
|
|
99
119
|
continue;
|
|
100
120
|
}
|
|
101
121
|
if (eventRecord.type === 'assistant') {
|
|
102
|
-
if (eventRecord.error)
|
|
122
|
+
if (eventRecord.error) {
|
|
123
|
+
// Not a model message — surface the notice text for turn-outcome
|
|
124
|
+
// classification instead of silently dropping the frame.
|
|
125
|
+
const message = asTrimmedString(eventRecord.error) ??
|
|
126
|
+
asTrimmedString(eventRecord.error?.message);
|
|
127
|
+
if (message)
|
|
128
|
+
yield { type: 'assistant_error', message };
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
// Verbose stream-json includes nested subagent frames. Their final
|
|
132
|
+
// tool_result returns through the root conversation; projecting their
|
|
133
|
+
// internal assistant/tool stream here would attribute background work
|
|
134
|
+
// to whichever root input currently owns stdout.
|
|
135
|
+
if (asTrimmedString(eventRecord.parent_tool_use_id))
|
|
103
136
|
continue;
|
|
104
137
|
const message = eventRecord.message;
|
|
105
138
|
const content = message && typeof message === 'object'
|
|
@@ -147,6 +180,8 @@ export async function* parseClaudeStreamJson(readable) {
|
|
|
147
180
|
continue;
|
|
148
181
|
}
|
|
149
182
|
if (eventRecord.type === 'user') {
|
|
183
|
+
if (asTrimmedString(eventRecord.parent_tool_use_id))
|
|
184
|
+
continue;
|
|
150
185
|
const message = eventRecord.message;
|
|
151
186
|
const content = message && typeof message === 'object'
|
|
152
187
|
? message.content
|
|
@@ -191,12 +226,68 @@ export async function* parseClaudeStreamJson(readable) {
|
|
|
191
226
|
// the map grow unbounded or mislabel durations on id collisions.
|
|
192
227
|
toolUses.clear();
|
|
193
228
|
const numTurns = asFiniteNumber(eventRecord.num_turns);
|
|
229
|
+
const userMessageUuid = asTrimmedString(eventRecord.user_message_uuid) || asTrimmedString(eventRecord.command_uuid);
|
|
194
230
|
yield {
|
|
195
231
|
type: 'turn_end',
|
|
196
232
|
sessionId: asTrimmedString(eventRecord.session_id),
|
|
233
|
+
...(userMessageUuid ? { userMessageUuid } : {}),
|
|
197
234
|
isError,
|
|
198
235
|
...(numTurns !== undefined ? { numTurns } : {}),
|
|
236
|
+
resultMeta: extractResultMeta(eventRecord, isError, numTurns),
|
|
199
237
|
};
|
|
200
238
|
}
|
|
201
239
|
}
|
|
202
240
|
}
|
|
241
|
+
/** Snapshot the result frame's discriminator + usage fields (all optional). */
|
|
242
|
+
function extractResultMeta(frame, isError, numTurns) {
|
|
243
|
+
const usage = frame.usage && typeof frame.usage === 'object'
|
|
244
|
+
? frame.usage
|
|
245
|
+
: undefined;
|
|
246
|
+
const modelUsage = frame.modelUsage && typeof frame.modelUsage === 'object'
|
|
247
|
+
? Object.keys(frame.modelUsage)
|
|
248
|
+
: [];
|
|
249
|
+
const meta = { isError };
|
|
250
|
+
if (numTurns !== undefined)
|
|
251
|
+
meta.numTurns = numTurns;
|
|
252
|
+
const subtype = asTrimmedString(frame.subtype);
|
|
253
|
+
if (subtype)
|
|
254
|
+
meta.subtype = subtype;
|
|
255
|
+
const terminalReason = asTrimmedString(frame.terminal_reason);
|
|
256
|
+
if (terminalReason)
|
|
257
|
+
meta.terminalReason = terminalReason;
|
|
258
|
+
const apiErrorStatus = asFiniteNumber(frame.api_error_status);
|
|
259
|
+
if (apiErrorStatus !== undefined)
|
|
260
|
+
meta.apiErrorStatus = apiErrorStatus;
|
|
261
|
+
const stopReason = asTrimmedString(frame.stop_reason);
|
|
262
|
+
if (stopReason)
|
|
263
|
+
meta.stopReason = stopReason;
|
|
264
|
+
const resultText = asTrimmedString(frame.result);
|
|
265
|
+
if (resultText)
|
|
266
|
+
meta.resultText = resultText;
|
|
267
|
+
const durationMs = asFiniteNumber(frame.duration_ms);
|
|
268
|
+
if (durationMs !== undefined)
|
|
269
|
+
meta.durationMs = durationMs;
|
|
270
|
+
const durationApiMs = asFiniteNumber(frame.duration_api_ms);
|
|
271
|
+
if (durationApiMs !== undefined)
|
|
272
|
+
meta.durationApiMs = durationApiMs;
|
|
273
|
+
const totalCostUsd = asFiniteNumber(frame.total_cost_usd);
|
|
274
|
+
if (totalCostUsd !== undefined)
|
|
275
|
+
meta.totalCostUsd = totalCostUsd;
|
|
276
|
+
if (usage) {
|
|
277
|
+
const inputTokens = asFiniteNumber(usage.input_tokens);
|
|
278
|
+
if (inputTokens !== undefined)
|
|
279
|
+
meta.inputTokens = inputTokens;
|
|
280
|
+
const outputTokens = asFiniteNumber(usage.output_tokens);
|
|
281
|
+
if (outputTokens !== undefined)
|
|
282
|
+
meta.outputTokens = outputTokens;
|
|
283
|
+
const cacheReadTokens = asFiniteNumber(usage.cache_read_input_tokens);
|
|
284
|
+
if (cacheReadTokens !== undefined)
|
|
285
|
+
meta.cacheReadTokens = cacheReadTokens;
|
|
286
|
+
const cacheCreationTokens = asFiniteNumber(usage.cache_creation_input_tokens);
|
|
287
|
+
if (cacheCreationTokens !== undefined)
|
|
288
|
+
meta.cacheCreationTokens = cacheCreationTokens;
|
|
289
|
+
}
|
|
290
|
+
if (modelUsage.length > 0)
|
|
291
|
+
meta.model = modelUsage[0];
|
|
292
|
+
return meta;
|
|
293
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { TurnOutcomeEvent } from '@parall/agent-core';
|
|
2
|
+
import type { ClaudeResultMeta } from './output-parser.js';
|
|
3
|
+
export declare function parseClaudeResetTime(text: string, now: Date): string | undefined;
|
|
4
|
+
/**
|
|
5
|
+
* Classify one finished Claude turn from its result-frame snapshot plus any
|
|
6
|
+
* synthetic notice texts observed during the turn (assistant_error frames,
|
|
7
|
+
* zero-usage assistant text like the session-limit notice).
|
|
8
|
+
*/
|
|
9
|
+
export declare function classifyClaudeTurn(meta: ClaudeResultMeta | undefined, noticeTexts: string[], now?: Date): TurnOutcomeEvent;
|
|
10
|
+
//# sourceMappingURL=turn-outcome.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turn-outcome.d.ts","sourceRoot":"","sources":["../src/turn-outcome.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAa,MAAM,oBAAoB,CAAC;AACtE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAwC3D,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,GAAG,MAAM,GAAG,SAAS,CA8ChF;AAcD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,gBAAgB,GAAG,SAAS,EAClC,WAAW,EAAE,MAAM,EAAE,EACrB,GAAG,GAAE,IAAiB,GACrB,gBAAgB,CAkElB"}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude turn-outcome classifier (agent-turn-outcome-design.md §4.1).
|
|
3
|
+
*
|
|
4
|
+
* Detection order is a hard rule: structured fields first
|
|
5
|
+
* (api_error_status / terminal_reason / is_error), behavioral heuristics
|
|
6
|
+
* second (zero-usage synthetic turns), message regex last — and regex only
|
|
7
|
+
* ever refines a classification, so a CLI wording change degrades to the
|
|
8
|
+
* generic `api_error`, never to a wrong `ok` or a wrong deferral.
|
|
9
|
+
* Classification uncertainty must never defer: deferring a real message is
|
|
10
|
+
* costlier than losing one automatic recovery.
|
|
11
|
+
*/
|
|
12
|
+
const LIMIT_TEXT = /you'?ve (hit|reached) your .*limit|usage limit reached|weekly limit/i;
|
|
13
|
+
const AUTH_TEXT = /not logged in|please run \/login|authentication_error|invalid api key|oauth token (has )?expired|\[action required\]/i;
|
|
14
|
+
const CONTEXT_TEXT = /prompt is too long|context (window|length) exceeded|request too large/i;
|
|
15
|
+
const DETAIL_MAX = 500;
|
|
16
|
+
/**
|
|
17
|
+
* Parse the reset time out of a limit notice like
|
|
18
|
+
* `You've hit your session limit · resets 1:20am (Asia/Shanghai)` /
|
|
19
|
+
* `… resets at 11pm (UTC)`. Returns the NEXT occurrence of that wall-clock
|
|
20
|
+
* time in the given zone as an ISO string, or undefined when the wording
|
|
21
|
+
* doesn't match — the server then applies its default deferral window.
|
|
22
|
+
*/
|
|
23
|
+
function minutesOfDayInZone(fmt, at) {
|
|
24
|
+
const parts = Object.fromEntries(fmt
|
|
25
|
+
.formatToParts(at)
|
|
26
|
+
.filter((p) => p.type !== 'literal')
|
|
27
|
+
.map((p) => [p.type, Number(p.value)]));
|
|
28
|
+
// "24" can appear for midnight under hour12:false on some ICU versions.
|
|
29
|
+
const hour = parts.hour === 24 ? 0 : parts.hour;
|
|
30
|
+
return hour * 60 + parts.minute;
|
|
31
|
+
}
|
|
32
|
+
export function parseClaudeResetTime(text, now) {
|
|
33
|
+
const m = /resets\s+(?:at\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\s*\(([^)]+)\)/i.exec(text);
|
|
34
|
+
if (!m)
|
|
35
|
+
return undefined;
|
|
36
|
+
let hour = Number(m[1]);
|
|
37
|
+
const minute = m[2] ? Number(m[2]) : 0;
|
|
38
|
+
const meridiem = m[3]?.toLowerCase();
|
|
39
|
+
const timeZone = m[4].trim();
|
|
40
|
+
if (!Number.isFinite(hour) || hour > 23 || minute > 59)
|
|
41
|
+
return undefined;
|
|
42
|
+
if (meridiem === 'pm' && hour < 12)
|
|
43
|
+
hour += 12;
|
|
44
|
+
if (meridiem === 'am' && hour === 12)
|
|
45
|
+
hour = 0;
|
|
46
|
+
// Intl throws on unknown zone names — treat that as no parse.
|
|
47
|
+
let fmt;
|
|
48
|
+
let nowMinutesOfDay;
|
|
49
|
+
try {
|
|
50
|
+
fmt = new Intl.DateTimeFormat('en-US', {
|
|
51
|
+
timeZone,
|
|
52
|
+
hour12: false,
|
|
53
|
+
hour: '2-digit',
|
|
54
|
+
minute: '2-digit',
|
|
55
|
+
});
|
|
56
|
+
nowMinutesOfDay = minutesOfDayInZone(fmt, now);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
const targetMinutesOfDay = hour * 60 + minute;
|
|
62
|
+
let deltaMinutes = targetMinutesOfDay - nowMinutesOfDay;
|
|
63
|
+
if (deltaMinutes <= 0)
|
|
64
|
+
deltaMinutes += 24 * 60;
|
|
65
|
+
// Anchor on the real instant (drop seconds so the result lands exactly on
|
|
66
|
+
// the notice's minute), advance by the wall-clock delta, then CORRECT for
|
|
67
|
+
// any DST transition inside the interval: wall-clock minutes ≠ elapsed UTC
|
|
68
|
+
// minutes across a shift, so re-read the candidate's wall-clock in the
|
|
69
|
+
// zone and nudge by the difference (two rounds absorb a single transition;
|
|
70
|
+
// a spring-forward nonexistent time settles within an hour, which the
|
|
71
|
+
// server-side clamp tolerates).
|
|
72
|
+
const anchored = now.getTime() - (now.getSeconds() * 1000 + now.getMilliseconds());
|
|
73
|
+
let target = anchored + deltaMinutes * 60_000;
|
|
74
|
+
for (let i = 0; i < 2; i++) {
|
|
75
|
+
let diff = targetMinutesOfDay - minutesOfDayInZone(fmt, new Date(target));
|
|
76
|
+
if (diff > 720)
|
|
77
|
+
diff -= 1440;
|
|
78
|
+
if (diff < -720)
|
|
79
|
+
diff += 1440;
|
|
80
|
+
if (diff === 0)
|
|
81
|
+
break;
|
|
82
|
+
target += diff * 60_000;
|
|
83
|
+
}
|
|
84
|
+
if (target <= now.getTime())
|
|
85
|
+
target += 24 * 60 * 60_000;
|
|
86
|
+
return new Date(target).toISOString();
|
|
87
|
+
}
|
|
88
|
+
function usageOf(meta) {
|
|
89
|
+
const usage = {};
|
|
90
|
+
if (meta.inputTokens !== undefined)
|
|
91
|
+
usage.inputTokens = meta.inputTokens;
|
|
92
|
+
if (meta.outputTokens !== undefined)
|
|
93
|
+
usage.outputTokens = meta.outputTokens;
|
|
94
|
+
if (meta.cacheReadTokens !== undefined)
|
|
95
|
+
usage.cacheReadTokens = meta.cacheReadTokens;
|
|
96
|
+
if (meta.cacheCreationTokens !== undefined)
|
|
97
|
+
usage.cacheCreationTokens = meta.cacheCreationTokens;
|
|
98
|
+
if (meta.totalCostUsd !== undefined)
|
|
99
|
+
usage.costUsd = meta.totalCostUsd;
|
|
100
|
+
if (meta.durationMs !== undefined)
|
|
101
|
+
usage.durationMs = meta.durationMs;
|
|
102
|
+
if (meta.durationApiMs !== undefined)
|
|
103
|
+
usage.durationApiMs = meta.durationApiMs;
|
|
104
|
+
return Object.keys(usage).length > 0 ? usage : undefined;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Classify one finished Claude turn from its result-frame snapshot plus any
|
|
108
|
+
* synthetic notice texts observed during the turn (assistant_error frames,
|
|
109
|
+
* zero-usage assistant text like the session-limit notice).
|
|
110
|
+
*/
|
|
111
|
+
export function classifyClaudeTurn(meta, noticeTexts, now = new Date()) {
|
|
112
|
+
if (!meta) {
|
|
113
|
+
// No result frame reached us (subprocess died mid-turn) — the dispatch
|
|
114
|
+
// layer's own error path reports the crash; classify what we know.
|
|
115
|
+
return { type: 'turn_outcome', outcome: 'runtime_crash' };
|
|
116
|
+
}
|
|
117
|
+
const usage = usageOf(meta);
|
|
118
|
+
const evidence = [meta.resultText, ...noticeTexts].filter(Boolean).join('\n');
|
|
119
|
+
const base = {
|
|
120
|
+
type: 'turn_outcome',
|
|
121
|
+
...(usage ? { usage } : {}),
|
|
122
|
+
...(meta.model ? { model: meta.model } : {}),
|
|
123
|
+
raw: {
|
|
124
|
+
...(meta.subtype ? { subtype: meta.subtype } : {}),
|
|
125
|
+
...(meta.terminalReason ? { terminal_reason: meta.terminalReason } : {}),
|
|
126
|
+
...(meta.apiErrorStatus !== undefined ? { api_error_status: meta.apiErrorStatus } : {}),
|
|
127
|
+
...(meta.stopReason ? { stop_reason: meta.stopReason } : {}),
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
const withDetail = (outcome, retryAt) => ({
|
|
131
|
+
...base,
|
|
132
|
+
outcome,
|
|
133
|
+
...(evidence ? { detail: evidence.slice(0, DETAIL_MAX) } : {}),
|
|
134
|
+
...(retryAt ? { retryAt } : {}),
|
|
135
|
+
});
|
|
136
|
+
// 1) Structured HTTP status.
|
|
137
|
+
if (meta.apiErrorStatus === 401 || meta.apiErrorStatus === 403)
|
|
138
|
+
return withDetail('auth');
|
|
139
|
+
if (meta.apiErrorStatus === 413)
|
|
140
|
+
return withDetail('context_overflow');
|
|
141
|
+
if (meta.apiErrorStatus === 429) {
|
|
142
|
+
return withDetail('usage_limit', evidence ? parseClaudeResetTime(evidence, now) : undefined);
|
|
143
|
+
}
|
|
144
|
+
// 2) Text families — refine only; they can never turn a failure into ok.
|
|
145
|
+
const failed = meta.isError ||
|
|
146
|
+
// Claude CLI 2.1.220 sets terminal_reason=completed on clean result
|
|
147
|
+
// frames. Any other terminal reason stays fail-closed.
|
|
148
|
+
(meta.terminalReason !== undefined && meta.terminalReason !== 'completed') ||
|
|
149
|
+
(meta.subtype !== undefined && meta.subtype !== 'success');
|
|
150
|
+
const syntheticQuiet = !failed &&
|
|
151
|
+
// A "successful" turn that never touched the API and produced no tool
|
|
152
|
+
// activity is the synthetic-notice shape (limit/auth text emitted
|
|
153
|
+
// locally by the CLI): zero usage + stop_sequence.
|
|
154
|
+
meta.stopReason === 'stop_sequence' &&
|
|
155
|
+
(meta.inputTokens ?? 0) === 0 &&
|
|
156
|
+
(meta.outputTokens ?? 0) === 0 &&
|
|
157
|
+
(meta.durationApiMs ?? 0) === 0;
|
|
158
|
+
if (failed || syntheticQuiet || noticeTexts.length > 0) {
|
|
159
|
+
if (CONTEXT_TEXT.test(evidence))
|
|
160
|
+
return withDetail('context_overflow');
|
|
161
|
+
if (LIMIT_TEXT.test(evidence)) {
|
|
162
|
+
return withDetail('usage_limit', parseClaudeResetTime(evidence, now));
|
|
163
|
+
}
|
|
164
|
+
if (AUTH_TEXT.test(evidence))
|
|
165
|
+
return withDetail('auth');
|
|
166
|
+
// Reached only when this block was entered (failed / syntheticQuiet /
|
|
167
|
+
// a captured synthetic notice) but no text family matched. A CLI wording
|
|
168
|
+
// change must degrade to api_error — never fall through to `ok` and settle
|
|
169
|
+
// a refused turn as success (the read-no-reply class this PR closes).
|
|
170
|
+
return withDetail('api_error');
|
|
171
|
+
}
|
|
172
|
+
return { ...base, outcome: 'ok' };
|
|
173
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parall/claude-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.52.0",
|
|
4
4
|
"description": "Claude Code bridge runtime for self-hosted Parall agents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
"src"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@parall/
|
|
29
|
-
"@parall/
|
|
30
|
-
"@parall/
|
|
28
|
+
"@parall/cli": "1.52.0",
|
|
29
|
+
"@parall/sdk": "1.52.0",
|
|
30
|
+
"@parall/agent-core": "1.52.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/node": "^22.0.0",
|