@parall/claude-agent 1.30.0 → 1.32.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/config.d.ts.map +1 -1
- package/dist/config.js +18 -18
- package/dist/dispatch.d.ts +9 -7
- package/dist/dispatch.d.ts.map +1 -1
- package/dist/dispatch.js +134 -88
- package/dist/index.js +185 -134
- package/dist/output-parser.d.ts +4 -4
- package/dist/output-parser.d.ts.map +1 -1
- package/dist/output-parser.js +41 -41
- package/dist/session-manager.d.ts +2 -2
- package/dist/session-manager.d.ts.map +1 -1
- package/dist/session-manager.js +12 -10
- package/dist/workspace.d.ts +1 -1
- package/dist/workspace.js +9 -9
- package/package.json +4 -4
- package/src/config.ts +27 -19
- package/src/dispatch.ts +182 -121
- package/src/index.ts +214 -142
- package/src/output-parser.ts +54 -49
- package/src/session-manager.ts +31 -28
- package/src/workspace.ts +10 -10
package/src/index.ts
CHANGED
|
@@ -1,8 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import * as os from
|
|
4
|
-
import {
|
|
5
|
-
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import {
|
|
5
|
+
ParallAgentGateway,
|
|
6
|
+
createPlatformConfigManager,
|
|
7
|
+
createLogger,
|
|
8
|
+
createOtelLogger,
|
|
9
|
+
childLogger,
|
|
10
|
+
isPlatformManagedProfile,
|
|
11
|
+
isPlatformModelOverride,
|
|
12
|
+
resolveRuntimeModel,
|
|
13
|
+
parseShutdownDeadlineMs,
|
|
14
|
+
parseForkDeadlineMs,
|
|
15
|
+
parseDispatchDeadlineMs,
|
|
16
|
+
parseProviderConfig,
|
|
17
|
+
clearAllProviderCreds,
|
|
18
|
+
llmSource,
|
|
19
|
+
initAgentTelemetry,
|
|
20
|
+
} from '@parall/agent-core';
|
|
21
|
+
import { ApiError, ParallClient, ParallWs } from '@parall/sdk';
|
|
6
22
|
import {
|
|
7
23
|
buildClaudeRuntimeKey,
|
|
8
24
|
contextFilePathForSession,
|
|
@@ -10,12 +26,12 @@ import {
|
|
|
10
26
|
resolveWsUrl,
|
|
11
27
|
sessionStateFilePathForRuntime,
|
|
12
28
|
stepIdFilePathForSession,
|
|
13
|
-
} from
|
|
14
|
-
import { ClaudeCodeAdapter } from
|
|
15
|
-
import { ClaudeSessionManager } from
|
|
16
|
-
import { ensureClaudeWorkspace } from
|
|
29
|
+
} from './config.js';
|
|
30
|
+
import { ClaudeCodeAdapter } from './dispatch.js';
|
|
31
|
+
import { ClaudeSessionManager } from './session-manager.js';
|
|
32
|
+
import { ensureClaudeWorkspace } from './workspace.js';
|
|
17
33
|
|
|
18
|
-
const log = createLogger(
|
|
34
|
+
const log = createLogger('claude-agent');
|
|
19
35
|
let activeLog = log;
|
|
20
36
|
|
|
21
37
|
async function getAgentMeWithLegacyFallback(client: ParallClient, orgId: string) {
|
|
@@ -30,151 +46,207 @@ async function getAgentMeWithLegacyFallback(client: ParallClient, orgId: string)
|
|
|
30
46
|
}
|
|
31
47
|
}
|
|
32
48
|
|
|
49
|
+
function resolveProviderEnv(): void {
|
|
50
|
+
const pc = parseProviderConfig(process.env);
|
|
51
|
+
if (!pc) return;
|
|
52
|
+
clearAllProviderCreds(process.env);
|
|
53
|
+
const source = llmSource(pc);
|
|
54
|
+
if (source === 'parall') {
|
|
55
|
+
process.env.ANTHROPIC_AUTH_TOKEN = process.env.PRLL_API_KEY;
|
|
56
|
+
process.env.ANTHROPIC_BASE_URL = `${process.env.PRLL_API_URL}/api/llm`;
|
|
57
|
+
process.env.PRLL_CLAUDE_ALLOW_API_KEY = '1';
|
|
58
|
+
} else if (source === 'custom') {
|
|
59
|
+
if (pc.anthropic_auth_token) {
|
|
60
|
+
process.env.ANTHROPIC_AUTH_TOKEN = pc.anthropic_auth_token;
|
|
61
|
+
process.env.PRLL_CLAUDE_ALLOW_API_KEY = '1';
|
|
62
|
+
}
|
|
63
|
+
if (pc.anthropic_base_url) process.env.ANTHROPIC_BASE_URL = pc.anthropic_base_url;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
33
67
|
async function main() {
|
|
34
|
-
const
|
|
68
|
+
const telemetry = await initAgentTelemetry('parall-claude-agent', 'claude-code');
|
|
69
|
+
activeLog = createOtelLogger('agent', 'claude-agent');
|
|
70
|
+
try {
|
|
71
|
+
resolveProviderEnv();
|
|
72
|
+
const config = resolveClaudeAgentConfig(process.env);
|
|
35
73
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
"PRLL_CLAUDE_ALLOW_API_KEY=1 — ANTHROPIC_API_KEY/ANTHROPIC_AUTH_TOKEN will reach the Claude CLI; billing routes through Anthropic API pay-per-use instead of Claude.ai OAuth.",
|
|
45
|
-
);
|
|
46
|
-
}
|
|
74
|
+
if (config.allowApiKey && (process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN)) {
|
|
75
|
+
// Surface the billing path: default is OAuth (Claude.ai subscription); this
|
|
76
|
+
// branch means the operator opted into Anthropic API pay-per-use, and the
|
|
77
|
+
// whole point of the strip default is that "silent" is the bad state.
|
|
78
|
+
activeLog.warn(
|
|
79
|
+
'PRLL_CLAUDE_ALLOW_API_KEY=1 — ANTHROPIC_API_KEY/ANTHROPIC_AUTH_TOKEN will reach the Claude CLI; billing routes through Anthropic API pay-per-use instead of Claude.ai OAuth.',
|
|
80
|
+
);
|
|
81
|
+
}
|
|
47
82
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
83
|
+
const client = new ParallClient({
|
|
84
|
+
baseUrl: config.apiUrl,
|
|
85
|
+
token: config.apiKey,
|
|
86
|
+
swimlaneName: config.swimlaneName,
|
|
87
|
+
});
|
|
53
88
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
runtimeKey,
|
|
67
|
-
sessionStateFilePath,
|
|
68
|
-
agentLog,
|
|
69
|
-
);
|
|
89
|
+
const me = await getAgentMeWithLegacyFallback(client, config.orgId);
|
|
90
|
+
const agentUserId = me.id;
|
|
91
|
+
const agentLog = childLogger(activeLog, agentUserId);
|
|
92
|
+
activeLog = agentLog;
|
|
93
|
+
ensureClaudeWorkspace(config.workspaceDir, agentLog, {
|
|
94
|
+
userId: agentUserId,
|
|
95
|
+
displayName: me.display_name,
|
|
96
|
+
description: me.agent_profile?.description ?? undefined,
|
|
97
|
+
});
|
|
98
|
+
const runtimeKey = config.runtimeKey || buildClaudeRuntimeKey(agentUserId);
|
|
99
|
+
const sessionStateFilePath = sessionStateFilePathForRuntime(config.stateDir, runtimeKey);
|
|
100
|
+
const sessionManager = new ClaudeSessionManager(runtimeKey, sessionStateFilePath, agentLog);
|
|
70
101
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
102
|
+
const resolvedWsUrl = resolveWsUrl(config.apiUrl, config.wsUrl, config.swimlaneName);
|
|
103
|
+
const ws = new ParallWs({
|
|
104
|
+
getTicket: () => client.getWsTicket(),
|
|
105
|
+
wsUrl: resolvedWsUrl,
|
|
106
|
+
});
|
|
76
107
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
108
|
+
const configMgr = createPlatformConfigManager({
|
|
109
|
+
client,
|
|
110
|
+
stateDir: config.stateDir,
|
|
111
|
+
runtimeType: 'claude-code',
|
|
112
|
+
log: agentLog,
|
|
113
|
+
});
|
|
114
|
+
const platformDefaults = await configMgr.fetch();
|
|
115
|
+
let platformManaged = isPlatformManagedProfile(me.agent_profile);
|
|
116
|
+
// Model precedence: operator override (platform-managed) > env > server floor.
|
|
117
|
+
// The server delivers a model floor for Parall-proxy routes even when
|
|
118
|
+
// model_management=self, so the CLI never falls back to a built-in default
|
|
119
|
+
// the catalog might reject — but that floor must lose to an explicit env
|
|
120
|
+
// override (env > floor). A profile is an operator-override only when it is
|
|
121
|
+
// platform-managed AND not explicitly self-managed: this treats legacy hosted
|
|
122
|
+
// agents (model_management=null, resolved to platform server-side) as
|
|
123
|
+
// overrides, while excluding local self agents whose delivered model is a
|
|
124
|
+
// floor. effort stays an operator override (platform).
|
|
125
|
+
let platformModelOverride = isPlatformModelOverride(me.agent_profile);
|
|
126
|
+
const resolvedModel = resolveRuntimeModel(
|
|
127
|
+
platformModelOverride,
|
|
128
|
+
platformDefaults.model,
|
|
129
|
+
config.model,
|
|
130
|
+
);
|
|
131
|
+
const resolvedEffort = platformManaged
|
|
132
|
+
? (platformDefaults.thinkingEffort ??
|
|
133
|
+
(process.env.CLAUDE_CODE_EFFORT_LEVEL?.trim() || undefined))
|
|
134
|
+
: process.env.CLAUDE_CODE_EFFORT_LEVEL?.trim() || undefined;
|
|
135
|
+
if (platformDefaults.model)
|
|
136
|
+
agentLog.info(
|
|
137
|
+
`platform config: model=${platformDefaults.model} → resolved=${resolvedModel ?? 'cli-default'} (${platformModelOverride ? 'platform-managed' : 'floor/self'})`,
|
|
138
|
+
);
|
|
139
|
+
if (platformDefaults.thinkingEffort)
|
|
140
|
+
agentLog.info(
|
|
141
|
+
`platform config: thinking_effort=${platformDefaults.thinkingEffort} (${platformModelOverride ? 'applied' : 'ignored, model_management=self'})`,
|
|
142
|
+
);
|
|
86
143
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
144
|
+
const adapter = new ClaudeCodeAdapter({
|
|
145
|
+
claudeBin: config.claudeBin,
|
|
146
|
+
claudeHome: config.claudeHome,
|
|
147
|
+
workspaceDir: config.workspaceDir,
|
|
148
|
+
model: resolvedModel,
|
|
149
|
+
permissionMode: config.permissionMode,
|
|
150
|
+
allowedTools: config.allowedTools,
|
|
151
|
+
disallowedTools: config.disallowedTools,
|
|
152
|
+
additionalDirs: config.additionalDirs,
|
|
153
|
+
appendSystemPrompt: config.appendSystemPrompt,
|
|
154
|
+
allowApiKey: config.allowApiKey,
|
|
155
|
+
sessionManager,
|
|
156
|
+
// Static Parall credentials. The long-lived subprocess must authenticate
|
|
157
|
+
// against Parall's API across many dispatches, so these cannot come from
|
|
158
|
+
// a per-dispatch DispatchContext.
|
|
159
|
+
apiUrl: config.apiUrl,
|
|
160
|
+
apiKey: config.apiKey,
|
|
161
|
+
orgId: config.orgId,
|
|
162
|
+
contextFilePathForSession: (sessionKey) =>
|
|
163
|
+
contextFilePathForSession(config.stateDir, sessionKey),
|
|
164
|
+
stepIdFilePathForSession: (sessionKey) =>
|
|
165
|
+
stepIdFilePathForSession(config.stateDir, sessionKey),
|
|
166
|
+
});
|
|
167
|
+
if (resolvedEffort) {
|
|
168
|
+
adapter.updateConfig({ effort: resolvedEffort });
|
|
169
|
+
}
|
|
111
170
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
171
|
+
const gateway = new ParallAgentGateway({
|
|
172
|
+
accountId: agentUserId,
|
|
173
|
+
client,
|
|
174
|
+
ws,
|
|
175
|
+
connectionLabel: resolvedWsUrl,
|
|
176
|
+
config: {
|
|
177
|
+
parall_url: config.apiUrl,
|
|
178
|
+
api_key: config.apiKey,
|
|
179
|
+
org_id: config.orgId,
|
|
180
|
+
},
|
|
181
|
+
agentUserId,
|
|
182
|
+
runtimeType: 'claude-code',
|
|
183
|
+
runtimeKey,
|
|
184
|
+
runtimeRef: {
|
|
185
|
+
hostname: os.hostname(),
|
|
186
|
+
pid: process.pid,
|
|
187
|
+
workspace_dir: config.workspaceDir,
|
|
188
|
+
claude_home: config.claudeHome,
|
|
189
|
+
},
|
|
190
|
+
dispatchAdapter: adapter,
|
|
191
|
+
log: agentLog,
|
|
192
|
+
shutdownDeadlineMs: parseShutdownDeadlineMs(process.env.PRLL_SHUTDOWN_DEADLINE_MS),
|
|
193
|
+
forkDeadlineMs: parseForkDeadlineMs(process.env.PRLL_FORK_DEADLINE_MS),
|
|
194
|
+
dispatchDeadlineMs: parseDispatchDeadlineMs(process.env.PRLL_DISPATCH_DEADLINE_MS),
|
|
195
|
+
contextFilePathForSession: (sessionKey) =>
|
|
196
|
+
contextFilePathForSession(config.stateDir, sessionKey),
|
|
197
|
+
stepIdFilePathForSession: (sessionKey) =>
|
|
198
|
+
stepIdFilePathForSession(config.stateDir, sessionKey),
|
|
199
|
+
onConfigUpdate: async () => {
|
|
200
|
+
const refreshed = await getAgentMeWithLegacyFallback(client, config.orgId);
|
|
201
|
+
platformManaged = isPlatformManagedProfile(refreshed.agent_profile);
|
|
202
|
+
platformModelOverride = isPlatformModelOverride(refreshed.agent_profile);
|
|
203
|
+
const updated = await configMgr.fetch();
|
|
204
|
+
const localEffort = process.env.CLAUDE_CODE_EFFORT_LEVEL?.trim() || null;
|
|
205
|
+
adapter.updateConfig({
|
|
206
|
+
model: resolveRuntimeModel(platformModelOverride, updated.model, config.model) ?? null,
|
|
207
|
+
effort: platformManaged ? (updated.thinkingEffort ?? localEffort) : localEffort,
|
|
208
|
+
});
|
|
209
|
+
},
|
|
210
|
+
onSessionReady: async () => {
|
|
211
|
+
const refreshed = await getAgentMeWithLegacyFallback(client, config.orgId);
|
|
212
|
+
platformManaged = isPlatformManagedProfile(refreshed.agent_profile);
|
|
213
|
+
platformModelOverride = isPlatformModelOverride(refreshed.agent_profile);
|
|
214
|
+
const updated = await configMgr.fetch();
|
|
215
|
+
const localEffort = process.env.CLAUDE_CODE_EFFORT_LEVEL?.trim() || null;
|
|
216
|
+
adapter.updateConfig({
|
|
217
|
+
model: resolveRuntimeModel(platformModelOverride, updated.model, config.model) ?? null,
|
|
218
|
+
effort: platformManaged ? (updated.thinkingEffort ?? localEffort) : localEffort,
|
|
219
|
+
});
|
|
220
|
+
},
|
|
221
|
+
// Long-lived `claude` subprocesses outlive a single dispatch, so the
|
|
222
|
+
// gateway's shuttingDown flag alone does not tear them down. Piggyback on
|
|
223
|
+
// onBeforeDisconnect to close stdin and SIGTERM any survivors after
|
|
224
|
+
// in-flight drains finish.
|
|
225
|
+
onBeforeDisconnect: () => adapter.shutdown(),
|
|
226
|
+
onNewSession: async () => {
|
|
227
|
+
sessionManager.clearMainSession();
|
|
228
|
+
adapter.resetProcesses();
|
|
229
|
+
},
|
|
230
|
+
onSessionStale: () => {
|
|
231
|
+
sessionManager.clearMainSession();
|
|
232
|
+
adapter.resetProcesses();
|
|
233
|
+
},
|
|
234
|
+
});
|
|
166
235
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
236
|
+
const abortController = new AbortController();
|
|
237
|
+
const abort = () => abortController.abort();
|
|
238
|
+
process.on('SIGINT', abort);
|
|
239
|
+
process.on('SIGTERM', abort);
|
|
171
240
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
241
|
+
try {
|
|
242
|
+
agentLog.info(`starting self-hosted Claude runtime for ${me.display_name} (${agentUserId})`);
|
|
243
|
+
await gateway.run(abortController.signal);
|
|
244
|
+
} finally {
|
|
245
|
+
process.off('SIGINT', abort);
|
|
246
|
+
process.off('SIGTERM', abort);
|
|
247
|
+
}
|
|
175
248
|
} finally {
|
|
176
|
-
|
|
177
|
-
process.off("SIGTERM", abort);
|
|
249
|
+
await telemetry.shutdown();
|
|
178
250
|
}
|
|
179
251
|
}
|
|
180
252
|
|
package/src/output-parser.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { Readable } from
|
|
2
|
-
import type { RuntimeEvent } from
|
|
1
|
+
import type { Readable } from 'node:stream';
|
|
2
|
+
import type { RuntimeEvent } from '@parall/agent-core';
|
|
3
3
|
|
|
4
4
|
export type ClaudeParsedEvent =
|
|
5
5
|
| RuntimeEvent
|
|
6
|
-
| { type:
|
|
6
|
+
| { type: 'session_id'; sessionId: string }
|
|
7
7
|
// turn_end is emitted for every `result` frame (both `is_error: true` and
|
|
8
8
|
// `is_error: false`); `isError` carries the frame's status so the consumer
|
|
9
9
|
// can tell a clean turn boundary from a failed one without inspecting the
|
|
@@ -14,7 +14,7 @@ export type ClaudeParsedEvent =
|
|
|
14
14
|
// stream. For a short-lived `--print <prompt>` invocation the final
|
|
15
15
|
// `result` frame arrives right before stdout EOF, so this event is
|
|
16
16
|
// redundant there — safe to ignore.
|
|
17
|
-
| { type:
|
|
17
|
+
| { type: 'turn_end'; sessionId?: string; isError: boolean };
|
|
18
18
|
|
|
19
19
|
type ToolUseMeta = {
|
|
20
20
|
toolName: string;
|
|
@@ -22,26 +22,26 @@ type ToolUseMeta = {
|
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
function asTrimmedString(value: unknown): string | undefined {
|
|
25
|
-
if (typeof value !==
|
|
25
|
+
if (typeof value !== 'string') return undefined;
|
|
26
26
|
const trimmed = value.trim();
|
|
27
27
|
return trimmed.length > 0 ? trimmed : undefined;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
function parseEventTimestampMs(event: unknown): number | undefined {
|
|
31
|
-
if (!event || typeof event !==
|
|
31
|
+
if (!event || typeof event !== 'object') return undefined;
|
|
32
32
|
const timestamp = (event as { timestamp?: unknown }).timestamp;
|
|
33
|
-
if (typeof timestamp !==
|
|
33
|
+
if (typeof timestamp !== 'string') return undefined;
|
|
34
34
|
const ms = Date.parse(timestamp);
|
|
35
35
|
return Number.isFinite(ms) ? ms : undefined;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
function stringifyContent(value: unknown): string {
|
|
39
|
-
if (typeof value ===
|
|
39
|
+
if (typeof value === 'string') return value;
|
|
40
40
|
if (Array.isArray(value)) {
|
|
41
41
|
return value
|
|
42
42
|
.map((item) => {
|
|
43
|
-
if (typeof item ===
|
|
44
|
-
if (!item || typeof item !==
|
|
43
|
+
if (typeof item === 'string') return item;
|
|
44
|
+
if (!item || typeof item !== 'object') return '';
|
|
45
45
|
const text = asTrimmedString((item as { text?: unknown }).text);
|
|
46
46
|
if (text) return text;
|
|
47
47
|
const thinking = asTrimmedString((item as { thinking?: unknown }).thinking);
|
|
@@ -49,19 +49,19 @@ function stringifyContent(value: unknown): string {
|
|
|
49
49
|
return JSON.stringify(item);
|
|
50
50
|
})
|
|
51
51
|
.filter(Boolean)
|
|
52
|
-
.join(
|
|
52
|
+
.join('\n');
|
|
53
53
|
}
|
|
54
|
-
if (value && typeof value ===
|
|
54
|
+
if (value && typeof value === 'object') {
|
|
55
55
|
return JSON.stringify(value);
|
|
56
56
|
}
|
|
57
|
-
return
|
|
57
|
+
return '';
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
function normalizeToolResultContent(content: unknown, fallback: unknown): string {
|
|
61
61
|
const direct = stringifyContent(content).trim();
|
|
62
62
|
if (direct) return direct;
|
|
63
63
|
|
|
64
|
-
if (fallback && typeof fallback ===
|
|
64
|
+
if (fallback && typeof fallback === 'object') {
|
|
65
65
|
const stdout = asTrimmedString((fallback as { stdout?: unknown }).stdout);
|
|
66
66
|
const stderr = asTrimmedString((fallback as { stderr?: unknown }).stderr);
|
|
67
67
|
if (stdout && stderr) return `${stdout}\n${stderr}`;
|
|
@@ -69,20 +69,20 @@ function normalizeToolResultContent(content: unknown, fallback: unknown): string
|
|
|
69
69
|
if (stderr) return stderr;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
return
|
|
72
|
+
return '';
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
async function* readJsonLines(readable: Readable): AsyncGenerator<string> {
|
|
76
|
-
let buffer =
|
|
77
|
-
readable.setEncoding(
|
|
76
|
+
let buffer = '';
|
|
77
|
+
readable.setEncoding('utf8');
|
|
78
78
|
for await (const chunk of readable) {
|
|
79
79
|
buffer += chunk;
|
|
80
|
-
let newlineIndex = buffer.indexOf(
|
|
80
|
+
let newlineIndex = buffer.indexOf('\n');
|
|
81
81
|
while (newlineIndex >= 0) {
|
|
82
82
|
const line = buffer.slice(0, newlineIndex).trim();
|
|
83
83
|
buffer = buffer.slice(newlineIndex + 1);
|
|
84
84
|
if (line) yield line;
|
|
85
|
-
newlineIndex = buffer.indexOf(
|
|
85
|
+
newlineIndex = buffer.indexOf('\n');
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
|
|
@@ -90,7 +90,9 @@ async function* readJsonLines(readable: Readable): AsyncGenerator<string> {
|
|
|
90
90
|
if (tail) yield tail;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
export async function* parseClaudeStreamJson(
|
|
93
|
+
export async function* parseClaudeStreamJson(
|
|
94
|
+
readable: Readable,
|
|
95
|
+
): AsyncGenerator<ClaudeParsedEvent> {
|
|
94
96
|
const toolUses = new Map<string, ToolUseMeta>();
|
|
95
97
|
|
|
96
98
|
for await (const line of readJsonLines(readable)) {
|
|
@@ -101,46 +103,47 @@ export async function* parseClaudeStreamJson(readable: Readable): AsyncGenerator
|
|
|
101
103
|
continue;
|
|
102
104
|
}
|
|
103
105
|
|
|
104
|
-
if (!event || typeof event !==
|
|
106
|
+
if (!event || typeof event !== 'object') continue;
|
|
105
107
|
const now = Date.now();
|
|
106
108
|
const eventTimestampMs = parseEventTimestampMs(event) ?? now;
|
|
107
109
|
const eventRecord = event as Record<string, unknown>;
|
|
108
110
|
|
|
109
|
-
if (eventRecord.type ===
|
|
111
|
+
if (eventRecord.type === 'system' && eventRecord.subtype === 'init') {
|
|
110
112
|
const sessionId = asTrimmedString(eventRecord.session_id);
|
|
111
113
|
if (sessionId) {
|
|
112
|
-
yield { type:
|
|
114
|
+
yield { type: 'session_id', sessionId };
|
|
113
115
|
}
|
|
114
116
|
continue;
|
|
115
117
|
}
|
|
116
118
|
|
|
117
|
-
if (eventRecord.type ===
|
|
119
|
+
if (eventRecord.type === 'assistant') {
|
|
118
120
|
if (eventRecord.error) continue;
|
|
119
121
|
|
|
120
122
|
const message = eventRecord.message;
|
|
121
|
-
const content =
|
|
122
|
-
|
|
123
|
-
|
|
123
|
+
const content =
|
|
124
|
+
message && typeof message === 'object'
|
|
125
|
+
? (message as { content?: unknown }).content
|
|
126
|
+
: undefined;
|
|
124
127
|
if (!Array.isArray(content)) continue;
|
|
125
128
|
|
|
126
129
|
for (const block of content) {
|
|
127
|
-
if (!block || typeof block !==
|
|
130
|
+
if (!block || typeof block !== 'object') continue;
|
|
128
131
|
const blockRecord = block as Record<string, unknown>;
|
|
129
132
|
|
|
130
133
|
switch (blockRecord.type) {
|
|
131
|
-
case
|
|
134
|
+
case 'thinking': {
|
|
132
135
|
const text = asTrimmedString(blockRecord.thinking);
|
|
133
|
-
if (text) yield { type:
|
|
136
|
+
if (text) yield { type: 'thinking', text };
|
|
134
137
|
break;
|
|
135
138
|
}
|
|
136
139
|
|
|
137
|
-
case
|
|
140
|
+
case 'text': {
|
|
138
141
|
const text = asTrimmedString(blockRecord.text);
|
|
139
|
-
if (text) yield { type:
|
|
142
|
+
if (text) yield { type: 'text', text, project: true };
|
|
140
143
|
break;
|
|
141
144
|
}
|
|
142
145
|
|
|
143
|
-
case
|
|
146
|
+
case 'tool_use': {
|
|
144
147
|
const callId = asTrimmedString(blockRecord.id);
|
|
145
148
|
const toolName = asTrimmedString(blockRecord.name);
|
|
146
149
|
if (!callId || !toolName) break;
|
|
@@ -151,7 +154,7 @@ export async function* parseClaudeStreamJson(readable: Readable): AsyncGenerator
|
|
|
151
154
|
});
|
|
152
155
|
|
|
153
156
|
yield {
|
|
154
|
-
type:
|
|
157
|
+
type: 'tool_call',
|
|
155
158
|
callId,
|
|
156
159
|
toolName,
|
|
157
160
|
input: blockRecord.input ?? {},
|
|
@@ -164,17 +167,18 @@ export async function* parseClaudeStreamJson(readable: Readable): AsyncGenerator
|
|
|
164
167
|
continue;
|
|
165
168
|
}
|
|
166
169
|
|
|
167
|
-
if (eventRecord.type ===
|
|
170
|
+
if (eventRecord.type === 'user') {
|
|
168
171
|
const message = eventRecord.message;
|
|
169
|
-
const content =
|
|
170
|
-
|
|
171
|
-
|
|
172
|
+
const content =
|
|
173
|
+
message && typeof message === 'object'
|
|
174
|
+
? (message as { content?: unknown }).content
|
|
175
|
+
: undefined;
|
|
172
176
|
if (!Array.isArray(content)) continue;
|
|
173
177
|
|
|
174
178
|
for (const block of content) {
|
|
175
|
-
if (!block || typeof block !==
|
|
179
|
+
if (!block || typeof block !== 'object') continue;
|
|
176
180
|
const blockRecord = block as Record<string, unknown>;
|
|
177
|
-
if (blockRecord.type !==
|
|
181
|
+
if (blockRecord.type !== 'tool_result') continue;
|
|
178
182
|
|
|
179
183
|
const callId = asTrimmedString(blockRecord.tool_use_id);
|
|
180
184
|
if (!callId) continue;
|
|
@@ -183,11 +187,11 @@ export async function* parseClaudeStreamJson(readable: Readable): AsyncGenerator
|
|
|
183
187
|
const output = normalizeToolResultContent(blockRecord.content, eventRecord.tool_use_result);
|
|
184
188
|
|
|
185
189
|
yield {
|
|
186
|
-
type:
|
|
190
|
+
type: 'tool_result',
|
|
187
191
|
callId,
|
|
188
|
-
toolName: meta?.toolName ??
|
|
192
|
+
toolName: meta?.toolName ?? 'unknown',
|
|
189
193
|
output,
|
|
190
|
-
...(blockRecord.is_error === true ? { error: output ||
|
|
194
|
+
...(blockRecord.is_error === true ? { error: output || 'Tool call failed' } : {}),
|
|
191
195
|
...(meta ? { durationMs: Math.max(0, eventTimestampMs - meta.startedAtMs) } : {}),
|
|
192
196
|
};
|
|
193
197
|
toolUses.delete(callId);
|
|
@@ -195,13 +199,14 @@ export async function* parseClaudeStreamJson(readable: Readable): AsyncGenerator
|
|
|
195
199
|
continue;
|
|
196
200
|
}
|
|
197
201
|
|
|
198
|
-
if (eventRecord.type ===
|
|
202
|
+
if (eventRecord.type === 'result') {
|
|
199
203
|
const isError = eventRecord.is_error === true;
|
|
200
204
|
if (isError) {
|
|
201
|
-
const message =
|
|
202
|
-
|
|
203
|
-
||
|
|
204
|
-
|
|
205
|
+
const message =
|
|
206
|
+
asTrimmedString(eventRecord.result) ||
|
|
207
|
+
asTrimmedString(eventRecord.error) ||
|
|
208
|
+
'Claude dispatch failed';
|
|
209
|
+
yield { type: 'error', message };
|
|
205
210
|
}
|
|
206
211
|
// Turn boundary: the subprocess lives across many turns, so any
|
|
207
212
|
// tool_use entries that never matched a tool_result this turn
|
|
@@ -210,7 +215,7 @@ export async function* parseClaudeStreamJson(readable: Readable): AsyncGenerator
|
|
|
210
215
|
// the map grow unbounded or mislabel durations on id collisions.
|
|
211
216
|
toolUses.clear();
|
|
212
217
|
yield {
|
|
213
|
-
type:
|
|
218
|
+
type: 'turn_end',
|
|
214
219
|
sessionId: asTrimmedString(eventRecord.session_id),
|
|
215
220
|
isError,
|
|
216
221
|
};
|