@northflare/runner 0.0.11 → 0.0.13
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/utils/config.d.ts +1 -0
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +13 -2
- package/dist/utils/config.js.map +1 -1
- package/package.json +1 -2
- package/coverage/base.css +0 -224
- package/coverage/block-navigation.js +0 -87
- package/coverage/coverage-final.json +0 -12
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +0 -176
- package/coverage/lib/index.html +0 -116
- package/coverage/lib/preload-script.js.html +0 -964
- package/coverage/prettify.css +0 -1
- package/coverage/prettify.js +0 -2
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +0 -196
- package/coverage/src/collections/index.html +0 -116
- package/coverage/src/collections/runner-messages.ts.html +0 -312
- package/coverage/src/components/claude-manager.ts.html +0 -1290
- package/coverage/src/components/index.html +0 -146
- package/coverage/src/components/message-handler.ts.html +0 -730
- package/coverage/src/components/repository-manager.ts.html +0 -841
- package/coverage/src/index.html +0 -131
- package/coverage/src/index.ts.html +0 -448
- package/coverage/src/runner.ts.html +0 -1239
- package/coverage/src/utils/config.ts.html +0 -780
- package/coverage/src/utils/console.ts.html +0 -121
- package/coverage/src/utils/index.html +0 -161
- package/coverage/src/utils/logger.ts.html +0 -475
- package/coverage/src/utils/status-line.ts.html +0 -445
- package/exceptions.log +0 -24
- package/lib/codex-sdk/src/codex.ts +0 -38
- package/lib/codex-sdk/src/codexOptions.ts +0 -10
- package/lib/codex-sdk/src/events.ts +0 -80
- package/lib/codex-sdk/src/exec.ts +0 -336
- package/lib/codex-sdk/src/index.ts +0 -39
- package/lib/codex-sdk/src/items.ts +0 -127
- package/lib/codex-sdk/src/outputSchemaFile.ts +0 -40
- package/lib/codex-sdk/src/thread.ts +0 -155
- package/lib/codex-sdk/src/threadOptions.ts +0 -18
- package/lib/codex-sdk/src/turnOptions.ts +0 -6
- package/lib/codex-sdk/tests/abort.test.ts +0 -165
- package/lib/codex-sdk/tests/codexExecSpy.ts +0 -37
- package/lib/codex-sdk/tests/responsesProxy.ts +0 -225
- package/lib/codex-sdk/tests/run.test.ts +0 -687
- package/lib/codex-sdk/tests/runStreamed.test.ts +0 -211
- package/lib/codex-sdk/tsconfig.json +0 -24
- package/rejections.log +0 -68
- package/runner.log +0 -488
- package/src/components/claude-sdk-manager.ts +0 -1425
- package/src/components/codex-sdk-manager.ts +0 -1358
- package/src/components/enhanced-repository-manager.ts +0 -823
- package/src/components/message-handler-sse.ts +0 -1097
- package/src/components/repository-manager.ts +0 -337
- package/src/index.ts +0 -168
- package/src/runner-sse.ts +0 -917
- package/src/services/RunnerAPIClient.ts +0 -175
- package/src/services/SSEClient.ts +0 -258
- package/src/types/claude.ts +0 -66
- package/src/types/computer-name.d.ts +0 -4
- package/src/types/index.ts +0 -64
- package/src/types/messages.ts +0 -39
- package/src/types/runner-interface.ts +0 -36
- package/src/utils/StateManager.ts +0 -187
- package/src/utils/config.ts +0 -316
- package/src/utils/console.ts +0 -15
- package/src/utils/debug.ts +0 -18
- package/src/utils/expand-env.ts +0 -22
- package/src/utils/logger.ts +0 -134
- package/src/utils/model.ts +0 -29
- package/src/utils/status-line.ts +0 -122
- package/src/utils/tool-response-sanitizer.ts +0 -160
- package/test-debug.sh +0 -26
- package/tests/retry-strategies.test.ts +0 -410
- package/tests/sdk-integration.test.ts +0 -329
- package/tests/sdk-streaming.test.ts +0 -1180
- package/tests/setup.ts +0 -5
- package/tests/test-claude-manager.ts +0 -120
- package/tests/tool-response-sanitizer.test.ts +0 -63
- package/tsconfig.json +0 -36
- package/vitest.config.ts +0 -27
|
@@ -1,1425 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ClaudeManager - Manages Claude conversations using SDK-native patterns
|
|
3
|
-
*
|
|
4
|
-
* This component handles stateful conversation lifecycle management, maintaining
|
|
5
|
-
* persistent conversation instances indexed by taskId. Uses the SDK's native
|
|
6
|
-
* capabilities including the builder pattern, native session management, and
|
|
7
|
-
* built-in streaming instead of custom wrappers.
|
|
8
|
-
*
|
|
9
|
-
* Key improvements:
|
|
10
|
-
* - Uses claude() builder pattern for simplified configuration
|
|
11
|
-
* - Native session management with withSessionId()
|
|
12
|
-
* - Direct AsyncGenerator streaming without custom wrappers
|
|
13
|
-
* - SDK's onProcessComplete() for proper cleanup
|
|
14
|
-
* - Simplified error handling while maintaining compatibility
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { query as sdkQuery } from "@anthropic-ai/claude-agent-sdk";
|
|
18
|
-
import type { Conversation as ClaudeConversation } from "@botanicastudios/claude-code-sdk-ts";
|
|
19
|
-
// Keep SDKMessage loosely typed to avoid tight coupling to SDK typings
|
|
20
|
-
type SDKMessage = any;
|
|
21
|
-
import { IRunnerApp } from "../types/runner-interface";
|
|
22
|
-
import { EnhancedRepositoryManager } from "./enhanced-repository-manager";
|
|
23
|
-
import { ConversationContext, ConversationConfig, Message } from "../types";
|
|
24
|
-
import { statusLineManager } from "../utils/status-line";
|
|
25
|
-
import { console } from "../utils/console";
|
|
26
|
-
import { expandEnv } from "../utils/expand-env";
|
|
27
|
-
import { isRunnerDebugEnabled } from "../utils/debug";
|
|
28
|
-
import * as jwt from "jsonwebtoken";
|
|
29
|
-
|
|
30
|
-
export class ClaudeManager {
|
|
31
|
-
private runner: IRunnerApp;
|
|
32
|
-
private repositoryManager: EnhancedRepositoryManager;
|
|
33
|
-
|
|
34
|
-
constructor(
|
|
35
|
-
runner: IRunnerApp,
|
|
36
|
-
repositoryManager: EnhancedRepositoryManager
|
|
37
|
-
) {
|
|
38
|
-
this.runner = runner;
|
|
39
|
-
this.repositoryManager = repositoryManager;
|
|
40
|
-
|
|
41
|
-
// Log debug mode status
|
|
42
|
-
if (isRunnerDebugEnabled()) {
|
|
43
|
-
console.log(
|
|
44
|
-
"[ClaudeManager] DEBUG MODE ENABLED - Claude SDK will log verbose output"
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Note: MCP host configuration is passed from orchestrator
|
|
49
|
-
// Runner does not define its own MCP tools
|
|
50
|
-
this.setupInternalMcpServer();
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
private setupInternalMcpServer(): void {
|
|
54
|
-
// Runner does not define its own MCP tools
|
|
55
|
-
// All MCP tool configuration is passed from orchestrator in conversation config
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async startConversation(
|
|
59
|
-
conversationObjectType: "Task" | "TaskPlan",
|
|
60
|
-
conversationObjectId: string,
|
|
61
|
-
config: ConversationConfig,
|
|
62
|
-
initialMessages: Message[],
|
|
63
|
-
conversationData?: {
|
|
64
|
-
id: string;
|
|
65
|
-
objectType: string;
|
|
66
|
-
objectId: string;
|
|
67
|
-
model: string;
|
|
68
|
-
globalInstructions: string;
|
|
69
|
-
workspaceInstructions: string;
|
|
70
|
-
permissionsMode: string;
|
|
71
|
-
agentSessionId: string;
|
|
72
|
-
}
|
|
73
|
-
): Promise<ConversationContext> {
|
|
74
|
-
// Returns conversation context
|
|
75
|
-
// Greenfield: conversationData.id is required as the authoritative DB conversation ID
|
|
76
|
-
if (!conversationData?.id) {
|
|
77
|
-
throw new Error(
|
|
78
|
-
"startConversation requires conversationData with a valid conversation.id"
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
// Use sessionId from config if resuming, or agentSessionId from conversationData if available
|
|
82
|
-
const agentSessionId =
|
|
83
|
-
config.sessionId || conversationData.agentSessionId || "";
|
|
84
|
-
const conversationId = conversationData.id;
|
|
85
|
-
|
|
86
|
-
const context: ConversationContext = {
|
|
87
|
-
conversationId,
|
|
88
|
-
agentSessionId, // Will be updated by onSessionId callback for new conversations
|
|
89
|
-
conversationObjectType,
|
|
90
|
-
conversationObjectId,
|
|
91
|
-
taskId:
|
|
92
|
-
conversationObjectType === "Task" ? conversationObjectId : undefined,
|
|
93
|
-
workspaceId: config.workspaceId,
|
|
94
|
-
status: "starting",
|
|
95
|
-
config,
|
|
96
|
-
startedAt: new Date(),
|
|
97
|
-
lastActivityAt: new Date(),
|
|
98
|
-
// Add conversation details if provided
|
|
99
|
-
model: conversationData?.model || "sonnet",
|
|
100
|
-
globalInstructions: conversationData?.globalInstructions || "",
|
|
101
|
-
workspaceInstructions: conversationData?.workspaceInstructions || "",
|
|
102
|
-
permissionsMode: conversationData?.permissionsMode || "all",
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
// Store with conversation.id as the key
|
|
106
|
-
this.runner.activeConversations_.set(conversationId, context);
|
|
107
|
-
console.log(`[ClaudeManager] Stored conversation context:`, {
|
|
108
|
-
conversationId,
|
|
109
|
-
agentSessionId: context.agentSessionId,
|
|
110
|
-
conversationObjectType: context.conversationObjectType,
|
|
111
|
-
conversationObjectId: context.conversationObjectId,
|
|
112
|
-
mapSize: this.runner.activeConversations_.size,
|
|
113
|
-
allKeys: Array.from(this.runner.activeConversations_.keys()),
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
const workspaceId = config.workspaceId;
|
|
117
|
-
|
|
118
|
-
// Checkout repository if specified
|
|
119
|
-
let workspacePath: string;
|
|
120
|
-
|
|
121
|
-
// Check if this is a local workspace by looking for runnerRepoPath in config
|
|
122
|
-
if (config.runnerRepoPath) {
|
|
123
|
-
// Local workspace - use the provided path directly
|
|
124
|
-
workspacePath = config.runnerRepoPath;
|
|
125
|
-
console.log(`Using local workspace path: ${workspacePath}`);
|
|
126
|
-
|
|
127
|
-
// For task conversations in local workspaces, create a local task handle
|
|
128
|
-
if (conversationObjectType === "Task") {
|
|
129
|
-
const taskHandle = await this.repositoryManager.createLocalTaskHandle(
|
|
130
|
-
conversationObjectId,
|
|
131
|
-
workspacePath
|
|
132
|
-
);
|
|
133
|
-
// Store task handle information in context for later use
|
|
134
|
-
(context as any).taskHandle = taskHandle;
|
|
135
|
-
}
|
|
136
|
-
} else if (
|
|
137
|
-
conversationObjectType === "Task" &&
|
|
138
|
-
config.repository &&
|
|
139
|
-
workspaceId
|
|
140
|
-
) {
|
|
141
|
-
// Use task-specific worktree for task conversations
|
|
142
|
-
console.log(
|
|
143
|
-
`[ClaudeManager] Creating task worktree with repository config:`,
|
|
144
|
-
{
|
|
145
|
-
conversationObjectId,
|
|
146
|
-
workspaceId,
|
|
147
|
-
repository: config.repository,
|
|
148
|
-
hasUrl: !!config.repository.url,
|
|
149
|
-
url: config.repository.url,
|
|
150
|
-
branch: config.repository.branch,
|
|
151
|
-
type: config.repository.type,
|
|
152
|
-
}
|
|
153
|
-
);
|
|
154
|
-
|
|
155
|
-
// Check if repository.url is missing
|
|
156
|
-
if (!config.repository.url) {
|
|
157
|
-
throw new Error(
|
|
158
|
-
`Repository URL is missing in config for task ${conversationObjectId}. Repository config: ${JSON.stringify(
|
|
159
|
-
config.repository
|
|
160
|
-
)}`
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const taskHandle = await this.repositoryManager.createTaskWorktree(
|
|
165
|
-
conversationObjectId,
|
|
166
|
-
workspaceId,
|
|
167
|
-
config.repository.url,
|
|
168
|
-
config.repository.branch,
|
|
169
|
-
config.githubToken
|
|
170
|
-
);
|
|
171
|
-
workspacePath = taskHandle.worktreePath;
|
|
172
|
-
|
|
173
|
-
// Store task handle information in context for later use
|
|
174
|
-
(context as any).taskHandle = taskHandle;
|
|
175
|
-
} else if (config.repository && workspaceId) {
|
|
176
|
-
// Check if it's a local repository
|
|
177
|
-
if (config.repository.type === "local" && config.repository.localPath) {
|
|
178
|
-
// Use the local path directly
|
|
179
|
-
workspacePath = config.repository.localPath;
|
|
180
|
-
console.log(`Using local repository path: ${workspacePath}`);
|
|
181
|
-
} else {
|
|
182
|
-
// Fall back to workspace-based checkout for non-task conversations
|
|
183
|
-
workspacePath = await this.repositoryManager.checkoutRepository(
|
|
184
|
-
workspaceId,
|
|
185
|
-
config.repository.url,
|
|
186
|
-
config.repository.branch,
|
|
187
|
-
config.githubToken
|
|
188
|
-
);
|
|
189
|
-
}
|
|
190
|
-
} else if (workspaceId) {
|
|
191
|
-
workspacePath = await this.repositoryManager.getWorkspacePath(
|
|
192
|
-
workspaceId
|
|
193
|
-
);
|
|
194
|
-
} else {
|
|
195
|
-
// Default workspace path when no workspaceId is provided
|
|
196
|
-
workspacePath = process.cwd();
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Fetch GitHub tokens from orchestrator API if we have a workspaceId
|
|
200
|
-
const githubToken = workspaceId
|
|
201
|
-
? await this.fetchGithubTokens(workspaceId)
|
|
202
|
-
: undefined;
|
|
203
|
-
|
|
204
|
-
// Generate TOOL_TOKEN for MCP tools authentication
|
|
205
|
-
let toolToken: string | undefined;
|
|
206
|
-
if (config.mcpServers && Object.keys(config.mcpServers).length > 0) {
|
|
207
|
-
const runnerToken = process.env["NORTHFLARE_RUNNER_TOKEN"];
|
|
208
|
-
const runnerUid = this.runner.getRunnerUid();
|
|
209
|
-
|
|
210
|
-
if (runnerToken && runnerUid && context.conversationId) {
|
|
211
|
-
// Sign JWT with runner's token
|
|
212
|
-
toolToken = jwt.sign(
|
|
213
|
-
{
|
|
214
|
-
conversationId: context.conversationId,
|
|
215
|
-
runnerUid: runnerUid,
|
|
216
|
-
},
|
|
217
|
-
runnerToken,
|
|
218
|
-
{
|
|
219
|
-
expiresIn: "60m", // 60 minutes expiry
|
|
220
|
-
}
|
|
221
|
-
);
|
|
222
|
-
console.log(
|
|
223
|
-
"[ClaudeManager] Generated TOOL_TOKEN for MCP authentication"
|
|
224
|
-
);
|
|
225
|
-
} else {
|
|
226
|
-
console.warn(
|
|
227
|
-
"[ClaudeManager] Unable to generate TOOL_TOKEN - missing required data"
|
|
228
|
-
);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// Simplified SDK configuration - using native query API with streamlined options
|
|
233
|
-
// const cliPath = require.resolve("@anthropic-ai/claude-code/cli.js");
|
|
234
|
-
|
|
235
|
-
// Debug logging for executable paths
|
|
236
|
-
//console.log("[ClaudeManager] SDK executable paths:", {
|
|
237
|
-
// cliPath,
|
|
238
|
-
// cwd: workspacePath,
|
|
239
|
-
//});
|
|
240
|
-
|
|
241
|
-
// Simplified environment configuration
|
|
242
|
-
const envVars: Record<string, string> = {
|
|
243
|
-
...(Object.fromEntries(
|
|
244
|
-
Object.entries(process.env).filter(([_, value]) => value !== undefined)
|
|
245
|
-
) as Record<string, string>), // Preserve parent environment, filter out undefined values
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
if (config.anthropicApiKey) {
|
|
249
|
-
envVars["ANTHROPIC_API_KEY"] = config.anthropicApiKey;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
if (config.accessToken) {
|
|
253
|
-
envVars["CLAUDE_CODE_OAUTH_TOKEN"] = config.accessToken;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
if (githubToken) {
|
|
257
|
-
envVars["GITHUB_TOKEN"] = githubToken;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
if (isRunnerDebugEnabled()) {
|
|
261
|
-
envVars["DEBUG"] = "1";
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Simplified system prompt handling
|
|
265
|
-
const appendSystemPrompt = [
|
|
266
|
-
context.globalInstructions,
|
|
267
|
-
context.workspaceInstructions,
|
|
268
|
-
]
|
|
269
|
-
.filter(Boolean)
|
|
270
|
-
.join("\n\n");
|
|
271
|
-
|
|
272
|
-
// Debug log the appendSystemPrompt being sent to Claude SDK
|
|
273
|
-
console.log(
|
|
274
|
-
`[ClaudeManager] Building appendSystemPrompt for conversation ${context.conversationId}:`,
|
|
275
|
-
{
|
|
276
|
-
hasGlobalInstructions: !!context.globalInstructions,
|
|
277
|
-
globalInstructionsLength: context.globalInstructions?.length ?? 0,
|
|
278
|
-
hasWorkspaceInstructions: !!context.workspaceInstructions,
|
|
279
|
-
workspaceInstructionsLength: context.workspaceInstructions?.length ?? 0,
|
|
280
|
-
appendSystemPromptLength: appendSystemPrompt.length,
|
|
281
|
-
appendSystemPromptPreview: appendSystemPrompt.slice(0, 150),
|
|
282
|
-
}
|
|
283
|
-
);
|
|
284
|
-
|
|
285
|
-
// Simplified tool restrictions for read-only mode
|
|
286
|
-
const disallowedTools: string[] =
|
|
287
|
-
context.permissionsMode === "read"
|
|
288
|
-
? [
|
|
289
|
-
"Write",
|
|
290
|
-
"Edit",
|
|
291
|
-
"MultiEdit",
|
|
292
|
-
"Bash",
|
|
293
|
-
"KillBash",
|
|
294
|
-
"NotebookEdit",
|
|
295
|
-
"ExitPlanMode",
|
|
296
|
-
]
|
|
297
|
-
: [];
|
|
298
|
-
|
|
299
|
-
if (disallowedTools.length) {
|
|
300
|
-
console.log("[ClaudeManager] Applied read-only mode tool restrictions");
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// Simplified MCP server configuration
|
|
304
|
-
let mcpServers: Record<string, any> | undefined;
|
|
305
|
-
if (config.mcpServers) {
|
|
306
|
-
mcpServers = expandEnv(config.mcpServers, { TOOL_TOKEN: toolToken });
|
|
307
|
-
console.log(
|
|
308
|
-
"[ClaudeManager] MCP servers configuration:",
|
|
309
|
-
JSON.stringify(mcpServers, null, 2)
|
|
310
|
-
);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// Create input stream for user messages (simplified message queueing)
|
|
314
|
-
const input = createUserMessageStream();
|
|
315
|
-
|
|
316
|
-
// Launch SDK with simplified configuration
|
|
317
|
-
const sdk = sdkQuery({
|
|
318
|
-
prompt: input.iterable,
|
|
319
|
-
options: {
|
|
320
|
-
//pathToClaudeCodeExecutable: cliPath,
|
|
321
|
-
cwd: workspacePath,
|
|
322
|
-
env: envVars,
|
|
323
|
-
model: context.model,
|
|
324
|
-
permissionMode: "bypassPermissions", // Runner handles permissions
|
|
325
|
-
resume:
|
|
326
|
-
config.sessionId || conversationData?.agentSessionId || undefined,
|
|
327
|
-
...(appendSystemPrompt
|
|
328
|
-
? {
|
|
329
|
-
systemPrompt: {
|
|
330
|
-
type: "preset" as const,
|
|
331
|
-
preset: "claude_code" as const,
|
|
332
|
-
append: appendSystemPrompt,
|
|
333
|
-
},
|
|
334
|
-
}
|
|
335
|
-
: {}),
|
|
336
|
-
...(disallowedTools.length ? { disallowedTools } : {}),
|
|
337
|
-
...(mcpServers ? { mcpServers } : {}),
|
|
338
|
-
...(isRunnerDebugEnabled()
|
|
339
|
-
? {
|
|
340
|
-
stderr: (data: string) => {
|
|
341
|
-
try {
|
|
342
|
-
console.log(`[Claude SDK] ${data}`);
|
|
343
|
-
} catch {}
|
|
344
|
-
},
|
|
345
|
-
}
|
|
346
|
-
: {}),
|
|
347
|
-
},
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
// Simplified conversation wrapper - reduced complexity while maintaining interface
|
|
351
|
-
const conversation = createConversationWrapper(
|
|
352
|
-
sdk,
|
|
353
|
-
input,
|
|
354
|
-
async (hadError: boolean, error?: any) => {
|
|
355
|
-
await this._finalizeConversation(context, hadError, error);
|
|
356
|
-
}
|
|
357
|
-
);
|
|
358
|
-
|
|
359
|
-
// Store conversation instance in context for reuse
|
|
360
|
-
context.conversation = conversation as any;
|
|
361
|
-
|
|
362
|
-
// Observe session id from first messages that include it
|
|
363
|
-
conversation.onSessionId(async (agentSessionId: string) => {
|
|
364
|
-
if (!agentSessionId) return;
|
|
365
|
-
const oldSessionId = context.agentSessionId;
|
|
366
|
-
if (oldSessionId !== agentSessionId) {
|
|
367
|
-
context.agentSessionId = agentSessionId;
|
|
368
|
-
context.status = "active";
|
|
369
|
-
await this.runner.notify("agentSessionId.changed", {
|
|
370
|
-
conversationId: context.conversationId,
|
|
371
|
-
conversationObjectType,
|
|
372
|
-
conversationObjectId,
|
|
373
|
-
oldAgentSessionId: oldSessionId,
|
|
374
|
-
newAgentSessionId: agentSessionId,
|
|
375
|
-
});
|
|
376
|
-
}
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
// Set up streaming message handler
|
|
380
|
-
const messageHandler = async (
|
|
381
|
-
message: SDKMessage,
|
|
382
|
-
sessionId: string | null
|
|
383
|
-
) => {
|
|
384
|
-
await this.handleStreamedMessage(context, message, sessionId);
|
|
385
|
-
|
|
386
|
-
// Heuristic: detect terminal system events and finalize
|
|
387
|
-
try {
|
|
388
|
-
if (message?.type === "system") {
|
|
389
|
-
const m: any = message;
|
|
390
|
-
const subtype = m.subtype || m?.message?.subtype || m?.event;
|
|
391
|
-
const exitSignals = [
|
|
392
|
-
"exit",
|
|
393
|
-
"exiting",
|
|
394
|
-
"session_end",
|
|
395
|
-
"conversation_end",
|
|
396
|
-
"process_exit",
|
|
397
|
-
"done",
|
|
398
|
-
"completed",
|
|
399
|
-
];
|
|
400
|
-
if (subtype && exitSignals.includes(String(subtype))) {
|
|
401
|
-
await this._finalizeConversation(context, false);
|
|
402
|
-
}
|
|
403
|
-
} else if (message?.type === "result") {
|
|
404
|
-
// Treat 'result' as an end-of-conversation signal for the SDK
|
|
405
|
-
try {
|
|
406
|
-
await this._finalizeConversation(context, false);
|
|
407
|
-
console.log(
|
|
408
|
-
"[ClaudeManager] Finalized conversation due to SDK 'result' message",
|
|
409
|
-
{
|
|
410
|
-
conversationId: context.conversationId,
|
|
411
|
-
agentSessionId: context.agentSessionId,
|
|
412
|
-
}
|
|
413
|
-
);
|
|
414
|
-
} catch (e) {
|
|
415
|
-
console.warn(
|
|
416
|
-
"[ClaudeManager] Error finalizing on 'result' message:",
|
|
417
|
-
e
|
|
418
|
-
);
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
} catch (e) {
|
|
422
|
-
console.warn("[ClaudeManager] finalize-on-system heuristic error:", e);
|
|
423
|
-
}
|
|
424
|
-
};
|
|
425
|
-
|
|
426
|
-
conversation.stream(messageHandler);
|
|
427
|
-
|
|
428
|
-
// Note: Error handling is done via process completion handler
|
|
429
|
-
// The Claude SDK doesn't have an onError method on conversations
|
|
430
|
-
|
|
431
|
-
// Send initial messages
|
|
432
|
-
try {
|
|
433
|
-
for (const message of initialMessages) {
|
|
434
|
-
const initialText = this.normalizeToText(message.content);
|
|
435
|
-
conversation.send({
|
|
436
|
-
type: "text",
|
|
437
|
-
text: initialText,
|
|
438
|
-
});
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
console.log(
|
|
442
|
-
`Started conversation for ${conversationObjectType} ${conversationObjectId} in workspace ${workspacePath}`
|
|
443
|
-
);
|
|
444
|
-
|
|
445
|
-
// Return the conversation context directly
|
|
446
|
-
return context;
|
|
447
|
-
} catch (error) {
|
|
448
|
-
// Handle startup errors
|
|
449
|
-
await this._handleConversationError(context, error as Error);
|
|
450
|
-
throw error;
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
async stopConversation(
|
|
455
|
-
agentSessionId: string,
|
|
456
|
-
context: ConversationContext,
|
|
457
|
-
isRunnerShutdown: boolean = false,
|
|
458
|
-
reason?: string
|
|
459
|
-
): Promise<void> {
|
|
460
|
-
if (context && context.conversation) {
|
|
461
|
-
context.status = "stopping";
|
|
462
|
-
|
|
463
|
-
try {
|
|
464
|
-
// Proactively send end notification to avoid missing it on abnormal exits
|
|
465
|
-
// Use provided reason, or fall back to 'runner_shutdown' if isRunnerShutdown is true
|
|
466
|
-
const finalReason =
|
|
467
|
-
reason || (isRunnerShutdown ? "runner_shutdown" : undefined);
|
|
468
|
-
await this._finalizeConversation(
|
|
469
|
-
context,
|
|
470
|
-
false,
|
|
471
|
-
undefined,
|
|
472
|
-
finalReason
|
|
473
|
-
).catch(() => {});
|
|
474
|
-
// Mark conversation as stopped BEFORE ending to prevent race conditions
|
|
475
|
-
context.status = "stopped";
|
|
476
|
-
|
|
477
|
-
// Properly end the conversation using the SDK
|
|
478
|
-
if (isClaudeConversation(context.conversation)) {
|
|
479
|
-
await context.conversation.end();
|
|
480
|
-
}
|
|
481
|
-
} catch (error) {
|
|
482
|
-
console.error(`Error ending conversation ${agentSessionId}:`, error);
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
// Clean up conversation reference
|
|
486
|
-
delete context.conversation;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
console.log(
|
|
490
|
-
`Stopped conversation ${agentSessionId} for ${context.conversationObjectType} ${context.conversationObjectId}`
|
|
491
|
-
);
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
async resumeConversation(
|
|
495
|
-
conversationObjectType: "Task" | "TaskPlan",
|
|
496
|
-
conversationObjectId: string,
|
|
497
|
-
agentSessionId: string,
|
|
498
|
-
config: ConversationConfig,
|
|
499
|
-
conversationData?: any,
|
|
500
|
-
resumeMessage?: string
|
|
501
|
-
): Promise<string> {
|
|
502
|
-
console.log(`[ClaudeManager] Resuming conversation ${agentSessionId}`);
|
|
503
|
-
|
|
504
|
-
// Resume is handled by starting a new conversation with the existing session ID
|
|
505
|
-
const context = await this.startConversation(
|
|
506
|
-
conversationObjectType,
|
|
507
|
-
conversationObjectId,
|
|
508
|
-
{ ...config, sessionId: agentSessionId },
|
|
509
|
-
[], // Don't send initial messages
|
|
510
|
-
conversationData
|
|
511
|
-
);
|
|
512
|
-
|
|
513
|
-
// After starting the conversation with the sessionId, we need to send a message
|
|
514
|
-
// to actually trigger the Claude process to continue
|
|
515
|
-
if (context.conversation && isClaudeConversation(context.conversation)) {
|
|
516
|
-
try {
|
|
517
|
-
// Use the provided resume message or default to system instruction
|
|
518
|
-
const messageToSend =
|
|
519
|
-
resumeMessage ||
|
|
520
|
-
"<system-instructions>Please continue</system-instructions>";
|
|
521
|
-
console.log(
|
|
522
|
-
`[ClaudeManager] Sending resume message to conversation ${agentSessionId}`
|
|
523
|
-
);
|
|
524
|
-
context.conversation.send({
|
|
525
|
-
type: "text",
|
|
526
|
-
text: messageToSend,
|
|
527
|
-
});
|
|
528
|
-
} catch (error) {
|
|
529
|
-
console.error(`[ClaudeManager] Error sending resume message:`, error);
|
|
530
|
-
}
|
|
531
|
-
} else {
|
|
532
|
-
console.warn(
|
|
533
|
-
"[ClaudeManager] Resume requested but conversation instance missing or incompatible"
|
|
534
|
-
);
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
return context.agentSessionId;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
private async _finalizeConversation(
|
|
541
|
-
context: ConversationContext,
|
|
542
|
-
hadError: boolean,
|
|
543
|
-
error?: any,
|
|
544
|
-
reason?: string
|
|
545
|
-
): Promise<void> {
|
|
546
|
-
// Ensure idempotency
|
|
547
|
-
if ((context as any)._finalized) return;
|
|
548
|
-
(context as any)._finalized = true;
|
|
549
|
-
|
|
550
|
-
try {
|
|
551
|
-
await this.runner.notify("conversation.end", {
|
|
552
|
-
conversationId: context.conversationId,
|
|
553
|
-
conversationObjectType: context.conversationObjectType,
|
|
554
|
-
conversationObjectId: context.conversationObjectId,
|
|
555
|
-
agentSessionId: context.agentSessionId,
|
|
556
|
-
isError: hadError,
|
|
557
|
-
errorMessage: error?.message,
|
|
558
|
-
reason: reason,
|
|
559
|
-
});
|
|
560
|
-
} catch (e) {
|
|
561
|
-
console.error("[ClaudeManager] Failed to notify conversation.end:", e);
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
// Clean up conversation from active conversations
|
|
565
|
-
try {
|
|
566
|
-
console.log(`[ClaudeManager] Removing conversation from active map:`, {
|
|
567
|
-
conversationId: context.conversationId,
|
|
568
|
-
agentSessionId: context.agentSessionId,
|
|
569
|
-
mapSizeBefore: this.runner.activeConversations_.size,
|
|
570
|
-
});
|
|
571
|
-
this.runner.activeConversations_.delete(context.conversationId);
|
|
572
|
-
statusLineManager.updateActiveCount(
|
|
573
|
-
this.runner.activeConversations_.size
|
|
574
|
-
);
|
|
575
|
-
} catch {}
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
async sendUserMessage(
|
|
579
|
-
conversationId: string,
|
|
580
|
-
content: any,
|
|
581
|
-
config?: ConversationConfig,
|
|
582
|
-
conversationObjectType?: "Task" | "TaskPlan",
|
|
583
|
-
conversationObjectId?: string,
|
|
584
|
-
conversation?: any,
|
|
585
|
-
_agentSessionIdOverride?: string
|
|
586
|
-
): Promise<void> {
|
|
587
|
-
console.log(`[ClaudeManager] sendUserMessage called with:`, {
|
|
588
|
-
conversationId,
|
|
589
|
-
conversationObjectType,
|
|
590
|
-
conversationObjectId,
|
|
591
|
-
hasConfig: !!config,
|
|
592
|
-
hasConversation: !!conversation,
|
|
593
|
-
activeConversations: this.runner.activeConversations_.size,
|
|
594
|
-
});
|
|
595
|
-
|
|
596
|
-
// Find by conversationId only
|
|
597
|
-
let context = this.runner.getConversationContext(conversationId);
|
|
598
|
-
console.log(`[ClaudeManager] Lookup by conversationId result:`, {
|
|
599
|
-
found: !!context,
|
|
600
|
-
conversationId: context?.conversationId,
|
|
601
|
-
agentSessionId: context?.agentSessionId,
|
|
602
|
-
});
|
|
603
|
-
|
|
604
|
-
if (!context && conversation) {
|
|
605
|
-
// Use provided conversation details
|
|
606
|
-
try {
|
|
607
|
-
const conversationDetails = conversation;
|
|
608
|
-
console.log(
|
|
609
|
-
`[ClaudeManager] Using provided config from RunnerMessage:`,
|
|
610
|
-
{
|
|
611
|
-
hasConfig: !!config,
|
|
612
|
-
hasRepository: !!config?.repository,
|
|
613
|
-
repositoryType: config?.repository?.type,
|
|
614
|
-
repositoryPath: config?.repository?.localPath,
|
|
615
|
-
hasTokens: !!(config?.accessToken || config?.githubToken),
|
|
616
|
-
hasMcpServers: !!config?.mcpServers,
|
|
617
|
-
}
|
|
618
|
-
);
|
|
619
|
-
|
|
620
|
-
// Use the config that was already prepared by TaskOrchestrator and sent in the RunnerMessage
|
|
621
|
-
const startConfig: ConversationConfig = {
|
|
622
|
-
anthropicApiKey:
|
|
623
|
-
config?.anthropicApiKey || process.env["ANTHROPIC_API_KEY"] || "",
|
|
624
|
-
systemPrompt: config?.systemPrompt,
|
|
625
|
-
workspaceId: conversationDetails.workspaceId,
|
|
626
|
-
...config, // Use the full config provided in the RunnerMessage
|
|
627
|
-
...(conversationDetails.agentSessionId
|
|
628
|
-
? { sessionId: conversationDetails.agentSessionId }
|
|
629
|
-
: {}),
|
|
630
|
-
};
|
|
631
|
-
|
|
632
|
-
// Start the SDK conversation (no initial messages); this attaches to existing session when provided
|
|
633
|
-
await this.startConversation(
|
|
634
|
-
conversationDetails.objectType,
|
|
635
|
-
conversationDetails.objectId,
|
|
636
|
-
startConfig,
|
|
637
|
-
[],
|
|
638
|
-
conversationDetails
|
|
639
|
-
);
|
|
640
|
-
|
|
641
|
-
// Refresh context after start/resume
|
|
642
|
-
context = this.runner.getConversationContext(conversationId);
|
|
643
|
-
} catch (error) {
|
|
644
|
-
console.error(`Failed to fetch conversation ${conversationId}:`, error);
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
if (!context) {
|
|
649
|
-
throw new Error(
|
|
650
|
-
`No active or fetchable conversation found for ${conversationId}`
|
|
651
|
-
);
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
try {
|
|
655
|
-
// Send immediately when a conversation instance exists; no need to wait for "active"
|
|
656
|
-
if (!context.conversation || !isClaudeConversation(context.conversation)) {
|
|
657
|
-
throw new Error(
|
|
658
|
-
`No conversation instance found for conversation ${context.conversationId}`
|
|
659
|
-
);
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
// Guard: Don't send messages if conversation is stopped or stopping
|
|
663
|
-
const conversationStatus = context.status as string;
|
|
664
|
-
if (
|
|
665
|
-
conversationStatus === "stopped" ||
|
|
666
|
-
conversationStatus === "stopping"
|
|
667
|
-
) {
|
|
668
|
-
console.warn(
|
|
669
|
-
`Attempted to send message to stopped/stopping conversation ${context.conversationId}`,
|
|
670
|
-
{
|
|
671
|
-
status: context.status,
|
|
672
|
-
conversationObjectType: context.conversationObjectType,
|
|
673
|
-
conversationObjectId: context.conversationObjectId,
|
|
674
|
-
}
|
|
675
|
-
);
|
|
676
|
-
return;
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
// Native message injection - SDK handles queueing and delivery
|
|
680
|
-
const normalizedText = this.normalizeToText(content);
|
|
681
|
-
|
|
682
|
-
if (isRunnerDebugEnabled()) {
|
|
683
|
-
console.log("[ClaudeManager] Normalized follow-up content", {
|
|
684
|
-
originalType: typeof content,
|
|
685
|
-
isArray: Array.isArray(content) || undefined,
|
|
686
|
-
normalizedPreview:
|
|
687
|
-
typeof normalizedText === "string"
|
|
688
|
-
? normalizedText.slice(0, 160)
|
|
689
|
-
: String(normalizedText).slice(0, 160),
|
|
690
|
-
});
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
context.conversation.send({
|
|
694
|
-
type: "text",
|
|
695
|
-
text: normalizedText,
|
|
696
|
-
});
|
|
697
|
-
|
|
698
|
-
// Update last activity timestamp
|
|
699
|
-
context.lastActivityAt = new Date();
|
|
700
|
-
|
|
701
|
-
console.log(
|
|
702
|
-
`Sent user message to conversation ${context.conversationId} (agentSessionId: ${context.agentSessionId}, status: ${context.status})`
|
|
703
|
-
);
|
|
704
|
-
} catch (error) {
|
|
705
|
-
// Handle errors properly
|
|
706
|
-
await this._handleConversationError(context, error as Error);
|
|
707
|
-
throw error; // Re-throw to maintain the same behavior
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
private async fetchGithubTokens(
|
|
712
|
-
workspaceId: string
|
|
713
|
-
): Promise<string | undefined> {
|
|
714
|
-
try {
|
|
715
|
-
const response = await fetch(
|
|
716
|
-
`${this.runner.config_.orchestratorUrl}/api/runner/tokens?workspaceId=${workspaceId}`,
|
|
717
|
-
{
|
|
718
|
-
method: "GET",
|
|
719
|
-
headers: {
|
|
720
|
-
Authorization: `Bearer ${process.env["NORTHFLARE_RUNNER_TOKEN"]}`,
|
|
721
|
-
},
|
|
722
|
-
}
|
|
723
|
-
);
|
|
724
|
-
|
|
725
|
-
if (!response.ok) {
|
|
726
|
-
console.error(`Failed to fetch GitHub tokens: ${response.status}`);
|
|
727
|
-
return undefined;
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
const data = (await response.json()) as { githubToken?: string };
|
|
731
|
-
return data.githubToken;
|
|
732
|
-
} catch (error) {
|
|
733
|
-
console.error("Error fetching GitHub tokens:", error);
|
|
734
|
-
return undefined;
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
private async _handleConversationError(
|
|
739
|
-
context: ConversationContext,
|
|
740
|
-
error: Error
|
|
741
|
-
): Promise<void> {
|
|
742
|
-
const errorType = this.classifyError(error);
|
|
743
|
-
|
|
744
|
-
// Notify orchestrator
|
|
745
|
-
await this.runner.notify("error.report", {
|
|
746
|
-
conversationId: context.conversationId,
|
|
747
|
-
conversationObjectType: context.conversationObjectType,
|
|
748
|
-
conversationObjectId: context.conversationObjectId,
|
|
749
|
-
agentSessionId: context.agentSessionId,
|
|
750
|
-
errorType,
|
|
751
|
-
message: error.message,
|
|
752
|
-
details: {
|
|
753
|
-
stack: error.stack,
|
|
754
|
-
timestamp: new Date(),
|
|
755
|
-
},
|
|
756
|
-
});
|
|
757
|
-
|
|
758
|
-
// Conversation continues on error - no automatic cleanup
|
|
759
|
-
console.error(
|
|
760
|
-
`Conversation error for ${context.conversationObjectType} ${context.conversationObjectId}:`,
|
|
761
|
-
error
|
|
762
|
-
);
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
private classifyError(error: Error): string {
|
|
766
|
-
if (error.message.includes("process exited")) {
|
|
767
|
-
return "process_exit";
|
|
768
|
-
} else if (error.message.includes("model")) {
|
|
769
|
-
return "model_error";
|
|
770
|
-
} else if (error.message.includes("tool")) {
|
|
771
|
-
return "tool_error";
|
|
772
|
-
} else if (error.message.includes("permission")) {
|
|
773
|
-
return "permission_error";
|
|
774
|
-
} else if (error.message.includes("timeout")) {
|
|
775
|
-
return "timeout_error";
|
|
776
|
-
}
|
|
777
|
-
return "unknown_error";
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
/**
|
|
781
|
-
* Normalize arbitrary content shapes into a plain string for the CLI
|
|
782
|
-
*/
|
|
783
|
-
private normalizeToText(value: any): string {
|
|
784
|
-
if (typeof value === "string") return value;
|
|
785
|
-
if (value == null) return "";
|
|
786
|
-
|
|
787
|
-
if (typeof value === "object") {
|
|
788
|
-
// Common simple shapes
|
|
789
|
-
if (typeof (value as any).text === "string") return (value as any).text;
|
|
790
|
-
if (
|
|
791
|
-
typeof (value as any).text === "object" &&
|
|
792
|
-
(value as any).text &&
|
|
793
|
-
typeof (value as any).text.text === "string"
|
|
794
|
-
)
|
|
795
|
-
return (value as any).text.text;
|
|
796
|
-
if (typeof (value as any).content === "string")
|
|
797
|
-
return (value as any).content;
|
|
798
|
-
|
|
799
|
-
// Array of blocks: [{ type: 'text', text: '...' }, ...]
|
|
800
|
-
if (Array.isArray(value)) {
|
|
801
|
-
const texts = value
|
|
802
|
-
.map((b) =>
|
|
803
|
-
b && b.type === "text" && typeof b.text === "string" ? b.text : null
|
|
804
|
-
)
|
|
805
|
-
.filter((t): t is string => !!t);
|
|
806
|
-
if (texts.length) return texts.join(" ");
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
// Nested message shapes
|
|
810
|
-
if (
|
|
811
|
-
typeof (value as any).message === "object" &&
|
|
812
|
-
(value as any).message &&
|
|
813
|
-
typeof (value as any).message.text === "string"
|
|
814
|
-
) {
|
|
815
|
-
return (value as any).message.text;
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
try {
|
|
820
|
-
return JSON.stringify(value);
|
|
821
|
-
} catch {
|
|
822
|
-
return String(value);
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
private async handleStreamedMessage(
|
|
827
|
-
context: ConversationContext,
|
|
828
|
-
message: SDKMessage,
|
|
829
|
-
sessionId: string | null
|
|
830
|
-
): Promise<void> {
|
|
831
|
-
/*
|
|
832
|
-
* SDK tool call payload reference (observed shapes)
|
|
833
|
-
*
|
|
834
|
-
* 1) Assistant tool call (tool_use)
|
|
835
|
-
* {
|
|
836
|
-
* "type": "assistant",
|
|
837
|
-
* "message": {
|
|
838
|
-
* "role": "assistant",
|
|
839
|
-
* "content": [
|
|
840
|
-
* { "type": "text", "text": "…optional text…" },
|
|
841
|
-
* { "type": "tool_use", "id": "toolu_01Nbv…", "name": "TodoWrite", "input": { ... } }
|
|
842
|
-
* ]
|
|
843
|
-
* },
|
|
844
|
-
* "session_id": "…"
|
|
845
|
-
* }
|
|
846
|
-
*
|
|
847
|
-
* 2) Tool result (often emitted as type 'user')
|
|
848
|
-
* {
|
|
849
|
-
* "type": "user",
|
|
850
|
-
* "message": {
|
|
851
|
-
* "role": "user",
|
|
852
|
-
* "content": [
|
|
853
|
-
* {
|
|
854
|
-
* "tool_use_id": "toolu_01E9R475…",
|
|
855
|
-
* "type": "tool_result",
|
|
856
|
-
* "content": "Todos have been modified successfully. …"
|
|
857
|
-
* }
|
|
858
|
-
* ]
|
|
859
|
-
* },
|
|
860
|
-
* "parent_tool_use_id": null,
|
|
861
|
-
* "session_id": "…",
|
|
862
|
-
* "uuid": "…"
|
|
863
|
-
* }
|
|
864
|
-
*
|
|
865
|
-
* Normalization (runner → server message.agent):
|
|
866
|
-
* - assistant tool_use → type: 'assistant', content: [{ toolCalls: [{ id, name, arguments }] }, …]
|
|
867
|
-
* - tool_result (any shape) → type: 'tool_result', subtype: 'tool_result', content:
|
|
868
|
-
* [ { type: 'tool_result', subtype: 'tool_result', tool_use_id, content } ]
|
|
869
|
-
*/
|
|
870
|
-
// Guard: Don't process messages if conversation is stopped or stopping
|
|
871
|
-
const status = context.status as string;
|
|
872
|
-
if (status === "stopped" || status === "stopping") {
|
|
873
|
-
console.log(
|
|
874
|
-
`Ignoring message for stopped/stopping conversation ${context.conversationId}`,
|
|
875
|
-
{
|
|
876
|
-
status: context.status,
|
|
877
|
-
messageType: message.type,
|
|
878
|
-
}
|
|
879
|
-
);
|
|
880
|
-
return;
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
try {
|
|
884
|
-
// High-level receipt log
|
|
885
|
-
console.log(
|
|
886
|
-
`Received streamed message for ${context.conversationObjectType} ${context.conversationObjectId}`,
|
|
887
|
-
{
|
|
888
|
-
type: message?.type,
|
|
889
|
-
}
|
|
890
|
-
);
|
|
891
|
-
|
|
892
|
-
// Raw SDK message diagnostics
|
|
893
|
-
try {
|
|
894
|
-
// Log the full raw SDK message safely (handles circular refs)
|
|
895
|
-
const safeStringify = (obj: any) => {
|
|
896
|
-
const seen = new WeakSet();
|
|
897
|
-
return JSON.stringify(
|
|
898
|
-
obj,
|
|
899
|
-
(key, value) => {
|
|
900
|
-
if (typeof value === "function") return undefined;
|
|
901
|
-
if (typeof value === "bigint") return String(value);
|
|
902
|
-
if (typeof value === "object" && value !== null) {
|
|
903
|
-
if (seen.has(value)) return "[Circular]";
|
|
904
|
-
seen.add(value);
|
|
905
|
-
}
|
|
906
|
-
return value;
|
|
907
|
-
},
|
|
908
|
-
2
|
|
909
|
-
);
|
|
910
|
-
};
|
|
911
|
-
|
|
912
|
-
console.log(
|
|
913
|
-
"[ClaudeManager] RAW SDK message FULL:",
|
|
914
|
-
safeStringify(message)
|
|
915
|
-
);
|
|
916
|
-
|
|
917
|
-
const summary = {
|
|
918
|
-
keys: Object.keys(message || {}),
|
|
919
|
-
hasMessage: !!(message as any)?.message,
|
|
920
|
-
contentType: typeof (message as any)?.content,
|
|
921
|
-
messageContentType: typeof (message as any)?.message?.content,
|
|
922
|
-
sessionId:
|
|
923
|
-
(message as any)?.session_id || (message as any)?.sessionId || null,
|
|
924
|
-
};
|
|
925
|
-
console.log("[ClaudeManager] RAW SDK message summary:", summary);
|
|
926
|
-
if ((message as any)?.content !== undefined) {
|
|
927
|
-
console.log(
|
|
928
|
-
"[ClaudeManager] RAW SDK content:",
|
|
929
|
-
(message as any).content
|
|
930
|
-
);
|
|
931
|
-
}
|
|
932
|
-
if ((message as any)?.message?.content !== undefined) {
|
|
933
|
-
console.log(
|
|
934
|
-
"[ClaudeManager] RAW SDK nested content:",
|
|
935
|
-
(message as any).message.content
|
|
936
|
-
);
|
|
937
|
-
}
|
|
938
|
-
} catch (e) {
|
|
939
|
-
console.warn("[ClaudeManager] Failed to log raw SDK message:", e);
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
// Build structured content based on message type
|
|
943
|
-
let messageType: string = message.type;
|
|
944
|
-
let subtype: string | undefined;
|
|
945
|
-
let structuredContent: any = {};
|
|
946
|
-
let isError = false;
|
|
947
|
-
let skipSend = false;
|
|
948
|
-
let metadata: any = null;
|
|
949
|
-
|
|
950
|
-
// Extract parent_tool_use_id if present (for all message types)
|
|
951
|
-
const msgAsAny = message as any;
|
|
952
|
-
if (msgAsAny.parent_tool_use_id) {
|
|
953
|
-
metadata = {
|
|
954
|
-
parent_tool_use_id: msgAsAny.parent_tool_use_id,
|
|
955
|
-
};
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
// Extract content based on message type
|
|
959
|
-
switch (message.type) {
|
|
960
|
-
case "assistant": {
|
|
961
|
-
const assistantMsg = message as any;
|
|
962
|
-
const blocks =
|
|
963
|
-
assistantMsg?.message?.content || assistantMsg?.content || [];
|
|
964
|
-
const textContent = Array.isArray(blocks)
|
|
965
|
-
? blocks
|
|
966
|
-
.filter(
|
|
967
|
-
(b: any) =>
|
|
968
|
-
b && b.type === "text" && typeof b.text === "string"
|
|
969
|
-
)
|
|
970
|
-
.map((b: any) => b.text)
|
|
971
|
-
.join("")
|
|
972
|
-
: "";
|
|
973
|
-
const toolCalls = Array.isArray(blocks)
|
|
974
|
-
? blocks
|
|
975
|
-
.filter((b: any) => b && b.type === "tool_use")
|
|
976
|
-
.map((b: any) => ({
|
|
977
|
-
id: b.id,
|
|
978
|
-
name: b.name,
|
|
979
|
-
arguments: b.input,
|
|
980
|
-
}))
|
|
981
|
-
: undefined;
|
|
982
|
-
|
|
983
|
-
structuredContent = {
|
|
984
|
-
...(textContent ? { text: textContent } : {}),
|
|
985
|
-
...(toolCalls && toolCalls.length ? { toolCalls } : {}),
|
|
986
|
-
timestamp: new Date().toISOString(),
|
|
987
|
-
};
|
|
988
|
-
break;
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
case "thinking" as any: {
|
|
992
|
-
messageType = "thinking";
|
|
993
|
-
subtype = undefined;
|
|
994
|
-
const thinkingMsg = message as any;
|
|
995
|
-
structuredContent = [
|
|
996
|
-
{
|
|
997
|
-
type: "thinking",
|
|
998
|
-
thinking: thinkingMsg.content || "",
|
|
999
|
-
text: thinkingMsg.content || "",
|
|
1000
|
-
timestamp: new Date().toISOString(),
|
|
1001
|
-
},
|
|
1002
|
-
];
|
|
1003
|
-
break;
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
case "tool_use" as any: {
|
|
1007
|
-
// Tool call request - map to assistant
|
|
1008
|
-
messageType = "assistant";
|
|
1009
|
-
subtype = "tool_use";
|
|
1010
|
-
const toolUseMsg = message as any;
|
|
1011
|
-
structuredContent = {
|
|
1012
|
-
toolCalls: [
|
|
1013
|
-
{
|
|
1014
|
-
id: toolUseMsg.id,
|
|
1015
|
-
name: toolUseMsg.name,
|
|
1016
|
-
arguments: toolUseMsg.input,
|
|
1017
|
-
},
|
|
1018
|
-
],
|
|
1019
|
-
timestamp: new Date().toISOString(),
|
|
1020
|
-
};
|
|
1021
|
-
break;
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
case "tool_result" as any: {
|
|
1025
|
-
// Tool execution result - normalize to v1-style tool_result blocks
|
|
1026
|
-
// so the server persists as messageType: tool_call_result with correct content
|
|
1027
|
-
messageType = "tool_result";
|
|
1028
|
-
subtype = "tool_result";
|
|
1029
|
-
const toolResultMsg = message as any;
|
|
1030
|
-
structuredContent = [
|
|
1031
|
-
{
|
|
1032
|
-
type: "tool_result",
|
|
1033
|
-
subtype: "tool_result",
|
|
1034
|
-
tool_use_id: toolResultMsg.tool_use_id,
|
|
1035
|
-
content: toolResultMsg.content, // Keep content as native (array or string)
|
|
1036
|
-
timestamp: new Date().toISOString(),
|
|
1037
|
-
},
|
|
1038
|
-
];
|
|
1039
|
-
break;
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
case "result": {
|
|
1043
|
-
const resultMsg = message as any;
|
|
1044
|
-
structuredContent = {
|
|
1045
|
-
text: resultMsg.content || resultMsg.result || "",
|
|
1046
|
-
timestamp: new Date().toISOString(),
|
|
1047
|
-
};
|
|
1048
|
-
break;
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
case "user": {
|
|
1052
|
-
const userMsg = message as any;
|
|
1053
|
-
|
|
1054
|
-
// Prefer nested message.content if present (SDK shape), fallback to top-level content
|
|
1055
|
-
const blocks =
|
|
1056
|
-
(userMsg && userMsg.message && userMsg.message.content) ||
|
|
1057
|
-
userMsg?.content ||
|
|
1058
|
-
[];
|
|
1059
|
-
|
|
1060
|
-
if (Array.isArray(blocks)) {
|
|
1061
|
-
const hasToolResult = blocks.some(
|
|
1062
|
-
(b: any) => b && typeof b === "object" && b.type === "tool_result"
|
|
1063
|
-
);
|
|
1064
|
-
|
|
1065
|
-
if (hasToolResult) {
|
|
1066
|
-
// Normalize tool_result blocks to v1-style
|
|
1067
|
-
messageType = "tool_result";
|
|
1068
|
-
subtype = "tool_result";
|
|
1069
|
-
structuredContent = blocks
|
|
1070
|
-
.filter((b: any) => b && b.type === "tool_result")
|
|
1071
|
-
.map((b: any) => ({
|
|
1072
|
-
type: "tool_result",
|
|
1073
|
-
subtype: "tool_result",
|
|
1074
|
-
tool_use_id: b.tool_use_id || b.toolUseId || b.id,
|
|
1075
|
-
content: b.content, // Keep content as native (array or string)
|
|
1076
|
-
}));
|
|
1077
|
-
} else {
|
|
1078
|
-
// Treat as plain text content by joining text blocks
|
|
1079
|
-
const textContent = blocks
|
|
1080
|
-
.filter(
|
|
1081
|
-
(b: any) =>
|
|
1082
|
-
b && b.type === "text" && typeof b.text === "string"
|
|
1083
|
-
)
|
|
1084
|
-
.map((b: any) => b.text)
|
|
1085
|
-
.join("");
|
|
1086
|
-
structuredContent = {
|
|
1087
|
-
text: textContent,
|
|
1088
|
-
timestamp: new Date().toISOString(),
|
|
1089
|
-
};
|
|
1090
|
-
|
|
1091
|
-
// Check if this is a subagent prompt (Task tool input being sent to subagent)
|
|
1092
|
-
// These have parent_tool_use_id but are NOT tool_result messages
|
|
1093
|
-
if (userMsg.parent_tool_use_id) {
|
|
1094
|
-
console.log(
|
|
1095
|
-
"[ClaudeManager] Detected subagent prompt message (parent_tool_use_id present, no tool_result)",
|
|
1096
|
-
{
|
|
1097
|
-
parent_tool_use_id: userMsg.parent_tool_use_id,
|
|
1098
|
-
}
|
|
1099
|
-
);
|
|
1100
|
-
messageType = "assistant"; // Change from "user" to "assistant"
|
|
1101
|
-
subtype = "subagent_prompt";
|
|
1102
|
-
// metadata already set above with parent_tool_use_id
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
// If content array only contains empty objects, skip sending
|
|
1107
|
-
if (
|
|
1108
|
-
Array.isArray(structuredContent) &&
|
|
1109
|
-
structuredContent.length > 0 &&
|
|
1110
|
-
structuredContent.every(
|
|
1111
|
-
(it: any) =>
|
|
1112
|
-
!it || typeof it !== "object" || Object.keys(it).length === 0
|
|
1113
|
-
)
|
|
1114
|
-
) {
|
|
1115
|
-
console.log(
|
|
1116
|
-
"[ClaudeManager] Skipping empty 'user' message with only empty objects from SDK"
|
|
1117
|
-
);
|
|
1118
|
-
skipSend = true;
|
|
1119
|
-
}
|
|
1120
|
-
} else if (typeof userMsg?.content === "string") {
|
|
1121
|
-
// Attempt to parse JSON arrays (common for tool_result payloads)
|
|
1122
|
-
const text = userMsg.content;
|
|
1123
|
-
try {
|
|
1124
|
-
const parsed = JSON.parse(text);
|
|
1125
|
-
if (Array.isArray(parsed)) {
|
|
1126
|
-
const hasToolResult = parsed.some(
|
|
1127
|
-
(item: any) =>
|
|
1128
|
-
item &&
|
|
1129
|
-
typeof item === "object" &&
|
|
1130
|
-
item.type === "tool_result"
|
|
1131
|
-
);
|
|
1132
|
-
if (hasToolResult) {
|
|
1133
|
-
messageType = "tool_result";
|
|
1134
|
-
subtype = "tool_result";
|
|
1135
|
-
structuredContent = parsed;
|
|
1136
|
-
} else {
|
|
1137
|
-
structuredContent = {
|
|
1138
|
-
text,
|
|
1139
|
-
timestamp: new Date().toISOString(),
|
|
1140
|
-
};
|
|
1141
|
-
}
|
|
1142
|
-
} else {
|
|
1143
|
-
structuredContent = {
|
|
1144
|
-
text,
|
|
1145
|
-
timestamp: new Date().toISOString(),
|
|
1146
|
-
};
|
|
1147
|
-
}
|
|
1148
|
-
} catch {
|
|
1149
|
-
// Not JSON - treat as plain text
|
|
1150
|
-
structuredContent = { text, timestamp: new Date().toISOString() };
|
|
1151
|
-
}
|
|
1152
|
-
} else {
|
|
1153
|
-
// Other object content - preserve as is
|
|
1154
|
-
structuredContent = userMsg?.content || {};
|
|
1155
|
-
}
|
|
1156
|
-
break;
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
case "system": {
|
|
1160
|
-
const systemMsg = message as any;
|
|
1161
|
-
const subtype = systemMsg.subtype || "system";
|
|
1162
|
-
const model = systemMsg.model || systemMsg?.message?.model;
|
|
1163
|
-
const permissionMode =
|
|
1164
|
-
systemMsg.permissionMode || systemMsg?.message?.permissionMode;
|
|
1165
|
-
const summary = [
|
|
1166
|
-
subtype && `[${subtype}]`,
|
|
1167
|
-
model && `model=${model}`,
|
|
1168
|
-
permissionMode && `perm=${permissionMode}`,
|
|
1169
|
-
]
|
|
1170
|
-
.filter(Boolean)
|
|
1171
|
-
.join(" ");
|
|
1172
|
-
structuredContent = {
|
|
1173
|
-
text: summary || "",
|
|
1174
|
-
timestamp: new Date().toISOString(),
|
|
1175
|
-
};
|
|
1176
|
-
break;
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
case "error" as any: {
|
|
1180
|
-
const errorMsg = message as any;
|
|
1181
|
-
messageType = "system";
|
|
1182
|
-
subtype = "error";
|
|
1183
|
-
isError = true;
|
|
1184
|
-
structuredContent = {
|
|
1185
|
-
text: errorMsg.message || errorMsg.error || "Unknown error",
|
|
1186
|
-
errorType: errorMsg.error_type || "unknown",
|
|
1187
|
-
errorDetails: {
|
|
1188
|
-
stack: errorMsg.stack,
|
|
1189
|
-
code: errorMsg.code,
|
|
1190
|
-
context: errorMsg,
|
|
1191
|
-
},
|
|
1192
|
-
timestamp: new Date().toISOString(),
|
|
1193
|
-
};
|
|
1194
|
-
break;
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
default: {
|
|
1198
|
-
// Unknown message type - log and send as assistant
|
|
1199
|
-
const unknownMsg = message as any;
|
|
1200
|
-
console.warn(`Unknown message type: ${unknownMsg.type}`, message);
|
|
1201
|
-
messageType = "assistant";
|
|
1202
|
-
structuredContent = {
|
|
1203
|
-
text: JSON.stringify(message),
|
|
1204
|
-
timestamp: new Date().toISOString(),
|
|
1205
|
-
};
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
// Generate a unique message ID
|
|
1210
|
-
const messageId = `${context.agentSessionId}-${Date.now()}-${Math.random()
|
|
1211
|
-
.toString(36)
|
|
1212
|
-
.substr(2, 9)}`;
|
|
1213
|
-
|
|
1214
|
-
// Send agent message to orchestrator with structured content
|
|
1215
|
-
// Skip if conversation is stopping/stopped to avoid race conditions
|
|
1216
|
-
const currentStatus = context.status as string;
|
|
1217
|
-
if (currentStatus !== "stopped" && currentStatus !== "stopping") {
|
|
1218
|
-
if (skipSend) {
|
|
1219
|
-
console.log(
|
|
1220
|
-
"[ClaudeManager] Not sending message.agent due to skipSend=true"
|
|
1221
|
-
);
|
|
1222
|
-
return;
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
const payload: any = {
|
|
1226
|
-
conversationId: context.conversationId,
|
|
1227
|
-
conversationObjectType: context.conversationObjectType,
|
|
1228
|
-
conversationObjectId: context.conversationObjectId,
|
|
1229
|
-
agentSessionId: context.agentSessionId,
|
|
1230
|
-
type: messageType,
|
|
1231
|
-
subtype,
|
|
1232
|
-
content: Array.isArray(structuredContent)
|
|
1233
|
-
? structuredContent
|
|
1234
|
-
: [structuredContent],
|
|
1235
|
-
messageId,
|
|
1236
|
-
isError,
|
|
1237
|
-
};
|
|
1238
|
-
|
|
1239
|
-
// Add metadata if present
|
|
1240
|
-
if (metadata) {
|
|
1241
|
-
payload.metadata = metadata;
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
try {
|
|
1245
|
-
console.log("[ClaudeManager] Sending message.agent payload:", {
|
|
1246
|
-
type: payload.type,
|
|
1247
|
-
subtype: payload.subtype,
|
|
1248
|
-
contentPreview: Array.isArray(payload.content)
|
|
1249
|
-
? payload.content.slice(0, 1)
|
|
1250
|
-
: payload.content,
|
|
1251
|
-
});
|
|
1252
|
-
} catch {}
|
|
1253
|
-
|
|
1254
|
-
await this.runner.notify("message.agent", payload as any);
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
// Tool calls are now handled directly by Claude through the MCP server
|
|
1258
|
-
// We just log that we saw them but don't intercept or process them
|
|
1259
|
-
if (
|
|
1260
|
-
structuredContent.toolCalls &&
|
|
1261
|
-
structuredContent.toolCalls.length > 0
|
|
1262
|
-
) {
|
|
1263
|
-
console.log(
|
|
1264
|
-
`Claude is making ${structuredContent.toolCalls.length} tool call(s) via MCP`,
|
|
1265
|
-
{
|
|
1266
|
-
conversationObjectId: context.conversationObjectId,
|
|
1267
|
-
toolNames: structuredContent.toolCalls.map((tc: any) => tc.name),
|
|
1268
|
-
}
|
|
1269
|
-
);
|
|
1270
|
-
}
|
|
1271
|
-
} catch (error) {
|
|
1272
|
-
// Check if this is a transport error due to stopped conversation
|
|
1273
|
-
const errorMessage =
|
|
1274
|
-
error instanceof Error ? error.message : String(error);
|
|
1275
|
-
const isTransportError =
|
|
1276
|
-
errorMessage.includes("Cannot read properties of undefined") ||
|
|
1277
|
-
errorMessage.includes("stdout") ||
|
|
1278
|
-
errorMessage.includes("transport");
|
|
1279
|
-
|
|
1280
|
-
const statusCheck = context.status as string;
|
|
1281
|
-
if (
|
|
1282
|
-
isTransportError &&
|
|
1283
|
-
(statusCheck === "stopped" || statusCheck === "stopping")
|
|
1284
|
-
) {
|
|
1285
|
-
// This is expected when conversation is stopped - just log it
|
|
1286
|
-
console.log(
|
|
1287
|
-
`Transport error for stopped/stopping conversation ${context.conversationId} (expected):`,
|
|
1288
|
-
errorMessage
|
|
1289
|
-
);
|
|
1290
|
-
return;
|
|
1291
|
-
}
|
|
1292
|
-
|
|
1293
|
-
console.error(
|
|
1294
|
-
`Error handling streamed message for ${context.conversationObjectType} ${context.conversationObjectId}:`,
|
|
1295
|
-
error
|
|
1296
|
-
);
|
|
1297
|
-
await this._handleConversationError(context, error as Error);
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
// Simplified helper functions for SDK integration
|
|
1303
|
-
|
|
1304
|
-
type UserMessageStream = {
|
|
1305
|
-
iterable: AsyncIterable<any>;
|
|
1306
|
-
enqueue: (msg: any) => void;
|
|
1307
|
-
close: () => void;
|
|
1308
|
-
};
|
|
1309
|
-
|
|
1310
|
-
function createUserMessageStream(): UserMessageStream {
|
|
1311
|
-
const queue: any[] = [];
|
|
1312
|
-
let resolver: (() => void) | null = null;
|
|
1313
|
-
let done = false;
|
|
1314
|
-
|
|
1315
|
-
async function* iterator() {
|
|
1316
|
-
while (true) {
|
|
1317
|
-
if (queue.length > 0) {
|
|
1318
|
-
const value = queue.shift();
|
|
1319
|
-
yield value;
|
|
1320
|
-
continue;
|
|
1321
|
-
}
|
|
1322
|
-
if (done) return;
|
|
1323
|
-
await new Promise<void>((resolve) => (resolver = resolve));
|
|
1324
|
-
resolver = null;
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
return {
|
|
1329
|
-
iterable: iterator(),
|
|
1330
|
-
enqueue: (msg: any) => {
|
|
1331
|
-
if (done) return;
|
|
1332
|
-
queue.push(msg);
|
|
1333
|
-
if (resolver) {
|
|
1334
|
-
const r = resolver;
|
|
1335
|
-
resolver = null;
|
|
1336
|
-
r();
|
|
1337
|
-
}
|
|
1338
|
-
},
|
|
1339
|
-
close: () => {
|
|
1340
|
-
done = true;
|
|
1341
|
-
if (resolver) {
|
|
1342
|
-
const r = resolver;
|
|
1343
|
-
resolver = null;
|
|
1344
|
-
r();
|
|
1345
|
-
}
|
|
1346
|
-
},
|
|
1347
|
-
};
|
|
1348
|
-
}
|
|
1349
|
-
|
|
1350
|
-
function createConversationWrapper(
|
|
1351
|
-
sdk: any,
|
|
1352
|
-
input: UserMessageStream,
|
|
1353
|
-
onComplete?: (hadError: boolean, error?: any) => Promise<void> | void
|
|
1354
|
-
) {
|
|
1355
|
-
let onSessionIdCb: ((id: string) => void) | null = null;
|
|
1356
|
-
let observedSessionId: string | null = null;
|
|
1357
|
-
let startedReader = false;
|
|
1358
|
-
|
|
1359
|
-
function toSdkUserMessage(text: string) {
|
|
1360
|
-
return {
|
|
1361
|
-
type: "user",
|
|
1362
|
-
session_id: observedSessionId || "",
|
|
1363
|
-
parent_tool_use_id: null,
|
|
1364
|
-
message: {
|
|
1365
|
-
role: "user",
|
|
1366
|
-
content: text,
|
|
1367
|
-
},
|
|
1368
|
-
};
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
return {
|
|
1372
|
-
send(payload: { type: string; text: string }) {
|
|
1373
|
-
const text = payload?.text ?? "";
|
|
1374
|
-
input.enqueue(toSdkUserMessage(text));
|
|
1375
|
-
},
|
|
1376
|
-
async end() {
|
|
1377
|
-
try {
|
|
1378
|
-
input.close();
|
|
1379
|
-
} finally {
|
|
1380
|
-
// Simplified process cleanup
|
|
1381
|
-
try {
|
|
1382
|
-
if (sdk?.abortController) {
|
|
1383
|
-
sdk.abortController.abort();
|
|
1384
|
-
}
|
|
1385
|
-
} catch {}
|
|
1386
|
-
}
|
|
1387
|
-
},
|
|
1388
|
-
onSessionId(cb: (id: string) => void) {
|
|
1389
|
-
onSessionIdCb = cb;
|
|
1390
|
-
},
|
|
1391
|
-
stream(
|
|
1392
|
-
handler: (message: SDKMessage, sessionId: string | null) => Promise<void>
|
|
1393
|
-
) {
|
|
1394
|
-
if (startedReader) return;
|
|
1395
|
-
startedReader = true;
|
|
1396
|
-
(async () => {
|
|
1397
|
-
try {
|
|
1398
|
-
for await (const msg of sdk) {
|
|
1399
|
-
// Simplified session ID extraction
|
|
1400
|
-
const sid = (msg && (msg.session_id || msg.sessionId)) || null;
|
|
1401
|
-
if (sid && sid !== observedSessionId) {
|
|
1402
|
-
observedSessionId = sid;
|
|
1403
|
-
if (onSessionIdCb) onSessionIdCb(sid);
|
|
1404
|
-
}
|
|
1405
|
-
await handler(msg, sid);
|
|
1406
|
-
}
|
|
1407
|
-
// Normal completion
|
|
1408
|
-
if (onComplete) await onComplete(false);
|
|
1409
|
-
} catch (e) {
|
|
1410
|
-
// Error completion
|
|
1411
|
-
if (onComplete) await onComplete(true, e);
|
|
1412
|
-
}
|
|
1413
|
-
})();
|
|
1414
|
-
},
|
|
1415
|
-
};
|
|
1416
|
-
}
|
|
1417
|
-
function isClaudeConversation(
|
|
1418
|
-
conversation: ConversationContext["conversation"]
|
|
1419
|
-
): conversation is ClaudeConversation {
|
|
1420
|
-
return (
|
|
1421
|
-
!!conversation &&
|
|
1422
|
-
typeof (conversation as ClaudeConversation).send === "function" &&
|
|
1423
|
-
typeof (conversation as ClaudeConversation).end === "function"
|
|
1424
|
-
);
|
|
1425
|
-
}
|