@synergenius/flow-weaver-pack-weaver 0.9.152 → 0.9.154
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/dist/ai-chat-provider.js +4 -4
- package/dist/ai-chat-provider.js.map +1 -1
- package/dist/bot/ai-client.d.ts +30 -0
- package/dist/bot/ai-client.d.ts.map +1 -1
- package/dist/bot/ai-client.js +37 -0
- package/dist/bot/ai-client.js.map +1 -1
- package/dist/bot/behavior-defaults.d.ts.map +1 -1
- package/dist/bot/behavior-defaults.js +7 -2
- package/dist/bot/behavior-defaults.js.map +1 -1
- package/dist/bot/capability-registry.d.ts.map +1 -1
- package/dist/bot/capability-registry.js +46 -33
- package/dist/bot/capability-registry.js.map +1 -1
- package/dist/bot/file-validator.d.ts +7 -0
- package/dist/bot/file-validator.d.ts.map +1 -1
- package/dist/bot/file-validator.js +76 -0
- package/dist/bot/file-validator.js.map +1 -1
- package/dist/bot/instance-manager.d.ts +22 -7
- package/dist/bot/instance-manager.d.ts.map +1 -1
- package/dist/bot/instance-manager.js +69 -7
- package/dist/bot/instance-manager.js.map +1 -1
- package/dist/bot/orchestrator.d.ts +11 -9
- package/dist/bot/orchestrator.d.ts.map +1 -1
- package/dist/bot/orchestrator.js +56 -107
- package/dist/bot/orchestrator.js.map +1 -1
- package/dist/bot/runner.d.ts +29 -0
- package/dist/bot/runner.d.ts.map +1 -1
- package/dist/bot/runner.js +114 -73
- package/dist/bot/runner.js.map +1 -1
- package/dist/bot/step-executor.d.ts.map +1 -1
- package/dist/bot/step-executor.js +28 -9
- package/dist/bot/step-executor.js.map +1 -1
- package/dist/bot/swarm-controller.d.ts +7 -6
- package/dist/bot/swarm-controller.d.ts.map +1 -1
- package/dist/bot/swarm-controller.js +64 -74
- package/dist/bot/swarm-controller.js.map +1 -1
- package/dist/bot/system-prompt.d.ts.map +1 -1
- package/dist/bot/system-prompt.js +2 -0
- package/dist/bot/system-prompt.js.map +1 -1
- package/dist/bot/task-types.d.ts +1 -0
- package/dist/bot/task-types.d.ts.map +1 -1
- package/dist/bot/weaver-tools.d.ts +1 -1
- package/dist/bot/weaver-tools.d.ts.map +1 -1
- package/dist/bot/weaver-tools.js +12 -1
- package/dist/bot/weaver-tools.js.map +1 -1
- package/dist/node-types/agent-execute.js +2 -2
- package/dist/node-types/agent-execute.js.map +1 -1
- package/dist/node-types/bot-report.d.ts.map +1 -1
- package/dist/node-types/bot-report.js +5 -2
- package/dist/node-types/bot-report.js.map +1 -1
- package/dist/node-types/build-context.d.ts.map +1 -1
- package/dist/node-types/build-context.js +13 -1
- package/dist/node-types/build-context.js.map +1 -1
- package/dist/node-types/exec-validate-retry.d.ts +3 -3
- package/dist/node-types/exec-validate-retry.d.ts.map +1 -1
- package/dist/node-types/exec-validate-retry.js +13 -184
- package/dist/node-types/exec-validate-retry.js.map +1 -1
- package/dist/node-types/load-config.d.ts +1 -0
- package/dist/node-types/load-config.d.ts.map +1 -1
- package/dist/node-types/load-config.js +1 -0
- package/dist/node-types/load-config.js.map +1 -1
- package/dist/node-types/plan-task.d.ts +7 -5
- package/dist/node-types/plan-task.d.ts.map +1 -1
- package/dist/node-types/plan-task.js +282 -83
- package/dist/node-types/plan-task.js.map +1 -1
- package/dist/ui/bot-panel.js +1 -1
- package/dist/ui/capability-editor.js +46 -33
- package/dist/ui/chat-task-result.js +7 -7
- package/dist/ui/profile-editor.js +44 -31
- package/dist/ui/swarm-dashboard.js +80 -47
- package/dist/ui/task-detail-view.js +31 -11
- package/dist/ui/task-editor.js +1 -1
- package/dist/ui/task-pool-list.js +1 -1
- package/dist/workflows/weaver-bot.d.ts +5 -4
- package/dist/workflows/weaver-bot.d.ts.map +1 -1
- package/dist/workflows/weaver-bot.js +8 -7
- package/dist/workflows/weaver-bot.js.map +1 -1
- package/flowweaver.manifest.json +1 -1
- package/package.json +1 -1
- package/src/ai-chat-provider.ts +4 -4
- package/src/bot/ai-client.ts +65 -0
- package/src/bot/behavior-defaults.ts +5 -2
- package/src/bot/capability-registry.ts +46 -33
- package/src/bot/file-validator.ts +97 -0
- package/src/bot/instance-manager.ts +77 -7
- package/src/bot/orchestrator.ts +63 -126
- package/src/bot/runner.ts +124 -70
- package/src/bot/step-executor.ts +30 -9
- package/src/bot/swarm-controller.ts +65 -76
- package/src/bot/system-prompt.ts +2 -0
- package/src/bot/task-types.ts +1 -0
- package/src/bot/weaver-tools.ts +14 -1
- package/src/node-types/agent-execute.ts +2 -2
- package/src/node-types/bot-report.ts +5 -2
- package/src/node-types/build-context.ts +13 -1
- package/src/node-types/exec-validate-retry.ts +14 -203
- package/src/node-types/load-config.ts +1 -0
- package/src/node-types/plan-task.ts +313 -88
- package/src/ui/bot-panel.tsx +1 -1
- package/src/ui/chat-task-result.tsx +10 -8
- package/src/ui/swarm-dashboard.tsx +4 -4
- package/src/ui/task-detail-view.tsx +35 -12
- package/src/ui/task-editor.tsx +2 -2
- package/src/ui/task-pool-list.tsx +2 -2
- package/src/workflows/weaver-bot.ts +8 -7
package/src/bot/ai-client.ts
CHANGED
|
@@ -307,6 +307,48 @@ export async function callPlatformWithTools(
|
|
|
307
307
|
};
|
|
308
308
|
}
|
|
309
309
|
|
|
310
|
+
// ---------------------------------------------------------------------------
|
|
311
|
+
// Multi-turn message types for agent loop
|
|
312
|
+
// ---------------------------------------------------------------------------
|
|
313
|
+
|
|
314
|
+
export interface ChatMessage {
|
|
315
|
+
role: 'system' | 'user' | 'assistant' | 'tool';
|
|
316
|
+
content?: string | null;
|
|
317
|
+
/** For assistant messages with tool_use */
|
|
318
|
+
tool_use?: { id: string; name: string; input: Record<string, unknown> };
|
|
319
|
+
/** For tool result messages */
|
|
320
|
+
tool_use_id?: string;
|
|
321
|
+
/** For user messages containing tool results */
|
|
322
|
+
tool_results?: Array<{ tool_use_id: string; content: string }>;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Call the platform AI proxy with a full message history and tool definitions.
|
|
327
|
+
* Supports multi-turn conversations for agent loops.
|
|
328
|
+
* The AI can return text AND/OR structured tool calls.
|
|
329
|
+
*/
|
|
330
|
+
export async function callPlatformWithMessages(
|
|
331
|
+
messages: ChatMessage[],
|
|
332
|
+
tools: AiTool[],
|
|
333
|
+
model?: string,
|
|
334
|
+
maxTokens?: number,
|
|
335
|
+
): Promise<AiCallResult> {
|
|
336
|
+
const provider = (globalThis as any).__fw_llm_provider__;
|
|
337
|
+
if (!provider) throw new Error('Platform AI provider not available');
|
|
338
|
+
const timeoutPromise = new Promise<never>((_, reject) =>
|
|
339
|
+
setTimeout(() => reject(new Error('AI call timeout (120s)')), AI_CALL_TIMEOUT_MS),
|
|
340
|
+
);
|
|
341
|
+
const response = await Promise.race([
|
|
342
|
+
provider.chat(messages, { model, maxTokens, tools }),
|
|
343
|
+
timeoutPromise,
|
|
344
|
+
]);
|
|
345
|
+
reportUsage(response, model);
|
|
346
|
+
return {
|
|
347
|
+
content: response.content ?? '',
|
|
348
|
+
toolCalls: (response.toolCalls ?? []) as AiToolCall[],
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
310
352
|
/**
|
|
311
353
|
* Unified AI call that dispatches to the right backend based on provider type.
|
|
312
354
|
*/
|
|
@@ -354,6 +396,29 @@ export async function callAIWithTools(
|
|
|
354
396
|
return { content: text, toolCalls: [] };
|
|
355
397
|
}
|
|
356
398
|
|
|
399
|
+
/**
|
|
400
|
+
* Multi-turn AI call with full message history and tool definitions.
|
|
401
|
+
* For platform providers, uses the multi-turn callPlatformWithMessages.
|
|
402
|
+
* For other providers, falls back to single-turn callAIWithTools using the
|
|
403
|
+
* last user message (multi-turn not supported).
|
|
404
|
+
*/
|
|
405
|
+
export async function callAIWithMessages(
|
|
406
|
+
pInfo: Pick<ProviderInfo, 'type' | 'apiKey' | 'model' | 'maxTokens'>,
|
|
407
|
+
messages: ChatMessage[],
|
|
408
|
+
tools: AiTool[],
|
|
409
|
+
defaultMaxTokens = 4096,
|
|
410
|
+
): Promise<AiCallResult> {
|
|
411
|
+
if (pInfo.type === 'platform') {
|
|
412
|
+
return callPlatformWithMessages(messages, tools, pInfo.model, pInfo.maxTokens ?? defaultMaxTokens);
|
|
413
|
+
}
|
|
414
|
+
// Fallback: extract system + last user message for single-turn call
|
|
415
|
+
const systemMsg = messages.find(m => m.role === 'system');
|
|
416
|
+
const userMsg = [...messages].reverse().find(m => m.role === 'user');
|
|
417
|
+
const systemPrompt = systemMsg?.content ?? '';
|
|
418
|
+
const userPrompt = typeof userMsg?.content === 'string' ? userMsg.content : JSON.stringify(userMsg?.content ?? '');
|
|
419
|
+
return callAIWithTools(pInfo, systemPrompt, userPrompt, tools, defaultMaxTokens);
|
|
420
|
+
}
|
|
421
|
+
|
|
357
422
|
export function parseJsonResponse(text: string): Record<string, unknown> {
|
|
358
423
|
let cleaned = text.trim();
|
|
359
424
|
if (cleaned.startsWith('```')) {
|
|
@@ -217,14 +217,17 @@ export function adjustBehaviorForComplexity(
|
|
|
217
217
|
behavior: ProfileBehavior,
|
|
218
218
|
complexity: 'trivial' | 'simple' | 'moderate' | 'complex' | undefined,
|
|
219
219
|
): ProfileBehavior {
|
|
220
|
-
if (!complexity || complexity === 'moderate'
|
|
220
|
+
if (!complexity || complexity === 'moderate') {
|
|
221
221
|
return behavior;
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
// Deep clone to avoid mutation
|
|
225
225
|
const adjusted: ProfileBehavior = JSON.parse(JSON.stringify(behavior));
|
|
226
226
|
|
|
227
|
-
if (complexity === '
|
|
227
|
+
if (complexity === 'complex') {
|
|
228
|
+
// Complex: upgrade to powerful tier for plan phase (the critical thinking step)
|
|
229
|
+
if (adjusted.phases['plan']?.tier) adjusted.phases['plan'].tier = 'powerful';
|
|
230
|
+
} else if (complexity === 'trivial') {
|
|
228
231
|
// Trivial: fast tier everywhere, skip review, 1 attempt, no evidence
|
|
229
232
|
for (const phase of Object.values(adjusted.phases)) {
|
|
230
233
|
if (phase.tier) phase.tier = 'fast';
|
|
@@ -48,35 +48,28 @@ const CAP_ROLE_ORCHESTRATOR: CapabilityDefinition = {
|
|
|
48
48
|
You DECOMPOSE and ASSIGN. You never write code or create files directly.
|
|
49
49
|
|
|
50
50
|
Your job:
|
|
51
|
-
1. Analyze the objective
|
|
52
|
-
2.
|
|
53
|
-
3.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
### Project Brief Format
|
|
66
|
-
Include this at the TOP of every subtask description:
|
|
67
|
-
"PROJECT: [what we're building]. STRUCTURE: [file layout]. CONVENTIONS: [naming, patterns, exports]."
|
|
51
|
+
1. Analyze the objective
|
|
52
|
+
2. Break it into focused subtasks via task_create. Set parentId to "@self" on every subtask.
|
|
53
|
+
3. ALWAYS set assignedProfile: "developer", "reviewer", or "ops".
|
|
54
|
+
NEVER set assignedProfile to "orchestrator" — assigning to yourself creates an infinite loop.
|
|
55
|
+
4. Use the EXACT title of a previous subtask as dependsOn. The system resolves titles to real task IDs.
|
|
56
|
+
5. Include a project brief in every subtask: "PROJECT: [what]. FILES: [exact paths from workspace root]. CONVENTIONS: [patterns]."
|
|
57
|
+
|
|
58
|
+
CRITICAL: You do NOT have write_file, patch_file, or run_shell. Only plan and delegate.
|
|
59
|
+
|
|
60
|
+
### Design Phase (MANDATORY)
|
|
61
|
+
Your FIRST subtask MUST be a design task assigned to ops that creates a .design.md file in the project root. This is the single source of truth. It must contain:
|
|
62
|
+
- Module map, TypeScript interfaces (copy-paste ready), export contracts (function signatures)
|
|
63
|
+
- Dependency graph, conventions (naming, error handling, patterns)
|
|
64
|
+
Every subsequent developer task MUST read .design.md before writing code.
|
|
68
65
|
|
|
69
66
|
### Subtask Quality
|
|
70
|
-
Each subtask
|
|
71
|
-
- Focused (one file or one concern)
|
|
72
|
-
- Self-contained (has enough context to execute independently)
|
|
73
|
-
- Properly routed (assignedProfile is set)
|
|
74
|
-
- Ordered (dependsOn reflects real dependencies)
|
|
67
|
+
Each subtask: focused (one concern), self-contained, properly routed, ordered by dependsOn.
|
|
75
68
|
|
|
76
69
|
### Example
|
|
77
|
-
|
|
78
|
-
{ operation: "task_create", args: { title: "Setup project", parentId: "
|
|
79
|
-
{ operation: "task_create", args: { title: "Write code", parentId: "
|
|
70
|
+
{ operation: "task_create", args: { title: "Design: Create project contract", parentId: "@self", assignedProfile: "ops", complexity: "complex", description: "Create todo-app/.design.md with module map, TypeScript interfaces, export contracts.", dependsOn: [] } }
|
|
71
|
+
{ operation: "task_create", args: { title: "Setup project", parentId: "@self", assignedProfile: "ops", dependsOn: ["Design: Create project contract"] } }
|
|
72
|
+
{ operation: "task_create", args: { title: "Write code", parentId: "@self", assignedProfile: "developer", dependsOn: ["Setup project"] } }`,
|
|
80
73
|
};
|
|
81
74
|
|
|
82
75
|
const CAP_ROLE_DEVELOPER: CapabilityDefinition = {
|
|
@@ -86,17 +79,28 @@ const CAP_ROLE_DEVELOPER: CapabilityDefinition = {
|
|
|
86
79
|
You WRITE CODE. Execute the task directly using write_file, patch_file, and run_shell.
|
|
87
80
|
|
|
88
81
|
Your job:
|
|
89
|
-
1. Read the
|
|
90
|
-
2.
|
|
91
|
-
3.
|
|
92
|
-
4. Verify your
|
|
82
|
+
1. Read .design.md in the project root to understand interfaces and contracts
|
|
83
|
+
2. Read files created by previous tasks (your dependencies are done — their files are on disk)
|
|
84
|
+
3. Write code that MATCHES the contracts in .design.md exactly — same types, same function signatures, same exports
|
|
85
|
+
4. Verify your imports resolve to real exports in existing files
|
|
93
86
|
|
|
94
87
|
You do NOT have task_create. You cannot create subtasks or delegate.
|
|
95
88
|
If the task seems too large, do your best — the orchestrator already decomposed it for you.
|
|
96
89
|
|
|
90
|
+
### File Paths
|
|
91
|
+
All paths in write_file/patch_file are RELATIVE TO THE WORKSPACE ROOT. If the task says "inside todo-app/", your paths MUST start with todo-app/ (e.g., todo-app/src/cli.ts, NOT src/cli.ts).
|
|
92
|
+
|
|
93
|
+
### Code Quality
|
|
94
|
+
- Write COMPLETE, WORKING code. No TODOs, no placeholders, no empty function bodies, no "// implement later".
|
|
95
|
+
- Every function must be fully implemented with real logic.
|
|
96
|
+
- Use proper TypeScript types. Use strict mode patterns.
|
|
97
|
+
- Export everything that other files will import.
|
|
98
|
+
- Handle edge cases (empty input, file not found, invalid args).
|
|
99
|
+
- Use ESM-compatible patterns: import.meta.url instead of __dirname, import.meta.filename instead of __filename. Use fileURLToPath(import.meta.url) for path resolution.
|
|
100
|
+
|
|
97
101
|
### Output Requirements
|
|
98
102
|
Your plan MUST include at least one write_file, patch_file, or run_shell step.
|
|
99
|
-
A plan with only
|
|
103
|
+
A plan with only read_file, list_files, or respond steps is a FAILURE — you must produce artifacts.`,
|
|
100
104
|
};
|
|
101
105
|
|
|
102
106
|
const CAP_ROLE_REVIEWER: CapabilityDefinition = {
|
|
@@ -122,10 +126,19 @@ const CAP_ROLE_OPS: CapabilityDefinition = {
|
|
|
122
126
|
You SET UP infrastructure — package.json, tsconfig.json, directory structure, dependencies.
|
|
123
127
|
|
|
124
128
|
Your job:
|
|
125
|
-
1.
|
|
126
|
-
2.
|
|
127
|
-
3.
|
|
129
|
+
1. Create the project directory first: run_shell with mkdir -p <project>/src
|
|
130
|
+
2. Write config files (package.json, tsconfig.json) using write_file
|
|
131
|
+
3. Install dependencies with run_shell (npm install)
|
|
132
|
+
4. Ensure the project structure is ready for developers
|
|
128
133
|
|
|
134
|
+
### File Paths
|
|
135
|
+
All paths are RELATIVE TO THE WORKSPACE ROOT. If the project is in a subfolder (e.g., todo-app/), ALL your paths must include that prefix: todo-app/package.json, todo-app/tsconfig.json, todo-app/src/.
|
|
136
|
+
|
|
137
|
+
### Design Tasks
|
|
138
|
+
When the task is a Design task, create a .design.md file with detailed TypeScript interfaces, module exports, and dependency graph. This file must contain copy-paste ready interface definitions that developers will implement exactly.
|
|
139
|
+
|
|
140
|
+
### Output Requirements
|
|
141
|
+
Your plan MUST include write_file and/or run_shell steps that create real files.
|
|
129
142
|
You do NOT have task_create. You execute infrastructure tasks directly.`,
|
|
130
143
|
};
|
|
131
144
|
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
1
4
|
import { checkDesignQuality, type DesignReport } from './design-checker.js';
|
|
2
5
|
import { fwValidate } from './fw-api.js';
|
|
3
6
|
|
|
@@ -17,6 +20,18 @@ export async function validateFiles(
|
|
|
17
20
|
|
|
18
21
|
for (const file of files) {
|
|
19
22
|
if (!file.endsWith('.ts')) continue;
|
|
23
|
+
|
|
24
|
+
// Only run FW validation on files that contain @flowWeaver annotations.
|
|
25
|
+
// Plain TypeScript files (like server.ts) should not be FW-validated.
|
|
26
|
+
const absFile = path.isAbsolute(file) ? file : path.resolve(projectDir, file);
|
|
27
|
+
let hasFwAnnotation = false;
|
|
28
|
+
try {
|
|
29
|
+
const content = fs.readFileSync(absFile, 'utf-8');
|
|
30
|
+
hasFwAnnotation = content.includes('@flowWeaver') || content.includes('@flow-weaver');
|
|
31
|
+
} catch { /* file read failed — skip FW validation */ }
|
|
32
|
+
|
|
33
|
+
if (!hasFwAnnotation) continue;
|
|
34
|
+
|
|
20
35
|
try {
|
|
21
36
|
const { valid, errors, warnings, ast } = await fwValidate(file);
|
|
22
37
|
|
|
@@ -39,3 +54,85 @@ export async function validateFiles(
|
|
|
39
54
|
|
|
40
55
|
return results;
|
|
41
56
|
}
|
|
57
|
+
|
|
58
|
+
export interface TscValidationResult {
|
|
59
|
+
valid: boolean;
|
|
60
|
+
errors: string[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Find the tsc binary: prefer the project's own node_modules/.bin/tsc,
|
|
65
|
+
* then walk up parent directories, then fall back to a bare 'tsc' (PATH lookup).
|
|
66
|
+
*/
|
|
67
|
+
function findTscBin(projectDir: string): string | null {
|
|
68
|
+
let dir = projectDir;
|
|
69
|
+
// eslint-disable-next-line no-constant-condition
|
|
70
|
+
while (true) {
|
|
71
|
+
const candidate = path.join(dir, 'node_modules', '.bin', 'tsc');
|
|
72
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
73
|
+
const parent = path.dirname(dir);
|
|
74
|
+
if (parent === dir) break;
|
|
75
|
+
dir = parent;
|
|
76
|
+
}
|
|
77
|
+
return null; // caller will try bare 'tsc'
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Strip ANSI escape codes from tsc output
|
|
81
|
+
function stripAnsi(str: string): string {
|
|
82
|
+
// eslint-disable-next-line no-control-regex
|
|
83
|
+
return str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function validateTypeScript(
|
|
87
|
+
projectDir: string,
|
|
88
|
+
opts?: { timeoutMs?: number },
|
|
89
|
+
): Promise<TscValidationResult> {
|
|
90
|
+
const timeoutMs = opts?.timeoutMs ?? 30_000;
|
|
91
|
+
|
|
92
|
+
// Check for tsconfig.json
|
|
93
|
+
const tsconfigPath = path.join(projectDir, 'tsconfig.json');
|
|
94
|
+
if (!fs.existsSync(tsconfigPath)) {
|
|
95
|
+
return { valid: true, errors: [] };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const tscBin = findTscBin(projectDir) ?? 'tsc';
|
|
99
|
+
|
|
100
|
+
return new Promise<TscValidationResult>((resolve) => {
|
|
101
|
+
try {
|
|
102
|
+
const child = execFile(
|
|
103
|
+
tscBin,
|
|
104
|
+
['--noEmit', '--pretty', 'false'],
|
|
105
|
+
{ cwd: projectDir, timeout: timeoutMs, env: { ...process.env } },
|
|
106
|
+
(error, stdout, stderr) => {
|
|
107
|
+
if (!error) {
|
|
108
|
+
return resolve({ valid: true, errors: [] });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// If the process was killed (timeout) or spawn failed, graceful fallback
|
|
112
|
+
if (error.killed || (error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
113
|
+
return resolve({ valid: true, errors: [] });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// tsc exits with code 2 on type errors; parse stdout for error lines
|
|
117
|
+
const output = stripAnsi((stdout || '') + (stderr || ''));
|
|
118
|
+
const lines = output.split('\n').filter((l) => l.trim().length > 0);
|
|
119
|
+
if (lines.length === 0) {
|
|
120
|
+
return resolve({ valid: true, errors: [] });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Strip absolute projectDir prefix so errors use relative paths
|
|
124
|
+
const cleaned = lines.map(l => l.replaceAll(projectDir + '/', ''));
|
|
125
|
+
return resolve({ valid: false, errors: cleaned });
|
|
126
|
+
},
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// Extra safety: if the child can't even spawn
|
|
130
|
+
child.on('error', () => {
|
|
131
|
+
resolve({ valid: true, errors: [] });
|
|
132
|
+
});
|
|
133
|
+
} catch {
|
|
134
|
+
// execFile itself threw (e.g. bad args) — graceful fallback
|
|
135
|
+
resolve({ valid: true, errors: [] });
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* InstanceManager — manages
|
|
3
|
-
*
|
|
2
|
+
* InstanceManager — manages a dynamic pool of generic worker slots.
|
|
3
|
+
*
|
|
4
|
+
* Workers are profile-agnostic: any worker can run any profile's task.
|
|
5
|
+
* The profile is loaded per-task at execution time, not per-worker.
|
|
6
|
+
*
|
|
7
|
+
* Worker naming: `worker-0`, `worker-1`, ... (no per-profile instances).
|
|
4
8
|
*/
|
|
5
9
|
|
|
6
10
|
import type { BotProfile, BotInstance } from './profile-types.js';
|
|
@@ -8,7 +12,61 @@ import type { BotProfile, BotInstance } from './profile-types.js';
|
|
|
8
12
|
export class InstanceManager {
|
|
9
13
|
private instances = new Map<string, BotInstance>();
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
// -----------------------------------------------------------------------
|
|
16
|
+
// Worker pool API (new)
|
|
17
|
+
// -----------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
/** Spawn a single generic worker slot at the given index. */
|
|
20
|
+
spawnWorker(index: number): BotInstance {
|
|
21
|
+
const instanceId = `worker-${index}`;
|
|
22
|
+
if (this.instances.has(instanceId)) {
|
|
23
|
+
throw new Error(`Worker already exists: ${instanceId}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const instance: BotInstance = {
|
|
27
|
+
instanceId,
|
|
28
|
+
profileId: '', // No profile until a task is assigned
|
|
29
|
+
index,
|
|
30
|
+
status: 'idle',
|
|
31
|
+
tokensUsed: 0,
|
|
32
|
+
cost: 0,
|
|
33
|
+
tasksCompleted: 0,
|
|
34
|
+
tasksFailed: 0,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
this.instances.set(instanceId, instance);
|
|
38
|
+
return { ...instance };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Spawn a pool of N generic workers (worker-0 through worker-N-1). */
|
|
42
|
+
spawnPool(size: number): BotInstance[] {
|
|
43
|
+
const spawned: BotInstance[] = [];
|
|
44
|
+
for (let i = 0; i < size; i++) {
|
|
45
|
+
spawned.push(this.spawnWorker(i));
|
|
46
|
+
}
|
|
47
|
+
return spawned;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Find any idle worker (profile-agnostic). Returns a copy or null. */
|
|
51
|
+
findIdleWorker(): BotInstance | null {
|
|
52
|
+
for (const inst of this.instances.values()) {
|
|
53
|
+
if (inst.status === 'idle') return { ...inst };
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Return all idle workers. */
|
|
59
|
+
findIdleWorkers(): BotInstance[] {
|
|
60
|
+
return [...this.instances.values()]
|
|
61
|
+
.filter((i) => i.status === 'idle')
|
|
62
|
+
.map((i) => ({ ...i }));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// -----------------------------------------------------------------------
|
|
66
|
+
// Legacy per-profile API (kept for backward compat, used by tests)
|
|
67
|
+
// -----------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
/** @deprecated Use spawnWorker/spawnPool instead. Spawn a profile-bound instance. */
|
|
12
70
|
spawn(profile: BotProfile): BotInstance {
|
|
13
71
|
const existing = this.listByProfile(profile.id);
|
|
14
72
|
if (existing.length >= profile.maxInstances) {
|
|
@@ -36,6 +94,10 @@ export class InstanceManager {
|
|
|
36
94
|
return { ...instance };
|
|
37
95
|
}
|
|
38
96
|
|
|
97
|
+
// -----------------------------------------------------------------------
|
|
98
|
+
// Shared API
|
|
99
|
+
// -----------------------------------------------------------------------
|
|
100
|
+
|
|
39
101
|
/** Get instance by ID (returns a copy), or null if not found. */
|
|
40
102
|
get(instanceId: string): BotInstance | null {
|
|
41
103
|
const inst = this.instances.get(instanceId);
|
|
@@ -61,17 +123,23 @@ export class InstanceManager {
|
|
|
61
123
|
.map((i) => ({ ...i }));
|
|
62
124
|
}
|
|
63
125
|
|
|
64
|
-
/**
|
|
65
|
-
|
|
126
|
+
/**
|
|
127
|
+
* Mark a worker as executing a specific task/run.
|
|
128
|
+
* @param profileId — the profile being loaded for this task execution.
|
|
129
|
+
*/
|
|
130
|
+
markExecuting(instanceId: string, taskId: string, runId: string, profileId?: string): void {
|
|
66
131
|
const inst = this.instances.get(instanceId);
|
|
67
132
|
if (!inst) throw new Error(`Instance not found: ${instanceId}`);
|
|
68
133
|
inst.status = 'executing';
|
|
69
134
|
inst.currentTaskId = taskId;
|
|
70
135
|
inst.currentRunId = runId;
|
|
71
136
|
inst.startedAt = new Date().toISOString();
|
|
137
|
+
if (profileId !== undefined) {
|
|
138
|
+
inst.profileId = profileId;
|
|
139
|
+
}
|
|
72
140
|
}
|
|
73
141
|
|
|
74
|
-
/** Mark
|
|
142
|
+
/** Mark a worker as idle after task completion/failure. Clears profile association. */
|
|
75
143
|
markIdle(instanceId: string, success: boolean): void {
|
|
76
144
|
const inst = this.instances.get(instanceId);
|
|
77
145
|
if (!inst) throw new Error(`Instance not found: ${instanceId}`);
|
|
@@ -79,6 +147,8 @@ export class InstanceManager {
|
|
|
79
147
|
inst.currentTaskId = undefined;
|
|
80
148
|
inst.currentRunId = undefined;
|
|
81
149
|
inst.startedAt = undefined;
|
|
150
|
+
// Clear profile — worker is generic again
|
|
151
|
+
inst.profileId = '';
|
|
82
152
|
if (success) {
|
|
83
153
|
inst.tasksCompleted++;
|
|
84
154
|
} else {
|
|
@@ -104,7 +174,7 @@ export class InstanceManager {
|
|
|
104
174
|
this.instances.clear();
|
|
105
175
|
}
|
|
106
176
|
|
|
107
|
-
/**
|
|
177
|
+
/** @deprecated No longer needed in pool model. Kept for backward compat. */
|
|
108
178
|
scaleTo(profile: BotProfile, target: number): void {
|
|
109
179
|
const clamped = Math.max(0, Math.min(target, profile.maxInstances));
|
|
110
180
|
const current = this.listByProfile(profile.id);
|