@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.
- package/bin/northflare-runner +17 -3
- package/dist/components/claude-sdk-manager.d.ts +6 -3
- package/dist/components/claude-sdk-manager.d.ts.map +1 -1
- package/dist/components/claude-sdk-manager.js +62 -42
- package/dist/components/claude-sdk-manager.js.map +1 -1
- package/dist/components/codex-sdk-manager.d.ts +6 -3
- package/dist/components/codex-sdk-manager.d.ts.map +1 -1
- package/dist/components/codex-sdk-manager.js +60 -16
- package/dist/components/codex-sdk-manager.js.map +1 -1
- package/dist/components/enhanced-repository-manager.d.ts.map +1 -1
- package/dist/components/enhanced-repository-manager.js +2 -1
- package/dist/components/enhanced-repository-manager.js.map +1 -1
- package/dist/components/message-handler-sse.d.ts.map +1 -1
- package/dist/components/message-handler-sse.js +157 -116
- package/dist/components/message-handler-sse.js.map +1 -1
- package/dist/components/northflare-agent-sdk-manager.d.ts +10 -6
- package/dist/components/northflare-agent-sdk-manager.d.ts.map +1 -1
- package/dist/components/northflare-agent-sdk-manager.js +350 -98
- package/dist/components/northflare-agent-sdk-manager.js.map +1 -1
- package/dist/components/repository-manager.d.ts.map +1 -1
- package/dist/components/repository-manager.js +2 -1
- package/dist/components/repository-manager.js.map +1 -1
- package/dist/runner-sse.d.ts.map +1 -1
- package/dist/runner-sse.js +7 -0
- package/dist/runner-sse.js.map +1 -1
- package/dist/types/claude.d.ts +4 -1
- package/dist/types/claude.d.ts.map +1 -1
- package/dist/utils/console.d.ts +5 -8
- package/dist/utils/console.d.ts.map +1 -1
- package/dist/utils/console.js +28 -10
- package/dist/utils/console.js.map +1 -1
- package/dist/utils/debug.d.ts +10 -0
- package/dist/utils/debug.d.ts.map +1 -1
- package/dist/utils/debug.js +86 -8
- package/dist/utils/debug.js.map +1 -1
- package/dist/utils/logger.d.ts +2 -1
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +43 -13
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/message-log.d.ts +23 -0
- package/dist/utils/message-log.d.ts.map +1 -0
- package/dist/utils/message-log.js +69 -0
- package/dist/utils/message-log.js.map +1 -0
- package/dist/utils/status-line.d.ts +5 -1
- package/dist/utils/status-line.d.ts.map +1 -1
- package/dist/utils/status-line.js +13 -3
- package/dist/utils/status-line.js.map +1 -1
- 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
|
|
13
|
-
import {
|
|
14
|
-
import { expandEnv } from
|
|
15
|
-
import {
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
214
|
-
"
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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]
|
|
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
|
-
|
|
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
|
-
...(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
504
|
+
sdkErrorDetected: sdkError?.isError,
|
|
505
|
+
effectiveHadError,
|
|
506
|
+
errorMessage: effectiveError?.message,
|
|
351
507
|
streamedMessageCount,
|
|
352
508
|
});
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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,
|
|
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 (
|
|
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
|
-
|
|
471
|
-
//
|
|
472
|
-
//
|
|
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 (
|
|
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
|
-
|
|
884
|
+
const msg = error.message.toLowerCase();
|
|
885
|
+
if (msg.includes("process exited")) {
|
|
720
886
|
return "process_exit";
|
|
721
887
|
}
|
|
722
|
-
else if (
|
|
888
|
+
else if (msg.includes("model")) {
|
|
723
889
|
return "model_error";
|
|
724
890
|
}
|
|
725
|
-
else if (
|
|
891
|
+
else if (msg.includes("tool")) {
|
|
726
892
|
return "tool_error";
|
|
727
893
|
}
|
|
728
|
-
else if (
|
|
894
|
+
else if (msg.includes("permission") ||
|
|
895
|
+
msg.includes("forbidden") ||
|
|
896
|
+
msg.includes("403")) {
|
|
729
897
|
return "permission_error";
|
|
730
898
|
}
|
|
731
|
-
else if (
|
|
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 &&
|
|
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 ===
|
|
1123
|
-
? [{ type:
|
|
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
|
}
|