@synergenius/flow-weaver-pack-weaver 0.9.199 → 0.9.201
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai-chat-provider.js +5 -5
- package/dist/ai-chat-provider.js.map +1 -1
- package/dist/bot/acceptance-merge.d.ts +21 -0
- package/dist/bot/acceptance-merge.d.ts.map +1 -0
- package/dist/bot/acceptance-merge.js +46 -0
- package/dist/bot/acceptance-merge.js.map +1 -0
- package/dist/bot/ai-client.d.ts +14 -2
- package/dist/bot/ai-client.d.ts.map +1 -1
- package/dist/bot/ai-client.js +71 -24
- package/dist/bot/ai-client.js.map +1 -1
- package/dist/bot/assistant-tools.js +3 -3
- package/dist/bot/assistant-tools.js.map +1 -1
- package/dist/bot/audit-logger.d.ts.map +1 -1
- package/dist/bot/audit-logger.js +34 -14
- package/dist/bot/audit-logger.js.map +1 -1
- package/dist/bot/audit-trail.d.ts +67 -0
- package/dist/bot/audit-trail.d.ts.map +1 -0
- package/dist/bot/audit-trail.js +153 -0
- package/dist/bot/audit-trail.js.map +1 -0
- package/dist/bot/behavior-defaults.d.ts +1 -1
- package/dist/bot/behavior-defaults.d.ts.map +1 -1
- package/dist/bot/behavior-defaults.js +7 -3
- package/dist/bot/behavior-defaults.js.map +1 -1
- package/dist/bot/capability-registry.d.ts +9 -0
- package/dist/bot/capability-registry.d.ts.map +1 -1
- package/dist/bot/capability-registry.js +81 -27
- package/dist/bot/capability-registry.js.map +1 -1
- package/dist/bot/capability-types.d.ts +10 -0
- package/dist/bot/capability-types.d.ts.map +1 -1
- package/dist/bot/cli-provider.d.ts.map +1 -1
- package/dist/bot/cli-provider.js +8 -7
- package/dist/bot/cli-provider.js.map +1 -1
- package/dist/bot/preflight.d.ts +48 -0
- package/dist/bot/preflight.d.ts.map +1 -0
- package/dist/bot/preflight.js +247 -0
- package/dist/bot/preflight.js.map +1 -0
- package/dist/bot/provider-shim.d.ts +74 -0
- package/dist/bot/provider-shim.d.ts.map +1 -0
- package/dist/bot/provider-shim.js +176 -0
- package/dist/bot/provider-shim.js.map +1 -0
- package/dist/bot/runner.d.ts +2 -0
- package/dist/bot/runner.d.ts.map +1 -1
- package/dist/bot/runner.js +60 -17
- package/dist/bot/runner.js.map +1 -1
- package/dist/bot/step-executor.d.ts.map +1 -1
- package/dist/bot/step-executor.js +72 -115
- package/dist/bot/step-executor.js.map +1 -1
- package/dist/bot/swarm-controller.d.ts +2 -0
- package/dist/bot/swarm-controller.d.ts.map +1 -1
- package/dist/bot/swarm-controller.js +92 -20
- package/dist/bot/swarm-controller.js.map +1 -1
- package/dist/bot/task-create-handler.d.ts +37 -0
- package/dist/bot/task-create-handler.d.ts.map +1 -0
- package/dist/bot/task-create-handler.js +124 -0
- package/dist/bot/task-create-handler.js.map +1 -0
- package/dist/bot/task-store.d.ts +1 -0
- package/dist/bot/task-store.d.ts.map +1 -1
- package/dist/bot/task-store.js +67 -0
- package/dist/bot/task-store.js.map +1 -1
- package/dist/bot/types.d.ts +1 -1
- package/dist/bot/types.d.ts.map +1 -1
- package/dist/bot/weaver-tools.d.ts.map +1 -1
- package/dist/bot/weaver-tools.js +7 -39
- package/dist/bot/weaver-tools.js.map +1 -1
- package/dist/node-types/agent-execute.d.ts +25 -8
- package/dist/node-types/agent-execute.d.ts.map +1 -1
- package/dist/node-types/agent-execute.js +89 -23
- package/dist/node-types/agent-execute.js.map +1 -1
- package/dist/node-types/bot-report.d.ts.map +1 -1
- package/dist/node-types/bot-report.js +24 -3
- package/dist/node-types/bot-report.js.map +1 -1
- package/dist/node-types/plan-task.d.ts +8 -17
- package/dist/node-types/plan-task.d.ts.map +1 -1
- package/dist/node-types/plan-task.js +217 -256
- package/dist/node-types/plan-task.js.map +1 -1
- package/dist/node-types/review-result.js +8 -6
- package/dist/node-types/review-result.js.map +1 -1
- package/dist/palindrome.d.ts +9 -0
- package/dist/palindrome.d.ts.map +1 -0
- package/dist/palindrome.js +14 -0
- package/dist/palindrome.js.map +1 -0
- package/dist/ui/approval-card.js +91 -82
- package/dist/ui/bot-activity.js +73 -56
- package/dist/ui/bot-config.js +48 -31
- package/dist/ui/bot-dashboard.js +52 -36
- package/dist/ui/bot-panel.js +230 -228
- package/dist/ui/bot-slot-card.js +100 -90
- package/dist/ui/bot-status.js +37 -15
- package/dist/ui/budget-bar.js +57 -31
- package/dist/ui/capability-editor.js +447 -378
- package/dist/ui/chat-task-result.js +78 -71
- package/dist/ui/decision-log.js +68 -81
- package/dist/ui/genesis-block.js +86 -95
- package/dist/ui/instance-stream-view.js +722 -0
- package/dist/ui/profile-card.js +96 -221
- package/dist/ui/profile-editor.js +532 -575
- package/dist/ui/settings-section.js +41 -45
- package/dist/ui/swarm-controls.js +212 -135
- package/dist/ui/swarm-dashboard.js +3992 -2715
- package/dist/ui/task-detail-view.js +415 -521
- package/dist/ui/task-editor.js +339 -390
- package/dist/ui/task-pool-list.js +60 -55
- package/dist/workflows/src/palindrome.d.ts +11 -0
- package/dist/workflows/src/palindrome.d.ts.map +1 -0
- package/dist/workflows/src/palindrome.js +16 -0
- package/dist/workflows/src/palindrome.js.map +1 -0
- package/dist/workflows/tests/palindrome.test.d.ts +2 -0
- package/dist/workflows/tests/palindrome.test.d.ts.map +1 -0
- package/dist/workflows/tests/palindrome.test.js +41 -0
- package/dist/workflows/tests/palindrome.test.js.map +1 -0
- package/dist/workflows/weaver-bot-batch.js +1 -1
- package/dist/workflows/weaver-bot-batch.js.map +1 -1
- package/dist/workflows/weaver-bot.js +1 -1
- package/dist/workflows/weaver-bot.js.map +1 -1
- package/flowweaver.manifest.json +1 -1
- package/package.json +8 -2
- package/src/ai-chat-provider.ts +5 -5
- package/src/bot/acceptance-merge.ts +62 -0
- package/src/bot/ai-client.ts +77 -21
- package/src/bot/assistant-tools.ts +3 -3
- package/src/bot/audit-logger.ts +42 -14
- package/src/bot/audit-trail.ts +211 -0
- package/src/bot/behavior-defaults.ts +7 -2
- package/src/bot/capability-registry.ts +84 -28
- package/src/bot/capability-types.ts +11 -0
- package/src/bot/cli-provider.ts +8 -7
- package/src/bot/preflight.ts +285 -0
- package/src/bot/provider-shim.ts +218 -0
- package/src/bot/runner.ts +68 -20
- package/src/bot/step-executor.ts +69 -127
- package/src/bot/swarm-controller.ts +94 -20
- package/src/bot/task-create-handler.ts +164 -0
- package/src/bot/task-store.ts +83 -0
- package/src/bot/types.ts +4 -1
- package/src/bot/weaver-tools.ts +7 -45
- package/src/node-types/agent-execute.ts +102 -16
- package/src/node-types/bot-report.ts +24 -3
- package/src/node-types/plan-task.ts +238 -280
- package/src/node-types/review-result.ts +8 -6
- package/src/palindrome.ts +14 -0
- package/src/ui/approval-card.tsx +78 -62
- package/src/ui/bot-activity.tsx +12 -10
- package/src/ui/bot-config.tsx +12 -10
- package/src/ui/bot-dashboard.tsx +13 -11
- package/src/ui/bot-panel.tsx +189 -171
- package/src/ui/bot-slot-card.tsx +125 -70
- package/src/ui/bot-status.tsx +4 -4
- package/src/ui/budget-bar.tsx +86 -25
- package/src/ui/capability-editor.tsx +392 -257
- package/src/ui/chat-task-result.tsx +81 -78
- package/src/ui/decision-log.tsx +76 -73
- package/src/ui/genesis-block.tsx +91 -61
- package/src/ui/instance-stream-view.tsx +861 -0
- package/src/ui/profile-card.tsx +195 -168
- package/src/ui/profile-editor.tsx +453 -370
- package/src/ui/settings-section.tsx +46 -39
- package/src/ui/swarm-controls.tsx +252 -123
- package/src/ui/swarm-dashboard.tsx +999 -466
- package/src/ui/task-detail-view.tsx +485 -428
- package/src/ui/task-editor.tsx +329 -271
- package/src/ui/task-pool-list.tsx +68 -62
- package/src/workflows/src/palindrome.ts +16 -0
- package/src/workflows/tests/palindrome.test.ts +49 -0
- package/src/workflows/weaver-bot-batch.ts +1 -1
- package/src/workflows/weaver-bot.ts +1 -1
- package/dist/ui/bot-constants.d.ts +0 -14
- package/dist/ui/bot-constants.d.ts.map +0 -1
- package/dist/ui/bot-constants.js +0 -189
- package/dist/ui/bot-constants.js.map +0 -1
- package/dist/ui/steer-api.d.ts +0 -7
- package/dist/ui/steer-api.d.ts.map +0 -1
- package/dist/ui/steer-api.js +0 -11
- package/dist/ui/steer-api.js.map +0 -1
- package/dist/ui/trace-to-timeline.d.ts +0 -91
- package/dist/ui/trace-to-timeline.d.ts.map +0 -1
- package/dist/ui/trace-to-timeline.js +0 -116
- package/dist/ui/trace-to-timeline.js.map +0 -1
- package/dist/ui/use-stream-timeline.d.ts +0 -50
- package/dist/ui/use-stream-timeline.d.ts.map +0 -1
- package/dist/ui/use-stream-timeline.js +0 -245
- package/dist/ui/use-stream-timeline.js.map +0 -1
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider Shim — bridges callPlatformWithMessages to ANY provider.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this exists
|
|
5
|
+
*
|
|
6
|
+
* Node-types (plan-task, review-result, etc.) call `callPlatformWithMessages`
|
|
7
|
+
* which checks `globalThis.__fw_llm_provider__`. This global is normally set
|
|
8
|
+
* by the platform via IPC (pack-worker-entry.mjs). When running WITHOUT the
|
|
9
|
+
* platform (CLI standalone, benchmarks, tests), it's missing and all AI calls
|
|
10
|
+
* throw "Platform AI provider not available".
|
|
11
|
+
*
|
|
12
|
+
* ## MCP Bridge
|
|
13
|
+
*
|
|
14
|
+
* The CLI session with --tools "" has zero built-in tools. Pack tools must be
|
|
15
|
+
* registered via MCP bridge so the model can call them as structured tool_use
|
|
16
|
+
* (not text). The shim creates a lightweight MCP bridge per worker using
|
|
17
|
+
* core's createMcpBridge with the pack's tool definitions and executor.
|
|
18
|
+
*
|
|
19
|
+
* ## Concurrency model
|
|
20
|
+
*
|
|
21
|
+
* The swarm runs multiple workers concurrently. Each worker calls runWorkflow
|
|
22
|
+
* which needs its own CLI session (its own `claude -p` process). We use
|
|
23
|
+
* AsyncLocalStorage to track the current worker ID so the single global shim
|
|
24
|
+
* can route each `.chat()` call to the correct per-worker CLI session.
|
|
25
|
+
*
|
|
26
|
+
* DO NOT overwrite `globalThis.__fw_llm_provider__` per worker — that causes
|
|
27
|
+
* race conditions where the last writer wins and all other workers share one
|
|
28
|
+
* session, producing zero-token ghost runs.
|
|
29
|
+
*
|
|
30
|
+
* ## DO NOT
|
|
31
|
+
*
|
|
32
|
+
* - Do NOT try to make individual nodes dispatch based on provider type.
|
|
33
|
+
* - Do NOT call callCliAsync directly — it's single-turn text-only.
|
|
34
|
+
* - Do NOT store per-worker shims on the global — use setActiveWorkerId().
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
38
|
+
import type { BotProviderConfig } from './types.js';
|
|
39
|
+
import type { AiTool, ChatMessage } from './ai-client.js';
|
|
40
|
+
import type { McpBridge } from '@synergenius/flow-weaver/agent';
|
|
41
|
+
|
|
42
|
+
export interface ProviderShim {
|
|
43
|
+
chat: (
|
|
44
|
+
messages: ChatMessage[],
|
|
45
|
+
options?: { model?: string; maxTokens?: number; tools?: AiTool[] },
|
|
46
|
+
) => Promise<{
|
|
47
|
+
content: string;
|
|
48
|
+
toolCalls?: Array<{ id: string; name: string; arguments: Record<string, unknown> }>;
|
|
49
|
+
thinking?: string;
|
|
50
|
+
usage?: Record<string, unknown>;
|
|
51
|
+
}>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* AsyncLocalStorage for the current worker ID.
|
|
56
|
+
* The runner sets this before each workflow execution so the routing shim
|
|
57
|
+
* dispatches .chat() to the correct per-worker CLI session.
|
|
58
|
+
*/
|
|
59
|
+
const workerIdStorage = new AsyncLocalStorage<string>();
|
|
60
|
+
|
|
61
|
+
/** Run a function with the given worker ID in the async context. */
|
|
62
|
+
export function runWithWorkerId<T>(workerId: string, fn: () => T): T {
|
|
63
|
+
return workerIdStorage.run(workerId, fn);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Get the current worker ID from the async context. */
|
|
67
|
+
export function getActiveWorkerId(): string | undefined {
|
|
68
|
+
return workerIdStorage.getStore();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Per-worker MCP bridges — created once, reused across chat() calls
|
|
72
|
+
const bridges = new Map<string, McpBridge>();
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get or create an MCP bridge for the given worker/project.
|
|
76
|
+
* The bridge registers pack tools (read_file, write_file, run_shell, etc.)
|
|
77
|
+
* as MCP tools so the CLI session model can call them via structured tool_use.
|
|
78
|
+
*/
|
|
79
|
+
async function getOrCreateBridge(cwd: string, bridgeKey: string): Promise<McpBridge> {
|
|
80
|
+
const existing = bridges.get(bridgeKey);
|
|
81
|
+
if (existing) return existing;
|
|
82
|
+
|
|
83
|
+
const { createMcpBridge } = await import('@synergenius/flow-weaver/agent');
|
|
84
|
+
const { WEAVER_TOOLS, createWeaverExecutor } = await import('./weaver-tools.js');
|
|
85
|
+
|
|
86
|
+
const executor = createWeaverExecutor(cwd);
|
|
87
|
+
const bridge = await createMcpBridge(
|
|
88
|
+
WEAVER_TOOLS,
|
|
89
|
+
executor,
|
|
90
|
+
undefined, // no tool event callback — audit emits are handled by plan-task
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
bridges.set(bridgeKey, bridge);
|
|
94
|
+
return bridge;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Build a routing shim that dispatches .chat() calls to per-worker CLI sessions.
|
|
99
|
+
* One instance is installed on globalThis; it reads the current worker ID from
|
|
100
|
+
* AsyncLocalStorage to select the right session.
|
|
101
|
+
*/
|
|
102
|
+
export function buildProviderShim(
|
|
103
|
+
providerConfig: BotProviderConfig,
|
|
104
|
+
projectDir?: string,
|
|
105
|
+
): ProviderShim {
|
|
106
|
+
return {
|
|
107
|
+
async chat(messages, options) {
|
|
108
|
+
const { getOrCreateCliSession } = await import(
|
|
109
|
+
'@synergenius/flow-weaver/agent'
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const cwd = projectDir ?? process.cwd();
|
|
113
|
+
const model = options?.model ?? providerConfig.model ?? 'claude-sonnet-4-6';
|
|
114
|
+
const workerId = getActiveWorkerId();
|
|
115
|
+
const sessionKey = workerId ? `shim:${cwd}:${workerId}` : `shim:${cwd}`;
|
|
116
|
+
const bridgeKey = workerId ? `bridge:${cwd}:${workerId}` : `bridge:${cwd}`;
|
|
117
|
+
|
|
118
|
+
// Create MCP bridge so the CLI session has pack tools registered
|
|
119
|
+
const bridge = await getOrCreateBridge(cwd, bridgeKey);
|
|
120
|
+
|
|
121
|
+
const { getCliSessionConfig } = await import('@synergenius/flow-weaver/agent');
|
|
122
|
+
const session = getOrCreateCliSession(sessionKey, getCliSessionConfig({
|
|
123
|
+
binPath: (providerConfig.options?.binPath as string | undefined),
|
|
124
|
+
cwd,
|
|
125
|
+
model,
|
|
126
|
+
mcpConfigPath: bridge.configPath,
|
|
127
|
+
appendSystemPrompt: 'You are Weaver, an AI workflow bot. Use the provided tools to complete tasks. Never attempt to use built-in tools.',
|
|
128
|
+
}));
|
|
129
|
+
|
|
130
|
+
if (!session.ready) await session.spawn();
|
|
131
|
+
|
|
132
|
+
// Build prompt from messages
|
|
133
|
+
const prompt = messages
|
|
134
|
+
.map((m: ChatMessage) => {
|
|
135
|
+
if (m.role === 'system') return '';
|
|
136
|
+
if (m.role === 'user') return typeof m.content === 'string' ? m.content : JSON.stringify(m.content);
|
|
137
|
+
if (m.role === 'tool') return `Tool result (${(m as unknown as Record<string, unknown>).tool_use_id ?? 'unknown'}): ${m.content}`;
|
|
138
|
+
return '';
|
|
139
|
+
})
|
|
140
|
+
.filter(Boolean)
|
|
141
|
+
.join('\n');
|
|
142
|
+
|
|
143
|
+
const systemMsg = messages.find(m => m.role === 'system');
|
|
144
|
+
const systemPrompt = typeof systemMsg?.content === 'string' ? systemMsg.content : undefined;
|
|
145
|
+
|
|
146
|
+
// Collect stream events into the platform response shape
|
|
147
|
+
let content = '';
|
|
148
|
+
let thinking = '';
|
|
149
|
+
const toolCalls: Array<{ id: string; name: string; arguments: Record<string, unknown> }> = [];
|
|
150
|
+
const pendingTools = new Map<string, { id: string; name: string }>();
|
|
151
|
+
let usage: { input_tokens: number; output_tokens: number } | undefined;
|
|
152
|
+
|
|
153
|
+
for await (const event of session.send(prompt, systemPrompt)) {
|
|
154
|
+
switch (event.type) {
|
|
155
|
+
case 'text_delta':
|
|
156
|
+
content += event.text;
|
|
157
|
+
break;
|
|
158
|
+
case 'thinking_delta':
|
|
159
|
+
thinking += event.text;
|
|
160
|
+
break;
|
|
161
|
+
case 'tool_use_start':
|
|
162
|
+
pendingTools.set(event.id, { id: event.id, name: event.name });
|
|
163
|
+
break;
|
|
164
|
+
case 'tool_use_end':
|
|
165
|
+
toolCalls.push({
|
|
166
|
+
id: event.id,
|
|
167
|
+
name: pendingTools.get(event.id)?.name ?? 'unknown',
|
|
168
|
+
arguments: event.arguments,
|
|
169
|
+
});
|
|
170
|
+
pendingTools.delete(event.id);
|
|
171
|
+
break;
|
|
172
|
+
case 'usage':
|
|
173
|
+
usage = { input_tokens: event.promptTokens, output_tokens: event.completionTokens };
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
content,
|
|
180
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
181
|
+
thinking: thinking || undefined,
|
|
182
|
+
usage,
|
|
183
|
+
};
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Install the provider shim as `globalThis.__fw_llm_provider__`.
|
|
190
|
+
* No-op if the platform already set it (avoids overriding IPC proxy).
|
|
191
|
+
* Called ONCE — the shim routes per-worker via AsyncLocalStorage.
|
|
192
|
+
*/
|
|
193
|
+
export async function installProviderShim(
|
|
194
|
+
providerConfig: BotProviderConfig,
|
|
195
|
+
projectDir?: string,
|
|
196
|
+
): Promise<void> {
|
|
197
|
+
if ((globalThis as Record<string, unknown>).__fw_llm_provider__) return;
|
|
198
|
+
|
|
199
|
+
// Kill any cached CLI sessions from previous runs that may have different
|
|
200
|
+
// options. The cache now validates options via fingerprint, but a clean
|
|
201
|
+
// start is still safest.
|
|
202
|
+
try {
|
|
203
|
+
const { killAllCliSessions } = await import('@synergenius/flow-weaver/agent');
|
|
204
|
+
killAllCliSessions();
|
|
205
|
+
} catch { /* non-fatal */ }
|
|
206
|
+
|
|
207
|
+
(globalThis as Record<string, unknown>).__fw_llm_provider__ = buildProviderShim(providerConfig, projectDir);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Clean up all MCP bridges (for shutdown).
|
|
212
|
+
*/
|
|
213
|
+
export function cleanupBridges(): void {
|
|
214
|
+
for (const [, bridge] of bridges) {
|
|
215
|
+
bridge.cleanup();
|
|
216
|
+
}
|
|
217
|
+
bridges.clear();
|
|
218
|
+
}
|
package/src/bot/runner.ts
CHANGED
|
@@ -36,6 +36,8 @@ import { RunStore } from './run-store.js';
|
|
|
36
36
|
import { CostTracker } from './cost-tracker.js';
|
|
37
37
|
import { CostStore } from './cost-store.js';
|
|
38
38
|
import { runRegistry } from './run-registry.js';
|
|
39
|
+
import { buildProviderShim, installProviderShim, runWithWorkerId, getActiveWorkerId } from './provider-shim.js';
|
|
40
|
+
export { buildProviderShim };
|
|
39
41
|
|
|
40
42
|
function resolveApproval(
|
|
41
43
|
approval: BotConfig['approval'],
|
|
@@ -246,6 +248,18 @@ export async function runWorkflow(
|
|
|
246
248
|
/** Bot slot that executes this run. */
|
|
247
249
|
botId?: string;
|
|
248
250
|
},
|
|
251
|
+
): Promise<WorkflowResult> {
|
|
252
|
+
// Wrap in AsyncLocalStorage context so the provider shim routes
|
|
253
|
+
// .chat() calls to the correct per-worker CLI session.
|
|
254
|
+
if (options?.botId) {
|
|
255
|
+
return runWithWorkerId(options.botId, () => runWorkflowInner(filePath, options));
|
|
256
|
+
}
|
|
257
|
+
return runWorkflowInner(filePath, options);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function runWorkflowInner(
|
|
261
|
+
filePath: string,
|
|
262
|
+
options?: Parameters<typeof runWorkflow>[1],
|
|
249
263
|
): Promise<WorkflowResult> {
|
|
250
264
|
const absPath = path.resolve(filePath);
|
|
251
265
|
const verbose = options?.verbose ?? false;
|
|
@@ -293,6 +307,15 @@ export async function runWorkflow(
|
|
|
293
307
|
|
|
294
308
|
const provider = await createProvider(providerConfig);
|
|
295
309
|
|
|
310
|
+
// Install provider shim so callPlatformWithMessages works for ALL providers.
|
|
311
|
+
// See src/bot/provider-shim.ts for full documentation on why this is needed.
|
|
312
|
+
// Use params.projectDir (workspace) when available (swarm runs), else workflow dir.
|
|
313
|
+
// Install ONE routing shim on the global. The shim uses AsyncLocalStorage
|
|
314
|
+
// to read the current worker ID and dispatch to per-worker CLI sessions.
|
|
315
|
+
// See src/bot/provider-shim.ts for full documentation.
|
|
316
|
+
const shimDir = (options?.params?.projectDir as string) ?? path.dirname(absPath);
|
|
317
|
+
installProviderShim(providerConfig, shimDir);
|
|
318
|
+
|
|
296
319
|
const costTracker = new CostTracker(providerConfig.model ?? 'unknown', providerConfig.name);
|
|
297
320
|
// Track cost per AI call, emit to EventLog for live streaming (throttled to 1/sec)
|
|
298
321
|
let lastCostEmit = 0;
|
|
@@ -309,25 +332,47 @@ export async function runWorkflow(
|
|
|
309
332
|
});
|
|
310
333
|
}
|
|
311
334
|
};
|
|
312
|
-
// Set global usage callback
|
|
313
|
-
//
|
|
314
|
-
//
|
|
315
|
-
(globalThis as Record<string, unknown>).__fw_ai_usage_callback__
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
335
|
+
// Set global usage callback. Uses AsyncLocalStorage (via getActiveWorkerId)
|
|
336
|
+
// to route usage to the correct worker's CostTracker in concurrent swarm mode.
|
|
337
|
+
// The callback is set once and routes per-worker — NOT overwritten per worker.
|
|
338
|
+
if (!(globalThis as Record<string, unknown>).__fw_ai_usage_callback__) {
|
|
339
|
+
// Map of workerId → { costTracker, eventLog, lastCostEmit }
|
|
340
|
+
const workerTrackers = new Map<string, { costTracker: CostTracker; eventLog?: typeof options extends { eventLog?: infer E } ? E : never; lastCostEmit: number }>();
|
|
341
|
+
(globalThis as Record<string, unknown>).__fw_ai_usage_callback__ = (
|
|
342
|
+
model: string,
|
|
343
|
+
usage: { inputTokens: number; outputTokens: number },
|
|
344
|
+
) => {
|
|
345
|
+
const workerId = getActiveWorkerId() ?? '__default__';
|
|
346
|
+
const tracker = workerTrackers.get(workerId);
|
|
347
|
+
if (!tracker) return;
|
|
348
|
+
tracker.costTracker.track('platform-call', model, usage);
|
|
349
|
+
const now = Date.now();
|
|
350
|
+
if (tracker.eventLog && now - tracker.lastCostEmit >= 1000) {
|
|
351
|
+
tracker.lastCostEmit = now;
|
|
352
|
+
const summary = tracker.costTracker.getRunSummary();
|
|
353
|
+
(tracker.eventLog as any).emit({
|
|
354
|
+
type: 'cost-update',
|
|
355
|
+
timestamp: now,
|
|
356
|
+
data: { totalCost: summary.totalCost, totalInputTokens: summary.totalInputTokens, totalOutputTokens: summary.totalOutputTokens },
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
(globalThis as Record<string, unknown>).__fw_usage_trackers__ = workerTrackers;
|
|
361
|
+
}
|
|
362
|
+
// Register this worker's tracker
|
|
363
|
+
const workerId = getActiveWorkerId() ?? '__default__';
|
|
364
|
+
const workerTrackers = (globalThis as Record<string, unknown>).__fw_usage_trackers__ as Map<string, { costTracker: CostTracker; eventLog?: unknown; lastCostEmit: number }>;
|
|
365
|
+
workerTrackers.set(workerId, { costTracker, eventLog: options?.eventLog, lastCostEmit: 0 });
|
|
366
|
+
|
|
367
|
+
// Set global stream event callback so callCliAsync (used by node types)
|
|
368
|
+
// emits conversation events (thinking_delta, text_delta, tool_use_start, etc.)
|
|
369
|
+
// to the EventLog for the InstanceStreamView to consume.
|
|
370
|
+
if (options?.eventLog) {
|
|
371
|
+
const { setStreamEventCallback } = await import('./ai-client.js');
|
|
372
|
+
setStreamEventCallback((event) => {
|
|
373
|
+
options.eventLog!.emit(event);
|
|
374
|
+
});
|
|
375
|
+
}
|
|
331
376
|
|
|
332
377
|
const channels = notifyConfigs.map(
|
|
333
378
|
(c) => new WebhookNotificationChannel(c, options?.onNotificationError),
|
|
@@ -632,7 +677,10 @@ export async function runWorkflow(
|
|
|
632
677
|
registryPromiseResolve?.();
|
|
633
678
|
try { options?.eventLog?.done(); } catch { /* already done or no log */ }
|
|
634
679
|
teardownAuditLogger();
|
|
635
|
-
delete (
|
|
680
|
+
// Unregister this worker's tracker — don't delete the globals (other workers may still be active)
|
|
681
|
+
const cleanupWorkerId = getActiveWorkerId() ?? '__default__';
|
|
682
|
+
const cleanupTrackers = (globalThis as Record<string, unknown>).__fw_usage_trackers__ as Map<string, unknown> | undefined;
|
|
683
|
+
cleanupTrackers?.delete(cleanupWorkerId);
|
|
636
684
|
}
|
|
637
685
|
}
|
|
638
686
|
|
package/src/bot/step-executor.ts
CHANGED
|
@@ -7,7 +7,9 @@ import { promisify } from 'node:util';
|
|
|
7
7
|
const execAsync = promisify(exec);
|
|
8
8
|
import { runCommand } from '@synergenius/flow-weaver';
|
|
9
9
|
import { BLOCKED_SHELL_PATTERNS } from './safety.js';
|
|
10
|
-
import { OP_WRITE_FILE, OP_READ_FILE, OP_PATCH_FILE, OP_LIST_FILES, OP_RUN_SHELL, OP_TASK_CREATE, OP_RESPOND, OP_NO_OP, OP_NOOP, OP_DONE, OP_COMPLETE, OP_CREATE_WORKFLOW, OP_MODIFY_SOURCE, OP_IMPLEMENT_NODE, OP_REMEMBER, OP_RECALL, normalizeOperation } from './operations.js';
|
|
10
|
+
import { OP_WRITE_FILE, OP_READ_FILE, OP_PATCH_FILE, OP_LIST_FILES, OP_RUN_SHELL, OP_TASK_CREATE, OP_RESPOND, OP_NO_OP, OP_NOOP, OP_DONE, OP_COMPLETE, OP_CREATE_WORKFLOW, OP_MODIFY_SOURCE, OP_IMPLEMENT_NODE, OP_REMEMBER, OP_RECALL, OP_TSC_CHECK, OP_RUN_TESTS, normalizeOperation } from './operations.js';
|
|
11
|
+
import { isBlockedUrl } from './safety.js';
|
|
12
|
+
import { execFileSync } from 'node:child_process';
|
|
11
13
|
import { TaskStore } from './task-store.js';
|
|
12
14
|
import type { CreateTaskInput } from './task-types.js';
|
|
13
15
|
|
|
@@ -55,8 +57,17 @@ export function resetPlanFileCounter(): void {
|
|
|
55
57
|
// ---------------------------------------------------------------------------
|
|
56
58
|
|
|
57
59
|
function assertSafePath(filePath: string, projectDir: string): void {
|
|
58
|
-
|
|
60
|
+
// Normalize absolute paths that fall inside projectDir to relative.
|
|
61
|
+
// Models sometimes generate absolute paths from context they see in prompts.
|
|
59
62
|
const resolvedBase = path.resolve(projectDir);
|
|
63
|
+
let normalizedPath = filePath;
|
|
64
|
+
if (path.isAbsolute(filePath)) {
|
|
65
|
+
const absPath = path.resolve(filePath);
|
|
66
|
+
if (absPath.startsWith(resolvedBase + path.sep) || absPath === resolvedBase) {
|
|
67
|
+
normalizedPath = path.relative(resolvedBase, absPath) || '.';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const resolved = path.resolve(projectDir, normalizedPath);
|
|
60
71
|
if (!resolved.startsWith(resolvedBase + path.sep) && resolved !== resolvedBase) {
|
|
61
72
|
throw new Error(
|
|
62
73
|
`Path traversal blocked: "${filePath}" resolves outside project directory.`,
|
|
@@ -418,135 +429,25 @@ export async function executeStep(
|
|
|
418
429
|
// Task management — create subtasks for recursive swarm execution
|
|
419
430
|
// -----------------------------------------------------------------
|
|
420
431
|
case OP_TASK_CREATE: {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
// Validate assignedProfile: must be a valid slug (lowercase alphanumeric + hyphens)
|
|
427
|
-
const rawProfile = args.assignedProfile as string | undefined;
|
|
428
|
-
let assignedProfile: string | undefined;
|
|
429
|
-
if (rawProfile !== undefined && rawProfile !== null) {
|
|
430
|
-
if (typeof rawProfile !== 'string' || !rawProfile.trim()) {
|
|
431
|
-
return { blocked: true, blockReason: 'task_create: assignedProfile must be a non-empty string.' };
|
|
432
|
-
}
|
|
433
|
-
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(rawProfile)) {
|
|
434
|
-
return { blocked: true, blockReason: `task_create: assignedProfile "${rawProfile}" is not a valid slug (use lowercase alphanumeric with hyphens).` };
|
|
435
|
-
}
|
|
436
|
-
assignedProfile = rawProfile;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// Validate complexity against enum, default to 'simple' if invalid
|
|
440
|
-
const VALID_COMPLEXITIES = new Set(['trivial', 'simple', 'moderate', 'complex']);
|
|
441
|
-
const rawComplexity = args.complexity as string | undefined;
|
|
442
|
-
const complexity: 'trivial' | 'simple' | 'moderate' | 'complex' =
|
|
443
|
-
rawComplexity && VALID_COMPLEXITIES.has(rawComplexity)
|
|
444
|
-
? (rawComplexity as 'trivial' | 'simple' | 'moderate' | 'complex')
|
|
445
|
-
: 'simple';
|
|
446
|
-
|
|
447
|
-
// Validate priority: coerce to number, default to 0 if NaN
|
|
448
|
-
const rawPriority = Number(args.priority);
|
|
449
|
-
const priority = Number.isFinite(rawPriority) ? rawPriority : 0;
|
|
450
|
-
|
|
451
|
-
// Validate parentId — must reference an existing task
|
|
452
|
-
const rawParentId = args.parentId as string | undefined;
|
|
453
|
-
let parentId: string | undefined;
|
|
454
|
-
if (rawParentId) {
|
|
455
|
-
// Resolve through symbolicIdMap first (in case it's a title or symbolic ref)
|
|
456
|
-
const resolved = symbolicIdMap?.[rawParentId] ?? rawParentId;
|
|
457
|
-
const parentTask = await store.get(resolved);
|
|
458
|
-
if (!parentTask) {
|
|
459
|
-
return { blocked: true, blockReason: `task_create: parentId "${rawParentId}" does not match any existing task. Use the exact Task ID from the prompt.` };
|
|
460
|
-
}
|
|
461
|
-
parentId = resolved;
|
|
432
|
+
// Use shared handler for validation, @self resolution, dedup, acceptance parsing
|
|
433
|
+
const { handleTaskCreate } = await import('./task-create-handler.js');
|
|
434
|
+
const createResult = await handleTaskCreate(args as any, projectDir, symbolicIdMap);
|
|
435
|
+
if (createResult.blocked) {
|
|
436
|
+
return { blocked: true, blockReason: createResult.blockReason };
|
|
462
437
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
if (parentId) {
|
|
467
|
-
const existing = (await store.list()).find(
|
|
468
|
-
t => t.parentId === parentId && t.title.toLowerCase() === title.toLowerCase(),
|
|
469
|
-
);
|
|
470
|
-
if (existing) {
|
|
471
|
-
if (symbolicIdMap) {
|
|
472
|
-
const symbolicKey = (args.symbolicId as string) ?? (args.id as string);
|
|
473
|
-
if (symbolicKey) symbolicIdMap[symbolicKey] = existing.id;
|
|
474
|
-
symbolicIdMap[title] = existing.id;
|
|
475
|
-
}
|
|
476
|
-
return { output: `Task "${title}" already exists (${existing.id}), skipped duplicate.` };
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
// Resolve symbolic IDs in dependsOn through the map
|
|
481
|
-
const rawDeps = (args.dependsOn as string[]) ?? [];
|
|
482
|
-
const resolvedDeps = symbolicIdMap
|
|
483
|
-
? rawDeps.map(dep => symbolicIdMap[dep] ?? dep)
|
|
484
|
-
: rawDeps;
|
|
485
|
-
|
|
486
|
-
// Parse acceptance criteria if provided
|
|
487
|
-
const rawAcceptance = args.acceptance as Record<string, unknown> | undefined;
|
|
488
|
-
const acceptance = rawAcceptance?.checks ? {
|
|
489
|
-
checks: (rawAcceptance.checks as Array<{ name: string; command: string }>).map(c => ({
|
|
490
|
-
name: String(c.name ?? ''),
|
|
491
|
-
command: String(c.command ?? ''),
|
|
492
|
-
})).filter(c => c.name && c.command),
|
|
493
|
-
} : undefined;
|
|
494
|
-
|
|
495
|
-
const input: CreateTaskInput = {
|
|
496
|
-
title,
|
|
497
|
-
description: (args.description as string) ?? title,
|
|
498
|
-
complexity,
|
|
499
|
-
priority,
|
|
500
|
-
parentId,
|
|
501
|
-
dependsOn: resolvedDeps,
|
|
502
|
-
assignedProfile,
|
|
503
|
-
acceptance,
|
|
504
|
-
};
|
|
505
|
-
|
|
506
|
-
// Support inline subtasks
|
|
507
|
-
const subtasksArg = args.subtasks as Array<Record<string, unknown>> | undefined;
|
|
508
|
-
if (subtasksArg?.length) {
|
|
509
|
-
// Validate subtask titles are non-empty
|
|
510
|
-
for (let i = 0; i < subtasksArg.length; i++) {
|
|
511
|
-
const subTitle = (subtasksArg[i].title as string) ?? '';
|
|
512
|
-
if (!subTitle.trim()) {
|
|
513
|
-
return { blocked: true, blockReason: `task_create: subtask[${i}] has an empty title. All subtasks must have non-empty titles.` };
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
input.subtasks = subtasksArg.map((s) => {
|
|
518
|
-
const subComplexity = s.complexity as string | undefined;
|
|
519
|
-
const subPriority = Number(s.priority);
|
|
520
|
-
return {
|
|
521
|
-
title: (s.title as string) ?? '',
|
|
522
|
-
description: (s.description as string) ?? '',
|
|
523
|
-
complexity: subComplexity && VALID_COMPLEXITIES.has(subComplexity)
|
|
524
|
-
? (subComplexity as 'trivial' | 'simple' | 'moderate' | 'complex')
|
|
525
|
-
: 'simple',
|
|
526
|
-
priority: Number.isFinite(subPriority) ? subPriority : 0,
|
|
527
|
-
assignedProfile: (s.assignedProfile as string) ?? assignedProfile ?? 'developer',
|
|
528
|
-
dependsOn: symbolicIdMap
|
|
529
|
-
? ((s.dependsOn as string[]) ?? []).map(dep => symbolicIdMap[dep] ?? dep)
|
|
530
|
-
: (s.dependsOn as string[]) ?? [],
|
|
531
|
-
acceptance: s.acceptance ? { checks: ((s.acceptance as Record<string, unknown>).checks as Array<{name:string,command:string}>).map(c => ({name:String(c.name),command:String(c.command)})).filter(c=>c.name&&c.command) } : undefined,
|
|
532
|
-
};
|
|
533
|
-
});
|
|
438
|
+
if (!createResult.taskId) {
|
|
439
|
+
// Dedup — task already existed
|
|
440
|
+
return { output: createResult.output };
|
|
534
441
|
}
|
|
535
442
|
|
|
536
|
-
const
|
|
537
|
-
|
|
538
|
-
// Register symbolic ID → real ID mapping.
|
|
539
|
-
// Also map the title so the AI can reference tasks by title in dependsOn.
|
|
540
|
-
if (symbolicIdMap) {
|
|
541
|
-
const symbolicKey = (args.symbolicId as string) ?? (args.id as string);
|
|
542
|
-
if (symbolicKey) symbolicIdMap[symbolicKey] = task.id;
|
|
543
|
-
// Title-based mapping — the AI often uses task titles as dependency references
|
|
544
|
-
symbolicIdMap[title] = task.id;
|
|
545
|
-
}
|
|
443
|
+
const store = new TaskStore(projectDir);
|
|
444
|
+
const title = String(args.title ?? '');
|
|
546
445
|
|
|
547
|
-
|
|
548
|
-
const
|
|
549
|
-
|
|
446
|
+
// Shared handler already created the task — return result
|
|
447
|
+
const task = await store.get(createResult.taskId!);
|
|
448
|
+
const subtasks = task?.isParent ? await store.getSubtasks(createResult.taskId!) : [];
|
|
449
|
+
const ids = [createResult.taskId!, ...subtasks.map(s => s.id)];
|
|
450
|
+
return { output: `Created task "${title}" (${createResult.taskId})${subtasks.length ? ` with ${subtasks.length} subtasks` : ''}. IDs: ${ids.join(', ')}` };
|
|
550
451
|
}
|
|
551
452
|
|
|
552
453
|
// -----------------------------------------------------------------
|
|
@@ -591,6 +492,47 @@ export async function executeStep(
|
|
|
591
492
|
return { output: response || undefined };
|
|
592
493
|
}
|
|
593
494
|
|
|
495
|
+
// -----------------------------------------------------------------
|
|
496
|
+
// TypeScript check + test runner (same as weaver-tools.ts handlers)
|
|
497
|
+
// -----------------------------------------------------------------
|
|
498
|
+
case OP_TSC_CHECK: {
|
|
499
|
+
try {
|
|
500
|
+
const output = execFileSync('npx', ['tsc', '--noEmit', '--pretty'], {
|
|
501
|
+
cwd: projectDir, encoding: 'utf-8', timeout: 60_000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
502
|
+
});
|
|
503
|
+
return { output: output.trim() || 'No TypeScript errors.' };
|
|
504
|
+
} catch (err: any) {
|
|
505
|
+
return { output: (err.stdout ?? err.message ?? '').slice(0, 5000) };
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
case OP_RUN_TESTS: {
|
|
510
|
+
const pattern = args.pattern ? String(args.pattern) : '';
|
|
511
|
+
try {
|
|
512
|
+
const testArgs = ['vitest', 'run', '--reporter', 'verbose'];
|
|
513
|
+
if (pattern) testArgs.push(pattern);
|
|
514
|
+
const output = execFileSync('npx', testArgs, {
|
|
515
|
+
cwd: projectDir, encoding: 'utf-8', timeout: 120_000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
516
|
+
});
|
|
517
|
+
return { output: output.slice(0, 5000) };
|
|
518
|
+
} catch (err: any) {
|
|
519
|
+
return { output: (err.stdout ?? err.stderr ?? err.message ?? '').slice(0, 5000) };
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
case 'web_fetch': {
|
|
524
|
+
const url = String(args.url ?? '');
|
|
525
|
+
if (!url) return { output: 'Error: url is required', blocked: true, blockReason: 'Missing url argument' };
|
|
526
|
+
if (isBlockedUrl(url)) return { output: 'Blocked: cannot fetch internal/localhost URLs.', blocked: true, blockReason: 'Blocked URL' };
|
|
527
|
+
try {
|
|
528
|
+
const resp = await fetch(url, { method: (args.method as string) ?? 'GET', signal: AbortSignal.timeout(15_000) });
|
|
529
|
+
const text = await resp.text();
|
|
530
|
+
return { output: text.slice(0, 10_000) };
|
|
531
|
+
} catch (err: any) {
|
|
532
|
+
return { output: `Fetch error: ${err.message}` };
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
594
536
|
default: {
|
|
595
537
|
const result = await runCommand(operation, { ...args, cwd: projectDir });
|
|
596
538
|
return {
|