@synergenius/flow-weaver-pack-weaver 0.9.151 → 0.9.153

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 (93) 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 +48 -30
  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 +106 -25
  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/task-types.d.ts +1 -0
  37. package/dist/bot/task-types.d.ts.map +1 -1
  38. package/dist/bot/weaver-tools.d.ts +1 -1
  39. package/dist/bot/weaver-tools.d.ts.map +1 -1
  40. package/dist/bot/weaver-tools.js +6 -1
  41. package/dist/bot/weaver-tools.js.map +1 -1
  42. package/dist/node-types/agent-execute.js +2 -2
  43. package/dist/node-types/agent-execute.js.map +1 -1
  44. package/dist/node-types/bot-report.d.ts.map +1 -1
  45. package/dist/node-types/bot-report.js +5 -2
  46. package/dist/node-types/bot-report.js.map +1 -1
  47. package/dist/node-types/build-context.js +2 -1
  48. package/dist/node-types/build-context.js.map +1 -1
  49. package/dist/node-types/exec-validate-retry.d.ts +3 -3
  50. package/dist/node-types/exec-validate-retry.d.ts.map +1 -1
  51. package/dist/node-types/exec-validate-retry.js +13 -184
  52. package/dist/node-types/exec-validate-retry.js.map +1 -1
  53. package/dist/node-types/load-config.d.ts +1 -0
  54. package/dist/node-types/load-config.d.ts.map +1 -1
  55. package/dist/node-types/load-config.js +1 -0
  56. package/dist/node-types/load-config.js.map +1 -1
  57. package/dist/node-types/plan-task.d.ts +7 -5
  58. package/dist/node-types/plan-task.d.ts.map +1 -1
  59. package/dist/node-types/plan-task.js +282 -83
  60. package/dist/node-types/plan-task.js.map +1 -1
  61. package/dist/ui/bot-panel.js +1 -1
  62. package/dist/ui/capability-editor.js +48 -30
  63. package/dist/ui/profile-editor.js +46 -28
  64. package/dist/ui/swarm-dashboard.js +71 -33
  65. package/dist/ui/task-detail-view.js +22 -2
  66. package/dist/workflows/weaver-bot.d.ts +2 -2
  67. package/dist/workflows/weaver-bot.d.ts.map +1 -1
  68. package/dist/workflows/weaver-bot.js +5 -4
  69. package/dist/workflows/weaver-bot.js.map +1 -1
  70. package/flowweaver.manifest.json +1 -1
  71. package/package.json +1 -1
  72. package/src/ai-chat-provider.ts +4 -4
  73. package/src/bot/ai-client.ts +65 -0
  74. package/src/bot/behavior-defaults.ts +5 -2
  75. package/src/bot/capability-registry.ts +48 -30
  76. package/src/bot/file-validator.ts +97 -0
  77. package/src/bot/instance-manager.ts +77 -7
  78. package/src/bot/orchestrator.ts +63 -126
  79. package/src/bot/runner.ts +124 -70
  80. package/src/bot/step-executor.ts +115 -25
  81. package/src/bot/swarm-controller.ts +65 -76
  82. package/src/bot/task-types.ts +1 -0
  83. package/src/bot/weaver-tools.ts +7 -1
  84. package/src/node-types/agent-execute.ts +2 -2
  85. package/src/node-types/bot-report.ts +5 -2
  86. package/src/node-types/build-context.ts +2 -1
  87. package/src/node-types/exec-validate-retry.ts +14 -203
  88. package/src/node-types/load-config.ts +1 -0
  89. package/src/node-types/plan-task.ts +313 -88
  90. package/src/ui/bot-panel.tsx +1 -1
  91. package/src/ui/swarm-dashboard.tsx +3 -3
  92. package/src/ui/task-detail-view.tsx +25 -2
  93. package/src/workflows/weaver-bot.ts +5 -4
@@ -48,30 +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. Set parentId to YOUR Task ID (provided in the prompt) on every subtask so they 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.
68
+
69
+ ### Example
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"] } }`,
75
73
  };
76
74
 
77
75
  const CAP_ROLE_DEVELOPER: CapabilityDefinition = {
@@ -81,17 +79,28 @@ const CAP_ROLE_DEVELOPER: CapabilityDefinition = {
81
79
  You WRITE CODE. Execute the task directly using write_file, patch_file, and run_shell.
82
80
 
83
81
  Your job:
84
- 1. Read the task description (including the project brief)
85
- 2. Create a plan with CONCRETE file operations (write_file, patch_file, run_shell)
86
- 3. Execute every stepproduce actual files on disk
87
- 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
88
86
 
89
87
  You do NOT have task_create. You cannot create subtasks or delegate.
90
88
  If the task seems too large, do your best — the orchestrator already decomposed it for you.
91
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
+
92
101
  ### Output Requirements
93
102
  Your plan MUST include at least one write_file, patch_file, or run_shell step.
94
- 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.`,
95
104
  };
96
105
 
97
106
  const CAP_ROLE_REVIEWER: CapabilityDefinition = {
@@ -117,10 +126,19 @@ const CAP_ROLE_OPS: CapabilityDefinition = {
117
126
  You SET UP infrastructure — package.json, tsconfig.json, directory structure, dependencies.
118
127
 
119
128
  Your job:
120
- 1. Initialize project structure (create config files, directories)
121
- 2. Install dependencies with run_shell
122
- 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
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/.
123
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.
124
142
  You do NOT have task_create. You execute infrastructure tasks directly.`,
125
143
  };
126
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);
@@ -1,8 +1,12 @@
1
1
  /**
2
- * Orchestrator — the routing brain that decides which tasks go to which bot instances.
2
+ * Orchestrator — the routing brain that decides which tasks go to which workers.
3
3
  *
4
- * Pure routing logic: no side-effects beyond decision logging.
5
- * Fast-path cascade: exact-match single-eligible ai-routed round-robin.
4
+ * In the worker pool model, workers are generic slots. The orchestrator:
5
+ * 1. Finds the profile for each task (via assignedProfile).
6
+ * 2. Assigns the task to any idle worker.
7
+ *
8
+ * Simplified cascade: exact-match profile → any idle worker.
9
+ * No more per-profile instance matching or scale-up actions.
6
10
  */
7
11
 
8
12
  import type {
@@ -15,7 +19,6 @@ import type {
15
19
 
16
20
  type Task = OrchestratorInput['pendingTasks'][number];
17
21
  type Assignment = OrchestratorOutput['assignments'][number];
18
- type ScaleAction = OrchestratorOutput['scaleActions'][number];
19
22
 
20
23
  // ---------------------------------------------------------------------------
21
24
  // AI Router types
@@ -50,11 +53,10 @@ export class Orchestrator {
50
53
 
51
54
  async route(input: OrchestratorInput): Promise<OrchestratorOutput> {
52
55
  const assignments: Assignment[] = [];
53
- const scaleActions: ScaleAction[] = [];
54
56
  const skippedTasks: OrchestratorOutput['skippedTasks'] = [];
55
57
 
56
- // Track instances claimed during this routing cycle to avoid double-assignment.
57
- const claimedInstanceIds = new Set<string>();
58
+ // Track workers claimed during this routing cycle to avoid double-assignment.
59
+ const claimedWorkerIds = new Set<string>();
58
60
 
59
61
  // Sort tasks by priority DESC (higher number = higher priority).
60
62
  const sorted = [...input.pendingTasks].sort((a, b) => b.priority - a.priority);
@@ -66,50 +68,43 @@ export class Orchestrator {
66
68
  continue;
67
69
  }
68
70
 
69
- const eligible = this._findEligible(task, input.profiles);
70
-
71
- if (eligible.length === 0) {
71
+ // Resolve profile for this task (includes routing method)
72
+ const resolution = await this._resolveProfile(task, input.profiles);
73
+ if (!resolution) {
72
74
  skippedTasks.push({ taskId: task.id, reason: 'no-eligible-profile' });
73
75
  continue;
74
76
  }
75
77
 
76
- const result = await this._selectInstance(task, eligible, input.instances, claimedInstanceIds);
77
-
78
- if (result) {
79
- claimedInstanceIds.add(result.instanceId);
80
- assignments.push({
81
- taskId: task.id,
82
- profileId: result.profileId,
83
- instanceId: result.instanceId,
84
- reason: result.reason,
85
- method: result.method,
86
- confidence: result.confidence,
87
- });
88
- this._recordDecision(task, result, eligible);
89
- } else {
90
- // No idle instance available — request scale-up if possible.
91
- const scaleProfile = eligible[0];
92
- const currentCount = input.instances.filter(
93
- (i) => i.profileId === scaleProfile.id,
94
- ).length;
95
-
96
- if (currentCount < scaleProfile.maxInstances) {
97
- // Only add one scale-up action per profile.
98
- if (!scaleActions.some((sa) => sa.profileId === scaleProfile.id)) {
99
- scaleActions.push({
100
- profileId: scaleProfile.id,
101
- action: 'scale-up',
102
- targetInstances: currentCount + 1,
103
- reason: `idle instances exhausted for profile ${scaleProfile.id}`,
104
- });
105
- }
106
- }
107
-
108
- skippedTasks.push({ taskId: task.id, reason: 'no-idle-instance' });
78
+ // Find any idle worker
79
+ const idleWorker = this._findIdleWorker(input.instances, claimedWorkerIds);
80
+ if (!idleWorker) {
81
+ skippedTasks.push({ taskId: task.id, reason: 'no-idle-worker' });
82
+ continue;
109
83
  }
84
+
85
+ claimedWorkerIds.add(idleWorker.instanceId);
86
+
87
+ const reason = `${resolution.method}: ${resolution.profileId} → ${idleWorker.instanceId}`;
88
+ assignments.push({
89
+ taskId: task.id,
90
+ profileId: resolution.profileId,
91
+ instanceId: idleWorker.instanceId,
92
+ reason,
93
+ method: resolution.method,
94
+ confidence: resolution.confidence,
95
+ });
96
+
97
+ this._recordDecision(task, {
98
+ profileId: resolution.profileId,
99
+ instanceId: idleWorker.instanceId,
100
+ method: resolution.method,
101
+ reason,
102
+ confidence: resolution.confidence,
103
+ }, input.profiles.filter(p => p.id === resolution.profileId));
110
104
  }
111
105
 
112
- return { assignments, scaleActions, skippedTasks };
106
+ // No scale actions in pool model — pool size is fixed at maxConcurrent
107
+ return { assignments, scaleActions: [], skippedTasks };
113
108
  }
114
109
 
115
110
  getDecisionLog(limit?: number): OrchestratorDecision[] {
@@ -126,106 +121,48 @@ export class Orchestrator {
126
121
  // Internal
127
122
  // ---------------------------------------------------------------------------
128
123
 
129
- /** Find profiles eligible for a task. */
130
- private _findEligible(task: Task, profiles: BotProfile[]): BotProfile[] {
124
+ /** Resolve which profileId should handle this task, including routing method. */
125
+ private async _resolveProfile(
126
+ task: Task,
127
+ profiles: BotProfile[],
128
+ ): Promise<{ profileId: string; method: OrchestratorDecision['method']; confidence?: number } | null> {
129
+ // Exact match: task has assignedProfile
131
130
  if (task.assignedProfile) {
132
- const match = profiles.filter(p => p.id === task.assignedProfile);
133
- if (match.length > 0) return match;
134
- // Assigned profile doesn't exist — return empty to skip this task
135
- // rather than falling through to all profiles (silent mis-routing).
131
+ const match = profiles.find(p => p.id === task.assignedProfile);
132
+ if (match) return { profileId: match.id, method: 'exact-match' };
136
133
  console.warn(
137
134
  `[orchestrator] Task "${task.id}" assigned to profile "${task.assignedProfile}" which does not exist — skipping`,
138
135
  );
139
- return [];
136
+ return null;
140
137
  }
141
- return profiles;
142
- }
143
138
 
144
- /** Select the best instance for a task from eligible profiles. */
145
- private async _selectInstance(
146
- task: Task,
147
- eligible: BotProfile[],
148
- instances: BotInstance[],
149
- claimed: Set<string>,
150
- ): Promise<{ profileId: string; instanceId: string; method: OrchestratorDecision['method']; reason: string; confidence?: number } | null> {
151
- // Determine routing method
152
- let method = this._routingMethod(task, eligible);
153
-
154
- if (eligible.length === 1) {
155
- const profile = eligible[0];
156
- const idle = instances.find(
157
- (i) => i.profileId === profile.id && i.status === 'idle' && !claimed.has(i.instanceId),
158
- );
159
- if (!idle) return null;
160
- return {
161
- profileId: profile.id,
162
- instanceId: idle.instanceId,
163
- method,
164
- reason: `${method}: ${profile.id}`,
165
- };
139
+ // Single eligible profile
140
+ if (profiles.length === 1) {
141
+ return { profileId: profiles[0].id, method: 'single-eligible' };
166
142
  }
167
143
 
168
- // AI routing: when multiple eligible profiles and AI router is provided
169
- if (this._aiRouter && eligible.length > 1) {
144
+ // Multiple eligible profiles try AI routing
145
+ if (this._aiRouter && profiles.length > 1) {
170
146
  try {
171
- const aiResult = await this._aiRouter.route(task, eligible);
172
- const aiProfile = eligible.find((p) => p.id === aiResult.profileId);
147
+ const aiResult = await this._aiRouter.route(task, profiles);
148
+ const aiProfile = profiles.find(p => p.id === aiResult.profileId);
173
149
  if (aiProfile) {
174
- const idle = instances.find(
175
- (i) => i.profileId === aiProfile.id && i.status === 'idle' && !claimed.has(i.instanceId),
176
- );
177
- if (idle) {
178
- return {
179
- profileId: aiProfile.id,
180
- instanceId: idle.instanceId,
181
- method: 'ai-routed',
182
- reason: `ai-routed: ${aiProfile.id} — ${aiResult.reason}`,
183
- confidence: aiResult.confidence,
184
- };
185
- }
186
- // AI chose a profile but no idle instance — fall through to round-robin
150
+ return { profileId: aiProfile.id, method: 'ai-routed', confidence: aiResult.confidence };
187
151
  }
188
- // AI returned unknown profile — fall through to round-robin
189
152
  } catch {
190
153
  // AI call failed — fall through to round-robin
191
154
  }
192
155
  }
193
156
 
194
- // Round-robin: pick profile with most idle instances
195
- method = 'round-robin';
196
- let bestProfile: BotProfile | null = null;
197
- let bestIdleCount = -1;
198
- let bestInstance: BotInstance | null = null;
199
-
200
- for (const profile of eligible) {
201
- const idleInstances = instances.filter(
202
- (i) => i.profileId === profile.id && i.status === 'idle' && !claimed.has(i.instanceId),
203
- );
204
- if (idleInstances.length > bestIdleCount) {
205
- bestIdleCount = idleInstances.length;
206
- bestProfile = profile;
207
- bestInstance = idleInstances[0] ?? null;
208
- }
209
- }
210
-
211
- if (!bestProfile || !bestInstance) return null;
212
-
213
- return {
214
- profileId: bestProfile.id,
215
- instanceId: bestInstance.instanceId,
216
- method,
217
- reason: `${method}: ${bestProfile.id}`,
218
- };
157
+ // Fallback: round-robin (first profile)
158
+ return profiles.length > 0 ? { profileId: profiles[0].id, method: 'round-robin' } : null;
219
159
  }
220
160
 
221
- /** Determine which routing method was used. */
222
- private _routingMethod(
223
- task: Task,
224
- eligible: BotProfile[],
225
- ): OrchestratorDecision['method'] {
226
- if (task.assignedProfile) return 'exact-match';
227
- if (eligible.length === 1) return 'single-eligible';
228
- return 'round-robin';
161
+ /** Find any idle worker that hasn't been claimed this cycle. */
162
+ private _findIdleWorker(instances: BotInstance[], claimed: Set<string>): BotInstance | null {
163
+ return instances.find(
164
+ (i) => i.status === 'idle' && !claimed.has(i.instanceId),
165
+ ) ?? null;
229
166
  }
230
167
 
231
168
  /** Record a decision in the log. */