@northflare/runner 0.0.30 → 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 +17 -17
  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,2032 +0,0 @@
1
- /**
2
- * NorthflareAgentManager - Manages conversations using the @northflare/agent SDK
3
- *
4
- * Mirrors the existing Claude integration but targets OpenRouter-style models
5
- * via the Northflare Agent SDK. Lifecycle, repository handling, and message
6
- * normalization stay aligned with the existing managers so downstream
7
- * interfaces remain compatible.
8
- */
9
- import { query as sdkQuery } from "@northflare/agent";
10
- import { createGroq, groq as groqProvider } from "@ai-sdk/groq";
11
- import { createOpenRouter } from "@openrouter/ai-sdk-provider";
12
- import { statusLineManager } from "../utils/status-line.js";
13
- import { createScopedConsole } from "../utils/console.js";
14
- import { expandEnv } from "../utils/expand-env.js";
15
- import { isDebugEnabledFor } from "../utils/debug.js";
16
- import jwt from "jsonwebtoken";
17
- import fs from "fs/promises";
18
- import path from "path";
19
- import { simpleGit } from "simple-git";
20
- const console = createScopedConsole(isDebugEnabledFor("manager") ? "manager" : "sdk");
21
- function buildSystemPromptWithNorthflare(basePrompt, northflarePrompt) {
22
- const parts = [northflarePrompt, basePrompt]
23
- .filter((p) => typeof p === "string" && p.trim().length > 0)
24
- .map((p) => p.trim());
25
- if (parts.length === 0)
26
- return undefined;
27
- return parts.join("\n\n");
28
- }
29
- export class NorthflareAgentManager {
30
- runner;
31
- repositoryManager;
32
- submitToolUseIds = new Map(); // conversationId -> Set of submit tool_use_ids
33
- constructor(runner, repositoryManager) {
34
- this.runner = runner;
35
- this.repositoryManager = repositoryManager;
36
- // Log debug mode status
37
- if (isDebugEnabledFor("sdk")) {
38
- console.log("[NorthflareAgentManager] DEBUG MODE ENABLED - Northflare Agent SDK will log verbose output");
39
- }
40
- // Note: MCP host configuration is passed from orchestrator
41
- // Runner does not define its own MCP tools
42
- this.setupInternalMcpServer();
43
- }
44
- setupInternalMcpServer() {
45
- // Runner does not define its own MCP tools
46
- // All MCP tool configuration is passed from orchestrator in conversation config
47
- }
48
- buildSubAgentConfig(subAgentTypes) {
49
- const enabled = subAgentTypes?.["enabled"] !== false;
50
- const allowInherit = subAgentTypes?.["inherit"] !== false;
51
- if (!enabled) {
52
- return { enabled: false, agents: {} };
53
- }
54
- const agents = {};
55
- if (allowInherit) {
56
- agents["inherit"] = {
57
- description: "Use the main task model and toolset.",
58
- prompt: "",
59
- model: "inherit",
60
- };
61
- }
62
- const RESERVED = new Set(["inherit", "enabled"]);
63
- Object.entries(subAgentTypes || {}).forEach(([name, value]) => {
64
- if (RESERVED.has(name))
65
- return;
66
- if (!value || typeof value !== "object")
67
- return;
68
- const description = typeof value.description === "string" &&
69
- value.description.trim().length
70
- ? value.description.trim()
71
- : `Use agentType \"${name}\" when its specialization fits the task.`;
72
- const prompt = typeof value.instructions === "string"
73
- ? value.instructions.trim()
74
- : "";
75
- const model = typeof value.model === "string" &&
76
- value.model.trim().length
77
- ? value.model.trim()
78
- : "inherit";
79
- agents[name] = {
80
- description,
81
- prompt,
82
- model,
83
- };
84
- });
85
- if (Object.keys(agents).length === 0) {
86
- return { enabled: false, agents: {} };
87
- }
88
- return { enabled: true, agents };
89
- }
90
- async startConversation(conversationObjectType, conversationObjectId, config, initialMessages, conversationData, provider = "openrouter") {
91
- // Returns conversation context
92
- // Greenfield: conversationData.id is required as the authoritative DB conversation ID
93
- if (!conversationData?.id) {
94
- throw new Error("startConversation requires conversationData with a valid conversation.id");
95
- }
96
- // Use sessionId from config if resuming, or agentSessionId from conversationData if available
97
- const agentSessionId = config.sessionId || conversationData.agentSessionId || "";
98
- const conversationId = conversationData.id;
99
- const rawModel = conversationData?.model ||
100
- config?.model ||
101
- config?.defaultModel;
102
- if (!rawModel) {
103
- throw new Error("Model is required for Northflare conversations (provide conversationData.model, config.model, or config.defaultModel).");
104
- }
105
- const model = this.normalizeModel(rawModel);
106
- const useWorktrees = config.useWorktrees !== false;
107
- const pendingSummary = this.runner.consumePendingConversationSummary(conversationId);
108
- const rawSummary = typeof conversationData.summary === "string"
109
- ? conversationData.summary
110
- : pendingSummary;
111
- const normalizedSummary = typeof rawSummary === "string"
112
- ? rawSummary.replace(/\s+/g, " ").trim()
113
- : null;
114
- const context = {
115
- conversationId,
116
- agentSessionId, // Will be updated by onSessionId callback for new conversations
117
- conversationObjectType,
118
- conversationObjectId,
119
- taskId: conversationObjectType === "Task" ? conversationObjectId : undefined,
120
- workspaceId: config.workspaceId,
121
- status: "starting",
122
- config,
123
- startedAt: new Date(),
124
- lastActivityAt: new Date(),
125
- // Add conversation details if provided
126
- model,
127
- summary: normalizedSummary,
128
- globalInstructions: conversationData?.globalInstructions || "",
129
- workspaceInstructions: conversationData?.workspaceInstructions || "",
130
- permissionsMode: conversationData?.permissionsMode || "all",
131
- provider,
132
- };
133
- // Store with conversation.id as the key
134
- this.runner.activeConversations_.set(conversationId, context);
135
- console.log(`[NorthflareAgentManager] Stored conversation context:`, {
136
- conversationId,
137
- agentSessionId: context.agentSessionId,
138
- conversationObjectType: context.conversationObjectType,
139
- conversationObjectId: context.conversationObjectId,
140
- mapSize: this.runner.activeConversations_.size,
141
- allKeys: Array.from(this.runner.activeConversations_.keys()),
142
- });
143
- const workspaceId = config.workspaceId;
144
- // Checkout repository if specified
145
- let workspacePath;
146
- // Check if this is a local workspace by looking for runnerRepoPath in config
147
- if (config.runnerRepoPath) {
148
- // Local workspace - use the provided path directly
149
- workspacePath = config.runnerRepoPath;
150
- console.log(`Using local workspace path: ${workspacePath}`);
151
- // For task conversations, isolate the worktree/branch when the repo has a .git directory
152
- if (conversationObjectType === "Task" && workspaceId) {
153
- const hasGitDir = await fs
154
- .access(path.join(workspacePath, ".git"))
155
- .then(() => true)
156
- .catch(() => false);
157
- if (hasGitDir && useWorktrees) {
158
- const repoUrl = config.repository?.url || `file://${workspacePath}`;
159
- const baseBranch = config.repository?.branch || "main";
160
- const taskHandle = await this.repositoryManager.createTaskWorktree(conversationObjectId, workspaceId, repoUrl, baseBranch, config.githubToken);
161
- workspacePath = taskHandle.worktreePath;
162
- context.taskHandle = taskHandle;
163
- }
164
- }
165
- }
166
- else if (conversationObjectType === "Task" &&
167
- config.repository &&
168
- workspaceId &&
169
- useWorktrees) {
170
- // Use task-specific worktree for all task conversations (keeps shared workspace checkout clean)
171
- console.log(`[NorthflareAgentManager] Creating task worktree with repository config:`, {
172
- conversationObjectId,
173
- workspaceId,
174
- repository: config.repository,
175
- hasUrl: !!config.repository.url,
176
- url: config.repository.url,
177
- branch: config.repository.branch,
178
- type: config.repository.type,
179
- });
180
- // Check if repository.url is missing
181
- if (!config.repository.url) {
182
- throw new Error(`Repository URL is missing in config for task ${conversationObjectId}. Repository config: ${JSON.stringify(config.repository)}`);
183
- }
184
- const taskHandle = await this.repositoryManager.createTaskWorktree(conversationObjectId, workspaceId, config.repository.url, config.repository.branch, config.githubToken);
185
- workspacePath = taskHandle.worktreePath;
186
- // Store task handle information in context for later use
187
- context.taskHandle = taskHandle;
188
- }
189
- else if (config.repository && workspaceId) {
190
- // Check if it's a local repository
191
- if (config.repository.type === "local" && config.repository.localPath) {
192
- // Use the local path directly
193
- workspacePath = config.repository.localPath;
194
- console.log(`Using local repository path: ${workspacePath}`);
195
- }
196
- else {
197
- // Fall back to workspace-based checkout for non-task conversations
198
- workspacePath = await this.repositoryManager.checkoutRepository(workspaceId, config.repository.url, config.repository.branch, config.githubToken);
199
- }
200
- }
201
- else if (workspaceId) {
202
- workspacePath =
203
- await this.repositoryManager.getWorkspacePath(workspaceId);
204
- }
205
- else {
206
- // Default workspace path when no workspaceId is provided
207
- workspacePath = process.cwd();
208
- }
209
- // Fetch GitHub tokens from orchestrator API if we have a workspaceId
210
- const githubToken = workspaceId
211
- ? await this.fetchGithubTokens(workspaceId)
212
- : undefined;
213
- // Generate TOOL_TOKEN for MCP tools authentication
214
- let toolToken;
215
- if (config.mcpServers && Object.keys(config.mcpServers).length > 0) {
216
- const runnerToken = process.env["NORTHFLARE_RUNNER_TOKEN"];
217
- const runnerUid = this.runner.getRunnerUid();
218
- if (runnerToken && runnerUid && context.conversationId) {
219
- // Sign JWT with runner's token
220
- toolToken = jwt.sign({
221
- conversationId: context.conversationId,
222
- runnerUid: runnerUid,
223
- }, runnerToken, {
224
- expiresIn: "60m", // 60 minutes expiry
225
- });
226
- console.log("[NorthflareAgentManager] Generated TOOL_TOKEN for MCP authentication");
227
- }
228
- else {
229
- console.warn("[NorthflareAgentManager] Unable to generate TOOL_TOKEN - missing required data");
230
- }
231
- }
232
- const managerDebug = isDebugEnabledFor("manager");
233
- const sdkDebug = isDebugEnabledFor("sdk");
234
- const debugEnabled = managerDebug || sdkDebug;
235
- // Simplified SDK configuration - using native query API with streamlined options
236
- // Simplified environment configuration
237
- const envVars = {
238
- ...Object.fromEntries(Object.entries(process.env).filter(([_, value]) => value !== undefined)), // Preserve parent environment, filter out undefined values
239
- };
240
- if (config.anthropicApiKey) {
241
- envVars["ANTHROPIC_API_KEY"] = config.anthropicApiKey;
242
- }
243
- // OpenRouter / Groq API key support (prefer provider-specific fields)
244
- const openRouterApiKey = config.openRouterApiKey || process.env["OPENROUTER_API_KEY"];
245
- const groqApiKey = config.groqApiKey || process.env["GROQ_API_KEY"];
246
- if (openRouterApiKey) {
247
- envVars["OPENROUTER_API_KEY"] = openRouterApiKey;
248
- }
249
- if (groqApiKey) {
250
- envVars["GROQ_API_KEY"] = groqApiKey;
251
- }
252
- // Pass through OpenAI API key if supplied for provider-prefixed models
253
- if (config.openaiApiKey) {
254
- envVars["OPENAI_API_KEY"] = config.openaiApiKey;
255
- }
256
- if (githubToken) {
257
- envVars["GITHUB_TOKEN"] = githubToken;
258
- }
259
- if (toolToken) {
260
- envVars["TOOL_TOKEN"] = toolToken;
261
- }
262
- if (sdkDebug) {
263
- envVars["DEBUG"] = "1";
264
- }
265
- const subAgentConfig = this.buildSubAgentConfig(config.subAgentTypes);
266
- const agentsOption = subAgentConfig.enabled && Object.keys(subAgentConfig.agents).length > 0
267
- ? subAgentConfig.agents
268
- : undefined;
269
- // Simplified system prompt handling
270
- const providedSystemPrompt = buildSystemPromptWithNorthflare(config.systemPrompt || conversationData?.systemPrompt, config?.northflareSystemPrompt ||
271
- conversationData?.northflareSystemPrompt);
272
- const systemPromptMode = config.systemPromptMode ||
273
- conversationData?.systemPromptMode ||
274
- // Default ProjectStep conversations to append so preset stays intact
275
- "append";
276
- let systemPromptOption;
277
- if (providedSystemPrompt) {
278
- if (systemPromptMode === "replace") {
279
- systemPromptOption = providedSystemPrompt;
280
- }
281
- else {
282
- systemPromptOption = {
283
- type: "preset",
284
- preset: "claude_code",
285
- append: providedSystemPrompt,
286
- };
287
- }
288
- }
289
- // Debug log the systemPrompt being sent to Northflare Agent SDK
290
- console.log(`[NorthflareAgentManager] System prompt configuration for conversation ${context.conversationId}:`, {
291
- mode: systemPromptMode,
292
- hasProvidedSystemPrompt: Boolean(providedSystemPrompt),
293
- finalPromptType: typeof systemPromptOption,
294
- finalPromptLength: typeof systemPromptOption === "string"
295
- ? systemPromptOption.length
296
- : systemPromptOption?.append?.length ?? 0,
297
- });
298
- // Tool restrictions based on permissions mode
299
- // "read" = read-only mode (no file writes, no shell)
300
- // "project" = no file writes but allows bash, no subagents (for Project conversation)
301
- const readOnlyTools = [
302
- "write_file",
303
- "edit_file",
304
- "multi_edit_file",
305
- "delete_file",
306
- "move_file",
307
- "copy_file",
308
- "run_command",
309
- "todo_write",
310
- ];
311
- const projectModeTools = [
312
- "write_file",
313
- "edit_file",
314
- "multi_edit_file",
315
- "delete_file",
316
- "move_file",
317
- "copy_file",
318
- "todo_write",
319
- "task",
320
- ];
321
- const disallowedTools = context.permissionsMode === "read"
322
- ? readOnlyTools
323
- : context.permissionsMode === "project"
324
- ? projectModeTools
325
- : [];
326
- if (!subAgentConfig.enabled) {
327
- disallowedTools.push("task");
328
- }
329
- if (disallowedTools.length) {
330
- console.log("[NorthflareAgentManager] Tool restrictions applied", {
331
- disallowedTools,
332
- });
333
- }
334
- // Simplified MCP server configuration
335
- let mcpServers;
336
- if (config.mcpServers) {
337
- mcpServers = expandEnv(config.mcpServers, { TOOL_TOKEN: toolToken });
338
- console.log("[NorthflareAgentManager] MCP servers configuration:", JSON.stringify(mcpServers, null, 2));
339
- }
340
- if (debugEnabled) {
341
- console.log("[NorthflareAgentManager] SDK launch summary", {
342
- conversationId: context.conversationId,
343
- model,
344
- provider,
345
- cwd: workspacePath,
346
- hasMcpServers: !!mcpServers && Object.keys(mcpServers).length > 0,
347
- disallowedTools,
348
- systemPromptMode,
349
- hasSystemPrompt: Boolean(systemPromptOption),
350
- systemPromptType: typeof systemPromptOption,
351
- subAgentsEnabled: subAgentConfig.enabled,
352
- agentTypes: agentsOption ? Object.keys(agentsOption) : [],
353
- env: {
354
- OPENROUTER_API_KEY: Boolean(envVars["OPENROUTER_API_KEY"]),
355
- GROQ_API_KEY: Boolean(envVars["GROQ_API_KEY"]),
356
- ANTHROPIC_API_KEY: Boolean(envVars["ANTHROPIC_API_KEY"]),
357
- OPENAI_API_KEY: Boolean(envVars["OPENAI_API_KEY"]),
358
- GITHUB_TOKEN: Boolean(envVars["GITHUB_TOKEN"]),
359
- TOOL_TOKEN: Boolean(envVars["TOOL_TOKEN"]),
360
- },
361
- });
362
- }
363
- // Create input stream for user messages (simplified message queueing)
364
- const input = createUserMessageStream();
365
- // NOTE: different @ai-sdk/provider versions (v2 via @northflare/agent and
366
- // v3 via @ai-sdk/groq/ai) can lead to nominally incompatible LanguageModel
367
- // types. We only need runtime compatibility here, so coerce to `any` to
368
- // avoid TS conflicts between the duplicated provider packages.
369
- // Build provider-specific model instance. OpenRouter requires an explicit
370
- // client factory instead of the generic model string.
371
- const sdkModel = (() => {
372
- if (provider === "groq") {
373
- const groqClient = groqApiKey && groqApiKey.length > 0
374
- ? createGroq({ apiKey: groqApiKey })
375
- : groqProvider;
376
- return groqClient(context.model);
377
- }
378
- if (provider === "openrouter") {
379
- const client = createOpenRouter({
380
- apiKey: openRouterApiKey || process.env["OPENROUTER_API_KEY"] || "",
381
- });
382
- return client(context.model);
383
- }
384
- return context.model;
385
- })();
386
- // Respect a maxTurns override if provided (falls back to SDK default when unset)
387
- const maxTurns = Number.isFinite(config.maxTurns)
388
- ? config.maxTurns
389
- : undefined;
390
- // Log how the model was resolved to help compare with the standalone script
391
- console.log("[NorthflareAgentManager] Model resolution", {
392
- provider,
393
- rawModel: rawModel,
394
- normalizedModel: context.model,
395
- openRouterApiKeyPresent: Boolean(openRouterApiKey || process.env["OPENROUTER_API_KEY"]),
396
- sdkModelType: sdkModel?.constructor?.name,
397
- });
398
- // Build a modelResolver for sub-agents that can resolve model strings to LanguageModel instances
399
- const modelResolver = (modelId) => {
400
- // Normalize the model ID (strip provider prefixes)
401
- const normalizedModelId = this.normalizeModel(modelId);
402
- // Determine the provider from the model ID prefix or fall back to the parent provider
403
- let modelProvider = provider;
404
- if (modelId.startsWith("groq:") || modelId.startsWith("groq/")) {
405
- modelProvider = "groq";
406
- }
407
- else if (modelId.startsWith("openrouter:") ||
408
- modelId.startsWith("openrouter/")) {
409
- modelProvider = "openrouter";
410
- }
411
- // Create the appropriate model instance
412
- if (modelProvider === "groq") {
413
- const groqClient = groqApiKey && groqApiKey.length > 0
414
- ? createGroq({ apiKey: groqApiKey })
415
- : groqProvider;
416
- return groqClient(normalizedModelId);
417
- }
418
- if (modelProvider === "openrouter") {
419
- const client = createOpenRouter({
420
- apiKey: openRouterApiKey || process.env["OPENROUTER_API_KEY"] || "",
421
- });
422
- return client(normalizedModelId);
423
- }
424
- // Unknown provider - return undefined to let SDK throw a helpful error
425
- return undefined;
426
- };
427
- // Launch SDK with simplified configuration
428
- let silenceTimer = null;
429
- let lastSdkActivity = Date.now();
430
- const handleTaskProgress = async (update) => {
431
- const pct = Number(update?.progressPercent);
432
- if (!Number.isFinite(pct))
433
- return;
434
- const clamped = Math.max(0, Math.min(100, pct));
435
- const messageId = `${context.agentSessionId}-progress-${Date.now()}-${Math.random()
436
- .toString(36)
437
- .slice(2, 8)}`;
438
- const progressPayload = {
439
- conversationId: context.conversationId,
440
- conversationObjectType: context.conversationObjectType,
441
- conversationObjectId: context.conversationObjectId,
442
- agentSessionId: context.agentSessionId,
443
- type: "assistant",
444
- subtype: "task_progress",
445
- messageMetaType: "task_progress",
446
- metadata: {
447
- progressPercent: clamped,
448
- toolCallId: update?.toolCallId,
449
- raw: update?.raw,
450
- },
451
- content: [
452
- {
453
- type: "text",
454
- text: `Task progress ${Math.round(clamped)}%`,
455
- },
456
- ],
457
- messageId,
458
- };
459
- try {
460
- await this.runner.notify("message.agent", progressPayload);
461
- }
462
- catch (err) {
463
- console.warn("[NorthflareAgentManager] Failed to send task progress", {
464
- conversationId: context.conversationId,
465
- error: err instanceof Error ? err.message : String(err),
466
- });
467
- }
468
- };
469
- const sdk = sdkQuery({
470
- prompt: input.iterable,
471
- options: {
472
- cwd: workspacePath,
473
- env: envVars,
474
- model: sdkModel,
475
- modelResolver,
476
- resume: config.sessionId || conversationData?.agentSessionId || undefined,
477
- // Enable vision support - allows passing images directly to the model
478
- visionModel: true,
479
- ...(agentsOption ? { agents: agentsOption } : {}),
480
- ...(maxTurns !== undefined ? { maxTurns } : {}),
481
- ...(systemPromptOption ? { systemPrompt: systemPromptOption } : {}),
482
- ...(disallowedTools.length ? { disallowedTools } : {}),
483
- ...(mcpServers ? { mcpServers } : {}),
484
- conversationId: context.conversationId,
485
- conversationObjectId: context.conversationObjectId,
486
- taskId: context.taskId,
487
- onTaskProgress: handleTaskProgress,
488
- ...(sdkDebug
489
- ? {
490
- debug: true,
491
- stderr: (data) => {
492
- try {
493
- console.log(`[Northflare Agent SDK] ${data}`);
494
- }
495
- catch { }
496
- },
497
- onStepFinish: (step) => {
498
- try {
499
- console.log("[NorthflareAgentManager] Agent step finished (full step object): " +
500
- JSON.stringify(step, null, 2));
501
- }
502
- catch { }
503
- },
504
- }
505
- : {}),
506
- },
507
- });
508
- console.log("[NorthflareAgentManager] sdkQuery options summary", {
509
- provider,
510
- model: context.model,
511
- cwd: workspacePath,
512
- maxTurns: maxTurns ?? "default (SDK)",
513
- hasSystemPrompt: Boolean(systemPromptOption),
514
- hasMcpServers: Boolean(mcpServers && Object.keys(mcpServers).length),
515
- disallowedToolsCount: disallowedTools.length,
516
- envKeys: Object.keys(envVars),
517
- });
518
- // Track number of streamed messages to aid debugging when sessions appear silent
519
- let streamedMessageCount = 0;
520
- const clearSilenceTimer = () => {
521
- if (silenceTimer) {
522
- clearInterval(silenceTimer);
523
- silenceTimer = null;
524
- }
525
- };
526
- // Simplified conversation wrapper - reduced complexity while maintaining interface
527
- const conversation = createConversationWrapper(sdk, input, async (hadError, error) => {
528
- // Check if SDK reported an error via result message (stored in metadata)
529
- const sdkError = context.metadata?.["_sdkError"];
530
- const effectiveHadError = hadError || sdkError?.isError === true;
531
- const effectiveError = error ||
532
- (sdkError?.isError
533
- ? new Error(sdkError.errorMessage || "SDK execution error")
534
- : undefined);
535
- console.log("[NorthflareAgentManager] SDK stream completed", {
536
- conversationId: context.conversationId,
537
- agentSessionId: context.agentSessionId,
538
- hadError,
539
- sdkErrorDetected: sdkError?.isError,
540
- effectiveHadError,
541
- errorMessage: effectiveError?.message,
542
- streamedMessageCount,
543
- });
544
- clearSilenceTimer();
545
- // Surface SDK-level failures to the orchestrator so the task
546
- // transitions to needs_attention and an error bubble appears in chat.
547
- if (effectiveHadError && effectiveError) {
548
- try {
549
- const normalizedError = effectiveError instanceof Error
550
- ? effectiveError
551
- : new Error(String(effectiveError));
552
- await this._handleConversationError(context, normalizedError);
553
- }
554
- catch (err) {
555
- console.error("[NorthflareAgentManager] Failed to report SDK error:", err);
556
- }
557
- }
558
- await this._handleRunCompletion(context, effectiveHadError, effectiveError);
559
- });
560
- // Store conversation instance in context for reuse
561
- context.conversation = conversation;
562
- context._clearSilenceTimer = clearSilenceTimer;
563
- // Observe session id from first messages that include it
564
- conversation.onSessionId(async (agentSessionId) => {
565
- if (!agentSessionId)
566
- return;
567
- const oldSessionId = context.agentSessionId;
568
- if (oldSessionId !== agentSessionId) {
569
- context.agentSessionId = agentSessionId;
570
- context.status = "active";
571
- await this.runner.notify("agentSessionId.changed", {
572
- conversationId: context.conversationId,
573
- conversationObjectType,
574
- conversationObjectId,
575
- oldAgentSessionId: oldSessionId,
576
- newAgentSessionId: agentSessionId,
577
- });
578
- }
579
- });
580
- // Set up streaming message handler
581
- const messageHandler = async (message, sessionId) => {
582
- streamedMessageCount += 1;
583
- lastSdkActivity = Date.now();
584
- // Lightweight visibility into every SDK message before normalization
585
- try {
586
- const preview = (() => {
587
- if (!message)
588
- return null;
589
- if (typeof message === "string")
590
- return message.slice(0, 300);
591
- const m = message;
592
- const content = m?.message?.content || m?.content || m?.message?.text || m?.text;
593
- if (typeof content === "string")
594
- return content.slice(0, 300);
595
- if (Array.isArray(content)) {
596
- const textBlock = content.find((b) => b && b.type === "text" && typeof b.text === "string");
597
- if (textBlock)
598
- return textBlock.text.slice(0, 300);
599
- }
600
- try {
601
- return JSON.stringify(message).slice(0, 300);
602
- }
603
- catch {
604
- return "[unstringifiable message]";
605
- }
606
- })();
607
- console.log("[NorthflareAgentManager] Agent stream message", {
608
- conversationId: context.conversationId,
609
- agentSessionId: sessionId || context.agentSessionId,
610
- count: streamedMessageCount,
611
- type: message?.type,
612
- preview,
613
- });
614
- }
615
- catch (e) {
616
- console.warn("[NorthflareAgentManager] Failed to log early agent stream message:", e);
617
- }
618
- await this.handleStreamedMessage(context, message, sessionId);
619
- // Heuristic: detect terminal system events and finalize
620
- try {
621
- if (message?.type === "system") {
622
- const m = message;
623
- const subtype = m.subtype || m?.message?.subtype || m?.event;
624
- const exitSignals = [
625
- "exit",
626
- "exiting",
627
- "session_end",
628
- "conversation_end",
629
- "process_exit",
630
- "done",
631
- "completed",
632
- ];
633
- if (subtype && exitSignals.includes(String(subtype))) {
634
- await this._handleRunCompletion(context, false);
635
- }
636
- }
637
- }
638
- catch (e) {
639
- console.warn("[NorthflareAgentManager] finalize-on-system heuristic error:", e);
640
- }
641
- };
642
- if (managerDebug) {
643
- silenceTimer = setInterval(() => {
644
- const now = Date.now();
645
- const idleMs = now - lastSdkActivity;
646
- if (idleMs >= 5000) {
647
- console.log("[NorthflareAgentManager] Waiting for agent output", {
648
- conversationId: context.conversationId,
649
- agentSessionId: context.agentSessionId || null,
650
- streamedMessageCount,
651
- idleMs,
652
- });
653
- }
654
- }, 5000);
655
- }
656
- // Send initial messages BEFORE starting the stream.
657
- // This ensures all initial messages are queued and available when the SDK
658
- // starts iterating, so it can collect them all without timing issues.
659
- try {
660
- for (const message of initialMessages) {
661
- // Use normalizeContentForSDK to preserve multimodal content (images)
662
- const normalizedContent = this.normalizeContentForSDK(message.content);
663
- const isMultimodal = Array.isArray(normalizedContent);
664
- console.log("[NorthflareAgentManager] Sending initial message to SDK", {
665
- conversationId: context.conversationId,
666
- agentSessionId: context.agentSessionId,
667
- isMultimodal,
668
- contentPreview: isMultimodal
669
- ? `[${normalizedContent.length} blocks: ${normalizedContent.map((b) => b.type).join(", ")}]`
670
- : normalizedContent.slice(0, 200),
671
- length: isMultimodal ? normalizedContent.length : normalizedContent.length,
672
- });
673
- if (isMultimodal) {
674
- // Send multimodal content blocks directly to the SDK
675
- conversation.send({
676
- type: "multimodal",
677
- content: normalizedContent,
678
- });
679
- }
680
- else {
681
- conversation.send({
682
- type: "text",
683
- text: normalizedContent,
684
- });
685
- }
686
- }
687
- // Kick off streaming; completion/error is handled by the wrapper's
688
- // onComplete callback above. (The stream method returns void, so awaiting
689
- // it would resolve immediately and incorrectly mark the conversation
690
- // finished.)
691
- conversation.stream(messageHandler);
692
- // Note: Error handling is done via process completion handler
693
- // The Northflare Agent SDK doesn't have an onError method on conversations
694
- console.log(`Started conversation for ${conversationObjectType} ${conversationObjectId} in workspace ${workspacePath}`);
695
- // Return the conversation context directly
696
- return context;
697
- }
698
- catch (error) {
699
- // Handle startup errors
700
- await this._handleConversationError(context, error);
701
- throw error;
702
- }
703
- }
704
- async stopConversation(agentSessionId, context, isRunnerShutdown = false, reason) {
705
- context._stopRequested = true;
706
- if (context && context.conversation) {
707
- context.status = "stopping";
708
- try {
709
- // Proactively send end notification to avoid missing it on abnormal exits
710
- // Use provided reason, or fall back to 'runner_shutdown' if isRunnerShutdown is true
711
- const finalReason = reason || (isRunnerShutdown ? "runner_shutdown" : undefined);
712
- context._stopReason = finalReason;
713
- await this._finalizeConversation(context, false, undefined, finalReason).catch(() => { });
714
- // Mark conversation as stopped BEFORE ending to prevent race conditions
715
- context.status = "stopped";
716
- // Properly end the conversation using the SDK
717
- if (isAgentConversation(context.conversation)) {
718
- await context.conversation.end();
719
- }
720
- }
721
- catch (error) {
722
- console.error(`Error ending conversation ${agentSessionId}:`, error);
723
- }
724
- // Clean up conversation reference
725
- delete context.conversation;
726
- }
727
- console.log(`Stopped conversation ${agentSessionId} for ${context.conversationObjectType} ${context.conversationObjectId}`);
728
- }
729
- async resumeConversation(conversationObjectType, conversationObjectId, agentSessionId, config, conversationData, resumeMessage, provider = "openrouter") {
730
- console.log(`[NorthflareAgentManager] Resuming conversation ${agentSessionId}`);
731
- // Resume is handled by starting a new conversation with the existing session ID
732
- const context = await this.startConversation(conversationObjectType, conversationObjectId, { ...config, sessionId: agentSessionId }, [], // Don't send initial messages
733
- conversationData, provider);
734
- // After starting the conversation with the sessionId, we need to send a message
735
- // to actually trigger the agent process to continue
736
- if (context.conversation && isAgentConversation(context.conversation)) {
737
- try {
738
- // Use the provided resume message or default to system instruction
739
- const messageToSend = resumeMessage ||
740
- "<system-instructions>Please continue</system-instructions>";
741
- console.log(`[NorthflareAgentManager] Sending resume message to conversation ${agentSessionId}`);
742
- context.conversation.send({
743
- type: "text",
744
- text: messageToSend,
745
- });
746
- }
747
- catch (error) {
748
- console.error(`[NorthflareAgentManager] Error sending resume message:`, error);
749
- }
750
- }
751
- else {
752
- console.warn("[NorthflareAgentManager] Resume requested but conversation instance missing or incompatible");
753
- }
754
- return context.agentSessionId;
755
- }
756
- async _handleRunCompletion(context, hadError, error) {
757
- if (context._finalized)
758
- return;
759
- const inFlight = context._runCompletionInFlight;
760
- if (inFlight)
761
- return inFlight;
762
- const promise = (async () => {
763
- const isTaskConversation = context.conversationObjectType === "Task";
764
- const stopRequested = context._stopRequested === true;
765
- const taskHandle = context.taskHandle;
766
- const shouldRunGitFlow = context.config.useWorktrees !== false &&
767
- isTaskConversation &&
768
- !!taskHandle &&
769
- taskHandle.branch !== "local";
770
- if (!shouldRunGitFlow || hadError || stopRequested) {
771
- await this._finalizeConversation(context, hadError, error);
772
- return;
773
- }
774
- const taskId = context.conversationObjectId;
775
- const baseBranch = context.config.repository?.branch || "main";
776
- try {
777
- const taskState = await this.repositoryManager.getTaskState(taskId);
778
- const taskRepoInfo = await this.repositoryManager.getTaskRepoInfo(taskId);
779
- if (!taskState ||
780
- !taskRepoInfo ||
781
- !taskRepoInfo.worktreePath ||
782
- taskState.branch === "local") {
783
- await this._finalizeConversation(context, false);
784
- return;
785
- }
786
- const isLocalRepo = taskRepoInfo.repoKey.startsWith("local__");
787
- const mergeWorkdir = isLocalRepo
788
- ? taskRepoInfo.controlPath
789
- : taskRepoInfo.worktreePath;
790
- // If we already have unresolved conflicts, immediately ask the agent to resolve them.
791
- const taskGit = simpleGit(taskRepoInfo.worktreePath);
792
- const initialTaskStatus = await taskGit.status();
793
- if (initialTaskStatus.conflicted.length > 0) {
794
- await this._resumeForGitConflictResolution(context, {
795
- kind: "conflicts",
796
- baseBranch,
797
- conflicts: initialTaskStatus.conflicted,
798
- workdir: taskRepoInfo.worktreePath,
799
- });
800
- return;
801
- }
802
- if (isLocalRepo && mergeWorkdir !== taskRepoInfo.worktreePath) {
803
- const mergeGit = simpleGit(mergeWorkdir);
804
- const mergeStatus = await mergeGit.status();
805
- if (mergeStatus.conflicted.length > 0) {
806
- await this._resumeForGitConflictResolution(context, {
807
- kind: "conflicts",
808
- baseBranch,
809
- conflicts: mergeStatus.conflicted,
810
- workdir: mergeWorkdir,
811
- });
812
- return;
813
- }
814
- }
815
- // Ensure we're on the task branch for commit/rebase operations.
816
- const currentBranch = (await taskGit.revparse(["--abbrev-ref", "HEAD"])).trim();
817
- if (currentBranch !== taskState.branch) {
818
- await taskGit.checkout(taskState.branch);
819
- }
820
- const status = await taskGit.status();
821
- if (!status.isClean()) {
822
- await this.repositoryManager.stageAll(taskId);
823
- try {
824
- const previousCommitCount = taskState?.commitCount ?? 0;
825
- const baseSubject = (context.summary ?? "").replace(/\s+/g, " ").trim() ||
826
- `Task ${taskId}`;
827
- const message = previousCommitCount > 0
828
- ? `${baseSubject} (followup #${previousCommitCount})`
829
- : baseSubject;
830
- await this.repositoryManager.commit(taskId, message);
831
- }
832
- catch (commitError) {
833
- const msg = commitError instanceof Error
834
- ? commitError.message
835
- : String(commitError);
836
- if (!msg.toLowerCase().includes("nothing to commit")) {
837
- throw commitError;
838
- }
839
- }
840
- }
841
- const integrateResult = await this.repositoryManager.integrateTask(taskId, baseBranch, "no-ff");
842
- if (!integrateResult.success) {
843
- await this._resumeForGitConflictResolution(context, {
844
- kind: integrateResult.phase,
845
- baseBranch,
846
- conflicts: integrateResult.conflicts ?? [],
847
- workdir: integrateResult.conflictWorkdir,
848
- error: integrateResult.error,
849
- });
850
- return;
851
- }
852
- // Worktree is no longer needed after a successful merge; preserve the branch
853
- await this.repositoryManager.removeTaskWorktree(taskId, {
854
- preserveBranch: true,
855
- });
856
- const wsId = context.config.workspaceId || context.workspaceId;
857
- const repoUrl = context.config.repository?.url;
858
- if (wsId && repoUrl) {
859
- await this.repositoryManager.syncWorkspaceWorktree(wsId, repoUrl, baseBranch);
860
- }
861
- await this._finalizeConversation(context, false);
862
- }
863
- catch (mergeError) {
864
- console.error("[NorthflareAgentManager] Post-task git flow failed", mergeError);
865
- await this._finalizeConversation(context, true, mergeError);
866
- }
867
- })();
868
- context._runCompletionInFlight = promise.finally(() => {
869
- delete context._runCompletionInFlight;
870
- });
871
- return context._runCompletionInFlight;
872
- }
873
- async _resumeForGitConflictResolution(context, payload) {
874
- const { kind, baseBranch, conflicts, workdir, error } = payload;
875
- const conflictsList = conflicts?.length
876
- ? `\n\nConflicted files:\n${conflicts.map((f) => `- ${f}`).join("\n")}`
877
- : "";
878
- const workdirHint = workdir ? `\n\nWork directory:\n- ${workdir}` : "";
879
- const errorHint = error ? `\n\nError:\n${error}` : "";
880
- const hasConflicts = !!conflicts?.length;
881
- let instruction;
882
- if (kind === "rebase") {
883
- instruction = hasConflicts
884
- ? `<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>`
885
- : `<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>`;
886
- }
887
- else if (kind === "merge") {
888
- instruction = hasConflicts
889
- ? `<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>`
890
- : `<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>`;
891
- }
892
- else {
893
- 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>`;
894
- }
895
- const conversationData = {
896
- id: context.conversationId,
897
- objectType: context.conversationObjectType,
898
- objectId: context.conversationObjectId,
899
- model: context.model,
900
- summary: context.summary ?? null,
901
- globalInstructions: context.globalInstructions,
902
- workspaceInstructions: context.workspaceInstructions,
903
- permissionsMode: context.permissionsMode,
904
- agentSessionId: context.agentSessionId,
905
- };
906
- await this.resumeConversation(context.conversationObjectType, context.conversationObjectId, context.agentSessionId, context.config, conversationData, instruction, context.provider === "groq" ? "groq" : "openrouter");
907
- }
908
- async _finalizeConversation(context, hadError, error, reason) {
909
- // Synchronous idempotency check - must happen before any async operations
910
- if (context._finalized)
911
- return;
912
- context._finalized = true;
913
- try {
914
- const clearSilenceTimer = context._clearSilenceTimer;
915
- if (typeof clearSilenceTimer === "function") {
916
- clearSilenceTimer();
917
- }
918
- }
919
- catch { }
920
- // Mark as completed immediately to prevent restart on catch-up
921
- // This is synchronous and happens before any async operations
922
- this.runner.markConversationCompleted(context.conversationId);
923
- // Clean up conversation from active conversations (synchronous)
924
- try {
925
- console.log(`[NorthflareAgentManager] Removing conversation from active map:`, {
926
- conversationId: context.conversationId,
927
- agentSessionId: context.agentSessionId,
928
- mapSizeBefore: this.runner.activeConversations_.size,
929
- });
930
- this.runner.activeConversations_.delete(context.conversationId);
931
- // Clean up submit tool tracking
932
- this.submitToolUseIds.delete(context.conversationId);
933
- statusLineManager.updateActiveCount(this.runner.activeConversations_.size);
934
- }
935
- catch { }
936
- // Now do async notification (after all sync cleanup)
937
- try {
938
- await this.runner.notify("conversation.end", {
939
- conversationId: context.conversationId,
940
- conversationObjectType: context.conversationObjectType,
941
- conversationObjectId: context.conversationObjectId,
942
- agentSessionId: context.agentSessionId,
943
- isError: hadError,
944
- errorMessage: error?.message,
945
- reason: reason,
946
- });
947
- }
948
- catch (e) {
949
- console.error("[NorthflareAgentManager] Failed to notify conversation.end:", e);
950
- }
951
- // Notify update coordinator that a conversation has ended
952
- // This may trigger an auto-update if one is pending and runner is now idle
953
- try {
954
- await this.runner.onConversationEnd();
955
- }
956
- catch (e) {
957
- console.error("[NorthflareAgentManager] Failed to notify onConversationEnd:", e);
958
- }
959
- }
960
- async sendUserMessage(conversationId, content, config, conversationObjectType, conversationObjectId, conversation, _agentSessionIdOverride) {
961
- console.log(`[NorthflareAgentManager] sendUserMessage called with:`, {
962
- conversationId,
963
- conversationObjectType,
964
- conversationObjectId,
965
- hasConfig: !!config,
966
- hasConversation: !!conversation,
967
- activeConversations: this.runner.activeConversations_.size,
968
- });
969
- // Find by conversationId only
970
- let context = this.runner.getConversationContext(conversationId);
971
- console.log(`[NorthflareAgentManager] Lookup by conversationId result:`, {
972
- found: !!context,
973
- conversationId: context?.conversationId,
974
- agentSessionId: context?.agentSessionId,
975
- });
976
- if (!context && conversation) {
977
- // Use provided conversation details
978
- try {
979
- const conversationDetails = conversation;
980
- console.log(`[NorthflareAgentManager] Using provided config from RunnerMessage:`, {
981
- hasConfig: !!config,
982
- hasRepository: !!config?.repository,
983
- repositoryType: config?.repository?.type,
984
- repositoryPath: config?.repository?.localPath,
985
- hasTokens: !!(config?.accessToken || config?.githubToken),
986
- hasMcpServers: !!config?.mcpServers,
987
- });
988
- // Use the config that was already prepared by TaskOrchestrator and sent in the RunnerMessage
989
- const startConfig = {
990
- anthropicApiKey: config?.anthropicApiKey || process.env["ANTHROPIC_API_KEY"] || "",
991
- systemPrompt: config?.systemPrompt || conversationDetails?.systemPrompt,
992
- systemPromptMode: config?.systemPromptMode ||
993
- conversationDetails?.systemPromptMode,
994
- workspaceId: conversationDetails.workspaceId,
995
- ...config, // Use the full config provided in the RunnerMessage
996
- ...(conversationDetails.agentSessionId
997
- ? { sessionId: conversationDetails.agentSessionId }
998
- : {}),
999
- };
1000
- // Start the SDK conversation (no initial messages); this attaches to existing session when provided
1001
- const derivedProvider = conversationDetails?.provider ||
1002
- startConfig?.providerType ||
1003
- "openrouter";
1004
- await this.startConversation(conversationDetails.objectType, conversationDetails.objectId, startConfig, [], conversationDetails, derivedProvider);
1005
- // Refresh context after start/resume
1006
- context = this.runner.getConversationContext(conversationId);
1007
- }
1008
- catch (error) {
1009
- console.error(`Failed to fetch conversation ${conversationId}:`, error);
1010
- }
1011
- }
1012
- if (!context) {
1013
- throw new Error(`No active or fetchable conversation found for ${conversationId}`);
1014
- }
1015
- try {
1016
- // Send immediately when a conversation instance exists; no need to wait for "active"
1017
- if (!context.conversation || !isAgentConversation(context.conversation)) {
1018
- throw new Error(`No conversation instance found for conversation ${context.conversationId}`);
1019
- }
1020
- // Guard: Don't send messages if conversation is stopped or stopping
1021
- const conversationStatus = context.status;
1022
- if (conversationStatus === "stopped" ||
1023
- conversationStatus === "stopping") {
1024
- console.warn(`Attempted to send message to stopped/stopping conversation ${context.conversationId}`, {
1025
- status: context.status,
1026
- conversationObjectType: context.conversationObjectType,
1027
- conversationObjectId: context.conversationObjectId,
1028
- });
1029
- return;
1030
- }
1031
- // Native message injection - SDK handles queueing and delivery
1032
- // Use normalizeContentForSDK to preserve multimodal content (images)
1033
- const normalizedContent = this.normalizeContentForSDK(content);
1034
- const isMultimodal = Array.isArray(normalizedContent);
1035
- if (isDebugEnabledFor("manager")) {
1036
- console.log("[NorthflareAgentManager] Normalized follow-up content", {
1037
- originalType: typeof content,
1038
- isArray: Array.isArray(content) || undefined,
1039
- isMultimodal,
1040
- normalizedPreview: isMultimodal
1041
- ? `[${normalizedContent.length} blocks: ${normalizedContent.map((b) => b.type).join(", ")}]`
1042
- : normalizedContent.slice(0, 160),
1043
- });
1044
- }
1045
- if (isMultimodal) {
1046
- // Send multimodal content blocks directly to the SDK
1047
- // Use any cast to bypass type restrictions - our wrapper supports this
1048
- context.conversation.send({
1049
- type: "multimodal",
1050
- content: normalizedContent,
1051
- });
1052
- }
1053
- else {
1054
- context.conversation.send({
1055
- type: "text",
1056
- text: normalizedContent,
1057
- });
1058
- }
1059
- // Update last activity timestamp
1060
- context.lastActivityAt = new Date();
1061
- console.log(`Sent user message to conversation ${context.conversationId} (agentSessionId: ${context.agentSessionId}, status: ${context.status})`);
1062
- }
1063
- catch (error) {
1064
- // Handle errors properly
1065
- await this._handleConversationError(context, error);
1066
- throw error; // Re-throw to maintain the same behavior
1067
- }
1068
- }
1069
- async fetchGithubTokens(workspaceId) {
1070
- try {
1071
- const response = await fetch(`${this.runner.config_.orchestratorUrl}/api/runner/tokens?workspaceId=${workspaceId}`, {
1072
- method: "GET",
1073
- headers: {
1074
- Authorization: `Bearer ${process.env["NORTHFLARE_RUNNER_TOKEN"]}`,
1075
- },
1076
- });
1077
- if (!response.ok) {
1078
- console.error(`Failed to fetch GitHub tokens: ${response.status}`);
1079
- return undefined;
1080
- }
1081
- const data = (await response.json());
1082
- return data.githubToken;
1083
- }
1084
- catch (error) {
1085
- console.error("Error fetching GitHub tokens:", error);
1086
- return undefined;
1087
- }
1088
- }
1089
- async _handleConversationError(context, error) {
1090
- const errorType = this.classifyError(error);
1091
- // Notify orchestrator
1092
- await this.runner.notify("error.report", {
1093
- conversationId: context.conversationId,
1094
- conversationObjectType: context.conversationObjectType,
1095
- conversationObjectId: context.conversationObjectId,
1096
- agentSessionId: context.agentSessionId,
1097
- errorType,
1098
- message: error.message,
1099
- details: {
1100
- stack: error.stack,
1101
- timestamp: new Date(),
1102
- },
1103
- });
1104
- // Conversation continues on error - no automatic cleanup
1105
- console.error(`Conversation error for ${context.conversationObjectType} ${context.conversationObjectId}:`, error);
1106
- }
1107
- classifyError(error) {
1108
- const msg = error.message.toLowerCase();
1109
- if (msg.includes("process exited")) {
1110
- return "process_exit";
1111
- }
1112
- else if (msg.includes("model")) {
1113
- return "model_error";
1114
- }
1115
- else if (msg.includes("tool")) {
1116
- return "tool_error";
1117
- }
1118
- else if (msg.includes("permission") ||
1119
- msg.includes("forbidden") ||
1120
- msg.includes("403")) {
1121
- return "permission_error";
1122
- }
1123
- else if (msg.includes("timeout")) {
1124
- return "timeout_error";
1125
- }
1126
- else if (msg.includes("unauthorized") ||
1127
- msg.includes("401") ||
1128
- msg.includes("authentication")) {
1129
- return "auth_error";
1130
- }
1131
- else if (msg.includes("rate limit") ||
1132
- msg.includes("429") ||
1133
- msg.includes("too many requests")) {
1134
- return "rate_limit_error";
1135
- }
1136
- else if (msg.includes("api") ||
1137
- msg.includes("network") ||
1138
- msg.includes("fetch") ||
1139
- msg.includes("connection")) {
1140
- return "api_error";
1141
- }
1142
- return "unknown_error";
1143
- }
1144
- normalizeModel(rawModel) {
1145
- if (!rawModel) {
1146
- throw new Error("normalizeModel called without a model; caller must supply one.");
1147
- }
1148
- const trimmed = rawModel.trim();
1149
- if (trimmed.startsWith("openrouter:")) {
1150
- return trimmed.slice("openrouter:".length);
1151
- }
1152
- if (trimmed.startsWith("openrouter/")) {
1153
- return trimmed.slice("openrouter/".length);
1154
- }
1155
- if (trimmed.startsWith("groq:")) {
1156
- return trimmed.slice("groq:".length);
1157
- }
1158
- if (trimmed.startsWith("groq/")) {
1159
- return trimmed.slice("groq/".length);
1160
- }
1161
- if (!trimmed) {
1162
- throw new Error("Model string cannot be empty.");
1163
- }
1164
- return trimmed;
1165
- }
1166
- /**
1167
- * Check if content contains multimodal blocks (images or documents)
1168
- */
1169
- hasMultimodalContent(value) {
1170
- if (!Array.isArray(value))
1171
- return false;
1172
- return value.some((item) => item &&
1173
- typeof item === "object" &&
1174
- (item.type === "image" || item.type === "document"));
1175
- }
1176
- /**
1177
- * Normalize content for the SDK, preserving multimodal blocks when present.
1178
- * Returns either a string (text-only) or an array of content blocks (multimodal).
1179
- */
1180
- normalizeContentForSDK(value) {
1181
- // If it's an array with multimodal content, convert to SDK format
1182
- if (Array.isArray(value) && this.hasMultimodalContent(value)) {
1183
- const blocks = [];
1184
- for (const item of value) {
1185
- if (!item || typeof item !== "object")
1186
- continue;
1187
- if (item.type === "text" && typeof item.text === "string") {
1188
- blocks.push({ type: "text", text: item.text });
1189
- }
1190
- else if (item.type === "image" && item.source) {
1191
- // Convert image blocks: { type: "image", source: { type: "url", url: "..." } }
1192
- // to SDK format: { type: "image", image: "..." }
1193
- const imageUrl = item.source.type === "url" ? item.source.url : item.source.data;
1194
- if (imageUrl) {
1195
- blocks.push({ type: "image", image: imageUrl });
1196
- }
1197
- }
1198
- else if (item.type === "document" && item.source) {
1199
- // Documents (PDFs) - the SDK doesn't have native document support,
1200
- // so we include them as text with the URL for now
1201
- // TODO: When SDK adds document support, update this
1202
- const docUrl = item.source.type === "url" ? item.source.url : item.source.data;
1203
- if (docUrl) {
1204
- // For now, add PDF URL as text since SDK doesn't support documents directly
1205
- // The model can still access PDFs via URL if supported
1206
- blocks.push({
1207
- type: "text",
1208
- text: `[PDF Document: ${docUrl}]`,
1209
- });
1210
- }
1211
- }
1212
- }
1213
- // If we have any image blocks, return the multimodal array
1214
- if (blocks.some((b) => b.type === "image")) {
1215
- return blocks;
1216
- }
1217
- // Otherwise, fall back to text extraction
1218
- }
1219
- // Fall back to text-only normalization
1220
- return this.normalizeToText(value);
1221
- }
1222
- /**
1223
- * Normalize arbitrary content shapes into a plain string for the CLI
1224
- */
1225
- normalizeToText(value) {
1226
- if (typeof value === "string")
1227
- return value;
1228
- if (value == null)
1229
- return "";
1230
- if (typeof value === "object") {
1231
- // Common simple shapes
1232
- if (typeof value.text === "string")
1233
- return value.text;
1234
- if (typeof value.text === "object" &&
1235
- value.text &&
1236
- typeof value.text.text === "string")
1237
- return value.text.text;
1238
- if (typeof value.content === "string")
1239
- return value.content;
1240
- // Array of blocks: [{ type: 'text', text: '...' }, ...]
1241
- if (Array.isArray(value)) {
1242
- const texts = value
1243
- .map((b) => b && b.type === "text" && typeof b.text === "string" ? b.text : null)
1244
- .filter((t) => !!t);
1245
- if (texts.length)
1246
- return texts.join(" ");
1247
- }
1248
- // Nested message shapes
1249
- if (typeof value.message === "object" &&
1250
- value.message &&
1251
- typeof value.message.text === "string") {
1252
- return value.message.text;
1253
- }
1254
- }
1255
- try {
1256
- return JSON.stringify(value);
1257
- }
1258
- catch {
1259
- return String(value);
1260
- }
1261
- }
1262
- async handleStreamedMessage(context, message, sessionId) {
1263
- /*
1264
- * SDK tool call payload reference (observed shapes)
1265
- *
1266
- * 1) Assistant tool call (tool_use)
1267
- * {
1268
- * "type": "assistant",
1269
- * "message": {
1270
- * "role": "assistant",
1271
- * "content": [
1272
- * { "type": "text", "text": "…optional text…" },
1273
- * { "type": "tool_use", "id": "toolu_01Nbv…", "name": "TodoWrite", "input": { ... } }
1274
- * ]
1275
- * },
1276
- * "session_id": "…"
1277
- * }
1278
- *
1279
- * 2) Tool result (often emitted as type 'user')
1280
- * {
1281
- * "type": "user",
1282
- * "message": {
1283
- * "role": "user",
1284
- * "content": [
1285
- * {
1286
- * "tool_use_id": "toolu_01E9R475…",
1287
- * "type": "tool_result",
1288
- * "content": "Todos have been modified successfully. …"
1289
- * }
1290
- * ]
1291
- * },
1292
- * "parent_tool_use_id": null,
1293
- * "session_id": "…",
1294
- * "uuid": "…"
1295
- * }
1296
- *
1297
- * Normalization (runner → server message.agent):
1298
- * - assistant tool_use → type: 'assistant', content: [{ toolCalls: [{ id, name, arguments }] }, …]
1299
- * - tool_result (any shape) → type: 'tool_result', subtype: 'tool_result', content:
1300
- * [ { type: 'tool_result', subtype: 'tool_result', tool_use_id, content } ]
1301
- */
1302
- // Guard: Don't process messages if conversation is stopped or stopping
1303
- const status = context.status;
1304
- if (status === "stopped" || status === "stopping") {
1305
- console.log(`Ignoring message for stopped/stopping conversation ${context.conversationId}`, {
1306
- status: context.status,
1307
- messageType: message.type,
1308
- });
1309
- return;
1310
- }
1311
- try {
1312
- // High-level receipt log
1313
- console.log(`Received streamed message for ${context.conversationObjectType} ${context.conversationObjectId}`, {
1314
- type: message?.type,
1315
- });
1316
- // Raw SDK message diagnostics
1317
- try {
1318
- // Log the full raw SDK message safely (handles circular refs)
1319
- const safeStringify = (obj) => {
1320
- const seen = new WeakSet();
1321
- return JSON.stringify(obj, (key, value) => {
1322
- if (typeof value === "function")
1323
- return undefined;
1324
- if (typeof value === "bigint")
1325
- return String(value);
1326
- if (typeof value === "object" && value !== null) {
1327
- if (seen.has(value))
1328
- return "[Circular]";
1329
- seen.add(value);
1330
- }
1331
- return value;
1332
- }, 2);
1333
- };
1334
- console.log("[NorthflareAgentManager] RAW SDK message FULL:", safeStringify(message));
1335
- const summary = {
1336
- keys: Object.keys(message || {}),
1337
- hasMessage: !!message?.message,
1338
- contentType: typeof message?.content,
1339
- messageContentType: typeof message?.message?.content,
1340
- sessionId: message?.session_id || message?.sessionId || null,
1341
- };
1342
- console.log("[NorthflareAgentManager] RAW SDK message summary:", summary);
1343
- if (message?.content !== undefined) {
1344
- console.log("[NorthflareAgentManager] RAW SDK content:", message.content);
1345
- }
1346
- if (message?.message?.content !== undefined) {
1347
- console.log("[NorthflareAgentManager] RAW SDK nested content:", message.message.content);
1348
- }
1349
- // Highlight tool calls as soon as they appear so downstream observers
1350
- // can correlate real-time activity without waiting for the final result.
1351
- if (message?.type === "assistant") {
1352
- const assistantMsg = message;
1353
- const toolBlocks = Array.isArray(assistantMsg?.message?.content)
1354
- ? assistantMsg.message.content.filter((part) => part?.type === "tool_use")
1355
- : [];
1356
- if (toolBlocks.length > 0) {
1357
- console.log("[NorthflareAgentManager] Agent announced tool calls", toolBlocks.map((part) => ({
1358
- id: part?.id,
1359
- name: part?.name,
1360
- })));
1361
- }
1362
- }
1363
- }
1364
- catch (e) {
1365
- console.warn("[NorthflareAgentManager] Failed to log raw SDK message:", e);
1366
- }
1367
- // Build structured content based on message type
1368
- let messageType = message.type;
1369
- let subtype;
1370
- let structuredContent = {};
1371
- let isError = false;
1372
- let skipSend = false;
1373
- let metadata = null;
1374
- const mergeMetadata = (extra) => {
1375
- if (!extra)
1376
- return;
1377
- metadata = metadata ? { ...metadata, ...extra } : extra;
1378
- };
1379
- // Extract parent_tool_use_id if present (for all message types)
1380
- const msgAsAny = message;
1381
- if (msgAsAny.parent_tool_use_id) {
1382
- mergeMetadata({
1383
- parent_tool_use_id: msgAsAny.parent_tool_use_id,
1384
- });
1385
- }
1386
- // Extract content based on message type
1387
- switch (message.type) {
1388
- case "assistant": {
1389
- const assistantMsg = message;
1390
- const blocks = assistantMsg?.message?.content || assistantMsg?.content || [];
1391
- const textContent = Array.isArray(blocks)
1392
- ? blocks
1393
- .filter((b) => b && b.type === "text" && typeof b.text === "string")
1394
- .map((b) => b.text)
1395
- .join("")
1396
- : "";
1397
- const toolCalls = Array.isArray(blocks)
1398
- ? blocks
1399
- .filter((b) => b && b.type === "tool_use")
1400
- .map((b) => ({
1401
- id: b.id,
1402
- name: b.name,
1403
- arguments: b.input,
1404
- }))
1405
- : undefined;
1406
- // Track submit tool calls for special handling of their results
1407
- if (toolCalls && toolCalls.length > 0) {
1408
- const submitTools = toolCalls.filter((tc) => tc.name === "submit");
1409
- if (submitTools.length > 0) {
1410
- if (!this.submitToolUseIds.has(context.conversationId)) {
1411
- this.submitToolUseIds.set(context.conversationId, new Set());
1412
- }
1413
- const submitIds = this.submitToolUseIds.get(context.conversationId);
1414
- submitTools.forEach((st) => submitIds.add(st.id));
1415
- console.log(`[NorthflareAgentManager] Tracking submit tool calls:`, {
1416
- conversationId: context.conversationId,
1417
- submitToolIds: submitTools.map((st) => st.id),
1418
- });
1419
- }
1420
- }
1421
- structuredContent = {
1422
- ...(textContent ? { text: textContent } : {}),
1423
- ...(toolCalls && toolCalls.length ? { toolCalls } : {}),
1424
- timestamp: new Date().toISOString(),
1425
- };
1426
- break;
1427
- }
1428
- case "thinking": {
1429
- messageType = "thinking";
1430
- subtype = undefined;
1431
- const thinkingMsg = message;
1432
- structuredContent = [
1433
- {
1434
- type: "thinking",
1435
- thinking: thinkingMsg.content || "",
1436
- text: thinkingMsg.content || "",
1437
- timestamp: new Date().toISOString(),
1438
- },
1439
- ];
1440
- break;
1441
- }
1442
- case "tool_use": {
1443
- // Tool call request - map to assistant
1444
- messageType = "assistant";
1445
- subtype = "tool_use";
1446
- const toolUseMsg = message;
1447
- structuredContent = {
1448
- toolCalls: [
1449
- {
1450
- id: toolUseMsg.id,
1451
- name: toolUseMsg.name,
1452
- arguments: toolUseMsg.input,
1453
- },
1454
- ],
1455
- timestamp: new Date().toISOString(),
1456
- };
1457
- break;
1458
- }
1459
- case "tool_result": {
1460
- const toolResultMsg = message;
1461
- const toolName = toolResultMsg.tool_name ||
1462
- toolResultMsg.name ||
1463
- toolResultMsg?.toolCall?.name ||
1464
- null;
1465
- const toolUseId = toolResultMsg.tool_use_id ||
1466
- toolResultMsg.tool_call_id ||
1467
- toolResultMsg.id ||
1468
- toolResultMsg?.message?.id;
1469
- const resolvedContent = (() => {
1470
- if ("content" in toolResultMsg &&
1471
- toolResultMsg.content !== undefined) {
1472
- return toolResultMsg.content;
1473
- }
1474
- if (toolResultMsg.result !== undefined) {
1475
- return toolResultMsg.result;
1476
- }
1477
- if (toolResultMsg.output !== undefined) {
1478
- return toolResultMsg.output;
1479
- }
1480
- if (toolResultMsg?.message?.content !== undefined) {
1481
- return toolResultMsg.message.content;
1482
- }
1483
- return null;
1484
- })();
1485
- // Check if this is a submit tool result
1486
- const submitIds = this.submitToolUseIds.get(context.conversationId);
1487
- const isSubmitTool = submitIds && submitIds.has(toolUseId);
1488
- if (isSubmitTool) {
1489
- // Convert submit tool result to assistant message
1490
- const previewSource = resolvedContent ?? toolResultMsg.content ?? null;
1491
- let previewText = "[no content]";
1492
- if (typeof previewSource === "string") {
1493
- previewText = previewSource.slice(0, 100);
1494
- }
1495
- else if (previewSource !== null) {
1496
- try {
1497
- previewText = JSON.stringify(previewSource).slice(0, 100);
1498
- }
1499
- catch {
1500
- previewText = String(previewSource);
1501
- }
1502
- }
1503
- console.log(`[NorthflareAgentManager] Converting submit tool result to assistant message:`, {
1504
- conversationId: context.conversationId,
1505
- toolUseId,
1506
- contentPreview: previewText,
1507
- });
1508
- messageType = "assistant";
1509
- subtype = "submit_result";
1510
- // Extract the text content from the tool result
1511
- let textContent = "";
1512
- if (typeof resolvedContent === "string") {
1513
- textContent = resolvedContent;
1514
- }
1515
- else if (Array.isArray(resolvedContent)) {
1516
- // Handle array of content blocks
1517
- textContent = resolvedContent
1518
- .filter((b) => b && b.type === "text" && typeof b.text === "string")
1519
- .map((b) => b.text)
1520
- .join("");
1521
- }
1522
- else if (resolvedContent && typeof resolvedContent === "object") {
1523
- // Handle object with text property
1524
- if (typeof resolvedContent.text === "string") {
1525
- textContent = resolvedContent.text;
1526
- }
1527
- else if (typeof resolvedContent.result === "string") {
1528
- textContent = resolvedContent.result;
1529
- }
1530
- }
1531
- structuredContent = {
1532
- text: textContent,
1533
- timestamp: new Date().toISOString(),
1534
- metadata: {
1535
- source: "submit_tool",
1536
- tool_use_id: toolUseId,
1537
- },
1538
- };
1539
- // Remove this tool_use_id from tracking
1540
- submitIds.delete(toolUseId);
1541
- if (submitIds.size === 0) {
1542
- this.submitToolUseIds.delete(context.conversationId);
1543
- }
1544
- }
1545
- else {
1546
- // Regular tool result - normalize to v1-style tool_result blocks
1547
- messageType = "tool_result";
1548
- subtype = "tool_result";
1549
- structuredContent = [
1550
- {
1551
- type: "tool_result",
1552
- subtype: "tool_result",
1553
- tool_use_id: toolUseId,
1554
- content: resolvedContent, // Keep content as native (array or string)
1555
- timestamp: new Date().toISOString(),
1556
- },
1557
- ];
1558
- const toolCallMetadata = {};
1559
- if (toolUseId) {
1560
- toolCallMetadata["id"] = toolUseId;
1561
- }
1562
- if (toolName) {
1563
- toolCallMetadata["name"] = toolName;
1564
- }
1565
- mergeMetadata({
1566
- ...(toolName ? { tool_name: toolName } : {}),
1567
- ...(Object.keys(toolCallMetadata).length > 0
1568
- ? { toolCall: toolCallMetadata }
1569
- : {}),
1570
- });
1571
- }
1572
- break;
1573
- }
1574
- case "result": {
1575
- const resultMsg = message;
1576
- structuredContent = {
1577
- text: resultMsg.content || resultMsg.result || "",
1578
- timestamp: new Date().toISOString(),
1579
- };
1580
- break;
1581
- }
1582
- case "user": {
1583
- const userMsg = message;
1584
- // Skip user messages that were sent by the runner itself (already in database via orchestrator)
1585
- // Only process user messages that contain tool results (which need special handling)
1586
- const rawContent = (userMsg && userMsg.message && userMsg.message.content) ||
1587
- userMsg?.content ||
1588
- [];
1589
- // Check if this is a tool result message (needs processing)
1590
- const blocks = typeof rawContent === "string"
1591
- ? [{ type: "text", text: rawContent }]
1592
- : rawContent;
1593
- if (Array.isArray(blocks)) {
1594
- const hasToolResult = blocks.some((b) => b && typeof b === "object" && b.type === "tool_result");
1595
- if (hasToolResult) {
1596
- // Normalize tool_result blocks to v1-style
1597
- messageType = "tool_result";
1598
- subtype = "tool_result";
1599
- structuredContent = blocks
1600
- .filter((b) => b && b.type === "tool_result")
1601
- .map((b) => ({
1602
- type: "tool_result",
1603
- subtype: "tool_result",
1604
- tool_use_id: b.tool_use_id || b.toolUseId || b.id,
1605
- content: b.content, // Keep content as native (array or string)
1606
- }));
1607
- }
1608
- else {
1609
- // Regular user messages (non-tool-result) are already created by the orchestrator
1610
- // Skip them to avoid duplicates
1611
- console.log("[NorthflareAgentManager] Skipping regular user message (already in database via orchestrator)");
1612
- skipSend = true;
1613
- // Special case: Check if this is a subagent prompt (has parent_tool_use_id)
1614
- if (userMsg.parent_tool_use_id) {
1615
- // These should still be processed as assistant messages
1616
- console.log("[NorthflareAgentManager] Detected subagent prompt message (parent_tool_use_id present, no tool_result)", {
1617
- parent_tool_use_id: userMsg.parent_tool_use_id,
1618
- });
1619
- skipSend = false; // Don't skip subagent prompts
1620
- messageType = "assistant"; // Change from "user" to "assistant"
1621
- subtype = "subagent_prompt";
1622
- const textContent = blocks
1623
- .filter((b) => b && b.type === "text" && typeof b.text === "string")
1624
- .map((b) => b.text)
1625
- .join("");
1626
- structuredContent = {
1627
- text: textContent,
1628
- timestamp: new Date().toISOString(),
1629
- };
1630
- // metadata already set above with parent_tool_use_id
1631
- }
1632
- }
1633
- // If content array only contains empty objects, skip sending
1634
- if (Array.isArray(structuredContent) &&
1635
- structuredContent.length > 0 &&
1636
- structuredContent.every((it) => !it || typeof it !== "object" || Object.keys(it).length === 0)) {
1637
- console.log("[NorthflareAgentManager] Skipping empty 'user' message with only empty objects from SDK");
1638
- skipSend = true;
1639
- }
1640
- }
1641
- else if (typeof userMsg?.content === "string") {
1642
- // Attempt to parse JSON arrays (common for tool_result payloads)
1643
- const text = userMsg.content;
1644
- try {
1645
- const parsed = JSON.parse(text);
1646
- if (Array.isArray(parsed)) {
1647
- const hasToolResult = parsed.some((item) => item &&
1648
- typeof item === "object" &&
1649
- item.type === "tool_result");
1650
- if (hasToolResult) {
1651
- messageType = "tool_result";
1652
- subtype = "tool_result";
1653
- structuredContent = parsed;
1654
- }
1655
- else {
1656
- // Regular user message in JSON array format - skip
1657
- console.log("[NorthflareAgentManager] Skipping regular user message from JSON array (already in database)");
1658
- skipSend = true;
1659
- }
1660
- }
1661
- else {
1662
- // Regular user message as parsed JSON - skip
1663
- console.log("[NorthflareAgentManager] Skipping regular user message from parsed JSON (already in database)");
1664
- skipSend = true;
1665
- }
1666
- }
1667
- catch {
1668
- // Not JSON - regular text user message - skip
1669
- console.log("[NorthflareAgentManager] Skipping regular text user message (already in database)");
1670
- skipSend = true;
1671
- }
1672
- }
1673
- else {
1674
- // Other object content - skip regular user messages
1675
- console.log("[NorthflareAgentManager] Skipping regular user message with object content (already in database)");
1676
- skipSend = true;
1677
- }
1678
- break;
1679
- }
1680
- case "system": {
1681
- const systemMsg = message;
1682
- const subtype = systemMsg.subtype || "system";
1683
- const model = systemMsg.model || systemMsg?.message?.model;
1684
- const permissionMode = systemMsg.permissionMode || systemMsg?.message?.permissionMode;
1685
- const summary = [
1686
- subtype && `[${subtype}]`,
1687
- model && `model=${model}`,
1688
- permissionMode && `perm=${permissionMode}`,
1689
- ]
1690
- .filter(Boolean)
1691
- .join(" ");
1692
- structuredContent = {
1693
- text: summary || "",
1694
- timestamp: new Date().toISOString(),
1695
- };
1696
- break;
1697
- }
1698
- case "error": {
1699
- const errorMsg = message;
1700
- messageType = "system";
1701
- subtype = "error";
1702
- isError = true;
1703
- structuredContent = {
1704
- text: errorMsg.message || errorMsg.error || "Unknown error",
1705
- errorType: errorMsg.error_type || "unknown",
1706
- errorDetails: {
1707
- stack: errorMsg.stack,
1708
- code: errorMsg.code,
1709
- context: errorMsg,
1710
- },
1711
- timestamp: new Date().toISOString(),
1712
- };
1713
- break;
1714
- }
1715
- case "result": {
1716
- // SDK result message - check if it's an error result
1717
- const resultMsg = message;
1718
- const resultIsError = resultMsg.is_error === true;
1719
- const resultSubtype = resultMsg.subtype || "success";
1720
- const errorMessage = resultMsg.error_message || resultMsg.result || "";
1721
- if (resultIsError) {
1722
- // Store the SDK error in context metadata so onComplete knows about it
1723
- context.metadata = context.metadata || {};
1724
- context.metadata["_sdkError"] = {
1725
- isError: true,
1726
- errorMessage: errorMessage,
1727
- subtype: resultSubtype,
1728
- };
1729
- console.log("[NorthflareAgentManager] SDK result message indicates error", {
1730
- conversationId: context.conversationId,
1731
- subtype: resultSubtype,
1732
- errorMessage: errorMessage?.slice?.(0, 200),
1733
- });
1734
- messageType = "system";
1735
- subtype = "error";
1736
- isError = true;
1737
- structuredContent = {
1738
- text: errorMessage || "SDK execution error",
1739
- errorType: resultSubtype,
1740
- errorDetails: {
1741
- sdk_subtype: resultSubtype,
1742
- duration_ms: resultMsg.duration_ms,
1743
- num_turns: resultMsg.num_turns,
1744
- },
1745
- timestamp: new Date().toISOString(),
1746
- };
1747
- }
1748
- else {
1749
- // Success result - just log it, don't send as a separate message
1750
- console.log("[NorthflareAgentManager] SDK result message (success)", {
1751
- conversationId: context.conversationId,
1752
- subtype: resultSubtype,
1753
- resultPreview: (resultMsg.result || "").slice?.(0, 100),
1754
- });
1755
- skipSend = true;
1756
- }
1757
- break;
1758
- }
1759
- default: {
1760
- // Unknown message type - log and send as assistant
1761
- const unknownMsg = message;
1762
- console.warn(`Unknown message type: ${unknownMsg.type}`, message);
1763
- messageType = "assistant";
1764
- structuredContent = {
1765
- text: JSON.stringify(message),
1766
- timestamp: new Date().toISOString(),
1767
- };
1768
- }
1769
- }
1770
- // Generate a unique message ID
1771
- const messageId = `${context.agentSessionId}-${Date.now()}-${Math.random()
1772
- .toString(36)
1773
- .substr(2, 9)}`;
1774
- // Send agent message to orchestrator with structured content
1775
- // Skip if conversation is stopping/stopped to avoid race conditions
1776
- const currentStatus = context.status;
1777
- if (currentStatus !== "stopped" && currentStatus !== "stopping") {
1778
- if (skipSend) {
1779
- console.log("[NorthflareAgentManager] Not sending message.agent due to skipSend=true");
1780
- return;
1781
- }
1782
- const payload = {
1783
- conversationId: context.conversationId,
1784
- conversationObjectType: context.conversationObjectType,
1785
- conversationObjectId: context.conversationObjectId,
1786
- agentSessionId: context.agentSessionId,
1787
- type: messageType,
1788
- subtype,
1789
- content: Array.isArray(structuredContent)
1790
- ? structuredContent
1791
- : [structuredContent],
1792
- messageId,
1793
- isError,
1794
- };
1795
- // Add metadata if present
1796
- if (metadata) {
1797
- payload.metadata = metadata;
1798
- }
1799
- try {
1800
- console.log("[NorthflareAgentManager] Sending message.agent payload:", {
1801
- type: payload.type,
1802
- subtype: payload.subtype,
1803
- contentPreview: Array.isArray(payload.content)
1804
- ? payload.content.slice(0, 1)
1805
- : payload.content,
1806
- });
1807
- }
1808
- catch { }
1809
- await this.runner.notify("message.agent", payload);
1810
- try {
1811
- console.log("[NorthflareAgentManager] message.agent dispatched", {
1812
- conversationId: context.conversationId,
1813
- agentSessionId: context.agentSessionId,
1814
- messageId,
1815
- type: payload.type,
1816
- subtype: payload.subtype,
1817
- });
1818
- // If the main agent sent a submit tool result, treat it as terminal and finalize
1819
- if (payload.subtype === "submit_result") {
1820
- console.log("[NorthflareAgentManager] Submit tool detected - finalizing conversation", {
1821
- conversationId: context.conversationId,
1822
- agentSessionId: context.agentSessionId,
1823
- });
1824
- // End SDK conversation to stop further streaming
1825
- try {
1826
- if (context.conversation &&
1827
- isAgentConversation(context.conversation)) {
1828
- await context.conversation.end();
1829
- }
1830
- }
1831
- catch (e) {
1832
- console.warn("[NorthflareAgentManager] Error ending conversation on submit:", e);
1833
- }
1834
- await this._finalizeConversation(context, false, undefined, "submit_tool");
1835
- }
1836
- }
1837
- catch { }
1838
- }
1839
- // Tool calls are handled directly by the agent through the MCP server
1840
- // We just log that we saw them but don't intercept or process them
1841
- if (structuredContent.toolCalls &&
1842
- structuredContent.toolCalls.length > 0) {
1843
- console.log(`Northflare Agent is making ${structuredContent.toolCalls.length} tool call(s) via MCP`, {
1844
- conversationObjectId: context.conversationObjectId,
1845
- toolNames: structuredContent.toolCalls.map((tc) => tc.name),
1846
- });
1847
- }
1848
- }
1849
- catch (error) {
1850
- // Check if this is a transport error due to stopped conversation
1851
- const errorMessage = error instanceof Error ? error.message : String(error);
1852
- const isTransportError = errorMessage.includes("Cannot read properties of undefined") ||
1853
- errorMessage.includes("stdout") ||
1854
- errorMessage.includes("transport");
1855
- const statusCheck = context.status;
1856
- if (isTransportError &&
1857
- (statusCheck === "stopped" || statusCheck === "stopping")) {
1858
- // This is expected when conversation is stopped - just log it
1859
- console.log(`Transport error for stopped/stopping conversation ${context.conversationId} (expected):`, errorMessage);
1860
- return;
1861
- }
1862
- console.error(`Error handling streamed message for ${context.conversationObjectType} ${context.conversationObjectId}:`, error);
1863
- await this._handleConversationError(context, error);
1864
- }
1865
- }
1866
- }
1867
- function createUserMessageStream() {
1868
- const queue = [];
1869
- let resolver = null;
1870
- let done = false;
1871
- async function* iterator() {
1872
- while (true) {
1873
- if (queue.length > 0) {
1874
- const value = queue.shift();
1875
- // Attach metadata about remaining queue length so consumers can check
1876
- // if more messages are immediately available without Promise.race tricks
1877
- if (value && typeof value === "object") {
1878
- value.__queueHasMore = queue.length > 0;
1879
- }
1880
- yield value;
1881
- continue;
1882
- }
1883
- if (done)
1884
- return;
1885
- await new Promise((resolve) => (resolver = resolve));
1886
- resolver = null;
1887
- }
1888
- }
1889
- return {
1890
- iterable: iterator(),
1891
- enqueue: (msg) => {
1892
- if (done)
1893
- return;
1894
- queue.push(msg);
1895
- if (resolver) {
1896
- const r = resolver;
1897
- resolver = null;
1898
- r();
1899
- }
1900
- },
1901
- close: () => {
1902
- done = true;
1903
- if (resolver) {
1904
- const r = resolver;
1905
- resolver = null;
1906
- r();
1907
- }
1908
- },
1909
- };
1910
- }
1911
- function createConversationWrapper(sdk, input, onComplete) {
1912
- let onSessionIdCb = null;
1913
- let observedSessionId = null;
1914
- let startedReader = false;
1915
- // Create SDK user message from text or multimodal content
1916
- function toSdkUserMessage(content) {
1917
- // If content is an array with image blocks, convert to SDK multimodal format
1918
- if (Array.isArray(content)) {
1919
- const sdkContent = content.map((block) => {
1920
- if (block.type === "text" && block.text) {
1921
- return { type: "text", text: block.text };
1922
- }
1923
- else if (block.type === "image" && block.image) {
1924
- return { type: "image", image: block.image };
1925
- }
1926
- return null;
1927
- }).filter(Boolean);
1928
- return {
1929
- type: "user",
1930
- session_id: observedSessionId || "",
1931
- parent_tool_use_id: null,
1932
- message: {
1933
- role: "user",
1934
- content: sdkContent,
1935
- },
1936
- };
1937
- }
1938
- // Plain text message
1939
- return {
1940
- type: "user",
1941
- session_id: observedSessionId || "",
1942
- parent_tool_use_id: null,
1943
- message: {
1944
- role: "user",
1945
- content: content,
1946
- },
1947
- };
1948
- }
1949
- return {
1950
- // Send a message - accepts either text-only or multimodal content
1951
- send(payload) {
1952
- // Check if multimodal content is provided
1953
- if (payload.content && Array.isArray(payload.content)) {
1954
- const hasImages = payload.content.some((b) => b.type === "image");
1955
- try {
1956
- console.log("[NorthflareAgentManager] -> SDK send (multimodal)", {
1957
- sessionId: observedSessionId,
1958
- type: payload?.type,
1959
- blockCount: payload.content.length,
1960
- hasImages,
1961
- blockTypes: payload.content.map((b) => b.type).join(", "),
1962
- });
1963
- }
1964
- catch { }
1965
- input.enqueue(toSdkUserMessage(payload.content));
1966
- return;
1967
- }
1968
- // Text-only message
1969
- const text = payload?.text ?? "";
1970
- try {
1971
- console.log("[NorthflareAgentManager] -> SDK send", {
1972
- sessionId: observedSessionId,
1973
- type: payload?.type,
1974
- textPreview: text.slice(0, 200),
1975
- length: text.length,
1976
- });
1977
- }
1978
- catch { }
1979
- input.enqueue(toSdkUserMessage(text));
1980
- },
1981
- async end() {
1982
- try {
1983
- input.close();
1984
- }
1985
- finally {
1986
- // Simplified process cleanup
1987
- try {
1988
- if (sdk?.abortController) {
1989
- sdk.abortController.abort();
1990
- }
1991
- }
1992
- catch { }
1993
- }
1994
- },
1995
- onSessionId(cb) {
1996
- onSessionIdCb = cb;
1997
- },
1998
- stream(handler) {
1999
- if (startedReader)
2000
- return;
2001
- startedReader = true;
2002
- (async () => {
2003
- try {
2004
- for await (const msg of sdk) {
2005
- // Simplified session ID extraction
2006
- const sid = (msg && (msg.session_id || msg.sessionId)) || null;
2007
- if (sid && sid !== observedSessionId) {
2008
- observedSessionId = sid;
2009
- if (onSessionIdCb)
2010
- onSessionIdCb(sid);
2011
- }
2012
- await handler(msg, sid);
2013
- }
2014
- // Normal completion
2015
- if (onComplete)
2016
- await onComplete(false);
2017
- }
2018
- catch (e) {
2019
- // Error completion
2020
- if (onComplete)
2021
- await onComplete(true, e);
2022
- }
2023
- })();
2024
- },
2025
- };
2026
- }
2027
- function isAgentConversation(conversation) {
2028
- return (!!conversation &&
2029
- typeof conversation.send === "function" &&
2030
- typeof conversation.end === "function");
2031
- }
2032
- //# sourceMappingURL=northflare-agent-sdk-manager.js.map