skimpyclaw 0.3.14 → 0.4.0

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 (222) hide show
  1. package/README.md +47 -37
  2. package/dist/__tests__/adapter-types.test.d.ts +4 -0
  3. package/dist/__tests__/adapter-types.test.js +63 -0
  4. package/dist/__tests__/anthropic-adapter.test.d.ts +4 -0
  5. package/dist/__tests__/anthropic-adapter.test.js +264 -0
  6. package/dist/__tests__/api.test.js +0 -1
  7. package/dist/__tests__/cli.integration.test.js +2 -4
  8. package/dist/__tests__/cli.test.js +0 -1
  9. package/dist/__tests__/code-agents-notifications.test.js +137 -0
  10. package/dist/__tests__/code-agents-parser.test.js +19 -1
  11. package/dist/__tests__/code-agents-preflight.test.js +3 -28
  12. package/dist/__tests__/code-agents-utils.test.js +34 -9
  13. package/dist/__tests__/code-agents-worktrees.test.js +116 -0
  14. package/dist/__tests__/codex-adapter.test.js +184 -0
  15. package/dist/__tests__/codex-auth.test.js +66 -0
  16. package/dist/__tests__/codex-provider-gating.test.js +35 -0
  17. package/dist/__tests__/codex-unified-loop.test.js +111 -0
  18. package/dist/__tests__/config-security.test.js +127 -0
  19. package/dist/__tests__/config.test.js +23 -0
  20. package/dist/__tests__/context-manager.test.js +243 -164
  21. package/dist/__tests__/cron-run.test.js +250 -0
  22. package/dist/__tests__/cron.test.js +12 -38
  23. package/dist/__tests__/digests.test.js +67 -0
  24. package/dist/__tests__/discord-attachments.test.js +211 -0
  25. package/dist/__tests__/discord-docs.test.d.ts +1 -0
  26. package/dist/__tests__/discord-docs.test.js +27 -0
  27. package/dist/__tests__/discord-thread-agents.test.d.ts +1 -0
  28. package/dist/__tests__/discord-thread-agents.test.js +115 -0
  29. package/dist/__tests__/discord-thread-context.test.d.ts +1 -0
  30. package/dist/__tests__/discord-thread-context.test.js +42 -0
  31. package/dist/__tests__/doctor.formatters.test.js +4 -4
  32. package/dist/__tests__/doctor.index.test.js +1 -1
  33. package/dist/__tests__/doctor.runner.test.js +3 -15
  34. package/dist/__tests__/env-sanitizer.test.d.ts +1 -0
  35. package/dist/__tests__/env-sanitizer.test.js +45 -0
  36. package/dist/__tests__/exec-approval.test.js +61 -0
  37. package/dist/__tests__/fetch-tool.test.d.ts +1 -0
  38. package/dist/__tests__/fetch-tool.test.js +85 -0
  39. package/dist/__tests__/gateway-status-auth.test.d.ts +1 -0
  40. package/dist/__tests__/gateway-status-auth.test.js +72 -0
  41. package/dist/__tests__/heartbeat.test.js +3 -3
  42. package/dist/__tests__/interactive-sessions.test.d.ts +1 -0
  43. package/dist/__tests__/interactive-sessions.test.js +96 -0
  44. package/dist/__tests__/langfuse.test.js +6 -18
  45. package/dist/__tests__/model-selection.test.js +3 -4
  46. package/dist/__tests__/providers-init.test.js +2 -8
  47. package/dist/__tests__/providers-routing.test.js +1 -1
  48. package/dist/__tests__/providers-utils.test.js +13 -3
  49. package/dist/__tests__/sessions.test.js +14 -10
  50. package/dist/__tests__/setup.test.js +12 -29
  51. package/dist/__tests__/skills.test.js +10 -7
  52. package/dist/__tests__/stream-formatter.test.d.ts +1 -0
  53. package/dist/__tests__/stream-formatter.test.js +114 -0
  54. package/dist/__tests__/token-efficiency.test.js +131 -15
  55. package/dist/__tests__/tool-loop.test.d.ts +4 -0
  56. package/dist/__tests__/tool-loop.test.js +505 -0
  57. package/dist/__tests__/tools.test.js +101 -276
  58. package/dist/__tests__/utils.test.d.ts +1 -0
  59. package/dist/__tests__/utils.test.js +14 -0
  60. package/dist/__tests__/voice.test.js +21 -0
  61. package/dist/agent.js +35 -4
  62. package/dist/api.js +113 -37
  63. package/dist/channels/discord/attachments.d.ts +50 -0
  64. package/dist/channels/discord/attachments.js +137 -0
  65. package/dist/channels/discord/delegation.d.ts +5 -0
  66. package/dist/channels/discord/delegation.js +136 -0
  67. package/dist/channels/discord/handlers.js +694 -7
  68. package/dist/channels/discord/index.d.ts +16 -1
  69. package/dist/channels/discord/index.js +64 -1
  70. package/dist/channels/discord/thread-agents.d.ts +54 -0
  71. package/dist/channels/discord/thread-agents.js +323 -0
  72. package/dist/channels/discord/threads.d.ts +58 -0
  73. package/dist/channels/discord/threads.js +192 -0
  74. package/dist/channels/discord/types.js +4 -2
  75. package/dist/channels/discord/utils.d.ts +16 -0
  76. package/dist/channels/discord/utils.js +86 -6
  77. package/dist/channels/telegram/index.d.ts +1 -1
  78. package/dist/channels/telegram/types.js +1 -1
  79. package/dist/channels/telegram/utils.js +9 -3
  80. package/dist/channels.d.ts +1 -1
  81. package/dist/cli.js +20 -400
  82. package/dist/code-agents/executor.d.ts +1 -1
  83. package/dist/code-agents/executor.js +101 -45
  84. package/dist/code-agents/index.d.ts +2 -7
  85. package/dist/code-agents/index.js +111 -80
  86. package/dist/code-agents/interactive-resume.d.ts +6 -0
  87. package/dist/code-agents/interactive-resume.js +98 -0
  88. package/dist/code-agents/interactive-sessions.d.ts +20 -0
  89. package/dist/code-agents/interactive-sessions.js +132 -0
  90. package/dist/code-agents/parser.js +5 -1
  91. package/dist/code-agents/registry.d.ts +7 -1
  92. package/dist/code-agents/registry.js +11 -23
  93. package/dist/code-agents/stream-formatter.d.ts +8 -0
  94. package/dist/code-agents/stream-formatter.js +92 -0
  95. package/dist/code-agents/types.d.ts +16 -24
  96. package/dist/code-agents/utils.d.ts +35 -11
  97. package/dist/code-agents/utils.js +349 -95
  98. package/dist/code-agents/worktrees.d.ts +37 -0
  99. package/dist/code-agents/worktrees.js +116 -0
  100. package/dist/config.d.ts +2 -4
  101. package/dist/config.js +123 -23
  102. package/dist/cron.d.ts +1 -6
  103. package/dist/cron.js +175 -82
  104. package/dist/dashboard/assets/index-B345aOO-.js +65 -0
  105. package/dist/dashboard/assets/index-ZWK4dalJ.css +1 -0
  106. package/dist/dashboard/index.html +2 -2
  107. package/dist/digests.d.ts +1 -0
  108. package/dist/digests.js +132 -42
  109. package/dist/doctor/checks.d.ts +0 -3
  110. package/dist/doctor/checks.js +1 -108
  111. package/dist/doctor/runner.js +1 -4
  112. package/dist/env-sanitizer.d.ts +2 -0
  113. package/dist/env-sanitizer.js +61 -0
  114. package/dist/exec-approval.d.ts +11 -1
  115. package/dist/exec-approval.js +17 -4
  116. package/dist/gateway.d.ts +3 -1
  117. package/dist/gateway.js +17 -7
  118. package/dist/heartbeat.js +1 -6
  119. package/dist/langfuse.js +3 -29
  120. package/dist/model-selection.js +3 -1
  121. package/dist/providers/adapter.d.ts +118 -0
  122. package/dist/providers/adapter.js +6 -0
  123. package/dist/providers/adapters/anthropic-adapter.d.ts +22 -0
  124. package/dist/providers/adapters/anthropic-adapter.js +204 -0
  125. package/dist/providers/adapters/codex-adapter.d.ts +26 -0
  126. package/dist/providers/adapters/codex-adapter.js +203 -0
  127. package/dist/providers/anthropic.d.ts +1 -0
  128. package/dist/providers/anthropic.js +10 -272
  129. package/dist/providers/codex.d.ts +21 -0
  130. package/dist/providers/codex.js +149 -330
  131. package/dist/providers/content.d.ts +1 -1
  132. package/dist/providers/content.js +2 -2
  133. package/dist/providers/context-manager.d.ts +18 -6
  134. package/dist/providers/context-manager.js +199 -223
  135. package/dist/providers/index.d.ts +9 -1
  136. package/dist/providers/index.js +73 -64
  137. package/dist/providers/loop-utils.d.ts +20 -0
  138. package/dist/providers/loop-utils.js +30 -0
  139. package/dist/providers/tool-loop.d.ts +12 -0
  140. package/dist/providers/tool-loop.js +251 -0
  141. package/dist/providers/utils.d.ts +19 -3
  142. package/dist/providers/utils.js +100 -29
  143. package/dist/secure-store.d.ts +8 -0
  144. package/dist/secure-store.js +80 -0
  145. package/dist/service.js +3 -28
  146. package/dist/sessions.d.ts +3 -0
  147. package/dist/sessions.js +147 -18
  148. package/dist/setup-templates.js +13 -25
  149. package/dist/setup.d.ts +10 -6
  150. package/dist/setup.js +84 -292
  151. package/dist/skills.js +3 -11
  152. package/dist/tools/agent-delegation.d.ts +19 -0
  153. package/dist/tools/agent-delegation.js +49 -0
  154. package/dist/tools/bash-tool.js +89 -34
  155. package/dist/tools/definitions.d.ts +199 -302
  156. package/dist/tools/definitions.js +70 -123
  157. package/dist/tools/execute-context.d.ts +13 -4
  158. package/dist/tools/fetch-tool.js +109 -13
  159. package/dist/tools/file-tools.js +7 -1
  160. package/dist/tools.d.ts +7 -7
  161. package/dist/tools.js +133 -151
  162. package/dist/types.d.ts +37 -30
  163. package/dist/utils.js +4 -6
  164. package/dist/voice.d.ts +1 -1
  165. package/dist/voice.js +17 -4
  166. package/package.json +33 -23
  167. package/templates/TOOLS.md +0 -27
  168. package/dist/__tests__/audit.test.js +0 -122
  169. package/dist/__tests__/code-agents-orchestrator.test.js +0 -216
  170. package/dist/__tests__/code-agents-sandbox.test.js +0 -163
  171. package/dist/__tests__/orchestrator.test.js +0 -425
  172. package/dist/__tests__/sandbox-bridge.test.js +0 -116
  173. package/dist/__tests__/sandbox-manager.test.js +0 -144
  174. package/dist/__tests__/sandbox-mount-security.test.js +0 -139
  175. package/dist/__tests__/sandbox-runtime.test.js +0 -176
  176. package/dist/__tests__/subagent.test.js +0 -240
  177. package/dist/__tests__/telegram.test.js +0 -42
  178. package/dist/code-agents/orchestrator.d.ts +0 -29
  179. package/dist/code-agents/orchestrator.js +0 -694
  180. package/dist/code-agents/worktree.d.ts +0 -40
  181. package/dist/code-agents/worktree.js +0 -215
  182. package/dist/dashboard/assets/index-BoTHPby4.js +0 -65
  183. package/dist/dashboard/assets/index-D4mufvBg.css +0 -1
  184. package/dist/dashboard.d.ts +0 -8
  185. package/dist/dashboard.js +0 -4071
  186. package/dist/discord.d.ts +0 -8
  187. package/dist/discord.js +0 -792
  188. package/dist/mcp-context-a8c.d.ts +0 -13
  189. package/dist/mcp-context-a8c.js +0 -34
  190. package/dist/orchestrator.d.ts +0 -15
  191. package/dist/orchestrator.js +0 -676
  192. package/dist/providers/openai.d.ts +0 -10
  193. package/dist/providers/openai.js +0 -355
  194. package/dist/sandbox/bridge.d.ts +0 -5
  195. package/dist/sandbox/bridge.js +0 -63
  196. package/dist/sandbox/index.d.ts +0 -5
  197. package/dist/sandbox/index.js +0 -4
  198. package/dist/sandbox/manager.d.ts +0 -7
  199. package/dist/sandbox/manager.js +0 -100
  200. package/dist/sandbox/mount-security.d.ts +0 -12
  201. package/dist/sandbox/mount-security.js +0 -122
  202. package/dist/sandbox/runtime.d.ts +0 -39
  203. package/dist/sandbox/runtime.js +0 -192
  204. package/dist/sandbox-utils.d.ts +0 -6
  205. package/dist/sandbox-utils.js +0 -36
  206. package/dist/subagent.d.ts +0 -19
  207. package/dist/subagent.js +0 -407
  208. package/dist/telegram.d.ts +0 -2
  209. package/dist/telegram.js +0 -11
  210. package/dist/tools/browser-tool.d.ts +0 -3
  211. package/dist/tools/browser-tool.js +0 -266
  212. package/sandbox/Dockerfile +0 -40
  213. /package/dist/__tests__/{audit.test.d.ts → code-agents-notifications.test.d.ts} +0 -0
  214. /package/dist/__tests__/{code-agents-orchestrator.test.d.ts → code-agents-worktrees.test.d.ts} +0 -0
  215. /package/dist/__tests__/{code-agents-sandbox.test.d.ts → codex-adapter.test.d.ts} +0 -0
  216. /package/dist/__tests__/{orchestrator.test.d.ts → codex-auth.test.d.ts} +0 -0
  217. /package/dist/__tests__/{sandbox-bridge.test.d.ts → codex-provider-gating.test.d.ts} +0 -0
  218. /package/dist/__tests__/{sandbox-manager.test.d.ts → codex-unified-loop.test.d.ts} +0 -0
  219. /package/dist/__tests__/{sandbox-mount-security.test.d.ts → config-security.test.d.ts} +0 -0
  220. /package/dist/__tests__/{sandbox-runtime.test.d.ts → cron-run.test.d.ts} +0 -0
  221. /package/dist/__tests__/{subagent.test.d.ts → digests.test.d.ts} +0 -0
  222. /package/dist/__tests__/{telegram.test.d.ts → discord-attachments.test.d.ts} +0 -0
@@ -1,676 +0,0 @@
1
- // Orchestrator: multi-phase task decomposition and parallel execution engine
2
- import { homedir } from 'os';
3
- import { join } from 'path';
4
- import { existsSync, mkdirSync, appendFileSync, readFileSync } from 'fs';
5
- import { runAgentTurn, buildSystemPrompt } from './agent.js';
6
- import { getToolDefinitions, ORCHESTRATOR_TOOLS } from './tools.js';
7
- import { getCurrentModel } from './gateway.js';
8
- import { ensureAgentSetup } from './subagent.js';
9
- // --- Constants ---
10
- const ORCH_DIR = join(homedir(), '.skimpyclaw', 'orchestrations');
11
- const MAX_WORKERS = 3;
12
- const POLL_INTERVAL_MS = 2000;
13
- // --- Module-level state ---
14
- const activeSessions = new Map();
15
- let currentPlanningSession = null;
16
- let deliverMessage = null;
17
- let orchTaskCounter = 0;
18
- let orchWorkerCounter = 0;
19
- let planningComplete = false;
20
- function ensureOrchDir() {
21
- if (!existsSync(ORCH_DIR)) {
22
- mkdirSync(ORCH_DIR, { recursive: true });
23
- }
24
- }
25
- function getSessionPath(sessionId) {
26
- return join(ORCH_DIR, `${sessionId}.jsonl`);
27
- }
28
- function appendEvent(sessionId, event) {
29
- ensureOrchDir();
30
- const full = {
31
- ...event,
32
- timestamp: new Date().toISOString(),
33
- sessionId,
34
- };
35
- appendFileSync(getSessionPath(sessionId), JSON.stringify(full) + '\n', 'utf-8');
36
- }
37
- /**
38
- * Reconstruct a session from its JSONL event log.
39
- * Returns null if the session file doesn't exist.
40
- */
41
- export function loadSession(sessionId) {
42
- const path = getSessionPath(sessionId);
43
- if (!existsSync(path))
44
- return null;
45
- const lines = readFileSync(path, 'utf-8').trim().split('\n').filter(Boolean);
46
- let session = null;
47
- for (const line of lines) {
48
- const event = JSON.parse(line);
49
- switch (event.type) {
50
- case 'session_created':
51
- session = event.data.session;
52
- break;
53
- case 'task_created':
54
- if (session) {
55
- session.tasks.push(event.data.task);
56
- }
57
- break;
58
- case 'task_started':
59
- if (session) {
60
- const task = session.tasks.find(t => t.id === event.data.taskId);
61
- if (task) {
62
- task.status = 'running';
63
- task.workerId = event.data.workerId;
64
- task.startedAt = event.timestamp;
65
- }
66
- }
67
- break;
68
- case 'task_completed':
69
- if (session) {
70
- const task = session.tasks.find(t => t.id === event.data.taskId);
71
- if (task) {
72
- task.status = 'completed';
73
- task.result = event.data.result;
74
- task.completedAt = event.timestamp;
75
- }
76
- }
77
- break;
78
- case 'task_failed':
79
- if (session) {
80
- const task = session.tasks.find(t => t.id === event.data.taskId);
81
- if (task) {
82
- task.status = 'failed';
83
- task.error = event.data.error;
84
- task.completedAt = event.timestamp;
85
- }
86
- }
87
- break;
88
- case 'phase_changed':
89
- if (session) {
90
- session.status = event.data.phase;
91
- }
92
- break;
93
- case 'message_sent':
94
- if (session) {
95
- session.messages.push(event.data.message);
96
- }
97
- break;
98
- case 'session_completed':
99
- if (session) {
100
- session.status = 'completed';
101
- session.finalResult = event.data.finalResult;
102
- session.completedAt = event.timestamp;
103
- }
104
- break;
105
- case 'session_failed':
106
- if (session) {
107
- session.status = 'failed';
108
- session.error = event.data.error;
109
- session.completedAt = event.timestamp;
110
- }
111
- break;
112
- }
113
- }
114
- return session;
115
- }
116
- // ORCHESTRATOR_TOOLS is imported from tools.ts to avoid circular deps
117
- // (tools.ts defines them, orchestrator.ts uses them in planning phase)
118
- // --- Initialization ---
119
- export function initOrchestrationSystem(deliverFn) {
120
- deliverMessage = deliverFn;
121
- console.log('[orchestrator] System initialized');
122
- }
123
- // --- Session Management ---
124
- export function getActiveOrchestrations() {
125
- return [...activeSessions.values()].filter(s => s.status !== 'completed' && s.status !== 'failed');
126
- }
127
- export function getOrchestration(id) {
128
- return activeSessions.get(id) || loadSession(id);
129
- }
130
- export function listRecentOrchestrations(limit = 10) {
131
- return [...activeSessions.values()]
132
- .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
133
- .slice(0, limit);
134
- }
135
- export function setCurrentPlanningSession(session) {
136
- currentPlanningSession = session;
137
- }
138
- export function getCurrentPlanningSession() {
139
- return currentPlanningSession;
140
- }
141
- // --- Orchestrator Tool Execution ---
142
- export function executeOrchestratorTool(name, input) {
143
- switch (name) {
144
- case 'CreateTask': {
145
- if (!currentPlanningSession) {
146
- return 'Error: No active planning session.';
147
- }
148
- const { title, description, agentType, dependsOn } = input;
149
- if (!title || !description || !agentType) {
150
- return 'Error: title, description, and agentType are required.';
151
- }
152
- if (!['coding', 'research', 'general'].includes(agentType)) {
153
- return `Error: Invalid agentType "${agentType}". Must be coding, research, or general.`;
154
- }
155
- orchTaskCounter++;
156
- const taskId = `ot${orchTaskCounter}`;
157
- // Resolve dependsOn — accept any reasonable reference format:
158
- // "ot1", "1", "task-1", "task_1", "#1" all resolve to the first created task
159
- const deps = (dependsOn || []).map((ref) => {
160
- // Already a valid task ID?
161
- if (currentPlanningSession.tasks.some(t => t.id === ref))
162
- return ref;
163
- // Extract number from reference and map to task by creation order
164
- const numMatch = ref.match(/(\d+)/);
165
- if (numMatch) {
166
- const idx = parseInt(numMatch[1], 10) - 1; // 1-based to 0-based
167
- if (idx >= 0 && idx < currentPlanningSession.tasks.length) {
168
- return currentPlanningSession.tasks[idx].id;
169
- }
170
- }
171
- return ref; // Pass through — will be caught by validation below
172
- });
173
- // Validate resolved deps
174
- const existingIds = new Set(currentPlanningSession.tasks.map(t => t.id));
175
- const invalidDeps = deps.filter(id => !existingIds.has(id));
176
- if (invalidDeps.length > 0) {
177
- orchTaskCounter--; // Roll back counter
178
- const validIds = currentPlanningSession.tasks.map((t, i) => `${t.id} (task ${i + 1}: "${t.title}")`).join(', ') || '(none yet)';
179
- return `Error: Could not resolve dependsOn references: ${invalidDeps.join(', ')}. Existing tasks: ${validIds}`;
180
- }
181
- const task = {
182
- id: taskId,
183
- sessionId: currentPlanningSession.id,
184
- title,
185
- description,
186
- status: 'pending',
187
- dependsOn: deps,
188
- createdAt: new Date().toISOString(),
189
- };
190
- currentPlanningSession.tasks.push(task);
191
- appendEvent(currentPlanningSession.id, {
192
- type: 'task_created',
193
- data: { task },
194
- });
195
- const taskNum = currentPlanningSession.tasks.length;
196
- return `Task ${taskNum} created (id: ${taskId}): "${title}" [${agentType}], depends on: [${deps.join(', ') || 'none'}]`;
197
- }
198
- case 'FinishPlanning': {
199
- if (!currentPlanningSession) {
200
- return 'Error: No active planning session.';
201
- }
202
- planningComplete = true;
203
- const taskCount = currentPlanningSession.tasks.length;
204
- return `Planning complete. ${taskCount} task(s) created. Execution will begin.`;
205
- }
206
- case 'GetTaskStatus': {
207
- if (!currentPlanningSession) {
208
- return 'Error: No active planning session.';
209
- }
210
- const tasks = currentPlanningSession.tasks;
211
- if (tasks.length === 0)
212
- return 'No tasks created yet.';
213
- const summary = tasks.map(t => {
214
- const deps = t.dependsOn.length > 0 ? ` (depends on: ${t.dependsOn.join(', ')})` : '';
215
- return `- ${t.id}: [${t.status}] ${t.title}${deps}`;
216
- });
217
- return summary.join('\n');
218
- }
219
- default:
220
- return `Error: Unknown orchestrator tool "${name}"`;
221
- }
222
- }
223
- // --- Main Entry Point ---
224
- export function startOrchestration(prompt, chatId, config, modelOverride) {
225
- const sessionId = `orch_${Date.now()}`;
226
- const model = modelOverride || getCurrentModel();
227
- const session = {
228
- id: sessionId,
229
- prompt,
230
- status: 'planning',
231
- model,
232
- chatId,
233
- tasks: [],
234
- workers: [],
235
- messages: [],
236
- createdAt: new Date().toISOString(),
237
- startedAt: new Date().toISOString(),
238
- };
239
- activeSessions.set(sessionId, session);
240
- appendEvent(sessionId, {
241
- type: 'session_created',
242
- data: { session },
243
- });
244
- // Fire and forget — run all phases async
245
- runOrchestration(session, config).catch(err => {
246
- console.error(`[orchestrator] Unhandled error in ${sessionId}:`, err);
247
- session.status = 'failed';
248
- session.error = err instanceof Error ? err.message : String(err);
249
- session.completedAt = new Date().toISOString();
250
- appendEvent(sessionId, {
251
- type: 'session_failed',
252
- data: { error: session.error },
253
- });
254
- if (deliverMessage) {
255
- deliverMessage(chatId, `❌ Orchestration ${sessionId} failed:\n\n${session.error}`).catch(() => { });
256
- }
257
- });
258
- return session;
259
- }
260
- // --- Phase Runner ---
261
- async function runOrchestration(session, config) {
262
- const { chatId } = session;
263
- try {
264
- // Phase 1: Planning
265
- if (deliverMessage) {
266
- await deliverMessage(chatId, `🎯 Orchestration started: planning phase...\n\nPrompt: ${session.prompt}`);
267
- }
268
- await planPhase(session, config);
269
- if (session.tasks.length === 0) {
270
- throw new Error('Planning phase produced no tasks');
271
- }
272
- if (deliverMessage) {
273
- const taskList = session.tasks
274
- .map(t => ` ${t.id}: ${t.title}`)
275
- .join('\n');
276
- await deliverMessage(chatId, `📋 Plan created (${session.tasks.length} tasks):\n${taskList}\n\nStarting execution...`);
277
- }
278
- // Phase 2: Execution
279
- session.status = 'executing';
280
- appendEvent(session.id, { type: 'phase_changed', data: { phase: 'executing' } });
281
- await executePhase(session, config);
282
- // Check for failures
283
- const failedTasks = session.tasks.filter(t => t.status === 'failed');
284
- if (failedTasks.length > 0 && session.tasks.every(t => t.status === 'failed' || t.status === 'completed')) {
285
- // Some failed but execution is over — continue to synthesis with partial results
286
- if (deliverMessage) {
287
- await deliverMessage(chatId, `⚠️ Execution complete with ${failedTasks.length} failure(s). Synthesizing results...`);
288
- }
289
- }
290
- else if (deliverMessage) {
291
- await deliverMessage(chatId, `✅ All tasks complete. Synthesizing results...`);
292
- }
293
- // Phase 3: Synthesis
294
- session.status = 'synthesizing';
295
- appendEvent(session.id, { type: 'phase_changed', data: { phase: 'synthesizing' } });
296
- await synthesizePhase(session, config);
297
- // Done
298
- session.status = 'completed';
299
- session.completedAt = new Date().toISOString();
300
- appendEvent(session.id, {
301
- type: 'session_completed',
302
- data: { finalResult: session.finalResult },
303
- });
304
- if (deliverMessage) {
305
- await deliverMessage(chatId, `🏁 Orchestration complete!\n\n${session.finalResult || '(no result)'}`);
306
- }
307
- }
308
- catch (err) {
309
- session.status = 'failed';
310
- session.error = err instanceof Error ? err.message : String(err);
311
- session.completedAt = new Date().toISOString();
312
- appendEvent(session.id, {
313
- type: 'session_failed',
314
- data: { error: session.error },
315
- });
316
- if (deliverMessage) {
317
- await deliverMessage(chatId, `❌ Orchestration failed:\n\n${session.error}`);
318
- }
319
- }
320
- }
321
- // --- Phase 1: Planning ---
322
- async function planPhase(session, config) {
323
- console.log(`[orchestrator] Planning phase for ${session.id}`);
324
- // Set module-level state so orchestrator tools can access the session
325
- currentPlanningSession = session;
326
- planningComplete = false;
327
- try {
328
- // Ensure coordinator agent exists
329
- ensureAgentSetup('general', config);
330
- const systemPrompt = buildSystemPrompt('general');
331
- const planningPrompt = `You are an orchestration coordinator. Your job is to break down a complex task into subtasks that can be executed by specialized agents.
332
-
333
- Available agent types:
334
- - coding: For code writing, file manipulation, bash commands, and technical tasks
335
- - research: For investigation, reading files, and information gathering
336
- - general: For miscellaneous tasks
337
-
338
- You have these tools:
339
- - CreateTask: Create a subtask with title, description, agentType, and optional dependsOn (array of task IDs)
340
- - FinishPlanning: Call when all tasks are created
341
- - GetTaskStatus: Check current task list
342
-
343
- Rules:
344
- 1. Break the task into 2-8 focused subtasks
345
- 2. Each task description should be self-contained (the worker won't see the original prompt)
346
- 3. Use dependsOn to specify ordering when one task needs results from another
347
- 4. Minimize dependencies to maximize parallel execution
348
- 5. Call FinishPlanning when done
349
-
350
- Task to decompose:
351
- ${session.prompt}`;
352
- const messages = [
353
- { role: 'system', content: systemPrompt },
354
- { role: 'user', content: planningPrompt },
355
- ];
356
- const toolConfig = {
357
- enabled: true,
358
- allowedPaths: [join(homedir(), '.skimpyclaw')],
359
- maxIterations: 30,
360
- bashTimeout: 15000,
361
- };
362
- // Get base tools and append orchestrator tools
363
- const baseDefs = await getToolDefinitions(toolConfig);
364
- const allTools = [...baseDefs, ...ORCHESTRATOR_TOOLS];
365
- const chatOptions = { model: session.model, thinking: 'medium' };
366
- // Use chatWithTools directly, but we need to handle orchestrator tools ourselves.
367
- // Instead, we call the Anthropic API loop manually via chatWithTools pattern.
368
- // Since chatWithTools resolves tools internally via getToolDefinitions, we need
369
- // a different approach: call chatWithTools but intercept orchestrator tool calls
370
- // through the executeTool function in tools.ts.
371
- //
372
- // Simplest approach: replicate a minimal tool loop here for planning only.
373
- await planningToolLoop(messages, chatOptions, config, toolConfig, allTools);
374
- }
375
- finally {
376
- currentPlanningSession = null;
377
- planningComplete = false;
378
- }
379
- }
380
- /**
381
- * Minimal Anthropic tool loop for the planning phase.
382
- * Routes orchestrator tools to executeOrchestratorTool, others to executeTool.
383
- */
384
- async function planningToolLoop(messages, chatOptions, config, toolConfig, toolDefs) {
385
- // Dynamic import to avoid circular deps at module level
386
- const Anthropic = (await import('@anthropic-ai/sdk')).default;
387
- const { resolveModel, buildSystemParam } = await import('./agent.js');
388
- const { executeTool } = await import('./tools.js');
389
- let resolvedModel = resolveModel(chatOptions.model, config);
390
- // planningToolLoop uses the Anthropic SDK directly, so it can only handle Claude models.
391
- // If the current model is non-Anthropic (e.g. gpt-*, codex), fall back to Sonnet.
392
- if (!resolvedModel.includes('claude')) {
393
- const fallback = config.models?.aliases?.['claude-think'] || 'claude-sonnet-4-5-20250929';
394
- console.log(`[orchestrator:plan] Non-Anthropic model "${chatOptions.model}" detected, falling back to "${fallback}" for planning`);
395
- resolvedModel = resolveModel(fallback, config);
396
- }
397
- // Strip provider prefix
398
- const slashIdx = resolvedModel.indexOf('/');
399
- const modelId = slashIdx > 0 ? resolvedModel.slice(slashIdx + 1) : resolvedModel;
400
- const maxIterations = toolConfig.maxIterations || 30;
401
- const systemMessage = messages.find(m => m.role === 'system');
402
- const systemParam = buildSystemParam(systemMessage?.content);
403
- const apiMessages = messages
404
- .filter(m => m.role !== 'system')
405
- .map(m => ({ role: m.role, content: m.content }));
406
- // We need the anthropic client — import from agent.ts won't expose it.
407
- // Use the same approach as chatWithTools: instantiate via config.
408
- // Actually, chatWithTools uses the module-level anthropicClient. We can't access it.
409
- // Alternative: use chat() with tool_use by calling the Anthropic SDK directly.
410
- // But we don't have the client. Let's use a workaround: create a temporary client
411
- // using the same config.
412
- const anthropicConfig = config.models.providers.anthropic;
413
- if (!anthropicConfig?.apiKey && !anthropicConfig?.authToken) {
414
- throw new Error('Anthropic provider not configured');
415
- }
416
- const opts = {};
417
- if (anthropicConfig.authToken) {
418
- opts.apiKey = null;
419
- opts.authToken = anthropicConfig.authToken;
420
- opts.defaultHeaders = {
421
- 'accept': 'application/json',
422
- 'anthropic-dangerous-direct-browser-access': 'true',
423
- 'anthropic-beta': 'claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14',
424
- 'user-agent': 'claude-cli/2.1.2 (external, cli)',
425
- 'x-app': 'cli',
426
- };
427
- opts.dangerouslyAllowBrowser = true;
428
- }
429
- else if (anthropicConfig.apiKey) {
430
- opts.apiKey = anthropicConfig.apiKey;
431
- }
432
- const client = new Anthropic(opts);
433
- const orchToolNames = new Set(ORCHESTRATOR_TOOLS.map(t => t.name));
434
- for (let i = 0; i < maxIterations; i++) {
435
- if (planningComplete) {
436
- console.log('[orchestrator] Planning complete signal received');
437
- break;
438
- }
439
- const params = {
440
- model: modelId,
441
- max_tokens: 8192,
442
- messages: apiMessages,
443
- tools: toolDefs,
444
- };
445
- if (systemParam)
446
- params.system = systemParam;
447
- if (chatOptions.thinking && chatOptions.thinking !== 'none') {
448
- const budgetTokens = { low: 2048, medium: 8192, high: 16384 };
449
- const budget = budgetTokens[chatOptions.thinking] || 2048;
450
- params.thinking = { type: 'enabled', budget_tokens: budget };
451
- params.max_tokens = Math.max(params.max_tokens, budget + 4096);
452
- }
453
- console.log(`[orchestrator:plan] Iteration ${i + 1}/${maxIterations}`);
454
- const response = await client.messages.create(params);
455
- // If no tool use, planning is done
456
- if (response.stop_reason !== 'tool_use') {
457
- const textBlocks = response.content.filter((c) => c.type === 'text');
458
- return textBlocks.map((b) => b.text).join('\n');
459
- }
460
- // Add assistant response
461
- apiMessages.push({ role: 'assistant', content: response.content });
462
- // Execute tools
463
- const toolResults = [];
464
- for (const block of response.content) {
465
- if (block.type !== 'tool_use')
466
- continue;
467
- const inputStr = JSON.stringify(block.input).slice(0, 200);
468
- console.log(`[orchestrator:plan] -> ${block.name}(${inputStr})`);
469
- let result;
470
- if (orchToolNames.has(block.name)) {
471
- result = executeOrchestratorTool(block.name, block.input);
472
- }
473
- else {
474
- result = await executeTool(block.name, block.input, toolConfig);
475
- }
476
- console.log(`[orchestrator:plan] <- ${result.slice(0, 200)}`);
477
- toolResults.push({
478
- type: 'tool_result',
479
- tool_use_id: block.id,
480
- content: result,
481
- });
482
- }
483
- apiMessages.push({ role: 'user', content: toolResults });
484
- }
485
- return '';
486
- }
487
- // --- Phase 2: Execution ---
488
- async function executePhase(session, config) {
489
- console.log(`[orchestrator] Execution phase for ${session.id} (${session.tasks.length} tasks)`);
490
- const activeWorkers = new Map();
491
- while (true) {
492
- // Check if all tasks are terminal
493
- const allDone = session.tasks.every(t => t.status === 'completed' || t.status === 'failed');
494
- if (allDone)
495
- break;
496
- // Check for deadlock: remaining tasks all have unresolvable dependencies
497
- const pendingTasks = session.tasks.filter(t => t.status === 'pending');
498
- const runningTasks = session.tasks.filter(t => t.status === 'running');
499
- if (pendingTasks.length > 0 && runningTasks.length === 0 && activeWorkers.size === 0) {
500
- // Check if any pending task can ever be unblocked
501
- const canUnblock = pendingTasks.some(t => {
502
- return t.dependsOn.every(depId => {
503
- const dep = session.tasks.find(d => d.id === depId);
504
- return dep && dep.status === 'completed';
505
- });
506
- });
507
- if (!canUnblock) {
508
- throw new Error(`Deadlock: ${pendingTasks.length} tasks blocked with no running workers. Failed dependencies may be preventing progress.`);
509
- }
510
- }
511
- // Find unblocked pending tasks
512
- const unblocked = pendingTasks.filter(t => {
513
- return t.dependsOn.every(depId => {
514
- const dep = session.tasks.find(d => d.id === depId);
515
- return dep && dep.status === 'completed';
516
- });
517
- });
518
- // Assign unblocked tasks to workers (up to MAX_WORKERS)
519
- for (const task of unblocked) {
520
- if (activeWorkers.size >= MAX_WORKERS)
521
- break;
522
- // Create worker
523
- orchWorkerCounter++;
524
- const workerId = `w${orchWorkerCounter}`;
525
- // Determine agent type from the task — stored during planning via CreateTask
526
- // We need to figure out the agentType. The orchestrator tool stores it...
527
- // Actually, OrchTask doesn't have an agentType field. We'll default to 'general'
528
- // and look for hints in the task title/description.
529
- const agentType = inferAgentType(task);
530
- const worker = {
531
- id: workerId,
532
- sessionId: session.id,
533
- agentType,
534
- status: 'busy',
535
- currentTaskId: task.id,
536
- createdAt: new Date().toISOString(),
537
- };
538
- session.workers.push(worker);
539
- task.status = 'running';
540
- task.workerId = workerId;
541
- task.startedAt = new Date().toISOString();
542
- appendEvent(session.id, {
543
- type: 'task_started',
544
- data: { taskId: task.id, workerId },
545
- });
546
- console.log(`[orchestrator] Assigned ${task.id} ("${task.title}") to worker ${workerId} (${agentType})`);
547
- // Launch worker
548
- const workerPromise = runWorkerTask(session, task, worker, config)
549
- .then(() => {
550
- task.status = 'completed';
551
- task.completedAt = new Date().toISOString();
552
- worker.status = 'stopped';
553
- appendEvent(session.id, {
554
- type: 'task_completed',
555
- data: { taskId: task.id, result: task.result },
556
- });
557
- console.log(`[orchestrator] Task ${task.id} completed by ${workerId}`);
558
- if (deliverMessage) {
559
- deliverMessage(session.chatId, `✅ Task ${task.id} ("${task.title}") completed`).catch(() => { });
560
- }
561
- })
562
- .catch(err => {
563
- task.status = 'failed';
564
- task.error = err instanceof Error ? err.message : String(err);
565
- task.completedAt = new Date().toISOString();
566
- worker.status = 'stopped';
567
- appendEvent(session.id, {
568
- type: 'task_failed',
569
- data: { taskId: task.id, error: task.error },
570
- });
571
- console.error(`[orchestrator] Task ${task.id} failed:`, task.error);
572
- if (deliverMessage) {
573
- deliverMessage(session.chatId, `❌ Task ${task.id} ("${task.title}") failed: ${task.error}`).catch(() => { });
574
- }
575
- })
576
- .finally(() => {
577
- activeWorkers.delete(workerId);
578
- });
579
- activeWorkers.set(workerId, workerPromise);
580
- }
581
- // Wait for at least one worker to finish, or poll timeout
582
- if (activeWorkers.size > 0) {
583
- await Promise.race([
584
- ...activeWorkers.values(),
585
- new Promise(resolve => setTimeout(resolve, POLL_INTERVAL_MS)),
586
- ]);
587
- }
588
- else {
589
- // No workers running, nothing to assign — wait briefly
590
- await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL_MS));
591
- }
592
- }
593
- }
594
- /**
595
- * Infer agent type from task metadata.
596
- * The planning phase CreateTask stores agentType in the task description as a hint.
597
- * We parse it or default to 'general'.
598
- */
599
- function inferAgentType(task) {
600
- const desc = (task.title + ' ' + task.description).toLowerCase();
601
- if (desc.includes('[coding]') || desc.includes('write code') || desc.includes('implement') || desc.includes('create file')) {
602
- return 'coding';
603
- }
604
- if (desc.includes('[research]') || desc.includes('research') || desc.includes('investigate') || desc.includes('find out')) {
605
- return 'research';
606
- }
607
- return 'general';
608
- }
609
- // --- Worker Execution ---
610
- async function runWorkerTask(session, task, worker, config) {
611
- // Build context from completed dependency tasks
612
- const depContext = task.dependsOn
613
- .map(id => session.tasks.find(t => t.id === id))
614
- .filter((t) => !!t && !!t.result)
615
- .map(t => `## Output from "${t.title}":\n${t.result}`)
616
- .join('\n\n');
617
- const prompt = depContext
618
- ? `${task.description}\n\n---\nContext from completed prerequisite tasks:\n${depContext}`
619
- : task.description;
620
- const toolConfig = {
621
- enabled: true,
622
- allowedPaths: [join(homedir(), '.skimpyclaw')],
623
- maxIterations: 100,
624
- bashTimeout: 30000,
625
- };
626
- // Ensure agent directory exists
627
- ensureAgentSetup(worker.agentType, config);
628
- const result = await runAgentTurn(worker.agentType, prompt, config, session.model, toolConfig, undefined, {
629
- channel: 'orchestrator',
630
- sessionId: session.id,
631
- metadata: { taskId: task.id, workerId: worker.id },
632
- });
633
- task.result = result;
634
- }
635
- // --- Phase 3: Synthesis ---
636
- async function synthesizePhase(session, config) {
637
- console.log(`[orchestrator] Synthesis phase for ${session.id}`);
638
- // Build a summary of all task results
639
- const taskSummaries = session.tasks.map(t => {
640
- const status = t.status === 'completed' ? '✅' : '❌';
641
- const output = t.status === 'completed' ? t.result : `Error: ${t.error}`;
642
- return `## ${status} Task ${t.id}: ${t.title}\n\n${output || '(no output)'}`;
643
- });
644
- const synthesisPrompt = `You are synthesizing the results of a multi-task orchestration.
645
-
646
- Original request:
647
- ${session.prompt}
648
-
649
- ---
650
-
651
- Task results:
652
-
653
- ${taskSummaries.join('\n\n---\n\n')}
654
-
655
- ---
656
-
657
- Provide a clear, comprehensive response to the original request based on all task results above. If any tasks failed, note what couldn't be completed and why.`;
658
- // Ensure agent exists
659
- ensureAgentSetup('general', config);
660
- const result = await runAgentTurn('general', synthesisPrompt, config, session.model, undefined, // No tools needed for synthesis
661
- undefined, {
662
- channel: 'orchestrator',
663
- sessionId: session.id,
664
- metadata: { phase: 'synthesis' },
665
- });
666
- session.finalResult = result;
667
- }
668
- // --- Testing ---
669
- export function resetForTesting() {
670
- activeSessions.clear();
671
- currentPlanningSession = null;
672
- deliverMessage = null;
673
- orchTaskCounter = 0;
674
- orchWorkerCounter = 0;
675
- planningComplete = false;
676
- }
@@ -1,10 +0,0 @@
1
- import OpenAI from 'openai';
2
- import type { ProviderChatParams, ProviderToolChatParams, ToolChatResult } from './types.js';
3
- export declare function addOpenAIClient(name: string, client: OpenAI): void;
4
- export declare function getOpenAIClient(name: string): OpenAI | undefined;
5
- export declare function hasOpenAIClient(name: string): boolean;
6
- export declare function clearOpenAIClients(): void;
7
- export declare function resetOpenAIProviderState(): void;
8
- export declare function isOpenAIAvailable(provider: string): boolean;
9
- export declare function chatOpenAI(params: ProviderChatParams, provider: string): Promise<string>;
10
- export declare function chatWithToolsOpenAI(params: ProviderToolChatParams, provider: string): Promise<ToolChatResult>;