@northflare/runner 0.0.31 → 0.0.32

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