opencode-swarm-plugin 0.38.0 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env +2 -0
- package/.hive/eval-results.json +26 -0
- package/.hive/issues.jsonl +27 -0
- package/.hive/memories.jsonl +23 -1
- package/.opencode/eval-history.jsonl +12 -0
- package/CHANGELOG.md +182 -0
- package/README.md +29 -12
- package/bin/swarm.test.ts +881 -0
- package/bin/swarm.ts +686 -0
- package/dist/compaction-hook.d.ts +8 -1
- package/dist/compaction-hook.d.ts.map +1 -1
- package/dist/compaction-observability.d.ts +173 -0
- package/dist/compaction-observability.d.ts.map +1 -0
- package/dist/compaction-prompt-scoring.d.ts +124 -0
- package/dist/compaction-prompt-scoring.d.ts.map +1 -0
- package/dist/eval-capture.d.ts +174 -1
- package/dist/eval-capture.d.ts.map +1 -1
- package/dist/eval-gates.d.ts +84 -0
- package/dist/eval-gates.d.ts.map +1 -0
- package/dist/eval-history.d.ts +117 -0
- package/dist/eval-history.d.ts.map +1 -0
- package/dist/eval-learning.d.ts +216 -0
- package/dist/eval-learning.d.ts.map +1 -0
- package/dist/hive.d.ts.map +1 -1
- package/dist/index.d.ts +80 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16098 -651
- package/dist/plugin.js +16012 -756
- package/dist/post-compaction-tracker.d.ts +133 -0
- package/dist/post-compaction-tracker.d.ts.map +1 -0
- package/dist/schemas/task.d.ts +3 -3
- package/dist/swarm-orchestrate.d.ts +23 -0
- package/dist/swarm-orchestrate.d.ts.map +1 -1
- package/dist/swarm-prompts.d.ts +25 -1
- package/dist/swarm-prompts.d.ts.map +1 -1
- package/dist/swarm.d.ts +4 -0
- package/dist/swarm.d.ts.map +1 -1
- package/evals/README.md +702 -105
- package/evals/compaction-prompt.eval.ts +149 -0
- package/evals/coordinator-behavior.eval.ts +8 -8
- package/evals/fixtures/compaction-prompt-cases.ts +305 -0
- package/evals/lib/compaction-loader.test.ts +248 -0
- package/evals/lib/compaction-loader.ts +320 -0
- package/evals/lib/data-loader.test.ts +345 -0
- package/evals/lib/data-loader.ts +107 -6
- package/evals/scorers/compaction-prompt-scorers.ts +145 -0
- package/evals/scorers/compaction-scorers.ts +13 -13
- package/evals/scorers/coordinator-discipline.evalite-test.ts +166 -2
- package/evals/scorers/coordinator-discipline.ts +348 -15
- package/evals/scorers/index.test.ts +146 -0
- package/evals/scorers/index.ts +104 -0
- package/evals/swarm-decomposition.eval.ts +9 -2
- package/examples/commands/swarm.md +291 -21
- package/examples/plugin-wrapper-template.ts +117 -0
- package/package.json +7 -5
- package/scripts/migrate-unknown-sessions.ts +349 -0
- package/src/compaction-capture.integration.test.ts +257 -0
- package/src/compaction-hook.test.ts +42 -0
- package/src/compaction-hook.ts +315 -86
- package/src/compaction-observability.integration.test.ts +139 -0
- package/src/compaction-observability.test.ts +187 -0
- package/src/compaction-observability.ts +324 -0
- package/src/compaction-prompt-scorers.test.ts +299 -0
- package/src/compaction-prompt-scoring.ts +298 -0
- package/src/eval-capture.test.ts +626 -1
- package/src/eval-capture.ts +286 -2
- package/src/eval-gates.test.ts +306 -0
- package/src/eval-gates.ts +218 -0
- package/src/eval-history.test.ts +508 -0
- package/src/eval-history.ts +214 -0
- package/src/eval-learning.test.ts +378 -0
- package/src/eval-learning.ts +360 -0
- package/src/eval-runner.test.ts +96 -0
- package/src/eval-runner.ts +356 -0
- package/src/hive.ts +34 -0
- package/src/index.ts +115 -2
- package/src/memory.test.ts +110 -0
- package/src/memory.ts +34 -0
- package/src/post-compaction-tracker.test.ts +251 -0
- package/src/post-compaction-tracker.ts +237 -0
- package/src/swarm-decompose.ts +2 -2
- package/src/swarm-orchestrate.ts +2 -2
- package/src/swarm-prompts.ts +2 -2
- package/src/swarm-review.ts +3 -3
- package/dist/beads.d.ts +0 -386
- package/dist/beads.d.ts.map +0 -1
- package/dist/schemas/bead-events.d.ts +0 -698
- package/dist/schemas/bead-events.d.ts.map +0 -1
- package/dist/schemas/bead.d.ts +0 -255
- package/dist/schemas/bead.d.ts.map +0 -1
- /package/evals/{evalite.config.ts → evalite.config.ts.bak} +0 -0
package/src/memory.ts
CHANGED
|
@@ -129,6 +129,39 @@ export interface OperationResult {
|
|
|
129
129
|
readonly message?: string;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
/** Arguments for upsert operation */
|
|
133
|
+
export interface UpsertArgs {
|
|
134
|
+
readonly information: string;
|
|
135
|
+
readonly collection?: string;
|
|
136
|
+
readonly tags?: string;
|
|
137
|
+
readonly metadata?: string;
|
|
138
|
+
readonly confidence?: number;
|
|
139
|
+
/** Auto-generate tags using LLM. Default true */
|
|
140
|
+
readonly autoTag?: boolean;
|
|
141
|
+
/** Auto-link to related memories. Default true */
|
|
142
|
+
readonly autoLink?: boolean;
|
|
143
|
+
/** Extract entities (people, places, technologies). Default false */
|
|
144
|
+
readonly extractEntities?: boolean;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Auto-generated tags result */
|
|
148
|
+
export interface AutoTags {
|
|
149
|
+
readonly tags: string[];
|
|
150
|
+
readonly keywords: string[];
|
|
151
|
+
readonly category: string;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Result from upsert operation */
|
|
155
|
+
export interface UpsertResult {
|
|
156
|
+
readonly operation: "ADD" | "UPDATE" | "DELETE" | "NOOP";
|
|
157
|
+
readonly reason: string;
|
|
158
|
+
readonly memoryId?: string;
|
|
159
|
+
readonly affectedMemoryIds?: string[];
|
|
160
|
+
readonly autoTags?: AutoTags;
|
|
161
|
+
readonly linksCreated?: number;
|
|
162
|
+
readonly entitiesExtracted?: number;
|
|
163
|
+
}
|
|
164
|
+
|
|
132
165
|
// ============================================================================
|
|
133
166
|
// Auto-Migration Logic
|
|
134
167
|
// ============================================================================
|
|
@@ -206,6 +239,7 @@ export interface MemoryAdapter {
|
|
|
206
239
|
readonly list: (args: ListArgs) => Promise<Memory[]>;
|
|
207
240
|
readonly stats: () => Promise<StatsResult>;
|
|
208
241
|
readonly checkHealth: () => Promise<HealthResult>;
|
|
242
|
+
readonly upsert: (args: UpsertArgs) => Promise<UpsertResult>;
|
|
209
243
|
}
|
|
210
244
|
|
|
211
245
|
/**
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-Compaction Tool Call Tracker Tests
|
|
3
|
+
*
|
|
4
|
+
* TDD: RED → GREEN → REFACTOR
|
|
5
|
+
*
|
|
6
|
+
* Tests tracking of tool calls after compaction resumption.
|
|
7
|
+
* Emits resumption_started on first tool call, then tool_call_tracked for each call (max 20).
|
|
8
|
+
* Detects coordinator violations: Edit, Write, swarmmail_reserve are forbidden.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, test, expect, beforeEach, mock } from "bun:test";
|
|
11
|
+
import {
|
|
12
|
+
createPostCompactionTracker,
|
|
13
|
+
type PostCompactionTracker,
|
|
14
|
+
type ToolCallEvent,
|
|
15
|
+
} from "./post-compaction-tracker";
|
|
16
|
+
|
|
17
|
+
describe("PostCompactionTracker - TDD", () => {
|
|
18
|
+
let tracker: PostCompactionTracker;
|
|
19
|
+
let mockCapture: ReturnType<typeof mock>;
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
mockCapture = mock((event: any) => {});
|
|
23
|
+
tracker = createPostCompactionTracker({
|
|
24
|
+
sessionId: "test-session",
|
|
25
|
+
epicId: "mjkwehsqnbm",
|
|
26
|
+
onEvent: mockCapture,
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// ============================================================================
|
|
31
|
+
// RED: Test resumption_started event
|
|
32
|
+
// ============================================================================
|
|
33
|
+
|
|
34
|
+
test("emits resumption_started on first tool call", () => {
|
|
35
|
+
const toolCall: ToolCallEvent = {
|
|
36
|
+
tool: "read",
|
|
37
|
+
args: { filePath: "/test/file.ts" },
|
|
38
|
+
timestamp: Date.now(),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
tracker.trackToolCall(toolCall);
|
|
42
|
+
|
|
43
|
+
expect(mockCapture).toHaveBeenCalledTimes(2); // resumption_started + tool_call_tracked
|
|
44
|
+
const firstCall = mockCapture.mock.calls[0][0];
|
|
45
|
+
expect(firstCall.compaction_type).toBe("resumption_started");
|
|
46
|
+
expect(firstCall.payload.session_id).toBe("test-session");
|
|
47
|
+
expect(firstCall.payload.epic_id).toBe("mjkwehsqnbm");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("resumption_started only emitted once", () => {
|
|
51
|
+
tracker.trackToolCall({
|
|
52
|
+
tool: "read",
|
|
53
|
+
args: {},
|
|
54
|
+
timestamp: Date.now(),
|
|
55
|
+
});
|
|
56
|
+
tracker.trackToolCall({
|
|
57
|
+
tool: "glob",
|
|
58
|
+
args: {},
|
|
59
|
+
timestamp: Date.now(),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// First call: resumption_started + tool_call_tracked
|
|
63
|
+
// Second call: tool_call_tracked only
|
|
64
|
+
expect(mockCapture).toHaveBeenCalledTimes(3);
|
|
65
|
+
|
|
66
|
+
const calls = mockCapture.mock.calls;
|
|
67
|
+
expect(calls[0][0].compaction_type).toBe("resumption_started");
|
|
68
|
+
expect(calls[1][0].compaction_type).toBe("tool_call_tracked");
|
|
69
|
+
expect(calls[2][0].compaction_type).toBe("tool_call_tracked");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// ============================================================================
|
|
73
|
+
// RED: Test tool_call_tracked event
|
|
74
|
+
// ============================================================================
|
|
75
|
+
|
|
76
|
+
test("emits tool_call_tracked for each of first 20 calls", () => {
|
|
77
|
+
for (let i = 0; i < 20; i++) {
|
|
78
|
+
tracker.trackToolCall({
|
|
79
|
+
tool: `tool-${i}`,
|
|
80
|
+
args: {},
|
|
81
|
+
timestamp: Date.now(),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// First call: resumption_started + tool_call_tracked = 2
|
|
86
|
+
// Next 19 calls: tool_call_tracked only = 19
|
|
87
|
+
// Total: 21 events (1 resumption_started + 20 tool_call_tracked)
|
|
88
|
+
expect(mockCapture).toHaveBeenCalledTimes(21);
|
|
89
|
+
|
|
90
|
+
const trackedEvents = mockCapture.mock.calls.filter(
|
|
91
|
+
(call: any) => call[0].compaction_type === "tool_call_tracked",
|
|
92
|
+
);
|
|
93
|
+
expect(trackedEvents).toHaveLength(20);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("tool_call_tracked includes tool name and args", () => {
|
|
97
|
+
tracker.trackToolCall({
|
|
98
|
+
tool: "edit",
|
|
99
|
+
args: { filePath: "/test.ts", oldString: "foo", newString: "bar" },
|
|
100
|
+
timestamp: Date.now(),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const trackedEvent = mockCapture.mock.calls.find(
|
|
104
|
+
(call: any) => call[0].compaction_type === "tool_call_tracked",
|
|
105
|
+
)?.[0];
|
|
106
|
+
|
|
107
|
+
expect(trackedEvent).toBeDefined();
|
|
108
|
+
expect(trackedEvent.payload.tool).toBe("edit");
|
|
109
|
+
expect(trackedEvent.payload.args.filePath).toBe("/test.ts");
|
|
110
|
+
expect(trackedEvent.payload.call_number).toBe(1);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// ============================================================================
|
|
114
|
+
// RED: Test coordinator violation detection
|
|
115
|
+
// ============================================================================
|
|
116
|
+
|
|
117
|
+
test("detects Edit as coordinator violation", () => {
|
|
118
|
+
tracker.trackToolCall({
|
|
119
|
+
tool: "edit",
|
|
120
|
+
args: { filePath: "/test.ts", oldString: "a", newString: "b" },
|
|
121
|
+
timestamp: Date.now(),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const trackedEvent = mockCapture.mock.calls.find(
|
|
125
|
+
(call: any) => call[0].compaction_type === "tool_call_tracked",
|
|
126
|
+
)?.[0];
|
|
127
|
+
|
|
128
|
+
expect(trackedEvent.payload.is_coordinator_violation).toBe(true);
|
|
129
|
+
expect(trackedEvent.payload.violation_reason).toBe(
|
|
130
|
+
"Coordinators NEVER edit files - spawn worker instead",
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("detects Write as coordinator violation", () => {
|
|
135
|
+
tracker.trackToolCall({
|
|
136
|
+
tool: "write",
|
|
137
|
+
args: { filePath: "/new.ts", content: "export {}" },
|
|
138
|
+
timestamp: Date.now(),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const trackedEvent = mockCapture.mock.calls.find(
|
|
142
|
+
(call: any) => call[0].compaction_type === "tool_call_tracked",
|
|
143
|
+
)?.[0];
|
|
144
|
+
|
|
145
|
+
expect(trackedEvent.payload.is_coordinator_violation).toBe(true);
|
|
146
|
+
expect(trackedEvent.payload.violation_reason).toBe(
|
|
147
|
+
"Coordinators NEVER write files - spawn worker instead",
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("detects swarmmail_reserve as coordinator violation", () => {
|
|
152
|
+
tracker.trackToolCall({
|
|
153
|
+
tool: "swarmmail_reserve",
|
|
154
|
+
args: { paths: ["/src/**"], reason: "test" },
|
|
155
|
+
timestamp: Date.now(),
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const trackedEvent = mockCapture.mock.calls.find(
|
|
159
|
+
(call: any) => call[0].compaction_type === "tool_call_tracked",
|
|
160
|
+
)?.[0];
|
|
161
|
+
|
|
162
|
+
expect(trackedEvent.payload.is_coordinator_violation).toBe(true);
|
|
163
|
+
expect(trackedEvent.payload.violation_reason).toBe(
|
|
164
|
+
"Coordinators NEVER reserve files - workers reserve files",
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("does not flag Read as violation", () => {
|
|
169
|
+
tracker.trackToolCall({
|
|
170
|
+
tool: "read",
|
|
171
|
+
args: { filePath: "/test.ts" },
|
|
172
|
+
timestamp: Date.now(),
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const trackedEvent = mockCapture.mock.calls.find(
|
|
176
|
+
(call: any) => call[0].compaction_type === "tool_call_tracked",
|
|
177
|
+
)?.[0];
|
|
178
|
+
|
|
179
|
+
expect(trackedEvent.payload.is_coordinator_violation).toBe(false);
|
|
180
|
+
expect(trackedEvent.payload.violation_reason).toBeUndefined();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("does not flag swarm_spawn_subtask as violation", () => {
|
|
184
|
+
tracker.trackToolCall({
|
|
185
|
+
tool: "swarm_spawn_subtask",
|
|
186
|
+
args: { bead_id: "bd-123", subtask_title: "Test" },
|
|
187
|
+
timestamp: Date.now(),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const trackedEvent = mockCapture.mock.calls.find(
|
|
191
|
+
(call: any) => call[0].compaction_type === "tool_call_tracked",
|
|
192
|
+
)?.[0];
|
|
193
|
+
|
|
194
|
+
expect(trackedEvent.payload.is_coordinator_violation).toBe(false);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// ============================================================================
|
|
198
|
+
// RED: Test tracking stops after 20 calls
|
|
199
|
+
// ============================================================================
|
|
200
|
+
|
|
201
|
+
test("stops tracking after 20 calls", () => {
|
|
202
|
+
for (let i = 0; i < 25; i++) {
|
|
203
|
+
tracker.trackToolCall({
|
|
204
|
+
tool: `tool-${i}`,
|
|
205
|
+
args: {},
|
|
206
|
+
timestamp: Date.now(),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Should only track first 20: 1 resumption_started + 20 tool_call_tracked
|
|
211
|
+
expect(mockCapture).toHaveBeenCalledTimes(21);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("returns tracking status", () => {
|
|
215
|
+
expect(tracker.isTracking()).toBe(true);
|
|
216
|
+
|
|
217
|
+
for (let i = 0; i < 20; i++) {
|
|
218
|
+
tracker.trackToolCall({
|
|
219
|
+
tool: `tool-${i}`,
|
|
220
|
+
args: {},
|
|
221
|
+
timestamp: Date.now(),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
expect(tracker.isTracking()).toBe(false);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// ============================================================================
|
|
229
|
+
// RED: Test configurable limit
|
|
230
|
+
// ============================================================================
|
|
231
|
+
|
|
232
|
+
test("respects custom call limit", () => {
|
|
233
|
+
const customTracker = createPostCompactionTracker({
|
|
234
|
+
sessionId: "test",
|
|
235
|
+
epicId: "test",
|
|
236
|
+
onEvent: mockCapture,
|
|
237
|
+
maxCalls: 5,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
for (let i = 0; i < 10; i++) {
|
|
241
|
+
customTracker.trackToolCall({
|
|
242
|
+
tool: `tool-${i}`,
|
|
243
|
+
args: {},
|
|
244
|
+
timestamp: Date.now(),
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 1 resumption_started + 5 tool_call_tracked
|
|
249
|
+
expect(mockCapture).toHaveBeenCalledTimes(6);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-Compaction Tool Call Tracker
|
|
3
|
+
*
|
|
4
|
+
* Tracks tool calls after compaction resumption to detect coordinator violations
|
|
5
|
+
* and provide learning signals for eval-driven development.
|
|
6
|
+
*
|
|
7
|
+
* ## Purpose
|
|
8
|
+
*
|
|
9
|
+
* When context is compacted, the continuation agent needs observation to learn
|
|
10
|
+
* if it's following coordinator discipline. This tracker:
|
|
11
|
+
*
|
|
12
|
+
* 1. Emits resumption_started on first tool call (marks compaction exit)
|
|
13
|
+
* 2. Tracks up to N tool calls (default 20) with violation detection
|
|
14
|
+
* 3. Stops tracking after limit to avoid noise in long sessions
|
|
15
|
+
*
|
|
16
|
+
* ## Coordinator Violations Detected
|
|
17
|
+
*
|
|
18
|
+
* - **Edit/Write**: Coordinators NEVER edit files - spawn worker instead
|
|
19
|
+
* - **swarmmail_reserve/agentmail_reserve**: Workers reserve, not coordinators
|
|
20
|
+
*
|
|
21
|
+
* ## Integration
|
|
22
|
+
*
|
|
23
|
+
* Used by compaction hook to wire tool.call events → eval capture.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* const tracker = createPostCompactionTracker({
|
|
28
|
+
* sessionId: "session-123",
|
|
29
|
+
* epicId: "bd-epic-456",
|
|
30
|
+
* onEvent: captureCompactionEvent,
|
|
31
|
+
* });
|
|
32
|
+
*
|
|
33
|
+
* // Wire to OpenCode hook
|
|
34
|
+
* hooks["tool.call"] = (input) => {
|
|
35
|
+
* tracker.trackToolCall({
|
|
36
|
+
* tool: input.tool,
|
|
37
|
+
* args: input.args,
|
|
38
|
+
* timestamp: Date.now(),
|
|
39
|
+
* });
|
|
40
|
+
* };
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Tool call event structure
|
|
46
|
+
*/
|
|
47
|
+
export interface ToolCallEvent {
|
|
48
|
+
tool: string;
|
|
49
|
+
args: Record<string, unknown>;
|
|
50
|
+
timestamp: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Compaction event payload (matches eval-capture.ts structure)
|
|
55
|
+
*/
|
|
56
|
+
export interface CompactionEvent {
|
|
57
|
+
session_id: string;
|
|
58
|
+
epic_id: string;
|
|
59
|
+
compaction_type:
|
|
60
|
+
| "detection_complete"
|
|
61
|
+
| "prompt_generated"
|
|
62
|
+
| "context_injected"
|
|
63
|
+
| "resumption_started"
|
|
64
|
+
| "tool_call_tracked";
|
|
65
|
+
payload: {
|
|
66
|
+
session_id?: string;
|
|
67
|
+
epic_id?: string;
|
|
68
|
+
tool?: string;
|
|
69
|
+
args?: Record<string, unknown>;
|
|
70
|
+
call_number?: number;
|
|
71
|
+
is_coordinator_violation?: boolean;
|
|
72
|
+
violation_reason?: string;
|
|
73
|
+
timestamp?: number;
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Tracker configuration
|
|
79
|
+
*/
|
|
80
|
+
export interface PostCompactionTrackerConfig {
|
|
81
|
+
sessionId: string;
|
|
82
|
+
epicId: string;
|
|
83
|
+
onEvent: (event: CompactionEvent) => void;
|
|
84
|
+
maxCalls?: number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Post-compaction tracker instance
|
|
89
|
+
*/
|
|
90
|
+
export interface PostCompactionTracker {
|
|
91
|
+
trackToolCall(event: ToolCallEvent): void;
|
|
92
|
+
isTracking(): boolean;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ============================================================================
|
|
96
|
+
// Constants
|
|
97
|
+
// ============================================================================
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Default maximum number of tool calls to track
|
|
101
|
+
*
|
|
102
|
+
* Chosen to balance:
|
|
103
|
+
* - Enough data for pattern detection (20 calls is ~2-3 minutes of coordinator work)
|
|
104
|
+
* - Avoiding noise pollution in long sessions
|
|
105
|
+
*/
|
|
106
|
+
export const DEFAULT_MAX_TRACKED_CALLS = 20;
|
|
107
|
+
|
|
108
|
+
// ============================================================================
|
|
109
|
+
// Coordinator Violation Detection
|
|
110
|
+
// ============================================================================
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Tools that coordinators are NEVER allowed to use
|
|
114
|
+
*
|
|
115
|
+
* Key insight from semantic memory: coordinators lose identity after compaction
|
|
116
|
+
* and start doing implementation work. These violations are observable signals
|
|
117
|
+
* that the coordinator mandate wasn't preserved in continuation prompt.
|
|
118
|
+
*/
|
|
119
|
+
const FORBIDDEN_COORDINATOR_TOOLS: Record<string, string> = {
|
|
120
|
+
edit: "Coordinators NEVER edit files - spawn worker instead",
|
|
121
|
+
write: "Coordinators NEVER write files - spawn worker instead",
|
|
122
|
+
swarmmail_reserve: "Coordinators NEVER reserve files - workers reserve files",
|
|
123
|
+
agentmail_reserve: "Coordinators NEVER reserve files - workers reserve files",
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Check if tool call is a coordinator violation
|
|
128
|
+
*
|
|
129
|
+
* @param tool - Tool name from OpenCode tool.call hook
|
|
130
|
+
* @returns Violation status with reason if forbidden
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```typescript
|
|
134
|
+
* const result = isCoordinatorViolation("edit");
|
|
135
|
+
* // { isViolation: true, reason: "Coordinators NEVER edit..." }
|
|
136
|
+
*
|
|
137
|
+
* const result = isCoordinatorViolation("read");
|
|
138
|
+
* // { isViolation: false }
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
export function isCoordinatorViolation(tool: string): {
|
|
142
|
+
isViolation: boolean;
|
|
143
|
+
reason?: string;
|
|
144
|
+
} {
|
|
145
|
+
const reason = FORBIDDEN_COORDINATOR_TOOLS[tool];
|
|
146
|
+
return {
|
|
147
|
+
isViolation: !!reason,
|
|
148
|
+
reason,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ============================================================================
|
|
153
|
+
// Tracker Factory
|
|
154
|
+
// ============================================================================
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Create a post-compaction tool call tracker
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* ```typescript
|
|
161
|
+
* const tracker = createPostCompactionTracker({
|
|
162
|
+
* sessionId: "session-123",
|
|
163
|
+
* epicId: "bd-epic-456",
|
|
164
|
+
* onEvent: (event) => captureCompactionEvent(event),
|
|
165
|
+
* maxCalls: 20
|
|
166
|
+
* });
|
|
167
|
+
*
|
|
168
|
+
* // Track tool calls
|
|
169
|
+
* tracker.trackToolCall({
|
|
170
|
+
* tool: "read",
|
|
171
|
+
* args: { filePath: "/test.ts" },
|
|
172
|
+
* timestamp: Date.now()
|
|
173
|
+
* });
|
|
174
|
+
* ```
|
|
175
|
+
*/
|
|
176
|
+
export function createPostCompactionTracker(
|
|
177
|
+
config: PostCompactionTrackerConfig,
|
|
178
|
+
): PostCompactionTracker {
|
|
179
|
+
const {
|
|
180
|
+
sessionId,
|
|
181
|
+
epicId,
|
|
182
|
+
onEvent,
|
|
183
|
+
maxCalls = DEFAULT_MAX_TRACKED_CALLS,
|
|
184
|
+
} = config;
|
|
185
|
+
|
|
186
|
+
let callCount = 0;
|
|
187
|
+
let resumptionEmitted = false;
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
trackToolCall(event: ToolCallEvent): void {
|
|
191
|
+
// Stop tracking after max calls reached
|
|
192
|
+
if (callCount >= maxCalls) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Emit resumption_started on first call
|
|
197
|
+
if (!resumptionEmitted) {
|
|
198
|
+
onEvent({
|
|
199
|
+
session_id: sessionId,
|
|
200
|
+
epic_id: epicId,
|
|
201
|
+
compaction_type: "resumption_started",
|
|
202
|
+
payload: {
|
|
203
|
+
session_id: sessionId,
|
|
204
|
+
epic_id: epicId,
|
|
205
|
+
timestamp: event.timestamp,
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
resumptionEmitted = true;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Increment before emitting so call_number is 1-based
|
|
212
|
+
callCount++;
|
|
213
|
+
|
|
214
|
+
// Check for coordinator violations
|
|
215
|
+
const violation = isCoordinatorViolation(event.tool);
|
|
216
|
+
|
|
217
|
+
// Emit tool_call_tracked event
|
|
218
|
+
onEvent({
|
|
219
|
+
session_id: sessionId,
|
|
220
|
+
epic_id: epicId,
|
|
221
|
+
compaction_type: "tool_call_tracked",
|
|
222
|
+
payload: {
|
|
223
|
+
tool: event.tool,
|
|
224
|
+
args: event.args,
|
|
225
|
+
call_number: callCount,
|
|
226
|
+
is_coordinator_violation: violation.isViolation,
|
|
227
|
+
violation_reason: violation.reason,
|
|
228
|
+
timestamp: event.timestamp,
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
isTracking(): boolean {
|
|
234
|
+
return callCount < maxCalls;
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
}
|
package/src/swarm-decompose.ts
CHANGED
|
@@ -753,7 +753,7 @@ export const swarm_delegate_planning = tool({
|
|
|
753
753
|
.default(true)
|
|
754
754
|
.describe("Query CASS for similar past tasks (default: true)"),
|
|
755
755
|
},
|
|
756
|
-
async execute(args) {
|
|
756
|
+
async execute(args, _ctx) {
|
|
757
757
|
// Import needed modules
|
|
758
758
|
const { selectStrategy, formatStrategyGuidelines } =
|
|
759
759
|
await import("./swarm-strategies");
|
|
@@ -777,7 +777,7 @@ export const swarm_delegate_planning = tool({
|
|
|
777
777
|
// Capture strategy selection decision
|
|
778
778
|
try {
|
|
779
779
|
captureCoordinatorEvent({
|
|
780
|
-
session_id:
|
|
780
|
+
session_id: _ctx.sessionID || "unknown",
|
|
781
781
|
epic_id: "planning", // No epic ID yet - this is pre-decomposition
|
|
782
782
|
timestamp: new Date().toISOString(),
|
|
783
783
|
event_type: "DECISION",
|
package/src/swarm-orchestrate.ts
CHANGED
|
@@ -1740,7 +1740,7 @@ Files touched: ${args.files_touched?.join(", ") || "none recorded"}`,
|
|
|
1740
1740
|
try {
|
|
1741
1741
|
const durationMs = args.start_time ? Date.now() - args.start_time : 0;
|
|
1742
1742
|
captureCoordinatorEvent({
|
|
1743
|
-
session_id:
|
|
1743
|
+
session_id: _ctx.sessionID || "unknown",
|
|
1744
1744
|
epic_id: epicId,
|
|
1745
1745
|
timestamp: new Date().toISOString(),
|
|
1746
1746
|
event_type: "OUTCOME",
|
|
@@ -1849,7 +1849,7 @@ Files touched: ${args.files_touched?.join(", ") || "none recorded"}`,
|
|
|
1849
1849
|
try {
|
|
1850
1850
|
const durationMs = args.start_time ? Date.now() - args.start_time : 0;
|
|
1851
1851
|
captureCoordinatorEvent({
|
|
1852
|
-
session_id:
|
|
1852
|
+
session_id: _ctx.sessionID || "unknown",
|
|
1853
1853
|
epic_id: epicId,
|
|
1854
1854
|
timestamp: new Date().toISOString(),
|
|
1855
1855
|
event_type: "OUTCOME",
|
package/src/swarm-prompts.ts
CHANGED
|
@@ -1358,7 +1358,7 @@ export const swarm_spawn_subtask = tool({
|
|
|
1358
1358
|
.optional()
|
|
1359
1359
|
.describe("Optional explicit model override (auto-selected if not provided)"),
|
|
1360
1360
|
},
|
|
1361
|
-
async execute(args) {
|
|
1361
|
+
async execute(args, _ctx) {
|
|
1362
1362
|
const prompt = formatSubtaskPromptV2({
|
|
1363
1363
|
bead_id: args.bead_id,
|
|
1364
1364
|
epic_id: args.epic_id,
|
|
@@ -1404,7 +1404,7 @@ export const swarm_spawn_subtask = tool({
|
|
|
1404
1404
|
// Capture worker spawn decision
|
|
1405
1405
|
try {
|
|
1406
1406
|
captureCoordinatorEvent({
|
|
1407
|
-
session_id:
|
|
1407
|
+
session_id: _ctx.sessionID || "unknown",
|
|
1408
1408
|
epic_id: args.epic_id,
|
|
1409
1409
|
timestamp: new Date().toISOString(),
|
|
1410
1410
|
event_type: "DECISION",
|
package/src/swarm-review.ts
CHANGED
|
@@ -470,7 +470,7 @@ export const swarm_review_feedback = tool({
|
|
|
470
470
|
.optional()
|
|
471
471
|
.describe("JSON array of ReviewIssue objects (for needs_changes)"),
|
|
472
472
|
},
|
|
473
|
-
async execute(args): Promise<string> {
|
|
473
|
+
async execute(args, _ctx): Promise<string> {
|
|
474
474
|
// Parse issues if provided
|
|
475
475
|
let parsedIssues: ReviewIssue[] = [];
|
|
476
476
|
if (args.issues) {
|
|
@@ -512,7 +512,7 @@ export const swarm_review_feedback = tool({
|
|
|
512
512
|
// Capture review approval decision
|
|
513
513
|
try {
|
|
514
514
|
captureCoordinatorEvent({
|
|
515
|
-
session_id:
|
|
515
|
+
session_id: _ctx.sessionID || "unknown",
|
|
516
516
|
epic_id: epicId,
|
|
517
517
|
timestamp: new Date().toISOString(),
|
|
518
518
|
event_type: "DECISION",
|
|
@@ -562,7 +562,7 @@ You may now complete the task with \`swarm_complete\`.`,
|
|
|
562
562
|
// Capture review rejection decision
|
|
563
563
|
try {
|
|
564
564
|
captureCoordinatorEvent({
|
|
565
|
-
session_id:
|
|
565
|
+
session_id: _ctx.sessionID || "unknown",
|
|
566
566
|
epic_id: epicId,
|
|
567
567
|
timestamp: new Date().toISOString(),
|
|
568
568
|
event_type: "DECISION",
|