@northflare/runner 0.0.13 → 0.0.16

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