@synergenius/flow-weaver-pack-weaver 0.9.193 → 0.9.196
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/ai-client.d.ts +5 -0
- package/dist/bot/ai-client.d.ts.map +1 -1
- package/dist/bot/ai-client.js +43 -0
- package/dist/bot/ai-client.js.map +1 -1
- package/dist/bot/assistant-core.js +2 -2
- package/dist/bot/assistant-core.js.map +1 -1
- package/dist/bot/behavior-defaults.d.ts +3 -1
- package/dist/bot/behavior-defaults.d.ts.map +1 -1
- package/dist/bot/behavior-defaults.js +7 -0
- package/dist/bot/behavior-defaults.js.map +1 -1
- package/dist/bot/capability-registry.js +3 -3
- 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/dream-task.d.ts +45 -0
- package/dist/bot/dream-task.d.ts.map +1 -0
- package/dist/bot/dream-task.js +125 -0
- package/dist/bot/dream-task.js.map +1 -0
- package/dist/bot/knowledge-store.d.ts +9 -0
- package/dist/bot/knowledge-store.d.ts.map +1 -1
- package/dist/bot/knowledge-store.js +21 -0
- package/dist/bot/knowledge-store.js.map +1 -1
- 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/post-turn-hooks.d.ts +57 -0
- package/dist/bot/post-turn-hooks.d.ts.map +1 -0
- package/dist/bot/post-turn-hooks.js +108 -0
- package/dist/bot/post-turn-hooks.js.map +1 -0
- package/dist/bot/profile-types.d.ts +16 -0
- package/dist/bot/profile-types.d.ts.map +1 -1
- package/dist/bot/swarm-controller.d.ts +7 -0
- package/dist/bot/swarm-controller.d.ts.map +1 -1
- package/dist/bot/swarm-controller.js +121 -1
- 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 +13 -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 +38 -17
- package/dist/node-types/agent-execute.js.map +1 -1
- package/dist/node-types/build-context.d.ts +4 -3
- package/dist/node-types/build-context.d.ts.map +1 -1
- package/dist/node-types/build-context.js +37 -6
- 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/node-types/verify-task.d.ts +22 -0
- package/dist/node-types/verify-task.d.ts.map +1 -0
- package/dist/node-types/verify-task.js +143 -0
- package/dist/node-types/verify-task.js.map +1 -0
- package/dist/ui/capability-editor.js +3 -3
- package/dist/ui/profile-editor.js +3 -3
- package/dist/ui/swarm-dashboard.js +3 -3
- package/dist/workflows/weaver-agent.d.ts +3 -3
- package/dist/workflows/weaver-agent.d.ts.map +1 -1
- package/dist/workflows/weaver-agent.js +267 -18
- package/dist/workflows/weaver-agent.js.map +1 -1
- package/dist/workflows/weaver-bot-batch.d.ts +3 -3
- package/dist/workflows/weaver-bot-batch.d.ts.map +1 -1
- package/dist/workflows/weaver-bot-batch.js +280 -24
- package/dist/workflows/weaver-bot-batch.js.map +1 -1
- package/dist/workflows/weaver-bot.d.ts +2 -0
- package/dist/workflows/weaver-bot.d.ts.map +1 -1
- package/dist/workflows/weaver-bot.js +15 -10
- package/dist/workflows/weaver-bot.js.map +1 -1
- package/flowweaver.manifest.json +1 -1
- package/package.json +3 -3
- package/src/bot/ai-client.ts +54 -0
- package/src/bot/assistant-core.ts +2 -2
- package/src/bot/behavior-defaults.ts +9 -1
- package/src/bot/capability-registry.ts +3 -3
- package/src/bot/context-compactor.ts +147 -0
- package/src/bot/dream-task.ts +167 -0
- package/src/bot/knowledge-store.ts +27 -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/post-turn-hooks.ts +137 -0
- package/src/bot/profile-types.ts +17 -0
- package/src/bot/swarm-controller.ts +129 -2
- package/src/bot/task-prompt-builder.ts +37 -21
- package/src/bot/task-types.ts +21 -0
- package/src/bot/tool-registry.ts +89 -0
- package/src/bot/types.ts +2 -0
- package/src/node-types/agent-execute.ts +44 -17
- package/src/node-types/build-context.ts +45 -7
- package/src/node-types/receive-task.ts +3 -0
- package/src/node-types/review-result.ts +22 -5
- package/src/node-types/verify-task.ts +181 -0
- package/src/workflows/weaver-agent.ts +429 -18
- package/src/workflows/weaver-bot-batch.ts +443 -24
- package/src/workflows/weaver-bot.ts +16 -11
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-turn hook system for the agent loop.
|
|
3
|
+
*
|
|
4
|
+
* Hooks run after each iteration of the agent loop, enabling cost checks,
|
|
5
|
+
* steering, progress reporting, and knowledge extraction between LLM turns.
|
|
6
|
+
*
|
|
7
|
+
* Hooks run sequentially (not parallel) so abort hooks fire before
|
|
8
|
+
* subsequent hooks. Errors are caught per-hook — a failing hook does
|
|
9
|
+
* not block other hooks or the agent loop.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { TurnEndContext, TurnEndResult } from '@synergenius/flow-weaver/agent';
|
|
13
|
+
import { CostTracker } from './cost-tracker.js';
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Hook interface
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
export type HookTiming = 'every' | 'final' | 'between';
|
|
20
|
+
|
|
21
|
+
export interface PostTurnHook {
|
|
22
|
+
name: string;
|
|
23
|
+
/** When to run: 'every' = every turn, 'final' = only final turn, 'between' = only between turns */
|
|
24
|
+
timing: HookTiming;
|
|
25
|
+
execute(context: TurnEndContext): Promise<PostTurnHookResult>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface PostTurnHookResult {
|
|
29
|
+
/** If false, abort the agent loop. Default: true. */
|
|
30
|
+
continue?: boolean;
|
|
31
|
+
/** Optional message to inject into conversation (steering nudge). */
|
|
32
|
+
injectMessage?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Hook runner
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
export class PostTurnHookRunner {
|
|
40
|
+
private hooks: PostTurnHook[] = [];
|
|
41
|
+
|
|
42
|
+
register(hook: PostTurnHook): void {
|
|
43
|
+
this.hooks.push(hook);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Returns the onTurnEnd callback to pass to runAgentLoop options. */
|
|
47
|
+
createCallback(): (ctx: TurnEndContext) => Promise<TurnEndResult | void> {
|
|
48
|
+
return async (ctx: TurnEndContext): Promise<TurnEndResult | void> => {
|
|
49
|
+
let injectMessage: string | undefined;
|
|
50
|
+
|
|
51
|
+
for (const hook of this.hooks) {
|
|
52
|
+
// Check timing
|
|
53
|
+
if (hook.timing === 'final' && !ctx.isFinalTurn) continue;
|
|
54
|
+
if (hook.timing === 'between' && ctx.isFinalTurn) continue;
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const result = await hook.execute(ctx);
|
|
58
|
+
if (result.continue === false) {
|
|
59
|
+
return { continue: false, injectMessage: result.injectMessage };
|
|
60
|
+
}
|
|
61
|
+
if (result.injectMessage) {
|
|
62
|
+
injectMessage = injectMessage
|
|
63
|
+
? injectMessage + '\n' + result.injectMessage
|
|
64
|
+
: result.injectMessage;
|
|
65
|
+
}
|
|
66
|
+
} catch (err) {
|
|
67
|
+
// Hook failure is non-fatal — log and continue
|
|
68
|
+
if (process.env.WEAVER_VERBOSE) {
|
|
69
|
+
console.error(`[post-turn-hook] ${hook.name} failed:`, err);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (injectMessage) return { injectMessage };
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Built-in hooks
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Cost checkpoint — aborts the loop when cumulative cost exceeds budget.
|
|
85
|
+
* Subsumes the standalone #6 per-turn budget enforcement approach.
|
|
86
|
+
*/
|
|
87
|
+
export class CostCheckpointHook implements PostTurnHook {
|
|
88
|
+
name = 'cost-checkpoint';
|
|
89
|
+
timing: HookTiming = 'between';
|
|
90
|
+
|
|
91
|
+
constructor(
|
|
92
|
+
private maxCost: number,
|
|
93
|
+
private model: string,
|
|
94
|
+
) {}
|
|
95
|
+
|
|
96
|
+
async execute(ctx: TurnEndContext): Promise<PostTurnHookResult> {
|
|
97
|
+
const cost = CostTracker.estimateCost(this.model, {
|
|
98
|
+
inputTokens: ctx.usage.promptTokens,
|
|
99
|
+
outputTokens: ctx.usage.completionTokens,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (cost >= this.maxCost) {
|
|
103
|
+
return {
|
|
104
|
+
continue: false,
|
|
105
|
+
injectMessage: `Budget exceeded: $${cost.toFixed(4)} >= $${this.maxCost.toFixed(4)}`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return {};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Progress report — emits a stream event with turn progress for UI updates.
|
|
114
|
+
*/
|
|
115
|
+
export class ProgressReportHook implements PostTurnHook {
|
|
116
|
+
name = 'progress-report';
|
|
117
|
+
timing: HookTiming = 'every';
|
|
118
|
+
|
|
119
|
+
constructor(
|
|
120
|
+
private emitEvent: (event: { type: string; timestamp: number; data: Record<string, unknown> }) => void,
|
|
121
|
+
) {}
|
|
122
|
+
|
|
123
|
+
async execute(ctx: TurnEndContext): Promise<PostTurnHookResult> {
|
|
124
|
+
this.emitEvent({
|
|
125
|
+
type: 'turn-progress',
|
|
126
|
+
timestamp: Date.now(),
|
|
127
|
+
data: {
|
|
128
|
+
iteration: ctx.iteration,
|
|
129
|
+
maxIterations: ctx.maxIterations,
|
|
130
|
+
toolCallCount: ctx.toolCallCount,
|
|
131
|
+
isFinalTurn: ctx.isFinalTurn,
|
|
132
|
+
usage: ctx.usage,
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
return {};
|
|
136
|
+
}
|
|
137
|
+
}
|
package/src/bot/profile-types.ts
CHANGED
|
@@ -92,6 +92,21 @@ export interface PhaseDescriptor {
|
|
|
92
92
|
* not hardcoded. Each workflow can have different phases (e.g. a review
|
|
93
93
|
* bot might have analyze/report/suggest instead of plan/execute/review).
|
|
94
94
|
*/
|
|
95
|
+
/** Post-run verification config — independent review of completed work. */
|
|
96
|
+
export interface VerificationConfig {
|
|
97
|
+
/** Whether verification is enabled. Default: false. */
|
|
98
|
+
enabled: boolean;
|
|
99
|
+
/** Model tier for the verification agent. Default: 'standard'. */
|
|
100
|
+
tier: ModelTier;
|
|
101
|
+
/**
|
|
102
|
+
* How often to run verification (1 = every completed task, 2 = every other, etc.).
|
|
103
|
+
* Default: 1 (always verify).
|
|
104
|
+
*/
|
|
105
|
+
frequency: number;
|
|
106
|
+
/** Re-open the task if verification fails. Default: true. */
|
|
107
|
+
reopenOnFail: boolean;
|
|
108
|
+
}
|
|
109
|
+
|
|
95
110
|
export interface ProfileBehavior {
|
|
96
111
|
/** Capability names this profile can use (e.g. ['core', 'file-ops', 'shell']). */
|
|
97
112
|
capabilities?: string[];
|
|
@@ -107,6 +122,8 @@ export interface ProfileBehavior {
|
|
|
107
122
|
exitProtocol: ExitProtocol;
|
|
108
123
|
/** Structured output requirements. */
|
|
109
124
|
outputContract?: OutputContract;
|
|
125
|
+
/** Post-run verification by an independent agent. */
|
|
126
|
+
verification?: VerificationConfig;
|
|
110
127
|
}
|
|
111
128
|
|
|
112
129
|
/** Structured exit status from a bot run. */
|
|
@@ -32,7 +32,11 @@ import { ProfileStore } from './profile-store.js';
|
|
|
32
32
|
import type { BotProfile, BotInstance, OrchestratorInput, OrchestratorDecision, ProfileBehavior } from './profile-types.js';
|
|
33
33
|
import { buildDefaultBehavior, adjustBehaviorForComplexity } from './behavior-defaults.js';
|
|
34
34
|
import type { Task, RunProgress } from './task-types.js';
|
|
35
|
-
import type { WorkflowResult } from './types.js';
|
|
35
|
+
import type { WorkflowResult, ProviderInfo } from './types.js';
|
|
36
|
+
import { scheduleMemoryExtraction } from './memory-extraction-worker.js';
|
|
37
|
+
import { shouldCompact, compactRunHistory } from './context-compactor.js';
|
|
38
|
+
import { DreamTask } from './dream-task.js';
|
|
39
|
+
import { callAI } from './ai-client.js';
|
|
36
40
|
|
|
37
41
|
// ---------------------------------------------------------------------------
|
|
38
42
|
// Types
|
|
@@ -118,6 +122,15 @@ export class SwarmController {
|
|
|
118
122
|
/** Last emitted dispatch-filter-summary JSON (for dedup / throttling). */
|
|
119
123
|
private lastFilterSummaryJson: string | null = null;
|
|
120
124
|
|
|
125
|
+
/** Frozen system prompt prefix for cross-slot Anthropic cache sharing. */
|
|
126
|
+
private frozenPromptPrefix: string | null = null;
|
|
127
|
+
|
|
128
|
+
/** Background knowledge consolidation during idle periods. */
|
|
129
|
+
private dreamTask: DreamTask;
|
|
130
|
+
|
|
131
|
+
/** Counter for verification frequency gating (incremented per completed task). */
|
|
132
|
+
private verificationCounter = 0;
|
|
133
|
+
|
|
121
134
|
// -----------------------------------------------------------------------
|
|
122
135
|
// Singleton
|
|
123
136
|
// -----------------------------------------------------------------------
|
|
@@ -148,6 +161,7 @@ export class SwarmController {
|
|
|
148
161
|
this.orchestrator = new Orchestrator({ aiRouter: new AIRouterImpl(projectDir) });
|
|
149
162
|
this.instanceManager = new InstanceManager();
|
|
150
163
|
this.profileStore = new ProfileStore(projectDir);
|
|
164
|
+
this.dreamTask = new DreamTask({ projectDir });
|
|
151
165
|
|
|
152
166
|
// Load persisted state or create default
|
|
153
167
|
this.state = this._loadState();
|
|
@@ -210,6 +224,16 @@ export class SwarmController {
|
|
|
210
224
|
this.state.startedAt = new Date().toISOString();
|
|
211
225
|
this._persist();
|
|
212
226
|
|
|
227
|
+
// Freeze the stable system prompt prefix for cross-slot cache sharing.
|
|
228
|
+
// All bot slots will use this identical prefix; only the per-task suffix varies.
|
|
229
|
+
try {
|
|
230
|
+
const { buildSystemPrompt } = await import('./system-prompt.js');
|
|
231
|
+
this.frozenPromptPrefix = await buildSystemPrompt();
|
|
232
|
+
} catch (err) {
|
|
233
|
+
if (process.env.WEAVER_VERBOSE) console.warn('[swarm] failed to freeze system prompt prefix:', err);
|
|
234
|
+
this.frozenPromptPrefix = null;
|
|
235
|
+
}
|
|
236
|
+
|
|
213
237
|
console.log(`\x1b[36m[swarm] started (pack-weaver v${PACK_VERSION})\x1b[0m`);
|
|
214
238
|
this.eventLog.emit({ type: 'swarm-started', timestamp: Date.now(), data: { packVersion: PACK_VERSION } });
|
|
215
239
|
|
|
@@ -593,6 +617,7 @@ export class SwarmController {
|
|
|
593
617
|
return !routableTasks.includes(t);
|
|
594
618
|
}).length;
|
|
595
619
|
_dl(`[dispatch] 0 routable from ${pendingTasks.length} open. parent=${pendingTasks.filter(t => t.isParent).length} budget=${pendingTasks.filter(t => t.context.budgetExhausted).length} deps-blocked=${depsBlocked}`);
|
|
620
|
+
await this._maybeDream();
|
|
596
621
|
await this._sleep(DISPATCH_LOOP_SLEEP_MS, signal);
|
|
597
622
|
continue;
|
|
598
623
|
}
|
|
@@ -775,6 +800,26 @@ export class SwarmController {
|
|
|
775
800
|
}
|
|
776
801
|
}
|
|
777
802
|
|
|
803
|
+
// -----------------------------------------------------------------------
|
|
804
|
+
// Idle-time knowledge consolidation
|
|
805
|
+
// -----------------------------------------------------------------------
|
|
806
|
+
|
|
807
|
+
private async _maybeDream(): Promise<void> {
|
|
808
|
+
if (!this.dreamTask.shouldRun()) return;
|
|
809
|
+
try {
|
|
810
|
+
const result = await this.dreamTask.consolidate();
|
|
811
|
+
if (result.staleEntriesCleaned + result.insightsConverted > 0) {
|
|
812
|
+
this.eventLog.emit({
|
|
813
|
+
type: 'dream-consolidation',
|
|
814
|
+
timestamp: Date.now(),
|
|
815
|
+
data: result as unknown as Record<string, unknown>,
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
} catch (err) {
|
|
819
|
+
if (process.env.WEAVER_VERBOSE) console.warn('[swarm] dream-task failed:', err);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
778
823
|
// -----------------------------------------------------------------------
|
|
779
824
|
// Task execution
|
|
780
825
|
// -----------------------------------------------------------------------
|
|
@@ -793,6 +838,31 @@ export class SwarmController {
|
|
|
793
838
|
const task = await this.taskStore.get(taskId);
|
|
794
839
|
if (!task) throw new Error(`Task not found: ${taskId}`);
|
|
795
840
|
|
|
841
|
+
// LLM-based context compaction — produces a structured summary of all runs
|
|
842
|
+
// when the task has enough history. The summary replaces verbose per-run
|
|
843
|
+
// sections in the prompt, preserving semantic signal.
|
|
844
|
+
if (shouldCompact(task, profile.preferences?.costStrategy)) {
|
|
845
|
+
try {
|
|
846
|
+
const { resolveModelTier } = await import('./behavior-defaults.js');
|
|
847
|
+
const { resolveProviderConfig } = await import('./agent-provider.js');
|
|
848
|
+
const detected = resolveProviderConfig('auto');
|
|
849
|
+
const providerType = detected.name;
|
|
850
|
+
const compactModel = detected.model ?? resolveModelTier('fast', providerType);
|
|
851
|
+
const compactPInfo = {
|
|
852
|
+
type: providerType as ProviderInfo['type'],
|
|
853
|
+
apiKey: process.env.ANTHROPIC_API_KEY ?? process.env.OPENAI_API_KEY,
|
|
854
|
+
model: compactModel,
|
|
855
|
+
};
|
|
856
|
+
const summary = await compactRunHistory(task, compactPInfo, callAI);
|
|
857
|
+
if (summary) {
|
|
858
|
+
task.context.compactedSummary = summary;
|
|
859
|
+
await this.taskStore.update(taskId, { context: task.context });
|
|
860
|
+
}
|
|
861
|
+
} catch {
|
|
862
|
+
// Compaction failure is non-fatal — prompt builder falls back to context decay
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
796
866
|
// Build prompt from task context
|
|
797
867
|
const parentTask = task.parentId ? await this.taskStore.get(task.parentId) : null;
|
|
798
868
|
const siblingTasks = task.parentId ? await this.taskStore.getSubtasks(task.parentId) : [];
|
|
@@ -834,7 +904,12 @@ export class SwarmController {
|
|
|
834
904
|
taskId,
|
|
835
905
|
botId: workerId,
|
|
836
906
|
config: { provider: 'auto' },
|
|
837
|
-
params: {
|
|
907
|
+
params: {
|
|
908
|
+
taskJson,
|
|
909
|
+
projectDir: this.projectDir,
|
|
910
|
+
behaviorJson,
|
|
911
|
+
...(this.frozenPromptPrefix ? { frozenPromptPrefix: this.frozenPromptPrefix } : {}),
|
|
912
|
+
},
|
|
838
913
|
eventLog: runEventLog,
|
|
839
914
|
});
|
|
840
915
|
|
|
@@ -911,6 +986,53 @@ export class SwarmController {
|
|
|
911
986
|
}
|
|
912
987
|
}
|
|
913
988
|
|
|
989
|
+
// Independent verification — runs after acceptance passes, before release.
|
|
990
|
+
// Uses a fresh LLM session with a potentially different model tier.
|
|
991
|
+
if (releaseStatus === 'done' && task) {
|
|
992
|
+
const behavior: import('./profile-types.js').ProfileBehavior | undefined =
|
|
993
|
+
profile.preferences?.behavior;
|
|
994
|
+
const vConfig = behavior?.verification;
|
|
995
|
+
if (vConfig?.enabled) {
|
|
996
|
+
this.verificationCounter++;
|
|
997
|
+
const shouldVerify = this.verificationCounter % vConfig.frequency === 0;
|
|
998
|
+
if (shouldVerify) {
|
|
999
|
+
try {
|
|
1000
|
+
const { runVerification } = await import('../node-types/verify-task.js');
|
|
1001
|
+
const { resolveProviderConfig } = await import('./agent-provider.js');
|
|
1002
|
+
const { resolveModelTier } = await import('./behavior-defaults.js');
|
|
1003
|
+
const detected = resolveProviderConfig('auto');
|
|
1004
|
+
const verifyModel = resolveModelTier(vConfig.tier, detected.name);
|
|
1005
|
+
const verifyResult = await runVerification(
|
|
1006
|
+
{
|
|
1007
|
+
taskTitle: task.title,
|
|
1008
|
+
taskDescription: task.description,
|
|
1009
|
+
filesCreated: runProgress.filesCreated,
|
|
1010
|
+
filesModified: runProgress.filesModified,
|
|
1011
|
+
summary: runProgress.summary,
|
|
1012
|
+
checks: runProgress.checks,
|
|
1013
|
+
},
|
|
1014
|
+
detected.name,
|
|
1015
|
+
verifyModel,
|
|
1016
|
+
process.env.ANTHROPIC_API_KEY ?? process.env.OPENAI_API_KEY,
|
|
1017
|
+
);
|
|
1018
|
+
await this.taskStore.update(taskId, { lastVerification: verifyResult } as Record<string, unknown>);
|
|
1019
|
+
this.eventLog.emit({
|
|
1020
|
+
type: 'verification',
|
|
1021
|
+
timestamp: Date.now(),
|
|
1022
|
+
data: { taskId, runId, verdict: verifyResult.verdict, summary: verifyResult.summary, cost: verifyResult.cost } as unknown as Record<string, unknown>,
|
|
1023
|
+
});
|
|
1024
|
+
if (verifyResult.verdict === 'fail' && vConfig.reopenOnFail) {
|
|
1025
|
+
releaseStatus = 'open';
|
|
1026
|
+
console.log(`\x1b[33m[swarm] verification FAILED for task ${taskId}: ${verifyResult.summary}\x1b[0m`);
|
|
1027
|
+
}
|
|
1028
|
+
} catch (verifyErr) {
|
|
1029
|
+
if (process.env.WEAVER_VERBOSE) console.warn('[swarm] verification failed:', verifyErr);
|
|
1030
|
+
// Verification failure is non-fatal — release as originally planned
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
914
1036
|
// Health checks — detect suspicious runs before releasing
|
|
915
1037
|
if (tokensUsed === 0 && runProgress.outcome === 'completed') {
|
|
916
1038
|
console.warn(`[swarm] HEALTH: zero-token completion task=${taskId} worker=${workerId} duration=${durationMs}ms`);
|
|
@@ -925,6 +1047,11 @@ export class SwarmController {
|
|
|
925
1047
|
|
|
926
1048
|
await this.taskStore.release(taskId, releaseStatus, runProgress);
|
|
927
1049
|
|
|
1050
|
+
// Fire-and-forget memory extraction — persists project facts for future runs
|
|
1051
|
+
if (task) {
|
|
1052
|
+
scheduleMemoryExtraction(this.projectDir, task, runProgress);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
928
1055
|
// Record token usage
|
|
929
1056
|
this.recordTokenUsage(workerId, taskId, tokensUsed, costUsed);
|
|
930
1057
|
|
|
@@ -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
|
@@ -60,6 +60,22 @@ export interface AcceptanceResult {
|
|
|
60
60
|
checkedAt: string;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Verification result — independent post-run review
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
export type VerificationVerdict = 'pass' | 'fail' | 'inconclusive';
|
|
68
|
+
|
|
69
|
+
export interface VerificationResult {
|
|
70
|
+
verdict: VerificationVerdict;
|
|
71
|
+
summary: string;
|
|
72
|
+
issues: string[];
|
|
73
|
+
filesReviewed: string[];
|
|
74
|
+
verifiedAt: string;
|
|
75
|
+
/** Cost of the verification run in USD. */
|
|
76
|
+
cost: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
63
79
|
// ---------------------------------------------------------------------------
|
|
64
80
|
// Task context
|
|
65
81
|
// ---------------------------------------------------------------------------
|
|
@@ -71,6 +87,8 @@ export interface TaskContext {
|
|
|
71
87
|
stagnationCount: number;
|
|
72
88
|
budgetExhausted?: boolean;
|
|
73
89
|
projectBrief?: string;
|
|
90
|
+
/** LLM-generated summary of all runs, replacing verbose run history in prompts. */
|
|
91
|
+
compactedSummary?: string;
|
|
74
92
|
}
|
|
75
93
|
|
|
76
94
|
// ---------------------------------------------------------------------------
|
|
@@ -103,6 +121,9 @@ export interface Task {
|
|
|
103
121
|
acceptance?: AcceptanceCriteria;
|
|
104
122
|
lastAcceptanceCheck?: AcceptanceResult;
|
|
105
123
|
|
|
124
|
+
// Verification
|
|
125
|
+
lastVerification?: VerificationResult;
|
|
126
|
+
|
|
106
127
|
// Context
|
|
107
128
|
context: TaskContext;
|
|
108
129
|
|
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 {
|