@synergenius/flow-weaver-pack-weaver 0.9.193 → 0.9.195
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/bot/assistant-core.js +2 -2
- package/dist/bot/assistant-core.js.map +1 -1
- package/dist/bot/capability-registry.js +2 -2
- package/dist/bot/capability-registry.js.map +1 -1
- package/dist/bot/context-compactor.d.ts +35 -0
- package/dist/bot/context-compactor.d.ts.map +1 -0
- package/dist/bot/context-compactor.js +130 -0
- package/dist/bot/context-compactor.js.map +1 -0
- package/dist/bot/memory-extraction-worker.d.ts +14 -0
- package/dist/bot/memory-extraction-worker.d.ts.map +1 -0
- package/dist/bot/memory-extraction-worker.js +42 -0
- package/dist/bot/memory-extraction-worker.js.map +1 -0
- package/dist/bot/memory-extractor.d.ts +27 -0
- package/dist/bot/memory-extractor.d.ts.map +1 -0
- package/dist/bot/memory-extractor.js +155 -0
- package/dist/bot/memory-extractor.js.map +1 -0
- package/dist/bot/operations.d.ts +3 -1
- package/dist/bot/operations.d.ts.map +1 -1
- package/dist/bot/operations.js +3 -1
- package/dist/bot/operations.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 +42 -0
- package/dist/bot/swarm-controller.js.map +1 -1
- package/dist/bot/task-prompt-builder.js +35 -21
- package/dist/bot/task-prompt-builder.js.map +1 -1
- package/dist/bot/task-types.d.ts +2 -0
- package/dist/bot/task-types.d.ts.map +1 -1
- package/dist/bot/tool-registry.d.ts +13 -0
- package/dist/bot/tool-registry.d.ts.map +1 -1
- package/dist/bot/tool-registry.js +80 -0
- package/dist/bot/tool-registry.js.map +1 -1
- package/dist/bot/types.d.ts +2 -0
- package/dist/bot/types.d.ts.map +1 -1
- package/dist/node-types/agent-execute.d.ts.map +1 -1
- package/dist/node-types/agent-execute.js +20 -15
- package/dist/node-types/agent-execute.js.map +1 -1
- package/dist/node-types/build-context.d.ts.map +1 -1
- package/dist/node-types/build-context.js +18 -3
- package/dist/node-types/build-context.js.map +1 -1
- package/dist/node-types/receive-task.d.ts +2 -1
- package/dist/node-types/receive-task.d.ts.map +1 -1
- package/dist/node-types/receive-task.js +4 -1
- package/dist/node-types/receive-task.js.map +1 -1
- package/dist/node-types/review-result.d.ts +9 -0
- package/dist/node-types/review-result.d.ts.map +1 -1
- package/dist/node-types/review-result.js +20 -5
- package/dist/node-types/review-result.js.map +1 -1
- package/dist/ui/capability-editor.js +2 -2
- package/dist/ui/profile-editor.js +2 -2
- package/dist/ui/swarm-dashboard.js +2 -2
- package/flowweaver.manifest.json +1 -1
- package/package.json +2 -2
- package/src/bot/assistant-core.ts +2 -2
- package/src/bot/capability-registry.ts +2 -2
- package/src/bot/context-compactor.ts +147 -0
- package/src/bot/memory-extraction-worker.ts +58 -0
- package/src/bot/memory-extractor.ts +213 -0
- package/src/bot/operations.ts +3 -1
- package/src/bot/swarm-controller.ts +43 -0
- package/src/bot/task-prompt-builder.ts +37 -21
- package/src/bot/task-types.ts +2 -0
- package/src/bot/tool-registry.ts +89 -0
- package/src/bot/types.ts +2 -0
- package/src/node-types/agent-execute.ts +25 -15
- package/src/node-types/build-context.ts +19 -3
- package/src/node-types/receive-task.ts +3 -0
- package/src/node-types/review-result.ts +22 -5
|
@@ -33,6 +33,9 @@ import type { BotProfile, BotInstance, OrchestratorInput, OrchestratorDecision,
|
|
|
33
33
|
import { buildDefaultBehavior, adjustBehaviorForComplexity } from './behavior-defaults.js';
|
|
34
34
|
import type { Task, RunProgress } from './task-types.js';
|
|
35
35
|
import type { WorkflowResult } from './types.js';
|
|
36
|
+
import { scheduleMemoryExtraction } from './memory-extraction-worker.js';
|
|
37
|
+
import { shouldCompact, compactRunHistory } from './context-compactor.js';
|
|
38
|
+
import { callAI } from './ai-client.js';
|
|
36
39
|
|
|
37
40
|
// ---------------------------------------------------------------------------
|
|
38
41
|
// Types
|
|
@@ -118,6 +121,9 @@ export class SwarmController {
|
|
|
118
121
|
/** Last emitted dispatch-filter-summary JSON (for dedup / throttling). */
|
|
119
122
|
private lastFilterSummaryJson: string | null = null;
|
|
120
123
|
|
|
124
|
+
/** Frozen system prompt prefix for cross-slot Anthropic cache sharing. */
|
|
125
|
+
private frozenPromptPrefix: string | null = null;
|
|
126
|
+
|
|
121
127
|
// -----------------------------------------------------------------------
|
|
122
128
|
// Singleton
|
|
123
129
|
// -----------------------------------------------------------------------
|
|
@@ -210,6 +216,16 @@ export class SwarmController {
|
|
|
210
216
|
this.state.startedAt = new Date().toISOString();
|
|
211
217
|
this._persist();
|
|
212
218
|
|
|
219
|
+
// Freeze the stable system prompt prefix for cross-slot cache sharing.
|
|
220
|
+
// All bot slots will use this identical prefix; only the per-task suffix varies.
|
|
221
|
+
try {
|
|
222
|
+
const { buildSystemPrompt } = await import('./system-prompt.js');
|
|
223
|
+
this.frozenPromptPrefix = await buildSystemPrompt();
|
|
224
|
+
} catch (err) {
|
|
225
|
+
if (process.env.WEAVER_VERBOSE) console.warn('[swarm] failed to freeze system prompt prefix:', err);
|
|
226
|
+
this.frozenPromptPrefix = null;
|
|
227
|
+
}
|
|
228
|
+
|
|
213
229
|
console.log(`\x1b[36m[swarm] started (pack-weaver v${PACK_VERSION})\x1b[0m`);
|
|
214
230
|
this.eventLog.emit({ type: 'swarm-started', timestamp: Date.now(), data: { packVersion: PACK_VERSION } });
|
|
215
231
|
|
|
@@ -793,6 +809,28 @@ export class SwarmController {
|
|
|
793
809
|
const task = await this.taskStore.get(taskId);
|
|
794
810
|
if (!task) throw new Error(`Task not found: ${taskId}`);
|
|
795
811
|
|
|
812
|
+
// LLM-based context compaction — produces a structured summary of all runs
|
|
813
|
+
// when the task has enough history. The summary replaces verbose per-run
|
|
814
|
+
// sections in the prompt, preserving semantic signal.
|
|
815
|
+
if (shouldCompact(task, profile.preferences?.costStrategy)) {
|
|
816
|
+
try {
|
|
817
|
+
const { resolveModelTier } = await import('./behavior-defaults.js');
|
|
818
|
+
const compactModel = resolveModelTier('fast', 'anthropic');
|
|
819
|
+
const compactPInfo = {
|
|
820
|
+
type: 'anthropic' as const,
|
|
821
|
+
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
822
|
+
model: compactModel,
|
|
823
|
+
};
|
|
824
|
+
const summary = await compactRunHistory(task, compactPInfo, callAI);
|
|
825
|
+
if (summary) {
|
|
826
|
+
task.context.compactedSummary = summary;
|
|
827
|
+
await this.taskStore.update(taskId, { context: task.context });
|
|
828
|
+
}
|
|
829
|
+
} catch {
|
|
830
|
+
// Compaction failure is non-fatal — prompt builder falls back to context decay
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
796
834
|
// Build prompt from task context
|
|
797
835
|
const parentTask = task.parentId ? await this.taskStore.get(task.parentId) : null;
|
|
798
836
|
const siblingTasks = task.parentId ? await this.taskStore.getSubtasks(task.parentId) : [];
|
|
@@ -925,6 +963,11 @@ export class SwarmController {
|
|
|
925
963
|
|
|
926
964
|
await this.taskStore.release(taskId, releaseStatus, runProgress);
|
|
927
965
|
|
|
966
|
+
// Fire-and-forget memory extraction — persists project facts for future runs
|
|
967
|
+
if (task) {
|
|
968
|
+
scheduleMemoryExtraction(this.projectDir, task, runProgress);
|
|
969
|
+
}
|
|
970
|
+
|
|
928
971
|
// Record token usage
|
|
929
972
|
this.recordTokenUsage(workerId, taskId, tokensUsed, costUsed);
|
|
930
973
|
|
|
@@ -65,10 +65,13 @@ function buildFull(
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
// --- Context decay: workspace is the source of truth, not history ---
|
|
68
|
-
//
|
|
69
|
-
//
|
|
68
|
+
// If a compacted summary exists (from LLM compaction after 3+ runs),
|
|
69
|
+
// use it instead of the per-run sections — it preserves semantic signal.
|
|
70
|
+
if (task.context.compactedSummary) {
|
|
71
|
+
sections.push(`### Execution History (Compacted)\n${task.context.compactedSummary}`);
|
|
72
|
+
}
|
|
70
73
|
|
|
71
|
-
// 2.3.2: Last acceptance check result
|
|
74
|
+
// 2.3.2: Last acceptance check result (always shown, even with compacted summary)
|
|
72
75
|
if (task.lastAcceptanceCheck) {
|
|
73
76
|
const ac = task.lastAcceptanceCheck;
|
|
74
77
|
const checkLines = ac.results
|
|
@@ -78,6 +81,7 @@ function buildFull(
|
|
|
78
81
|
}
|
|
79
82
|
|
|
80
83
|
// 2.3.3: Continue from last run's remaining work
|
|
84
|
+
// (always shown — most recent actionable data, even with compacted summary)
|
|
81
85
|
const lastRun = task.context.runHistory.length > 0
|
|
82
86
|
? task.context.runHistory[task.context.runHistory.length - 1]
|
|
83
87
|
: undefined;
|
|
@@ -90,18 +94,21 @@ function buildFull(
|
|
|
90
94
|
sections.push(`### Previous Run Blocked By\n${(lastRun.blockers as string[]).map((b: string) => `- ${b}`).join('\n')}`);
|
|
91
95
|
}
|
|
92
96
|
|
|
93
|
-
//
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
-
|
|
97
|
+
// Per-run sections — skipped when compacted summary exists (it covers this info)
|
|
98
|
+
if (!task.context.compactedSummary) {
|
|
99
|
+
// Last run summary (one run only, not full history)
|
|
100
|
+
if (lastRun && 'summary' in lastRun) {
|
|
101
|
+
sections.push(`### Last Run\nOutcome: ${lastRun.outcome} | ${lastRun.summary}`);
|
|
102
|
+
}
|
|
97
103
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
104
|
+
// Run count + stagnation
|
|
105
|
+
if (task.context.runHistory.length > 0) {
|
|
106
|
+
let meta = `Total runs: ${task.context.runHistory.length}`;
|
|
107
|
+
if (task.context.stagnationCount > 0) {
|
|
108
|
+
meta += ` | Stagnation: ${task.context.stagnationCount} run(s) with no new changes — try a different approach`;
|
|
109
|
+
}
|
|
110
|
+
sections.push(`### Run History\n${meta}`);
|
|
103
111
|
}
|
|
104
|
-
sections.push(`### Run History\n${meta}`);
|
|
105
112
|
}
|
|
106
113
|
|
|
107
114
|
// Directive: read the workspace, don't rely on stale context
|
|
@@ -226,6 +233,11 @@ function buildWithTruncation(
|
|
|
226
233
|
sections.push(`### Relevant Files\n${task.context.files.join('\n')}`);
|
|
227
234
|
}
|
|
228
235
|
|
|
236
|
+
// Compacted summary (same guard as buildFull)
|
|
237
|
+
if (task.context.compactedSummary) {
|
|
238
|
+
sections.push(`### Execution History (Compacted)\n${task.context.compactedSummary}`);
|
|
239
|
+
}
|
|
240
|
+
|
|
229
241
|
// Context decay: last acceptance check + last run only
|
|
230
242
|
if (task.lastAcceptanceCheck) {
|
|
231
243
|
const ac = task.lastAcceptanceCheck;
|
|
@@ -244,16 +256,20 @@ function buildWithTruncation(
|
|
|
244
256
|
if (lastRunT && 'blockers' in lastRunT && Array.isArray(lastRunT.blockers) && lastRunT.blockers.length > 0) {
|
|
245
257
|
sections.push(`### Previous Run Blocked By\n${(lastRunT.blockers as string[]).map((b: string) => `- ${b}`).join('\n')}`);
|
|
246
258
|
}
|
|
247
|
-
if (lastRunT && 'summary' in lastRunT) {
|
|
248
|
-
sections.push(`### Last Run\nOutcome: ${lastRunT.outcome} | ${lastRunT.summary}`);
|
|
249
|
-
}
|
|
250
259
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
if (
|
|
254
|
-
|
|
260
|
+
// Per-run sections — skipped when compacted summary exists
|
|
261
|
+
if (!task.context.compactedSummary) {
|
|
262
|
+
if (lastRunT && 'summary' in lastRunT) {
|
|
263
|
+
sections.push(`### Last Run\nOutcome: ${lastRunT.outcome} | ${lastRunT.summary}`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (task.context.runHistory.length > 0) {
|
|
267
|
+
let meta = `Total runs: ${task.context.runHistory.length}`;
|
|
268
|
+
if (task.context.stagnationCount > 0) {
|
|
269
|
+
meta += ` | Stagnation: ${task.context.stagnationCount} — try a different approach`;
|
|
270
|
+
}
|
|
271
|
+
sections.push(`### Run History\n${meta}`);
|
|
255
272
|
}
|
|
256
|
-
sections.push(`### Run History\n${meta}`);
|
|
257
273
|
}
|
|
258
274
|
|
|
259
275
|
// Directive: read the workspace, don't rely on stale context
|
package/src/bot/task-types.ts
CHANGED
|
@@ -71,6 +71,8 @@ export interface TaskContext {
|
|
|
71
71
|
stagnationCount: number;
|
|
72
72
|
budgetExhausted?: boolean;
|
|
73
73
|
projectBrief?: string;
|
|
74
|
+
/** LLM-generated summary of all runs, replacing verbose run history in prompts. */
|
|
75
|
+
compactedSummary?: string;
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
// ---------------------------------------------------------------------------
|
package/src/bot/tool-registry.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { ToolDefinition } from '@synergenius/flow-weaver/agent';
|
|
8
|
+
import { getCapability } from './capability-registry.js';
|
|
8
9
|
|
|
9
10
|
export interface WeaverTool extends ToolDefinition {
|
|
10
11
|
verboseOutput?: boolean;
|
|
@@ -575,6 +576,94 @@ export const BOT_TOOLS: ToolDefinition[] = ALL_TOOLS.filter(t => t.contexts.incl
|
|
|
575
576
|
export const ASSISTANT_TOOLS: ToolDefinition[] = ALL_TOOLS.filter(t => t.contexts.includes('assistant'));
|
|
576
577
|
export const VERBOSE_TOOL_NAMES = new Set(ALL_TOOLS.filter(t => t.verboseOutput).map(t => t.name));
|
|
577
578
|
|
|
579
|
+
// ── Mode-based tool filtering ───────────────────────────────────────
|
|
580
|
+
|
|
581
|
+
/** Core tools included in every mode regardless of profile. */
|
|
582
|
+
const CORE_TOOLS = new Set([
|
|
583
|
+
'read_file', 'list_files', 'run_shell', 'validate', 'learn', 'recall',
|
|
584
|
+
]);
|
|
585
|
+
|
|
586
|
+
/** Tools allowed per task mode. Keys match task.mode values. */
|
|
587
|
+
const MODE_TOOLS: Record<string, Set<string>> = {
|
|
588
|
+
create: new Set([
|
|
589
|
+
'read_file', 'list_files', 'write_file', 'patch_file',
|
|
590
|
+
'run_shell', 'validate', 'tsc_check', 'run_tests',
|
|
591
|
+
'learn', 'recall',
|
|
592
|
+
]),
|
|
593
|
+
modify: new Set([
|
|
594
|
+
'read_file', 'list_files', 'patch_file',
|
|
595
|
+
'run_shell', 'validate', 'tsc_check', 'run_tests',
|
|
596
|
+
'learn', 'recall',
|
|
597
|
+
]),
|
|
598
|
+
read: new Set([
|
|
599
|
+
'read_file', 'list_files', 'run_shell', 'validate',
|
|
600
|
+
'learn', 'recall',
|
|
601
|
+
]),
|
|
602
|
+
batch: new Set([
|
|
603
|
+
'read_file', 'list_files', 'write_file', 'patch_file',
|
|
604
|
+
'run_shell', 'validate', 'tsc_check', 'run_tests',
|
|
605
|
+
'learn', 'recall',
|
|
606
|
+
]),
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Resolve which tools a bot should have for a given task and profile.
|
|
611
|
+
*
|
|
612
|
+
* Uses the task mode to select a base tool pool, then intersects with
|
|
613
|
+
* profile-granted tools (from capabilities). Core tools are always included.
|
|
614
|
+
*
|
|
615
|
+
* @param task - Task with mode and optional capabilities
|
|
616
|
+
* @param capabilities - Profile capability names (e.g., ['role-developer', 'file-ops', 'shell'])
|
|
617
|
+
* @returns Set of tool names the bot should receive
|
|
618
|
+
*/
|
|
619
|
+
export function resolveToolsForTask(
|
|
620
|
+
task: { mode?: string },
|
|
621
|
+
capabilities?: string[],
|
|
622
|
+
): Set<string> {
|
|
623
|
+
// Start with the mode-based pool (default to 'create' = full set)
|
|
624
|
+
const modePool = MODE_TOOLS[task.mode ?? 'create'] ?? MODE_TOOLS.create;
|
|
625
|
+
|
|
626
|
+
// If capabilities are specified, compute the capability-granted tools
|
|
627
|
+
if (capabilities && capabilities.length > 0) {
|
|
628
|
+
const capTools = new Set<string>();
|
|
629
|
+
for (const capName of capabilities) {
|
|
630
|
+
const cap = getCapability(capName);
|
|
631
|
+
if (cap?.tools) {
|
|
632
|
+
for (const tool of cap.tools) capTools.add(tool);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Build the tool set in two steps:
|
|
637
|
+
// 1. Mode-restricted tools: must be in BOTH mode pool AND capability set (or core).
|
|
638
|
+
// This ensures modify mode excludes write_file even if the capability grants it.
|
|
639
|
+
// 2. Role-specific tools: tools granted by capabilities but not present in ANY
|
|
640
|
+
// mode pool (e.g., task_create, ask_user). These are additive — the capability
|
|
641
|
+
// is the sole authority for them.
|
|
642
|
+
const allModeTools = new Set<string>();
|
|
643
|
+
for (const pool of Object.values(MODE_TOOLS)) {
|
|
644
|
+
for (const t of pool) allModeTools.add(t);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const result = new Set<string>();
|
|
648
|
+
// Step 1: mode-restricted intersection
|
|
649
|
+
for (const tool of modePool) {
|
|
650
|
+
if (capTools.has(tool) || CORE_TOOLS.has(tool)) {
|
|
651
|
+
result.add(tool);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
// Step 2: role-specific tools (not in any mode pool)
|
|
655
|
+
for (const tool of capTools) {
|
|
656
|
+
if (!allModeTools.has(tool)) {
|
|
657
|
+
result.add(tool);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
return result;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// No capability restriction — use mode pool as-is
|
|
664
|
+
return new Set(modePool);
|
|
665
|
+
}
|
|
666
|
+
|
|
578
667
|
/**
|
|
579
668
|
* Generate a prompt section grouping assistant tools by category.
|
|
580
669
|
*/
|
package/src/bot/types.ts
CHANGED
|
@@ -642,6 +642,8 @@ export interface WeaverContext {
|
|
|
642
642
|
allValid?: boolean;
|
|
643
643
|
gitResultJson?: string;
|
|
644
644
|
reviewJson?: string;
|
|
645
|
+
/** Frozen system prompt prefix from swarm controller for cross-slot cache sharing. */
|
|
646
|
+
frozenPromptPrefix?: string;
|
|
645
647
|
}
|
|
646
648
|
|
|
647
649
|
export interface GenesisContext {
|
|
@@ -4,14 +4,17 @@ import {
|
|
|
4
4
|
createAnthropicProvider,
|
|
5
5
|
getOrCreateCliSession,
|
|
6
6
|
killAllCliSessions,
|
|
7
|
+
joinSplitPrompt,
|
|
7
8
|
type AgentProvider,
|
|
8
9
|
type AgentMessage,
|
|
9
10
|
type ToolDefinition,
|
|
10
11
|
type StreamEvent,
|
|
11
12
|
type StreamOptions,
|
|
12
13
|
type ToolEvent,
|
|
14
|
+
type SplitPrompt,
|
|
13
15
|
} from '@synergenius/flow-weaver/agent';
|
|
14
16
|
import { WEAVER_TOOLS, createWeaverExecutor } from '../bot/weaver-tools.js';
|
|
17
|
+
import { resolveToolsForTask } from '../bot/tool-registry.js';
|
|
15
18
|
import { auditEmit } from '../bot/audit-logger.js';
|
|
16
19
|
import { withRetry, getErrorGuidance } from '../bot/error-classifier.js';
|
|
17
20
|
import { CostTracker } from '../bot/cost-tracker.js';
|
|
@@ -64,15 +67,16 @@ class CliSessionProvider implements AgentProvider {
|
|
|
64
67
|
|
|
65
68
|
if (!prompt) return;
|
|
66
69
|
|
|
67
|
-
// Only pass system prompt on the first call
|
|
68
|
-
const
|
|
70
|
+
// Only pass system prompt on the first call — CLI sessions accept a string
|
|
71
|
+
const splitPrompt = this.sentCount <= messages.length ? options?.systemPrompt : undefined;
|
|
72
|
+
const systemPromptStr = splitPrompt ? joinSplitPrompt(splitPrompt) : undefined;
|
|
69
73
|
|
|
70
74
|
// Forward usage events to the runner's CostTracker via the global callback.
|
|
71
75
|
// This bridges CLI session usage → runner cost tracking → swarm budget enforcement.
|
|
72
76
|
const usageCb = (globalThis as Record<string, unknown>).__fw_ai_usage_callback__ as
|
|
73
77
|
((model: string, usage: { inputTokens: number; outputTokens: number }) => void) | undefined;
|
|
74
78
|
|
|
75
|
-
for await (const event of this.session.send(prompt,
|
|
79
|
+
for await (const event of this.session.send(prompt, systemPromptStr)) {
|
|
76
80
|
if (event.type === 'usage' && usageCb) {
|
|
77
81
|
usageCb(this.model, {
|
|
78
82
|
inputTokens: event.promptTokens,
|
|
@@ -150,21 +154,24 @@ export async function weaverAgentExecute(
|
|
|
150
154
|
return { onSuccess: false, onFailure: true, ctx: JSON.stringify(context) };
|
|
151
155
|
}
|
|
152
156
|
|
|
153
|
-
// Build system prompt
|
|
154
|
-
|
|
157
|
+
// Build system prompt as SplitPrompt — prefix is stable (cacheable),
|
|
158
|
+
// suffix is per-task (contextBundle, project plan).
|
|
159
|
+
// If frozenPromptPrefix is available from the swarm controller, use it
|
|
160
|
+
// to ensure all bot slots share the same cached prefix bytes.
|
|
161
|
+
let systemPrompt: SplitPrompt;
|
|
155
162
|
try {
|
|
156
163
|
const mod = await import('../bot/system-prompt.js');
|
|
157
|
-
const
|
|
164
|
+
const prefix = context.frozenPromptPrefix ?? await mod.buildSystemPrompt();
|
|
158
165
|
let cliCommands: { name: string; description: string; botCompatible?: boolean; options?: { flags: string; arg?: string; description: string }[] }[] = [];
|
|
159
166
|
try {
|
|
160
167
|
const docMeta = await import('@synergenius/flow-weaver/doc-metadata');
|
|
161
168
|
cliCommands = docMeta.CLI_COMMANDS ?? [];
|
|
162
169
|
} catch (err) { if (process.env.WEAVER_VERBOSE) console.error('[agent-execute] doc-metadata unavailable (older fw):', err); }
|
|
163
|
-
const
|
|
164
|
-
systemPrompt =
|
|
170
|
+
const suffix = mod.buildBotSystemPrompt(context.contextBundle, cliCommands, projectDir);
|
|
171
|
+
systemPrompt = { prefix, suffix };
|
|
165
172
|
} catch (err) {
|
|
166
173
|
if (process.env.WEAVER_VERBOSE) console.error('[agent-execute] system prompt build failed, using fallback:', err);
|
|
167
|
-
systemPrompt = 'You are Weaver, an AI workflow bot. Use the provided tools to complete tasks.';
|
|
174
|
+
systemPrompt = { prefix: 'You are Weaver, an AI workflow bot. Use the provided tools to complete tasks.', suffix: '' };
|
|
168
175
|
}
|
|
169
176
|
|
|
170
177
|
const taskPrompt = task.instruction.startsWith('## Task:')
|
|
@@ -219,14 +226,17 @@ export async function weaverAgentExecute(
|
|
|
219
226
|
|
|
220
227
|
const onStreamEvent = (event: StreamEvent) => renderer.onStreamEvent(event);
|
|
221
228
|
|
|
222
|
-
// Filter tools by
|
|
223
|
-
//
|
|
229
|
+
// Filter tools by task mode and profile capabilities.
|
|
230
|
+
// Mode-based filtering removes tools the task doesn't need (e.g., modify mode
|
|
231
|
+
// excludes write_file). Capability intersection ensures profiles only get their
|
|
232
|
+
// granted tools (e.g., orchestrator gets task_create, developer does not).
|
|
224
233
|
const behavior = context.behaviorJson ? JSON.parse(context.behaviorJson) : undefined;
|
|
225
234
|
const caps: string[] = behavior?.capabilities ?? [];
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
?
|
|
229
|
-
|
|
235
|
+
const grantedToolNames = resolveToolsForTask(
|
|
236
|
+
{ mode: task.mode },
|
|
237
|
+
caps.length > 0 ? caps : undefined,
|
|
238
|
+
);
|
|
239
|
+
const tools = WEAVER_TOOLS.filter(t => grantedToolNames.has(t.name));
|
|
230
240
|
|
|
231
241
|
const result = await withRetry(
|
|
232
242
|
() => runAgentLoop(
|
|
@@ -102,13 +102,29 @@ export function weaverBuildContext(ctx: string): { ctx: string } {
|
|
|
102
102
|
}
|
|
103
103
|
} catch { /* non-fatal — memory is best-effort */ }
|
|
104
104
|
|
|
105
|
-
// Auto-recall learned knowledge from previous bot runs
|
|
105
|
+
// Auto-recall learned knowledge from previous bot runs (with aging caveats)
|
|
106
106
|
try {
|
|
107
107
|
const knowledge = new KnowledgeStore(projectDir);
|
|
108
108
|
const entries = knowledge.list();
|
|
109
109
|
if (entries.length > 0) {
|
|
110
|
-
const
|
|
111
|
-
|
|
110
|
+
const now = Date.now();
|
|
111
|
+
const NINETY_DAYS_MS = 90 * 24 * 60 * 60 * 1000;
|
|
112
|
+
|
|
113
|
+
// Auto-prune entries older than 90 days
|
|
114
|
+
const staleKeys = entries.filter(e => now - e.createdAt > NINETY_DAYS_MS).map(e => e.key);
|
|
115
|
+
for (const key of staleKeys) knowledge.forget(key);
|
|
116
|
+
|
|
117
|
+
const fresh = entries.filter(e => now - e.createdAt <= NINETY_DAYS_MS);
|
|
118
|
+
if (fresh.length > 0) {
|
|
119
|
+
const knowledgeLines = fresh.map((e: { key: string; value: string; createdAt: number }) => {
|
|
120
|
+
const ageDays = Math.floor((now - e.createdAt) / (24 * 60 * 60 * 1000));
|
|
121
|
+
const caveat = ageDays >= 1
|
|
122
|
+
? ` _(${ageDays}d ago — may be outdated, verify before asserting)_`
|
|
123
|
+
: '';
|
|
124
|
+
return `- **${e.key}**: ${e.value}${caveat}`;
|
|
125
|
+
});
|
|
126
|
+
sections.push(`## Learned Knowledge\n\nFacts discovered by previous runs — use these instead of re-discovering:\n${knowledgeLines.join('\n')}`);
|
|
127
|
+
}
|
|
112
128
|
}
|
|
113
129
|
} catch { /* non-fatal — knowledge recall is best-effort */ }
|
|
114
130
|
|
|
@@ -10,6 +10,7 @@ import type { WeaverEnv, WeaverContext } from '../bot/types.js';
|
|
|
10
10
|
* @color purple
|
|
11
11
|
* @input env [order:0] - Weaver environment bundle
|
|
12
12
|
* @input [taskJson] [order:1] - Pre-supplied task (JSON, optional)
|
|
13
|
+
* @input [frozenPromptPrefix] [order:2] [hidden] - Frozen system prompt prefix for cache sharing
|
|
13
14
|
* @output ctx [order:0] - Weaver context (JSON)
|
|
14
15
|
* @output onSuccess [order:-2] - On Success
|
|
15
16
|
* @output onFailure [order:-1] [hidden] - On Failure
|
|
@@ -18,11 +19,13 @@ export async function weaverReceiveTask(
|
|
|
18
19
|
execute: boolean,
|
|
19
20
|
env: WeaverEnv,
|
|
20
21
|
taskJson?: string,
|
|
22
|
+
frozenPromptPrefix?: string,
|
|
21
23
|
): Promise<{
|
|
22
24
|
onSuccess: boolean; onFailure: boolean;
|
|
23
25
|
ctx: string;
|
|
24
26
|
}> {
|
|
25
27
|
const context: WeaverContext = { env, taskJson: '{}', hasTask: false };
|
|
28
|
+
if (frozenPromptPrefix) context.frozenPromptPrefix = frozenPromptPrefix;
|
|
26
29
|
|
|
27
30
|
if (!execute) {
|
|
28
31
|
return { onSuccess: true, onFailure: false, ctx: JSON.stringify(context) };
|
|
@@ -11,6 +11,18 @@ import {
|
|
|
11
11
|
} from '@synergenius/flow-weaver/agent';
|
|
12
12
|
import { createWeaverExecutor } from '../bot/weaver-tools.js';
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Strip `<analysis>...</analysis>` scratchpad blocks from LLM response text.
|
|
16
|
+
* The analysis is a reasoning scaffold that improves verdict quality but
|
|
17
|
+
* should not leak into the parsed JSON output.
|
|
18
|
+
*/
|
|
19
|
+
export function stripAnalysis(text: string): { cleaned: string; analysis: string | undefined } {
|
|
20
|
+
const match = text.match(/<analysis>([\s\S]*?)<\/analysis>/);
|
|
21
|
+
const analysis = match?.[1]?.trim() || undefined;
|
|
22
|
+
const cleaned = text.replace(/<analysis>[\s\S]*?<\/analysis>/g, '').trim();
|
|
23
|
+
return { cleaned, analysis };
|
|
24
|
+
}
|
|
25
|
+
|
|
14
26
|
/**
|
|
15
27
|
* LLM-powered task completion reviewer.
|
|
16
28
|
* Makes a single judgment call: did the bot accomplish the assigned task?
|
|
@@ -95,7 +107,9 @@ Rate each criterion as PASS or FAIL:
|
|
|
95
107
|
|
|
96
108
|
If you need to verify file contents to judge the RESULT criterion, use the read_file tool. Only read files if the evidence is ambiguous.
|
|
97
109
|
|
|
98
|
-
|
|
110
|
+
First, write your reasoning inside <analysis> tags. Work through each criterion step by step, examining the evidence.
|
|
111
|
+
|
|
112
|
+
After your <analysis> block, output only the following JSON — no other text outside the tags:
|
|
99
113
|
{"pass": true/false, "intent": "PASS/FAIL", "execution": "PASS/FAIL", "result": "PASS/FAIL", "completeness": "PASS/FAIL", "reason": "one sentence summary"}`;
|
|
100
114
|
|
|
101
115
|
try {
|
|
@@ -129,11 +143,14 @@ Respond with exactly:
|
|
|
129
143
|
{ maxIterations: 2 },
|
|
130
144
|
);
|
|
131
145
|
|
|
146
|
+
// Strip <analysis> scratchpad before parsing JSON verdict
|
|
147
|
+
const { cleaned: cleanedSummary } = stripAnalysis(result.summary);
|
|
148
|
+
|
|
132
149
|
// Parse the structured response
|
|
133
150
|
let pass = true;
|
|
134
151
|
let reason = 'Review completed';
|
|
135
152
|
let criteria: Record<string, string> = {};
|
|
136
|
-
const jsonMatch =
|
|
153
|
+
const jsonMatch = cleanedSummary.match(/\{[\s\S]*"pass"[\s\S]*\}/);
|
|
137
154
|
if (jsonMatch) {
|
|
138
155
|
try {
|
|
139
156
|
const parsed = JSON.parse(jsonMatch[0]);
|
|
@@ -146,14 +163,14 @@ Respond with exactly:
|
|
|
146
163
|
} catch {
|
|
147
164
|
if (jsonMatch[0].includes('"pass": false') || jsonMatch[0].includes('"pass":false')) {
|
|
148
165
|
pass = false;
|
|
149
|
-
reason =
|
|
166
|
+
reason = cleanedSummary.slice(0, 200);
|
|
150
167
|
}
|
|
151
168
|
}
|
|
152
169
|
} else {
|
|
153
|
-
const lower =
|
|
170
|
+
const lower = cleanedSummary.toLowerCase();
|
|
154
171
|
if (lower.includes('"pass": false') || lower.includes('"pass":false')) {
|
|
155
172
|
pass = false;
|
|
156
|
-
reason =
|
|
173
|
+
reason = cleanedSummary.slice(0, 200);
|
|
157
174
|
}
|
|
158
175
|
}
|
|
159
176
|
|