@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.
Files changed (104) hide show
  1. package/dist/ai-chat-provider.js +4 -4
  2. package/dist/ai-chat-provider.js.map +1 -1
  3. package/dist/bot/ai-client.d.ts +30 -0
  4. package/dist/bot/ai-client.d.ts.map +1 -1
  5. package/dist/bot/ai-client.js +37 -0
  6. package/dist/bot/ai-client.js.map +1 -1
  7. package/dist/bot/behavior-defaults.d.ts.map +1 -1
  8. package/dist/bot/behavior-defaults.js +7 -2
  9. package/dist/bot/behavior-defaults.js.map +1 -1
  10. package/dist/bot/capability-registry.d.ts.map +1 -1
  11. package/dist/bot/capability-registry.js +46 -33
  12. package/dist/bot/capability-registry.js.map +1 -1
  13. package/dist/bot/file-validator.d.ts +7 -0
  14. package/dist/bot/file-validator.d.ts.map +1 -1
  15. package/dist/bot/file-validator.js +76 -0
  16. package/dist/bot/file-validator.js.map +1 -1
  17. package/dist/bot/instance-manager.d.ts +22 -7
  18. package/dist/bot/instance-manager.d.ts.map +1 -1
  19. package/dist/bot/instance-manager.js +69 -7
  20. package/dist/bot/instance-manager.js.map +1 -1
  21. package/dist/bot/orchestrator.d.ts +11 -9
  22. package/dist/bot/orchestrator.d.ts.map +1 -1
  23. package/dist/bot/orchestrator.js +56 -107
  24. package/dist/bot/orchestrator.js.map +1 -1
  25. package/dist/bot/runner.d.ts +29 -0
  26. package/dist/bot/runner.d.ts.map +1 -1
  27. package/dist/bot/runner.js +114 -73
  28. package/dist/bot/runner.js.map +1 -1
  29. package/dist/bot/step-executor.d.ts.map +1 -1
  30. package/dist/bot/step-executor.js +28 -9
  31. package/dist/bot/step-executor.js.map +1 -1
  32. package/dist/bot/swarm-controller.d.ts +7 -6
  33. package/dist/bot/swarm-controller.d.ts.map +1 -1
  34. package/dist/bot/swarm-controller.js +64 -74
  35. package/dist/bot/swarm-controller.js.map +1 -1
  36. package/dist/bot/system-prompt.d.ts.map +1 -1
  37. package/dist/bot/system-prompt.js +2 -0
  38. package/dist/bot/system-prompt.js.map +1 -1
  39. package/dist/bot/task-types.d.ts +1 -0
  40. package/dist/bot/task-types.d.ts.map +1 -1
  41. package/dist/bot/weaver-tools.d.ts +1 -1
  42. package/dist/bot/weaver-tools.d.ts.map +1 -1
  43. package/dist/bot/weaver-tools.js +12 -1
  44. package/dist/bot/weaver-tools.js.map +1 -1
  45. package/dist/node-types/agent-execute.js +2 -2
  46. package/dist/node-types/agent-execute.js.map +1 -1
  47. package/dist/node-types/bot-report.d.ts.map +1 -1
  48. package/dist/node-types/bot-report.js +5 -2
  49. package/dist/node-types/bot-report.js.map +1 -1
  50. package/dist/node-types/build-context.d.ts.map +1 -1
  51. package/dist/node-types/build-context.js +13 -1
  52. package/dist/node-types/build-context.js.map +1 -1
  53. package/dist/node-types/exec-validate-retry.d.ts +3 -3
  54. package/dist/node-types/exec-validate-retry.d.ts.map +1 -1
  55. package/dist/node-types/exec-validate-retry.js +13 -184
  56. package/dist/node-types/exec-validate-retry.js.map +1 -1
  57. package/dist/node-types/load-config.d.ts +1 -0
  58. package/dist/node-types/load-config.d.ts.map +1 -1
  59. package/dist/node-types/load-config.js +1 -0
  60. package/dist/node-types/load-config.js.map +1 -1
  61. package/dist/node-types/plan-task.d.ts +7 -5
  62. package/dist/node-types/plan-task.d.ts.map +1 -1
  63. package/dist/node-types/plan-task.js +282 -83
  64. package/dist/node-types/plan-task.js.map +1 -1
  65. package/dist/ui/bot-panel.js +1 -1
  66. package/dist/ui/capability-editor.js +46 -33
  67. package/dist/ui/chat-task-result.js +7 -7
  68. package/dist/ui/profile-editor.js +44 -31
  69. package/dist/ui/swarm-dashboard.js +80 -47
  70. package/dist/ui/task-detail-view.js +31 -11
  71. package/dist/ui/task-editor.js +1 -1
  72. package/dist/ui/task-pool-list.js +1 -1
  73. package/dist/workflows/weaver-bot.d.ts +5 -4
  74. package/dist/workflows/weaver-bot.d.ts.map +1 -1
  75. package/dist/workflows/weaver-bot.js +8 -7
  76. package/dist/workflows/weaver-bot.js.map +1 -1
  77. package/flowweaver.manifest.json +1 -1
  78. package/package.json +1 -1
  79. package/src/ai-chat-provider.ts +4 -4
  80. package/src/bot/ai-client.ts +65 -0
  81. package/src/bot/behavior-defaults.ts +5 -2
  82. package/src/bot/capability-registry.ts +46 -33
  83. package/src/bot/file-validator.ts +97 -0
  84. package/src/bot/instance-manager.ts +77 -7
  85. package/src/bot/orchestrator.ts +63 -126
  86. package/src/bot/runner.ts +124 -70
  87. package/src/bot/step-executor.ts +30 -9
  88. package/src/bot/swarm-controller.ts +65 -76
  89. package/src/bot/system-prompt.ts +2 -0
  90. package/src/bot/task-types.ts +1 -0
  91. package/src/bot/weaver-tools.ts +14 -1
  92. package/src/node-types/agent-execute.ts +2 -2
  93. package/src/node-types/bot-report.ts +5 -2
  94. package/src/node-types/build-context.ts +13 -1
  95. package/src/node-types/exec-validate-retry.ts +14 -203
  96. package/src/node-types/load-config.ts +1 -0
  97. package/src/node-types/plan-task.ts +313 -88
  98. package/src/ui/bot-panel.tsx +1 -1
  99. package/src/ui/chat-task-result.tsx +10 -8
  100. package/src/ui/swarm-dashboard.tsx +4 -4
  101. package/src/ui/task-detail-view.tsx +35 -12
  102. package/src/ui/task-editor.tsx +2 -2
  103. package/src/ui/task-pool-list.tsx +2 -2
  104. package/src/workflows/weaver-bot.ts +8 -7
@@ -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' || complexity === 'complex') {
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 === 'trivial') {
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 and understand the project scope
52
- 2. Create a PROJECT BRIEF (a concise description of what we're building, how pieces connect, conventions to follow)
53
- 3. Break the objective into focused subtasks using task_create. The prompt contains "Task ID: <id>" — use that exact ID value as parentId in every task_create call so subtasks appear as children.
54
- 4. ALWAYS set assignedProfile on every subtask to one of these three:
55
- - "developer" code writing, file creation, implementation
56
- - "reviewer" code review, quality checks, security audit
57
- - "ops" → project setup, dependencies, config, infrastructure
58
- NEVER set assignedProfile to "orchestrator". You are the orchestrator assigning to yourself creates an infinite loop. Only "developer", "reviewer", or "ops".
59
- 5. Set dependencies so tasks execute in the right order
60
- Use the EXACT title of a previous subtask as the dependsOn value. The system resolves titles to real task IDs at execution time. Example: dependsOn: ["Setup: Initialize project structure"]
61
- 6. Include the project brief in every subtask's description
62
-
63
- CRITICAL: You do NOT have write_file, patch_file, or run_shell. You cannot execute code — only plan and delegate to developer/reviewer/ops.
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 must be:
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
- For a task with Task ID "abc123":
78
- { operation: "task_create", args: { title: "Setup project", parentId: "abc123", assignedProfile: "ops", dependsOn: [] } }
79
- { operation: "task_create", args: { title: "Write code", parentId: "abc123", assignedProfile: "developer", dependsOn: ["Setup project"] } }`,
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 task description (including the project brief)
90
- 2. Create a plan with CONCRETE file operations (write_file, patch_file, run_shell)
91
- 3. Execute every stepproduce actual files on disk
92
- 4. Verify your work compiles and is correct
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 "respond" steps is a FAILURE — you must produce artifacts.`,
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. Initialize project structure (create config files, directories)
126
- 2. Install dependencies with run_shell
127
- 3. Ensure the project builds and tests can run
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 in-memory bot instances (workers) per profile.
3
- * Instances represent running workers that don't survive process restart.
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
- /** Spawn a new instance for the given profile. Throws if maxInstances reached. */
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
- /** Mark an instance as executing a specific task/run. */
65
- markExecuting(instanceId: string, taskId: string, runId: string): void {
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 an instance as idle after task completion/failure. */
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
- /** Scale instances for a profile to the target count. Clamps to [0, maxInstances]. */
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);