@veedubin/boomerang-v3 0.3.4 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.opencode/agents/boomerang-agent-builder.md +0 -1
- package/.opencode/agents/boomerang-architect.md +1 -2
- package/.opencode/agents/boomerang-coder.md +1 -2
- package/.opencode/agents/boomerang-explorer.md +2 -3
- package/.opencode/agents/boomerang-git.md +0 -1
- package/.opencode/agents/boomerang-handoff.md +0 -1
- package/.opencode/agents/boomerang-init.md +0 -1
- package/.opencode/agents/boomerang-linter.md +0 -1
- package/.opencode/agents/boomerang-release.md +0 -1
- package/.opencode/agents/boomerang-scraper.md +0 -1
- package/.opencode/agents/boomerang-tester.md +0 -1
- package/.opencode/agents/boomerang-writer.md +2 -3
- package/.opencode/agents/boomerang.md +2 -3
- package/.opencode/agents/mcp-specialist.md +0 -1
- package/.opencode/agents/researcher.md +0 -1
- package/AGENTS.md +7 -7
- package/dist/context-buffer.js +290 -0
- package/dist/index.js +5 -1
- package/dist/memini-client/index.js +1 -1
- package/dist/memory/contradictions.js +1 -1
- package/dist/memory/index.js +1 -1
- package/dist/memory/trust.js +1 -1
- package/dist/orchestrator.js +78 -5
- package/dist/telemetry-client.js +98 -0
- package/dist/types.js +4 -1
- package/package.json +1 -1
- package/scripts/install-boomerang.js +1 -1
|
@@ -18,7 +18,6 @@ permission:
|
|
|
18
18
|
tool:
|
|
19
19
|
"memini-ai-dev_*": allow
|
|
20
20
|
"searxng_*": allow
|
|
21
|
-
"sequential-thinking_*": allow
|
|
22
21
|
"markitdown_*": allow
|
|
23
22
|
"github-mcp_*": allow
|
|
24
23
|
"playwright_*": allow
|
|
@@ -49,7 +48,7 @@ You are the **Boomerang Architect** - the authority on design decisions, archite
|
|
|
49
48
|
## MANDATORY MEMORY PROTOCOL
|
|
50
49
|
|
|
51
50
|
1. **Query memini-ai FIRST** - `memini-ai-dev_query_memories` for previous decisions
|
|
52
|
-
2. **Use
|
|
51
|
+
2. **Use thought chains** - `memini-ai-dev_add_thought` for complex analysis
|
|
53
52
|
3. **Query knowledge graph** - `memini-ai-dev_query_kg` for entity relationships
|
|
54
53
|
4. **Save when complete** - `memini-ai-dev_add_memory` with key decisions
|
|
55
54
|
|
|
@@ -18,7 +18,6 @@ permission:
|
|
|
18
18
|
tool:
|
|
19
19
|
"memini-ai-dev_*": allow
|
|
20
20
|
"searxng_*": allow
|
|
21
|
-
"sequential-thinking_*": allow
|
|
22
21
|
"markitdown_*": allow
|
|
23
22
|
"github-mcp_*": allow
|
|
24
23
|
"playwright_*": allow
|
|
@@ -49,7 +48,7 @@ Implement features, fix bugs, and write tests efficiently using the Context Pack
|
|
|
49
48
|
## MANDATORY MEMORY PROTOCOL
|
|
50
49
|
|
|
51
50
|
1. **Query memini-ai FIRST** - Call `memini-ai-dev_query_memories` before doing ANY work
|
|
52
|
-
2. **Use
|
|
51
|
+
2. **Use thought chains** - Call `memini-ai-dev_add_thought` for complex tasks
|
|
53
52
|
3. **Save when complete** - Call `memini-ai-dev_add_memory` with a summary of your work
|
|
54
53
|
|
|
55
54
|
## Context Requirements
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Boomerang Explorer v3 - Fast file finding using devstral-2:cloud (Ollama Cloud) with memini-ai semantic search.
|
|
2
|
+
description: Boomerang Explorer v3 - Fast file finding using devstral-2:123b-cloud (Ollama Cloud) with memini-ai semantic search.
|
|
3
3
|
mode: subagent
|
|
4
|
-
model: ollama-cloud/devstral-2:cloud
|
|
4
|
+
model: ollama-cloud/devstral-2:123b-cloud
|
|
5
5
|
steps: 30
|
|
6
6
|
permission:
|
|
7
7
|
read:
|
|
@@ -18,7 +18,6 @@ permission:
|
|
|
18
18
|
tool:
|
|
19
19
|
"memini-ai-dev_*": allow
|
|
20
20
|
"searxng_*": allow
|
|
21
|
-
"sequential-thinking_*": allow
|
|
22
21
|
"markitdown_*": allow
|
|
23
22
|
"github-mcp_*": allow
|
|
24
23
|
"playwright_*": allow
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Boomerang Writer v3 - Documentation specialist using gemma4:cloud (Ollama Cloud) with memini-ai for context.
|
|
2
|
+
description: Boomerang Writer v3 - Documentation specialist using gemma4:31b-cloud (Ollama Cloud) with memini-ai for context.
|
|
3
3
|
mode: subagent
|
|
4
|
-
model: ollama-cloud/gemma4:cloud
|
|
4
|
+
model: ollama-cloud/gemma4:31b-cloud
|
|
5
5
|
steps: 40
|
|
6
6
|
permission:
|
|
7
7
|
read:
|
|
@@ -18,7 +18,6 @@ permission:
|
|
|
18
18
|
tool:
|
|
19
19
|
"memini-ai-dev_*": allow
|
|
20
20
|
"searxng_*": allow
|
|
21
|
-
"sequential-thinking_*": allow
|
|
22
21
|
"markitdown_*": allow
|
|
23
22
|
"github-mcp_*": allow
|
|
24
23
|
"playwright_*": allow
|
|
@@ -18,7 +18,6 @@ permission:
|
|
|
18
18
|
tool:
|
|
19
19
|
"memini-ai-dev_*": allow
|
|
20
20
|
"searxng_*": allow
|
|
21
|
-
"sequential-thinking_*": allow
|
|
22
21
|
"markitdown_*": allow
|
|
23
22
|
"github-mcp_*": allow
|
|
24
23
|
"playwright_*": allow
|
|
@@ -72,8 +71,8 @@ You are the **Boomerang v3 Orchestrator** - the central coordinator using memini
|
|
|
72
71
|
Immediately call `memini-ai-dev_query_memories` with the user's request.
|
|
73
72
|
Do not write any text before calling this tool.
|
|
74
73
|
|
|
75
|
-
### STEP 2: Use
|
|
76
|
-
Immediately call `
|
|
74
|
+
### STEP 2: Use thought chains (MANDATORY SECOND ACTION)
|
|
75
|
+
Immediately call `memini-ai-dev_add_thought` with your analysis. Note: This creates a `thinkingChainId` that must be passed to sub-agents in their Context Package.
|
|
77
76
|
|
|
78
77
|
### STEP 3: Plan (MANDATORY unless explicitly waived)
|
|
79
78
|
Create an implementation plan UNLESS user says "skip planning", "just do it", "/boomerang-handoff", "do a handoff", or "no plan needed".
|
package/AGENTS.md
CHANGED
|
@@ -19,11 +19,11 @@ Failure to use memini-ai causes context loss, duplicate work, and wasted tokens.
|
|
|
19
19
|
| **boomerang** | boomerang-orchestrator | kimi-k2.6:cloud | Specifically built for swarm-based task orchestration and proactive autonomous delegation. |
|
|
20
20
|
| **boomerang-coder** | boomerang-coder | glm-5.1:cloud | Flagship for agentic engineering; achieves SOTA on SWE-Bench Pro for complex, multi-file generation. |
|
|
21
21
|
| **boomerang-architect** | boomerang-architect | deepseek-v4-pro:cloud | Offers frontier reasoning with dedicated "thinking modes" for analyzing complex architectural trade-offs. |
|
|
22
|
-
| **boomerang-explorer** | boomerang-explorer | devstral-2:cloud | Explicitly designed to navigate codebases, trace dependencies, and map repository structures. |
|
|
22
|
+
| **boomerang-explorer** | boomerang-explorer | devstral-2:123b-cloud | Explicitly designed to navigate codebases, trace dependencies, and map repository structures. |
|
|
23
23
|
| **boomerang-tester** | boomerang-tester | deepseek-v4-flash:cloud | Massive 1M context window for ingesting deep error logs and codebase context quickly and efficiently. |
|
|
24
24
|
| **boomerang-linter** | boomerang-linter | qwen3-coder-next:cloud | Highly optimized for agentic coding workflows; blazing fast for syntax formatting and style checks. |
|
|
25
25
|
| **boomerang-git** | boomerang-git | minimax-m2.7:cloud | Fast and highly reliable for standard professional productivity and executing structured terminal commands. |
|
|
26
|
-
| **boomerang-writer** | boomerang-writer | gemma4:cloud | Frontier-level instruction following; excels at translating technical logic into clean, readable Markdown. |
|
|
26
|
+
| **boomerang-writer** | boomerang-writer | gemma4:31b-cloud | Frontier-level instruction following; excels at translating technical logic into clean, readable Markdown. |
|
|
27
27
|
| **boomerang-scraper** | boomerang-scraper | qwen3.5:cloud | Strong, lightweight generalist with excellent tool-use capabilities for reliable data extraction. |
|
|
28
28
|
| **boomerang-release** | boomerang-release | devstral-small-2:cloud | Fast 24B model perfect for targeted automation tasks like bumping versions and summarizing changelogs. |
|
|
29
29
|
| **boomerang-agent-builder** | boomerang-agent-builder | glm-5.1:cloud | Excels at long-horizon tasks and ambiguous problems; ideal for writing and optimizing new agent logic. |
|
|
@@ -77,12 +77,12 @@ The orchestrator MUST delegate based on these rules. No exceptions.
|
|
|
77
77
|
|-----------|------------------|-------|-------------------|
|
|
78
78
|
| Complex planning / orchestration | `boomerang` | kimi-k2.6:cloud | `general` |
|
|
79
79
|
| Architecture / design decisions | `boomerang-architect` | deepseek-v4-pro:cloud | `general`, `boomerang-coder` |
|
|
80
|
-
| Documentation writing | `boomerang-writer` | gemma4:cloud | `general` |
|
|
80
|
+
| Documentation writing | `boomerang-writer` | gemma4:31b-cloud | `general` |
|
|
81
81
|
| Session initialization | `boomerang-init` | kimi-k2.6:cloud | Everything else |
|
|
82
82
|
| Session wrap-up / handoff | `boomerang-handoff` | kimi-k2.6:cloud | Everything else |
|
|
83
83
|
| Skill/agent creation | `boomerang-agent-builder` | glm-5.1:cloud | `general` |
|
|
84
84
|
| Fast code generation / bug fixes | `boomerang-coder` | glm-5.1:cloud | `general`, `boomerang-explorer` |
|
|
85
|
-
| Code exploration / finding files | `boomerang-explorer` | devstral-2:cloud | Everything else |
|
|
85
|
+
| Code exploration / finding files | `boomerang-explorer` | devstral-2:123b-cloud | Everything else |
|
|
86
86
|
| Writing / running tests | `boomerang-tester` | deepseek-v4-flash:cloud | `general`, `boomerang-coder` |
|
|
87
87
|
| Linting / formatting | `boomerang-linter` | qwen3-coder-next:cloud | Everything else |
|
|
88
88
|
| Git operations | `boomerang-git` | minimax-m2.7:cloud | Everything else |
|
|
@@ -139,7 +139,7 @@ All agents **MUST** follow the **8-Step Boomerang Protocol** — enforcement is
|
|
|
139
139
|
### 8-Step Protocol (MANDATORY)
|
|
140
140
|
|
|
141
141
|
1. **Query Memory** — `memini-ai-dev_query_memories` FIRST
|
|
142
|
-
2. **Think** — `
|
|
142
|
+
2. **Think** — `memini-ai-dev_add_thought` for complex tasks
|
|
143
143
|
3. **Plan** — Create/refine implementation plan (MANDATORY unless user explicitly waives)
|
|
144
144
|
4. **Delegate** — OpenCode executes selected agent with Context Package
|
|
145
145
|
5. **Git Check** — Verify working tree state before code changes
|
|
@@ -209,7 +209,7 @@ Boomerang v3 uses **memini-ai** for memory — a Python-based semantic memory se
|
|
|
209
209
|
|
|
210
210
|
All agents SHOULD:
|
|
211
211
|
1. **Query memory FIRST** — `memini-ai-dev_query_memories` before work
|
|
212
|
-
2. **Use
|
|
212
|
+
2. **Use thought chains** — For complex tasks
|
|
213
213
|
3. **Save results** — `memini-ai-dev_add_memory` when complete
|
|
214
214
|
|
|
215
215
|
### Trust-Weighted Memory
|
|
@@ -330,7 +330,7 @@ IDLE → MEMORY_QUERY → SEQUENTIAL_THINK → PLAN → DELEGATE → GIT_CHECK
|
|
|
330
330
|
### 8-Step Mandatory Protocol
|
|
331
331
|
|
|
332
332
|
1. **MEMORY_QUERY** — MUST call `memini-ai-dev_query_memories` first
|
|
333
|
-
2. **SEQUENTIAL_THINK** — MUST call `
|
|
333
|
+
2. **SEQUENTIAL_THINK** — MUST call `memini-ai-dev_add_thought` for complex tasks
|
|
334
334
|
3. **PLAN** — MUST create plan or delegate to architect for build tasks
|
|
335
335
|
4. **DELEGATE** — OpenCode handles agent execution
|
|
336
336
|
5. **GIT_CHECK** — MUST verify working tree state before code changes
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Buffer Middleware — Transparent orchestrator integration
|
|
3
|
+
* for capturing and restoring agent invocation context via memini-ai.
|
|
4
|
+
*
|
|
5
|
+
* This middleware wraps around agent dispatch in the orchestrator,
|
|
6
|
+
* saving invocation context (output, tool calls, decisions, errors)
|
|
7
|
+
* as segmented memories in memini-ai. It also provides before/after
|
|
8
|
+
* hooks for telemetry and context injection.
|
|
9
|
+
*
|
|
10
|
+
* Design:
|
|
11
|
+
* - Segmenting: ~4 chars per token. Split at ~50K tokens = ~200K chars.
|
|
12
|
+
* - Memory metadata includes session_id, agent_name, segment_index.
|
|
13
|
+
* - All segments of an invocation are linked via DERIVED_FROM relationships.
|
|
14
|
+
* - The middleware is OPTIONAL — if not configured, the orchestrator
|
|
15
|
+
* operates identically to before this integration.
|
|
16
|
+
*/
|
|
17
|
+
import { TelemetryClient } from './telemetry-client.js';
|
|
18
|
+
/** Estimated ratio: 1 token ≈ 4 characters. */
|
|
19
|
+
const CHARS_PER_TOKEN = 4;
|
|
20
|
+
/**
|
|
21
|
+
* Context buffer middleware that works transparently with the existing
|
|
22
|
+
* orchestrator to capture, segment, and restore agent invocation context.
|
|
23
|
+
*/
|
|
24
|
+
export class ContextBufferMiddleware {
|
|
25
|
+
sessionId;
|
|
26
|
+
config;
|
|
27
|
+
memini;
|
|
28
|
+
telemetry;
|
|
29
|
+
segmentIds = [];
|
|
30
|
+
constructor(sessionId, config) {
|
|
31
|
+
this.sessionId = sessionId;
|
|
32
|
+
this.config = config;
|
|
33
|
+
this.memini = config.meminiClient;
|
|
34
|
+
this.telemetry = new TelemetryClient(config.telemetryEndpoint);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Called BEFORE an agent runs.
|
|
38
|
+
*
|
|
39
|
+
* Queries memini-ai for relevant context based on the task description,
|
|
40
|
+
* injects it into the agent's context package, and emits a start event
|
|
41
|
+
* to telemetry.
|
|
42
|
+
*
|
|
43
|
+
* @returns A context string to inject into the agent invocation, or
|
|
44
|
+
* an empty string if no relevant context is found.
|
|
45
|
+
*/
|
|
46
|
+
async beforeInvocation(agentName, task, context) {
|
|
47
|
+
const _startTime = Date.now();
|
|
48
|
+
await this.ensureSessionRow(task);
|
|
49
|
+
await this.emitTelemetry({
|
|
50
|
+
event_type: 'invocation_start',
|
|
51
|
+
session_id: this.sessionId,
|
|
52
|
+
agent_name: agentName,
|
|
53
|
+
metadata: { task, contextProvided: context !== undefined },
|
|
54
|
+
});
|
|
55
|
+
let injectedContext = '';
|
|
56
|
+
try {
|
|
57
|
+
const results = await this.memini.search(task, {
|
|
58
|
+
topK: 5,
|
|
59
|
+
strategy: 'TIERED',
|
|
60
|
+
});
|
|
61
|
+
if (results.length > 0) {
|
|
62
|
+
const contextParts = results
|
|
63
|
+
.slice(0, 5)
|
|
64
|
+
.map((r) => `[trust=${r.entry.trustScore?.toFixed(2) ?? '0.50'}] ${r.entry.text}`)
|
|
65
|
+
.join('\n\n---\n\n');
|
|
66
|
+
injectedContext = `\n## Relevant Context from Previous Sessions\n\n${contextParts}\n`;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
console.warn('[ContextBuffer] beforeInvocation search failed:', err);
|
|
71
|
+
}
|
|
72
|
+
return injectedContext;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Called AFTER an agent completes (or fails).
|
|
76
|
+
*
|
|
77
|
+
* Captures the full invocation payload, segments if needed, saves
|
|
78
|
+
* each segment to memini-ai, links them via relationships, and emits
|
|
79
|
+
* an end event to telemetry.
|
|
80
|
+
*
|
|
81
|
+
* @returns Array of memory IDs for the saved segments.
|
|
82
|
+
*/
|
|
83
|
+
async afterInvocation(agentName, result, durationMs) {
|
|
84
|
+
const success = 'success' in result ? result.success : true;
|
|
85
|
+
const error = 'success' in result && !result.success ? result.error : undefined;
|
|
86
|
+
await this.emitTelemetry({
|
|
87
|
+
event_type: success ? 'invocation_end' : 'invocation_error',
|
|
88
|
+
session_id: this.sessionId,
|
|
89
|
+
agent_name: agentName,
|
|
90
|
+
duration_ms: durationMs,
|
|
91
|
+
success,
|
|
92
|
+
error_message: error,
|
|
93
|
+
});
|
|
94
|
+
const payload = this.buildPayload(agentName, result, durationMs);
|
|
95
|
+
const memoryIds = await this.saveContextSegments(agentName, payload);
|
|
96
|
+
this.segmentIds = memoryIds;
|
|
97
|
+
// Link segments to each other via DERIVED_FROM relationships
|
|
98
|
+
await this.linkSegments(memoryIds);
|
|
99
|
+
return memoryIds;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Get the memory IDs saved during the current session.
|
|
103
|
+
*/
|
|
104
|
+
getSegmentIds() {
|
|
105
|
+
return [...this.segmentIds];
|
|
106
|
+
}
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Private helpers
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
/**
|
|
111
|
+
* Build an AgentContextPayload from a task result.
|
|
112
|
+
*/
|
|
113
|
+
buildPayload(agentName, result, durationMs) {
|
|
114
|
+
const now = Date.now();
|
|
115
|
+
if ('success' in result && !result.success) {
|
|
116
|
+
return {
|
|
117
|
+
output: '',
|
|
118
|
+
toolCalls: [],
|
|
119
|
+
filesModified: [],
|
|
120
|
+
decisions: [],
|
|
121
|
+
errors: [result.error],
|
|
122
|
+
startTime: now - durationMs,
|
|
123
|
+
endTime: now,
|
|
124
|
+
durationMs,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
const taskResult = result;
|
|
128
|
+
return {
|
|
129
|
+
output: taskResult.output ?? '',
|
|
130
|
+
toolCalls: taskResult.toolCalls ?? [],
|
|
131
|
+
filesModified: taskResult.filesModified ?? [],
|
|
132
|
+
decisions: taskResult.decisions ?? [],
|
|
133
|
+
errors: taskResult.errors ?? [],
|
|
134
|
+
startTime: now - durationMs,
|
|
135
|
+
endTime: now,
|
|
136
|
+
durationMs,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Save context segments to memini-ai.
|
|
141
|
+
* If the summary exceeds the segment threshold, it is split into
|
|
142
|
+
* multiple segments, each saved separately.
|
|
143
|
+
*/
|
|
144
|
+
async saveContextSegments(agentName, payload) {
|
|
145
|
+
const summary = this.generateContextSummary(agentName, payload);
|
|
146
|
+
const thresholdChars = this.config.segmentThreshold * CHARS_PER_TOKEN;
|
|
147
|
+
const segments = this.splitIfNeeded(summary, thresholdChars);
|
|
148
|
+
const memoryIds = [];
|
|
149
|
+
for (let i = 0; i < segments.length; i++) {
|
|
150
|
+
const segment = segments[i];
|
|
151
|
+
try {
|
|
152
|
+
const entry = await this.memini.addMemory({
|
|
153
|
+
text: segment,
|
|
154
|
+
sourceType: 'boomerang',
|
|
155
|
+
sourcePath: `context-buffer://${this.sessionId}/${agentName}`,
|
|
156
|
+
metadataJson: JSON.stringify({
|
|
157
|
+
type: 'context_segment',
|
|
158
|
+
session_id: this.sessionId,
|
|
159
|
+
agent_name: agentName,
|
|
160
|
+
segment_index: i,
|
|
161
|
+
total_segments: segments.length,
|
|
162
|
+
files_modified: payload.filesModified,
|
|
163
|
+
decisions: payload.decisions,
|
|
164
|
+
errors: payload.errors,
|
|
165
|
+
duration_ms: payload.durationMs,
|
|
166
|
+
success: payload.errors.length === 0,
|
|
167
|
+
}),
|
|
168
|
+
sessionId: this.sessionId,
|
|
169
|
+
});
|
|
170
|
+
memoryIds.push(entry.id);
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
console.warn(`[ContextBuffer] Failed to save segment ${i}/${segments.length}:`, err);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return memoryIds;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Link segments via DERIVED_FROM relationships in memini-ai.
|
|
180
|
+
* Each segment (except the first) derives from the previous one.
|
|
181
|
+
*/
|
|
182
|
+
async linkSegments(memoryIds) {
|
|
183
|
+
if (memoryIds.length < 2) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
for (let i = 1; i < memoryIds.length; i++) {
|
|
187
|
+
try {
|
|
188
|
+
await this.memini.createRelationship(memoryIds[i], memoryIds[i - 1], 'DERIVED_FROM', 0.9);
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
console.warn(`[ContextBuffer] Failed to link segments ${i - 1} → ${i}:`, err);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Split content into segments if it exceeds the threshold.
|
|
197
|
+
* Tries to split on paragraph boundaries (double newlines).
|
|
198
|
+
*/
|
|
199
|
+
splitIfNeeded(content, thresholdChars) {
|
|
200
|
+
if (content.length <= thresholdChars) {
|
|
201
|
+
return [content];
|
|
202
|
+
}
|
|
203
|
+
const segments = [];
|
|
204
|
+
let remaining = content;
|
|
205
|
+
while (remaining.length > 0) {
|
|
206
|
+
if (remaining.length <= thresholdChars) {
|
|
207
|
+
segments.push(remaining);
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
// Find a paragraph boundary near the threshold
|
|
211
|
+
const searchStart = Math.max(0, thresholdChars - 1000);
|
|
212
|
+
const searchEnd = Math.min(remaining.length, thresholdChars + 1000);
|
|
213
|
+
const searchRegion = remaining.slice(searchStart, searchEnd);
|
|
214
|
+
const newlinePos = searchRegion.indexOf('\n\n');
|
|
215
|
+
if (newlinePos !== -1) {
|
|
216
|
+
const splitPos = searchStart + newlinePos + 2;
|
|
217
|
+
segments.push(remaining.slice(0, splitPos));
|
|
218
|
+
remaining = remaining.slice(splitPos);
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
// No good boundary; hard split at threshold
|
|
222
|
+
segments.push(remaining.slice(0, thresholdChars));
|
|
223
|
+
remaining = remaining.slice(thresholdChars);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return segments;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Generate a human-readable context summary from the agent payload.
|
|
230
|
+
*/
|
|
231
|
+
generateContextSummary(agentName, payload) {
|
|
232
|
+
const lines = [
|
|
233
|
+
`# Agent Invocation: ${agentName}`,
|
|
234
|
+
`Duration: ${payload.durationMs}ms`,
|
|
235
|
+
`Status: ${payload.errors.length === 0 ? 'SUCCESS' : 'FAILED'}`,
|
|
236
|
+
'',
|
|
237
|
+
];
|
|
238
|
+
if (payload.output) {
|
|
239
|
+
lines.push('## Output');
|
|
240
|
+
lines.push(payload.output);
|
|
241
|
+
lines.push('');
|
|
242
|
+
}
|
|
243
|
+
if (payload.filesModified.length > 0) {
|
|
244
|
+
lines.push('## Files Modified');
|
|
245
|
+
for (const f of payload.filesModified) {
|
|
246
|
+
lines.push(`- ${f}`);
|
|
247
|
+
}
|
|
248
|
+
lines.push('');
|
|
249
|
+
}
|
|
250
|
+
if (payload.decisions.length > 0) {
|
|
251
|
+
lines.push('## Decisions');
|
|
252
|
+
for (const d of payload.decisions) {
|
|
253
|
+
lines.push(`- ${d}`);
|
|
254
|
+
}
|
|
255
|
+
lines.push('');
|
|
256
|
+
}
|
|
257
|
+
if (payload.toolCalls.length > 0) {
|
|
258
|
+
lines.push('## Tool Calls');
|
|
259
|
+
for (const tc of payload.toolCalls) {
|
|
260
|
+
lines.push(`- ${tc.name}(${JSON.stringify(tc.arguments).slice(0, 200)})`);
|
|
261
|
+
}
|
|
262
|
+
lines.push('');
|
|
263
|
+
}
|
|
264
|
+
if (payload.errors.length > 0) {
|
|
265
|
+
lines.push('## Errors');
|
|
266
|
+
for (const e of payload.errors) {
|
|
267
|
+
lines.push(`- ${e}`);
|
|
268
|
+
}
|
|
269
|
+
lines.push('');
|
|
270
|
+
}
|
|
271
|
+
return lines.join('\n');
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Ensure a session row exists in telemetry.
|
|
275
|
+
*/
|
|
276
|
+
async ensureSessionRow(task) {
|
|
277
|
+
try {
|
|
278
|
+
await this.telemetry.createSession(this.sessionId, 'default', task);
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
// Session may already exist; ignore errors
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Emit a telemetry event. Fire-and-forget — errors are logged only.
|
|
286
|
+
*/
|
|
287
|
+
async emitTelemetry(event) {
|
|
288
|
+
await this.telemetry.emit(event);
|
|
289
|
+
}
|
|
290
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
* Boomerang v3 — Multi-agent orchestration plugin for OpenCode
|
|
3
3
|
*
|
|
4
4
|
* Exports:
|
|
5
|
-
* - orchestrator: Concurrency-aware orchestrator
|
|
5
|
+
* - orchestrator: Concurrency-aware orchestrator with context buffer
|
|
6
6
|
* - plugin: Plugin metadata
|
|
7
7
|
* - concurrency: TaskLimiter, RetryExecutor, TimeoutEnforcer
|
|
8
|
+
* - context-buffer: ContextBufferMiddleware
|
|
9
|
+
* - telemetry: TelemetryClient
|
|
8
10
|
* - types: Core type definitions
|
|
9
11
|
*/
|
|
10
12
|
export { BoomerangOrchestrator, createOrchestrator } from './orchestrator.js';
|
|
11
13
|
export { TaskLimiter, executeWithRetry, executeWithTimeout, } from './concurrency/index.js';
|
|
14
|
+
export { ContextBufferMiddleware } from './context-buffer.js';
|
|
15
|
+
export { TelemetryClient } from './telemetry-client.js';
|
|
12
16
|
export { TimeoutError, } from './types.js';
|
|
13
17
|
export const orchestrator = {
|
|
14
18
|
name: 'boomerang-v3',
|
|
@@ -552,7 +552,7 @@ export function getClient() {
|
|
|
552
552
|
}
|
|
553
553
|
return clientInstance;
|
|
554
554
|
}
|
|
555
|
-
export async function initializeClient(
|
|
555
|
+
export async function initializeClient(_pythonPath, _serverModule) {
|
|
556
556
|
const client = getClient();
|
|
557
557
|
await client.initialize();
|
|
558
558
|
return client;
|
|
@@ -89,7 +89,7 @@ export async function challengeMemory(memoryId, challengeText, client) {
|
|
|
89
89
|
/**
|
|
90
90
|
* Adapt a raw memory from memini-ai to our MemoryEntry type
|
|
91
91
|
*/
|
|
92
|
-
function
|
|
92
|
+
function _adaptMemoryEntry(raw) {
|
|
93
93
|
let timestamp;
|
|
94
94
|
if (typeof raw.timestamp === 'number') {
|
|
95
95
|
timestamp = raw.timestamp;
|
package/dist/memory/index.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { initializeClient, } from '../memini-client/index.js';
|
|
7
7
|
import { DEFAULT_SEARCH_OPTIONS } from './schema.js';
|
|
8
|
-
const
|
|
8
|
+
const _PROJECT_ID = process.env.BOOMERANG_PROJECT_ID || 'boomerang-default';
|
|
9
9
|
class MemorySystem {
|
|
10
10
|
static instance = null;
|
|
11
11
|
_initialized = false;
|
package/dist/memory/trust.js
CHANGED
|
@@ -130,7 +130,7 @@ export async function triggerConsolidation(force = false, client) {
|
|
|
130
130
|
/**
|
|
131
131
|
* Adapt a raw memory from memini-ai to our MemoryEntry type
|
|
132
132
|
*/
|
|
133
|
-
function
|
|
133
|
+
function _adaptMemoryEntry(meminiEntry) {
|
|
134
134
|
return {
|
|
135
135
|
id: meminiEntry.id,
|
|
136
136
|
text: meminiEntry.text,
|
package/dist/orchestrator.js
CHANGED
|
@@ -10,7 +10,7 @@ const ROUTING_MATRIX = {
|
|
|
10
10
|
'mcp-debug': 'mcp-specialist',
|
|
11
11
|
'release': 'boomerang-release',
|
|
12
12
|
};
|
|
13
|
-
function
|
|
13
|
+
function _validateAgentRouting(taskType, agentName) {
|
|
14
14
|
const expected = ROUTING_MATRIX[taskType];
|
|
15
15
|
if (expected && expected !== agentName) {
|
|
16
16
|
console.warn(`ROUTING VIOLATION: ${taskType} should use ${expected}, not ${agentName}`);
|
|
@@ -20,10 +20,12 @@ function validateAgentRouting(taskType, agentName) {
|
|
|
20
20
|
}
|
|
21
21
|
import { TimeoutError } from './types.js';
|
|
22
22
|
import { TaskLimiter, executeWithRetry, executeWithTimeout, } from './concurrency/index.js';
|
|
23
|
+
import { ContextBufferMiddleware } from './context-buffer.js';
|
|
23
24
|
export class BoomerangOrchestrator {
|
|
24
25
|
taskLimiter;
|
|
25
26
|
config;
|
|
26
|
-
|
|
27
|
+
contextBuffer;
|
|
28
|
+
constructor(config, contextBufferConfig) {
|
|
27
29
|
this.config = {
|
|
28
30
|
maxConcurrentSubAgents: 2,
|
|
29
31
|
defaultTimeoutMs: 60000,
|
|
@@ -39,6 +41,11 @@ export class BoomerangOrchestrator {
|
|
|
39
41
|
...config,
|
|
40
42
|
};
|
|
41
43
|
this.taskLimiter = new TaskLimiter(this.config.maxConcurrentSubAgents);
|
|
44
|
+
this.contextBuffer = null;
|
|
45
|
+
if (contextBufferConfig?.meminiClient) {
|
|
46
|
+
const sessionId = `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
47
|
+
this.contextBuffer = new ContextBufferMiddleware(sessionId, contextBufferConfig);
|
|
48
|
+
}
|
|
42
49
|
}
|
|
43
50
|
/**
|
|
44
51
|
* Check if an agent can be dispatched without exceeding slot limits.
|
|
@@ -53,13 +60,36 @@ export class BoomerangOrchestrator {
|
|
|
53
60
|
return this.taskLimiter.getSlotUsage();
|
|
54
61
|
}
|
|
55
62
|
/**
|
|
56
|
-
*
|
|
63
|
+
* Get the context buffer middleware, if configured.
|
|
64
|
+
*/
|
|
65
|
+
getContextBuffer() {
|
|
66
|
+
return this.contextBuffer;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Dispatch a task with concurrency check, retry, timeout enforcement,
|
|
70
|
+
* and optional context buffer middleware integration.
|
|
71
|
+
*
|
|
72
|
+
* When the context buffer is configured, it wraps the task execution
|
|
73
|
+
* with beforeInvocation/afterInvocation hooks for context capture.
|
|
57
74
|
*/
|
|
58
75
|
async dispatchTask(agentName, task, timeoutMs) {
|
|
59
76
|
if (!this.canDispatch(agentName)) {
|
|
60
77
|
throw new Error(`Cannot dispatch "${agentName}": no available slots (max ${this.config.maxConcurrentSubAgents})`);
|
|
61
78
|
}
|
|
62
79
|
this.taskLimiter.registerAgent(agentName, timeoutMs ?? this.config.defaultTimeoutMs);
|
|
80
|
+
const startTime = Date.now();
|
|
81
|
+
let _injectedContext = '';
|
|
82
|
+
let _contextMemoryIds = [];
|
|
83
|
+
// Context buffer: before invocation
|
|
84
|
+
if (this.contextBuffer) {
|
|
85
|
+
try {
|
|
86
|
+
_injectedContext = await this.contextBuffer.beforeInvocation(agentName, task.toString().slice(0, 200) // Brief description
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
console.warn('[Orchestrator] Context buffer beforeInvocation failed:', err);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
63
93
|
try {
|
|
64
94
|
const retryResult = await executeWithRetry(() => executeWithTimeout(task, agentName, timeoutMs), {
|
|
65
95
|
maxRetries: this.config.maxRetries,
|
|
@@ -67,12 +97,55 @@ export class BoomerangOrchestrator {
|
|
|
67
97
|
maxDelayMs: this.config.retryMaxDelayMs,
|
|
68
98
|
isRetryable: (err) => !(err instanceof TimeoutError),
|
|
69
99
|
});
|
|
100
|
+
// Context buffer: after successful invocation
|
|
101
|
+
if (this.contextBuffer) {
|
|
102
|
+
try {
|
|
103
|
+
const taskResult = this.extractTaskResult(retryResult.result);
|
|
104
|
+
_contextMemoryIds = await this.contextBuffer.afterInvocation(agentName, taskResult, Date.now() - startTime);
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
console.warn('[Orchestrator] Context buffer afterInvocation failed:', err);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
70
110
|
return retryResult;
|
|
71
111
|
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
// Context buffer: after failed invocation
|
|
114
|
+
if (this.contextBuffer) {
|
|
115
|
+
try {
|
|
116
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
117
|
+
_contextMemoryIds = await this.contextBuffer.afterInvocation(agentName, { success: false, error: errorMessage }, Date.now() - startTime);
|
|
118
|
+
}
|
|
119
|
+
catch (ctxErr) {
|
|
120
|
+
console.warn('[Orchestrator] Context buffer afterInvocation (error) failed:', ctxErr);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
throw err;
|
|
124
|
+
}
|
|
72
125
|
finally {
|
|
73
126
|
this.taskLimiter.releaseAgent(agentName);
|
|
74
127
|
}
|
|
75
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Extract a TaskResult from a dispatch result value.
|
|
131
|
+
* Handles the case where T may or may not be a TaskResult-shaped object.
|
|
132
|
+
*/
|
|
133
|
+
extractTaskResult(result) {
|
|
134
|
+
if (typeof result === 'object' && result !== null) {
|
|
135
|
+
const obj = result;
|
|
136
|
+
if ('output' in obj || 'filesModified' in obj || 'decisions' in obj || 'errors' in obj) {
|
|
137
|
+
return obj;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Wrap raw results
|
|
141
|
+
return {
|
|
142
|
+
output: typeof result === 'string' ? result : JSON.stringify(result),
|
|
143
|
+
toolCalls: [],
|
|
144
|
+
filesModified: [],
|
|
145
|
+
decisions: [],
|
|
146
|
+
errors: [],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
76
149
|
/**
|
|
77
150
|
* Reset all concurrency state (useful for testing and recovery).
|
|
78
151
|
*/
|
|
@@ -83,6 +156,6 @@ export class BoomerangOrchestrator {
|
|
|
83
156
|
/**
|
|
84
157
|
* Factory function to create a concurrency-aware orchestrator.
|
|
85
158
|
*/
|
|
86
|
-
export function createOrchestrator(config) {
|
|
87
|
-
return new BoomerangOrchestrator(config);
|
|
159
|
+
export function createOrchestrator(config, contextBufferConfig) {
|
|
160
|
+
return new BoomerangOrchestrator(config, contextBufferConfig);
|
|
88
161
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telemetry Client — Simple HTTP client for emitting telemetry events
|
|
3
|
+
* to the observability proxy.
|
|
4
|
+
*
|
|
5
|
+
* Uses native fetch (Node 20+) with fire-and-forget semantics.
|
|
6
|
+
* Failures are logged but never throw, ensuring telemetry never blocks
|
|
7
|
+
* the orchestrator pipeline.
|
|
8
|
+
*/
|
|
9
|
+
const DEFAULT_TIMEOUT_MS = 5000;
|
|
10
|
+
/**
|
|
11
|
+
* Simple HTTP telemetry client for the boomerang observability proxy.
|
|
12
|
+
*
|
|
13
|
+
* All methods are fire-and-forget: errors are logged but never propagated,
|
|
14
|
+
* so telemetry failures never block the orchestrator.
|
|
15
|
+
*/
|
|
16
|
+
export class TelemetryClient {
|
|
17
|
+
endpoint;
|
|
18
|
+
timeoutMs;
|
|
19
|
+
constructor(endpoint, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
20
|
+
this.endpoint = endpoint.replace(/\/+$/, '');
|
|
21
|
+
this.timeoutMs = timeoutMs;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Emit a telemetry event to the proxy.
|
|
25
|
+
* Fire-and-forget: logs errors but never throws.
|
|
26
|
+
*/
|
|
27
|
+
async emit(event) {
|
|
28
|
+
try {
|
|
29
|
+
await this.post('/events', event);
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
console.warn('[TelemetryClient] emit failed:', err);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Create a new session record in the telemetry proxy.
|
|
37
|
+
*/
|
|
38
|
+
async createSession(sessionId, tenantId, taskDescription) {
|
|
39
|
+
try {
|
|
40
|
+
await this.post('/sessions', {
|
|
41
|
+
session_id: sessionId,
|
|
42
|
+
tenant_id: tenantId,
|
|
43
|
+
task_description: taskDescription,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
console.warn('[TelemetryClient] createSession failed:', err);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Update an existing session in the telemetry proxy.
|
|
52
|
+
*/
|
|
53
|
+
async updateSession(sessionId, updates) {
|
|
54
|
+
try {
|
|
55
|
+
await this.patch(`/sessions/${sessionId}`, updates);
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
console.warn('[TelemetryClient] updateSession failed:', err);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Private helpers
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
async post(path, body) {
|
|
65
|
+
const url = `${this.endpoint}${path}`;
|
|
66
|
+
const controller = new AbortController();
|
|
67
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
68
|
+
try {
|
|
69
|
+
const response = await fetch(url, {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
headers: { 'Content-Type': 'application/json' },
|
|
72
|
+
body: JSON.stringify(body),
|
|
73
|
+
signal: controller.signal,
|
|
74
|
+
});
|
|
75
|
+
return response;
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
clearTimeout(timer);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async patch(path, body) {
|
|
82
|
+
const url = `${this.endpoint}${path}`;
|
|
83
|
+
const controller = new AbortController();
|
|
84
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
85
|
+
try {
|
|
86
|
+
const response = await fetch(url, {
|
|
87
|
+
method: 'PATCH',
|
|
88
|
+
headers: { 'Content-Type': 'application/json' },
|
|
89
|
+
body: JSON.stringify(body),
|
|
90
|
+
signal: controller.signal,
|
|
91
|
+
});
|
|
92
|
+
return response;
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
clearTimeout(timer);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
package/dist/types.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Boomerang v3 —
|
|
2
|
+
* Boomerang v3 — Core Type Definitions
|
|
3
|
+
*
|
|
4
|
+
* Includes concurrency types, context buffer types, telemetry types,
|
|
5
|
+
* and shared interfaces used across the orchestrator and middleware layers.
|
|
3
6
|
*/
|
|
4
7
|
export class TimeoutError extends Error {
|
|
5
8
|
agentName;
|
package/package.json
CHANGED
|
@@ -49,7 +49,7 @@ const PROVIDERS = {
|
|
|
49
49
|
'kimi-k2.6:cloud': { name: 'Kimi K2.6 (Cloud)' },
|
|
50
50
|
'glm-5.1:cloud': { name: 'GLM 5.1 (Cloud)' },
|
|
51
51
|
'deepseek-v4-pro:cloud': { name: 'DeepSeek V4 Pro (Cloud)' },
|
|
52
|
-
'devstral-2:cloud': { name: 'Devstral 2 (Cloud)' },
|
|
52
|
+
'devstral-2:123b-cloud': { name: 'Devstral 2 (Cloud)' },
|
|
53
53
|
'deepseek-v4-flash:cloud': { name: 'DeepSeek V4 Flash (Cloud)' },
|
|
54
54
|
'qwen3-coder-next:cloud': { name: 'Qwen3 Coder Next (Cloud)' },
|
|
55
55
|
'minimax-m2.7:cloud': { name: 'MiniMax M2.7 (Cloud)' },
|