@northflare/runner 0.0.19 → 0.0.21

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 (48) hide show
  1. package/bin/northflare-runner +17 -3
  2. package/dist/components/claude-sdk-manager.d.ts +6 -3
  3. package/dist/components/claude-sdk-manager.d.ts.map +1 -1
  4. package/dist/components/claude-sdk-manager.js +62 -42
  5. package/dist/components/claude-sdk-manager.js.map +1 -1
  6. package/dist/components/codex-sdk-manager.d.ts +6 -3
  7. package/dist/components/codex-sdk-manager.d.ts.map +1 -1
  8. package/dist/components/codex-sdk-manager.js +60 -16
  9. package/dist/components/codex-sdk-manager.js.map +1 -1
  10. package/dist/components/enhanced-repository-manager.d.ts.map +1 -1
  11. package/dist/components/enhanced-repository-manager.js +2 -1
  12. package/dist/components/enhanced-repository-manager.js.map +1 -1
  13. package/dist/components/message-handler-sse.d.ts.map +1 -1
  14. package/dist/components/message-handler-sse.js +157 -116
  15. package/dist/components/message-handler-sse.js.map +1 -1
  16. package/dist/components/northflare-agent-sdk-manager.d.ts +10 -6
  17. package/dist/components/northflare-agent-sdk-manager.d.ts.map +1 -1
  18. package/dist/components/northflare-agent-sdk-manager.js +350 -98
  19. package/dist/components/northflare-agent-sdk-manager.js.map +1 -1
  20. package/dist/components/repository-manager.d.ts.map +1 -1
  21. package/dist/components/repository-manager.js +2 -1
  22. package/dist/components/repository-manager.js.map +1 -1
  23. package/dist/runner-sse.d.ts.map +1 -1
  24. package/dist/runner-sse.js +7 -0
  25. package/dist/runner-sse.js.map +1 -1
  26. package/dist/types/claude.d.ts +4 -1
  27. package/dist/types/claude.d.ts.map +1 -1
  28. package/dist/utils/console.d.ts +5 -8
  29. package/dist/utils/console.d.ts.map +1 -1
  30. package/dist/utils/console.js +28 -10
  31. package/dist/utils/console.js.map +1 -1
  32. package/dist/utils/debug.d.ts +10 -0
  33. package/dist/utils/debug.d.ts.map +1 -1
  34. package/dist/utils/debug.js +86 -8
  35. package/dist/utils/debug.js.map +1 -1
  36. package/dist/utils/logger.d.ts +2 -1
  37. package/dist/utils/logger.d.ts.map +1 -1
  38. package/dist/utils/logger.js +43 -13
  39. package/dist/utils/logger.js.map +1 -1
  40. package/dist/utils/message-log.d.ts +23 -0
  41. package/dist/utils/message-log.d.ts.map +1 -0
  42. package/dist/utils/message-log.js +69 -0
  43. package/dist/utils/message-log.js.map +1 -0
  44. package/dist/utils/status-line.d.ts +5 -1
  45. package/dist/utils/status-line.d.ts.map +1 -1
  46. package/dist/utils/status-line.js +13 -3
  47. package/dist/utils/status-line.js.map +1 -1
  48. package/package.json +1 -2
@@ -9,11 +9,20 @@
9
9
  import { query as sdkQuery } from "@northflare/agent";
10
10
  import { createGroq, groq as groqProvider } from "@ai-sdk/groq";
11
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';
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
16
  import jwt from "jsonwebtoken";
17
+ const console = createScopedConsole(isDebugEnabledFor("manager") ? "manager" : "sdk");
18
+ function buildSystemPromptWithNorthflare(basePrompt, northflarePrompt) {
19
+ const parts = [northflarePrompt, basePrompt]
20
+ .filter((p) => typeof p === "string" && p.trim().length > 0)
21
+ .map((p) => p.trim());
22
+ if (parts.length === 0)
23
+ return undefined;
24
+ return parts.join("\n\n");
25
+ }
17
26
  export class NorthflareAgentManager {
18
27
  runner;
19
28
  repositoryManager;
@@ -22,7 +31,7 @@ export class NorthflareAgentManager {
22
31
  this.runner = runner;
23
32
  this.repositoryManager = repositoryManager;
24
33
  // Log debug mode status
25
- if (isRunnerDebugEnabled()) {
34
+ if (isDebugEnabledFor("sdk")) {
26
35
  console.log("[NorthflareAgentManager] DEBUG MODE ENABLED - Northflare Agent SDK will log verbose output");
27
36
  }
28
37
  // Note: MCP host configuration is passed from orchestrator
@@ -33,6 +42,48 @@ export class NorthflareAgentManager {
33
42
  // Runner does not define its own MCP tools
34
43
  // All MCP tool configuration is passed from orchestrator in conversation config
35
44
  }
45
+ buildSubAgentConfig(subAgentTypes) {
46
+ const enabled = subAgentTypes?.["enabled"] !== false;
47
+ const allowInherit = subAgentTypes?.["inherit"] !== false;
48
+ if (!enabled) {
49
+ return { enabled: false, agents: {} };
50
+ }
51
+ const agents = {};
52
+ if (allowInherit) {
53
+ agents["inherit"] = {
54
+ description: "Use the main task model and toolset.",
55
+ prompt: "",
56
+ model: "inherit",
57
+ };
58
+ }
59
+ const RESERVED = new Set(["inherit", "enabled"]);
60
+ Object.entries(subAgentTypes || {}).forEach(([name, value]) => {
61
+ if (RESERVED.has(name))
62
+ return;
63
+ if (!value || typeof value !== "object")
64
+ return;
65
+ const description = typeof value.description === "string" &&
66
+ value.description.trim().length
67
+ ? value.description.trim()
68
+ : `Use agentType \"${name}\" when its specialization fits the task.`;
69
+ const prompt = typeof value.instructions === "string"
70
+ ? value.instructions.trim()
71
+ : "";
72
+ const model = typeof value.model === "string" &&
73
+ value.model.trim().length
74
+ ? value.model.trim()
75
+ : "inherit";
76
+ agents[name] = {
77
+ description,
78
+ prompt,
79
+ model,
80
+ };
81
+ });
82
+ if (Object.keys(agents).length === 0) {
83
+ return { enabled: false, agents: {} };
84
+ }
85
+ return { enabled: true, agents };
86
+ }
36
87
  async startConversation(conversationObjectType, conversationObjectId, config, initialMessages, conversationData, provider = "openrouter") {
37
88
  // Returns conversation context
38
89
  // Greenfield: conversationData.id is required as the authoritative DB conversation ID
@@ -155,7 +206,9 @@ export class NorthflareAgentManager {
155
206
  console.warn("[NorthflareAgentManager] Unable to generate TOOL_TOKEN - missing required data");
156
207
  }
157
208
  }
158
- const debugEnabled = isRunnerDebugEnabled();
209
+ const managerDebug = isDebugEnabledFor("manager");
210
+ const sdkDebug = isDebugEnabledFor("sdk");
211
+ const debugEnabled = managerDebug || sdkDebug;
159
212
  // Simplified SDK configuration - using native query API with streamlined options
160
213
  // Simplified environment configuration
161
214
  const envVars = {
@@ -165,13 +218,8 @@ export class NorthflareAgentManager {
165
218
  envVars["ANTHROPIC_API_KEY"] = config.anthropicApiKey;
166
219
  }
167
220
  // 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);
221
+ const openRouterApiKey = config.openRouterApiKey || process.env["OPENROUTER_API_KEY"];
222
+ const groqApiKey = config.groqApiKey || process.env["GROQ_API_KEY"];
175
223
  if (openRouterApiKey) {
176
224
  envVars["OPENROUTER_API_KEY"] = openRouterApiKey;
177
225
  }
@@ -188,40 +236,67 @@ export class NorthflareAgentManager {
188
236
  if (toolToken) {
189
237
  envVars["TOOL_TOKEN"] = toolToken;
190
238
  }
191
- if (debugEnabled) {
239
+ if (sdkDebug) {
192
240
  envVars["DEBUG"] = "1";
193
241
  }
242
+ const subAgentConfig = this.buildSubAgentConfig(config.subAgentTypes);
243
+ const agentsOption = subAgentConfig.enabled && Object.keys(subAgentConfig.agents).length > 0
244
+ ? subAgentConfig.agents
245
+ : undefined;
194
246
  // 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),
247
+ const providedSystemPrompt = buildSystemPromptWithNorthflare(config.systemPrompt || conversationData?.systemPrompt, config?.northflareSystemPrompt ||
248
+ conversationData?.northflareSystemPrompt);
249
+ const systemPromptMode = config.systemPromptMode ||
250
+ conversationData?.systemPromptMode ||
251
+ // Default ProjectStep conversations to append so preset stays intact
252
+ "append";
253
+ let systemPromptOption;
254
+ if (providedSystemPrompt) {
255
+ if (systemPromptMode === "replace") {
256
+ systemPromptOption = providedSystemPrompt;
257
+ }
258
+ else {
259
+ systemPromptOption = {
260
+ type: "preset",
261
+ preset: "claude_code",
262
+ append: providedSystemPrompt,
263
+ };
264
+ }
265
+ }
266
+ // Debug log the systemPrompt being sent to Northflare Agent SDK
267
+ console.log(`[NorthflareAgentManager] System prompt configuration for conversation ${context.conversationId}:`, {
268
+ mode: systemPromptMode,
269
+ hasProvidedSystemPrompt: Boolean(providedSystemPrompt),
270
+ finalPromptType: typeof systemPromptOption,
271
+ finalPromptLength: typeof systemPromptOption === "string"
272
+ ? systemPromptOption.length
273
+ : systemPromptOption?.append?.length ?? 0,
209
274
  });
210
- // Simplified tool restrictions for read-only mode
275
+ // Tool restrictions based on permissions mode
276
+ // "read" = read-only mode (no file writes, no shell)
277
+ // "project" = read + no subagents (for ProjectStep orchestrators)
278
+ const readOnlyTools = [
279
+ "write_file",
280
+ "edit_file",
281
+ "multi_edit_file",
282
+ "delete_file",
283
+ "move_file",
284
+ "copy_file",
285
+ "run_command",
286
+ "todo_write",
287
+ ];
211
288
  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
- : [];
289
+ ? readOnlyTools
290
+ : context.permissionsMode === "project"
291
+ ? [...readOnlyTools, "task"]
292
+ : [];
293
+ if (!subAgentConfig.enabled) {
294
+ disallowedTools.push("task");
295
+ }
223
296
  if (disallowedTools.length) {
224
- console.log("[NorthflareAgentManager] Applied read-only mode tool restrictions");
297
+ console.log("[NorthflareAgentManager] Tool restrictions applied", {
298
+ disallowedTools,
299
+ });
225
300
  }
226
301
  // Simplified MCP server configuration
227
302
  let mcpServers;
@@ -237,7 +312,11 @@ export class NorthflareAgentManager {
237
312
  cwd: workspacePath,
238
313
  hasMcpServers: !!mcpServers && Object.keys(mcpServers).length > 0,
239
314
  disallowedTools,
240
- hasAppendSystemPrompt: Boolean(appendSystemPrompt),
315
+ systemPromptMode,
316
+ hasSystemPrompt: Boolean(systemPromptOption),
317
+ systemPromptType: typeof systemPromptOption,
318
+ subAgentsEnabled: subAgentConfig.enabled,
319
+ agentTypes: agentsOption ? Object.keys(agentsOption) : [],
241
320
  env: {
242
321
  OPENROUTER_API_KEY: Boolean(envVars["OPENROUTER_API_KEY"]),
243
322
  GROQ_API_KEY: Boolean(envVars["GROQ_API_KEY"]),
@@ -283,29 +362,95 @@ export class NorthflareAgentManager {
283
362
  openRouterApiKeyPresent: Boolean(openRouterApiKey || process.env["OPENROUTER_API_KEY"]),
284
363
  sdkModelType: sdkModel?.constructor?.name,
285
364
  });
365
+ // Build a modelResolver for sub-agents that can resolve model strings to LanguageModel instances
366
+ const modelResolver = (modelId) => {
367
+ // Normalize the model ID (strip provider prefixes)
368
+ const normalizedModelId = this.normalizeModel(modelId);
369
+ // Determine the provider from the model ID prefix or fall back to the parent provider
370
+ let modelProvider = provider;
371
+ if (modelId.startsWith("groq:") || modelId.startsWith("groq/")) {
372
+ modelProvider = "groq";
373
+ }
374
+ else if (modelId.startsWith("openrouter:") ||
375
+ modelId.startsWith("openrouter/")) {
376
+ modelProvider = "openrouter";
377
+ }
378
+ // Create the appropriate model instance
379
+ if (modelProvider === "groq") {
380
+ const groqClient = groqApiKey && groqApiKey.length > 0
381
+ ? createGroq({ apiKey: groqApiKey })
382
+ : groqProvider;
383
+ return groqClient(normalizedModelId);
384
+ }
385
+ if (modelProvider === "openrouter") {
386
+ const client = createOpenRouter({
387
+ apiKey: openRouterApiKey || process.env["OPENROUTER_API_KEY"] || "",
388
+ });
389
+ return client(normalizedModelId);
390
+ }
391
+ // Unknown provider - return undefined to let SDK throw a helpful error
392
+ return undefined;
393
+ };
286
394
  // Launch SDK with simplified configuration
287
395
  let silenceTimer = null;
288
396
  let lastSdkActivity = Date.now();
397
+ const handleTaskProgress = async (update) => {
398
+ const pct = Number(update?.progressPercent);
399
+ if (!Number.isFinite(pct))
400
+ return;
401
+ const clamped = Math.max(0, Math.min(100, pct));
402
+ const messageId = `${context.agentSessionId}-progress-${Date.now()}-${Math.random()
403
+ .toString(36)
404
+ .slice(2, 8)}`;
405
+ const progressPayload = {
406
+ conversationId: context.conversationId,
407
+ conversationObjectType: context.conversationObjectType,
408
+ conversationObjectId: context.conversationObjectId,
409
+ agentSessionId: context.agentSessionId,
410
+ type: "assistant",
411
+ subtype: "task_progress",
412
+ messageMetaType: "task_progress",
413
+ metadata: {
414
+ progressPercent: clamped,
415
+ toolCallId: update?.toolCallId,
416
+ raw: update?.raw,
417
+ },
418
+ content: [
419
+ {
420
+ type: "text",
421
+ text: `Task progress ${Math.round(clamped)}%`,
422
+ },
423
+ ],
424
+ messageId,
425
+ };
426
+ try {
427
+ await this.runner.notify("message.agent", progressPayload);
428
+ }
429
+ catch (err) {
430
+ console.warn("[NorthflareAgentManager] Failed to send task progress", {
431
+ conversationId: context.conversationId,
432
+ error: err instanceof Error ? err.message : String(err),
433
+ });
434
+ }
435
+ };
289
436
  const sdk = sdkQuery({
290
437
  prompt: input.iterable,
291
438
  options: {
292
439
  cwd: workspacePath,
293
440
  env: envVars,
294
441
  model: sdkModel,
442
+ modelResolver,
295
443
  resume: config.sessionId || conversationData?.agentSessionId || undefined,
444
+ ...(agentsOption ? { agents: agentsOption } : {}),
296
445
  ...(maxTurns !== undefined ? { maxTurns } : {}),
297
- ...(appendSystemPrompt
298
- ? {
299
- systemPrompt: {
300
- type: "preset",
301
- preset: "claude_code",
302
- append: appendSystemPrompt,
303
- },
304
- }
305
- : {}),
446
+ ...(systemPromptOption ? { systemPrompt: systemPromptOption } : {}),
306
447
  ...(disallowedTools.length ? { disallowedTools } : {}),
307
448
  ...(mcpServers ? { mcpServers } : {}),
308
- ...(debugEnabled
449
+ conversationId: context.conversationId,
450
+ conversationObjectId: context.conversationObjectId,
451
+ taskId: context.taskId,
452
+ onTaskProgress: handleTaskProgress,
453
+ ...(sdkDebug
309
454
  ? {
310
455
  debug: true,
311
456
  stderr: (data) => {
@@ -316,12 +461,8 @@ export class NorthflareAgentManager {
316
461
  },
317
462
  onStepFinish: (step) => {
318
463
  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
- });
464
+ console.log("[NorthflareAgentManager] Agent step finished (full step object): " +
465
+ JSON.stringify(step, null, 2));
325
466
  }
326
467
  catch { }
327
468
  },
@@ -334,30 +475,56 @@ export class NorthflareAgentManager {
334
475
  model: context.model,
335
476
  cwd: workspacePath,
336
477
  maxTurns: maxTurns ?? "default (SDK)",
337
- hasSystemPrompt: Boolean(appendSystemPrompt),
478
+ hasSystemPrompt: Boolean(systemPromptOption),
338
479
  hasMcpServers: Boolean(mcpServers && Object.keys(mcpServers).length),
339
480
  disallowedToolsCount: disallowedTools.length,
340
481
  envKeys: Object.keys(envVars),
341
482
  });
342
483
  // Track number of streamed messages to aid debugging when sessions appear silent
343
484
  let streamedMessageCount = 0;
485
+ const clearSilenceTimer = () => {
486
+ if (silenceTimer) {
487
+ clearInterval(silenceTimer);
488
+ silenceTimer = null;
489
+ }
490
+ };
344
491
  // Simplified conversation wrapper - reduced complexity while maintaining interface
345
492
  const conversation = createConversationWrapper(sdk, input, async (hadError, error) => {
493
+ // Check if SDK reported an error via result message (stored in metadata)
494
+ const sdkError = context.metadata?.["_sdkError"];
495
+ const effectiveHadError = hadError || sdkError?.isError === true;
496
+ const effectiveError = error ||
497
+ (sdkError?.isError
498
+ ? new Error(sdkError.errorMessage || "SDK execution error")
499
+ : undefined);
346
500
  console.log("[NorthflareAgentManager] SDK stream completed", {
347
501
  conversationId: context.conversationId,
348
502
  agentSessionId: context.agentSessionId,
349
503
  hadError,
350
- errorMessage: error?.message,
504
+ sdkErrorDetected: sdkError?.isError,
505
+ effectiveHadError,
506
+ errorMessage: effectiveError?.message,
351
507
  streamedMessageCount,
352
508
  });
353
- if (silenceTimer) {
354
- clearInterval(silenceTimer);
355
- silenceTimer = null;
509
+ clearSilenceTimer();
510
+ // Surface SDK-level failures to the orchestrator so the task
511
+ // transitions to needs_attention and an error bubble appears in chat.
512
+ if (effectiveHadError && effectiveError) {
513
+ try {
514
+ const normalizedError = effectiveError instanceof Error
515
+ ? effectiveError
516
+ : new Error(String(effectiveError));
517
+ await this._handleConversationError(context, normalizedError);
518
+ }
519
+ catch (err) {
520
+ console.error("[NorthflareAgentManager] Failed to report SDK error:", err);
521
+ }
356
522
  }
357
- await this._finalizeConversation(context, hadError, error);
523
+ await this._finalizeConversation(context, effectiveHadError, effectiveError);
358
524
  });
359
525
  // Store conversation instance in context for reuse
360
526
  context.conversation = conversation;
527
+ context._clearSilenceTimer = clearSilenceTimer;
361
528
  // Observe session id from first messages that include it
362
529
  conversation.onSessionId(async (agentSessionId) => {
363
530
  if (!agentSessionId)
@@ -387,10 +554,7 @@ export class NorthflareAgentManager {
387
554
  if (typeof message === "string")
388
555
  return message.slice(0, 300);
389
556
  const m = message;
390
- const content = m?.message?.content ||
391
- m?.content ||
392
- m?.message?.text ||
393
- m?.text;
557
+ const content = m?.message?.content || m?.content || m?.message?.text || m?.text;
394
558
  if (typeof content === "string")
395
559
  return content.slice(0, 300);
396
560
  if (Array.isArray(content)) {
@@ -435,25 +599,12 @@ export class NorthflareAgentManager {
435
599
  await this._finalizeConversation(context, false);
436
600
  }
437
601
  }
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
602
  }
452
603
  catch (e) {
453
604
  console.warn("[NorthflareAgentManager] finalize-on-system heuristic error:", e);
454
605
  }
455
606
  };
456
- if (debugEnabled) {
607
+ if (managerDebug) {
457
608
  silenceTimer = setInterval(() => {
458
609
  const now = Date.now();
459
610
  const idleMs = now - lastSdkActivity;
@@ -467,10 +618,9 @@ export class NorthflareAgentManager {
467
618
  }
468
619
  }, 5000);
469
620
  }
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
621
+ // Send initial messages BEFORE starting the stream.
622
+ // This ensures all initial messages are queued and available when the SDK
623
+ // starts iterating, so it can collect them all without timing issues.
474
624
  try {
475
625
  for (const message of initialMessages) {
476
626
  const initialText = this.normalizeToText(message.content);
@@ -485,6 +635,13 @@ export class NorthflareAgentManager {
485
635
  text: initialText,
486
636
  });
487
637
  }
638
+ // Kick off streaming; completion/error is handled by the wrapper's
639
+ // onComplete callback above. (The stream method returns void, so awaiting
640
+ // it would resolve immediately and incorrectly mark the conversation
641
+ // finished.)
642
+ conversation.stream(messageHandler);
643
+ // Note: Error handling is done via process completion handler
644
+ // The Northflare Agent SDK doesn't have an onError method on conversations
488
645
  console.log(`Started conversation for ${conversationObjectType} ${conversationObjectId} in workspace ${workspacePath}`);
489
646
  // Return the conversation context directly
490
647
  return context;
@@ -550,6 +707,13 @@ export class NorthflareAgentManager {
550
707
  if (context._finalized)
551
708
  return;
552
709
  context._finalized = true;
710
+ try {
711
+ const clearSilenceTimer = context._clearSilenceTimer;
712
+ if (typeof clearSilenceTimer === "function") {
713
+ clearSilenceTimer();
714
+ }
715
+ }
716
+ catch { }
553
717
  // Mark as completed immediately to prevent restart on catch-up
554
718
  // This is synchronous and happens before any async operations
555
719
  this.runner.markConversationCompleted(context.conversationId);
@@ -613,7 +777,9 @@ export class NorthflareAgentManager {
613
777
  // Use the config that was already prepared by TaskOrchestrator and sent in the RunnerMessage
614
778
  const startConfig = {
615
779
  anthropicApiKey: config?.anthropicApiKey || process.env["ANTHROPIC_API_KEY"] || "",
616
- systemPrompt: config?.systemPrompt,
780
+ systemPrompt: config?.systemPrompt || conversationDetails?.systemPrompt,
781
+ systemPromptMode: config?.systemPromptMode ||
782
+ conversationDetails?.systemPromptMode,
617
783
  workspaceId: conversationDetails.workspaceId,
618
784
  ...config, // Use the full config provided in the RunnerMessage
619
785
  ...(conversationDetails.agentSessionId
@@ -637,8 +803,7 @@ export class NorthflareAgentManager {
637
803
  }
638
804
  try {
639
805
  // Send immediately when a conversation instance exists; no need to wait for "active"
640
- if (!context.conversation ||
641
- !isAgentConversation(context.conversation)) {
806
+ if (!context.conversation || !isAgentConversation(context.conversation)) {
642
807
  throw new Error(`No conversation instance found for conversation ${context.conversationId}`);
643
808
  }
644
809
  // Guard: Don't send messages if conversation is stopped or stopping
@@ -654,7 +819,7 @@ export class NorthflareAgentManager {
654
819
  }
655
820
  // Native message injection - SDK handles queueing and delivery
656
821
  const normalizedText = this.normalizeToText(content);
657
- if (isRunnerDebugEnabled()) {
822
+ if (isDebugEnabledFor("manager")) {
658
823
  console.log("[NorthflareAgentManager] Normalized follow-up content", {
659
824
  originalType: typeof content,
660
825
  isArray: Array.isArray(content) || undefined,
@@ -716,21 +881,40 @@ export class NorthflareAgentManager {
716
881
  console.error(`Conversation error for ${context.conversationObjectType} ${context.conversationObjectId}:`, error);
717
882
  }
718
883
  classifyError(error) {
719
- if (error.message.includes("process exited")) {
884
+ const msg = error.message.toLowerCase();
885
+ if (msg.includes("process exited")) {
720
886
  return "process_exit";
721
887
  }
722
- else if (error.message.includes("model")) {
888
+ else if (msg.includes("model")) {
723
889
  return "model_error";
724
890
  }
725
- else if (error.message.includes("tool")) {
891
+ else if (msg.includes("tool")) {
726
892
  return "tool_error";
727
893
  }
728
- else if (error.message.includes("permission")) {
894
+ else if (msg.includes("permission") ||
895
+ msg.includes("forbidden") ||
896
+ msg.includes("403")) {
729
897
  return "permission_error";
730
898
  }
731
- else if (error.message.includes("timeout")) {
899
+ else if (msg.includes("timeout")) {
732
900
  return "timeout_error";
733
901
  }
902
+ else if (msg.includes("unauthorized") ||
903
+ msg.includes("401") ||
904
+ msg.includes("authentication")) {
905
+ return "auth_error";
906
+ }
907
+ else if (msg.includes("rate limit") ||
908
+ msg.includes("429") ||
909
+ msg.includes("too many requests")) {
910
+ return "rate_limit_error";
911
+ }
912
+ else if (msg.includes("api") ||
913
+ msg.includes("network") ||
914
+ msg.includes("fetch") ||
915
+ msg.includes("connection")) {
916
+ return "api_error";
917
+ }
734
918
  return "unknown_error";
735
919
  }
736
920
  normalizeModel(rawModel) {
@@ -1000,7 +1184,8 @@ export class NorthflareAgentManager {
1000
1184
  toolResultMsg.id ||
1001
1185
  toolResultMsg?.message?.id;
1002
1186
  const resolvedContent = (() => {
1003
- if ("content" in toolResultMsg && toolResultMsg.content !== undefined) {
1187
+ if ("content" in toolResultMsg &&
1188
+ toolResultMsg.content !== undefined) {
1004
1189
  return toolResultMsg.content;
1005
1190
  }
1006
1191
  if (toolResultMsg.result !== undefined) {
@@ -1119,8 +1304,8 @@ export class NorthflareAgentManager {
1119
1304
  userMsg?.content ||
1120
1305
  [];
1121
1306
  // Check if this is a tool result message (needs processing)
1122
- const blocks = typeof rawContent === 'string'
1123
- ? [{ type: 'text', text: rawContent }]
1307
+ const blocks = typeof rawContent === "string"
1308
+ ? [{ type: "text", text: rawContent }]
1124
1309
  : rawContent;
1125
1310
  if (Array.isArray(blocks)) {
1126
1311
  const hasToolResult = blocks.some((b) => b && typeof b === "object" && b.type === "tool_result");
@@ -1244,6 +1429,50 @@ export class NorthflareAgentManager {
1244
1429
  };
1245
1430
  break;
1246
1431
  }
1432
+ case "result": {
1433
+ // SDK result message - check if it's an error result
1434
+ const resultMsg = message;
1435
+ const resultIsError = resultMsg.is_error === true;
1436
+ const resultSubtype = resultMsg.subtype || "success";
1437
+ const errorMessage = resultMsg.error_message || resultMsg.result || "";
1438
+ if (resultIsError) {
1439
+ // Store the SDK error in context metadata so onComplete knows about it
1440
+ context.metadata = context.metadata || {};
1441
+ context.metadata["_sdkError"] = {
1442
+ isError: true,
1443
+ errorMessage: errorMessage,
1444
+ subtype: resultSubtype,
1445
+ };
1446
+ console.log("[NorthflareAgentManager] SDK result message indicates error", {
1447
+ conversationId: context.conversationId,
1448
+ subtype: resultSubtype,
1449
+ errorMessage: errorMessage?.slice?.(0, 200),
1450
+ });
1451
+ messageType = "system";
1452
+ subtype = "error";
1453
+ isError = true;
1454
+ structuredContent = {
1455
+ text: errorMessage || "SDK execution error",
1456
+ errorType: resultSubtype,
1457
+ errorDetails: {
1458
+ sdk_subtype: resultSubtype,
1459
+ duration_ms: resultMsg.duration_ms,
1460
+ num_turns: resultMsg.num_turns,
1461
+ },
1462
+ timestamp: new Date().toISOString(),
1463
+ };
1464
+ }
1465
+ else {
1466
+ // Success result - just log it, don't send as a separate message
1467
+ console.log("[NorthflareAgentManager] SDK result message (success)", {
1468
+ conversationId: context.conversationId,
1469
+ subtype: resultSubtype,
1470
+ resultPreview: (resultMsg.result || "").slice?.(0, 100),
1471
+ });
1472
+ skipSend = true;
1473
+ }
1474
+ break;
1475
+ }
1247
1476
  default: {
1248
1477
  // Unknown message type - log and send as assistant
1249
1478
  const unknownMsg = message;
@@ -1303,6 +1532,24 @@ export class NorthflareAgentManager {
1303
1532
  type: payload.type,
1304
1533
  subtype: payload.subtype,
1305
1534
  });
1535
+ // If the main agent sent a submit tool result, treat it as terminal and finalize
1536
+ if (payload.subtype === "submit_result") {
1537
+ console.log("[NorthflareAgentManager] Submit tool detected - finalizing conversation", {
1538
+ conversationId: context.conversationId,
1539
+ agentSessionId: context.agentSessionId,
1540
+ });
1541
+ // End SDK conversation to stop further streaming
1542
+ try {
1543
+ if (context.conversation &&
1544
+ isAgentConversation(context.conversation)) {
1545
+ await context.conversation.end();
1546
+ }
1547
+ }
1548
+ catch (e) {
1549
+ console.warn("[NorthflareAgentManager] Error ending conversation on submit:", e);
1550
+ }
1551
+ await this._finalizeConversation(context, false, undefined, "submit_tool");
1552
+ }
1306
1553
  }
1307
1554
  catch { }
1308
1555
  }
@@ -1342,6 +1589,11 @@ function createUserMessageStream() {
1342
1589
  while (true) {
1343
1590
  if (queue.length > 0) {
1344
1591
  const value = queue.shift();
1592
+ // Attach metadata about remaining queue length so consumers can check
1593
+ // if more messages are immediately available without Promise.race tricks
1594
+ if (value && typeof value === "object") {
1595
+ value.__queueHasMore = queue.length > 0;
1596
+ }
1345
1597
  yield value;
1346
1598
  continue;
1347
1599
  }