opencode-swarm 5.1.1 → 5.1.4
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/README.md +8 -8
- package/dist/commands/benchmark.d.ts +1 -0
- package/dist/commands/index.d.ts +2 -0
- package/dist/commands/retrieve.d.ts +5 -0
- package/dist/config/schema.d.ts +15 -0
- package/dist/hooks/index.d.ts +2 -1
- package/dist/hooks/tool-summarizer.d.ts +28 -0
- package/dist/index.js +633 -118
- package/dist/summaries/index.d.ts +2 -0
- package/dist/summaries/manager.d.ts +29 -0
- package/dist/summaries/summarizer.d.ts +38 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="https://img.shields.io/badge/version-5.
|
|
2
|
+
<img src="https://img.shields.io/badge/version-5.1.4-blue" alt="Version">
|
|
3
3
|
<img src="https://img.shields.io/badge/license-MIT-green" alt="License">
|
|
4
4
|
<img src="https://img.shields.io/badge/opencode-plugin-purple" alt="OpenCode Plugin">
|
|
5
|
-
<img src="https://img.shields.io/badge/agents-
|
|
6
|
-
<img src="https://img.shields.io/badge/tests-
|
|
5
|
+
<img src="https://img.shields.io/badge/agents-7-orange" alt="Agents">
|
|
6
|
+
<img src="https://img.shields.io/badge/tests-1027-brightgreen" alt="Tests">
|
|
7
7
|
</p>
|
|
8
8
|
|
|
9
9
|
<h1 align="center">🐝 OpenCode Swarm</h1>
|
|
@@ -341,7 +341,7 @@ bunx opencode-swarm uninstall --clean
|
|
|
341
341
|
- **Context injection budget** — `max_injection_tokens` config controls how much context is injected into system prompts. Priority-ordered: phase → task → decisions → agent context. Lower-priority items dropped when budget exhausted.
|
|
342
342
|
- **Enhanced `/swarm agents`** — Agent count summary, `⚡ custom limits` indicator for profiled agents, guardrail profiles section.
|
|
343
343
|
- **Packaging smoke tests** — CI-safe `dist/` validation (8 tests).
|
|
344
|
-
- **
|
|
344
|
+
- **151 new tests** — 1027 total tests across 44 files (up from 876 in v4.6.0).
|
|
345
345
|
|
|
346
346
|
### v4.6.0 — Agent Guardrails
|
|
347
347
|
- **Circuit breaker** — Two-layer protection against runaway agents. Soft warning at 50% of limits, hard block at 100%. Prevents infinite loops and runaway API costs.
|
|
@@ -422,6 +422,8 @@ All features are opt-in via configuration. See [Installation Guide](docs/install
|
|
|
422
422
|
| `/swarm reset --confirm` | Clear swarm state files (with safety gate) |
|
|
423
423
|
| `/swarm evidence [task]` | View evidence bundles for a task or all tasks |
|
|
424
424
|
| `/swarm archive [--dry-run]` | Archive old evidence bundles with retention policy |
|
|
425
|
+
| `/swarm benchmark` | Run performance benchmarks and display metrics |
|
|
426
|
+
| `/swarm retrieve [id]` | Retrieve auto-summarized tool outputs by ID |
|
|
425
427
|
|
|
426
428
|
---
|
|
427
429
|
|
|
@@ -508,9 +510,7 @@ Override limits for specific agents that need more (or less) room:
|
|
|
508
510
|
|
|
509
511
|
Profiles merge with base config — only specified fields are overridden.
|
|
510
512
|
|
|
511
|
-
> **
|
|
512
|
-
> (600 tool calls, 90 min duration, 8 consecutive errors, 0.7 warning threshold) without any
|
|
513
|
-
> configuration. These built-in defaults can be overridden via a `profiles.architect` entry.
|
|
513
|
+
> **Architect is exempt/unlimited by default:** The architect agent has no guardrail limits by default. To override, add a `profiles.architect` entry in your guardrails config.
|
|
514
514
|
|
|
515
515
|
### Disable Guardrails
|
|
516
516
|
|
|
@@ -564,7 +564,7 @@ bun test
|
|
|
564
564
|
bun test tests/unit/config/schema.test.ts
|
|
565
565
|
```
|
|
566
566
|
|
|
567
|
-
|
|
567
|
+
1027 unit tests across 44 files covering config, tools, agents, hooks, commands, state, guardrails, evidence, and plan schemas. Uses Bun's built-in test runner — zero additional test dependencies.
|
|
568
568
|
|
|
569
569
|
## Troubleshooting
|
|
570
570
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function handleBenchmarkCommand(directory: string, args: string[]): Promise<string>;
|
package/dist/commands/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AgentDefinition } from '../agents';
|
|
2
2
|
export { handleAgentsCommand } from './agents';
|
|
3
3
|
export { handleArchiveCommand } from './archive';
|
|
4
|
+
export { handleBenchmarkCommand } from './benchmark';
|
|
4
5
|
export { handleConfigCommand } from './config';
|
|
5
6
|
export { handleDiagnoseCommand } from './diagnose';
|
|
6
7
|
export { handleEvidenceCommand } from './evidence';
|
|
@@ -8,6 +9,7 @@ export { handleExportCommand } from './export';
|
|
|
8
9
|
export { handleHistoryCommand } from './history';
|
|
9
10
|
export { handlePlanCommand } from './plan';
|
|
10
11
|
export { handleResetCommand } from './reset';
|
|
12
|
+
export { handleRetrieveCommand } from './retrieve';
|
|
11
13
|
export { handleStatusCommand } from './status';
|
|
12
14
|
/**
|
|
13
15
|
* Creates a command.execute.before handler for /swarm commands.
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -120,6 +120,14 @@ export declare const EvidenceConfigSchema: z.ZodObject<{
|
|
|
120
120
|
auto_archive: z.ZodDefault<z.ZodBoolean>;
|
|
121
121
|
}, z.core.$strip>;
|
|
122
122
|
export type EvidenceConfig = z.infer<typeof EvidenceConfigSchema>;
|
|
123
|
+
export declare const SummaryConfigSchema: z.ZodObject<{
|
|
124
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
125
|
+
threshold_bytes: z.ZodDefault<z.ZodNumber>;
|
|
126
|
+
max_summary_chars: z.ZodDefault<z.ZodNumber>;
|
|
127
|
+
max_stored_bytes: z.ZodDefault<z.ZodNumber>;
|
|
128
|
+
retention_days: z.ZodDefault<z.ZodNumber>;
|
|
129
|
+
}, z.core.$strip>;
|
|
130
|
+
export type SummaryConfig = z.infer<typeof SummaryConfigSchema>;
|
|
123
131
|
export declare const GuardrailsProfileSchema: z.ZodObject<{
|
|
124
132
|
max_tool_calls: z.ZodOptional<z.ZodNumber>;
|
|
125
133
|
max_duration_minutes: z.ZodOptional<z.ZodNumber>;
|
|
@@ -262,6 +270,13 @@ export declare const PluginConfigSchema: z.ZodObject<{
|
|
|
262
270
|
max_bundles: z.ZodDefault<z.ZodNumber>;
|
|
263
271
|
auto_archive: z.ZodDefault<z.ZodBoolean>;
|
|
264
272
|
}, z.core.$strip>>;
|
|
273
|
+
summaries: z.ZodOptional<z.ZodObject<{
|
|
274
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
275
|
+
threshold_bytes: z.ZodDefault<z.ZodNumber>;
|
|
276
|
+
max_summary_chars: z.ZodDefault<z.ZodNumber>;
|
|
277
|
+
max_stored_bytes: z.ZodDefault<z.ZodNumber>;
|
|
278
|
+
retention_days: z.ZodDefault<z.ZodNumber>;
|
|
279
|
+
}, z.core.$strip>>;
|
|
265
280
|
}, z.core.$strip>;
|
|
266
281
|
export type PluginConfig = z.infer<typeof PluginConfigSchema>;
|
|
267
282
|
export type { AgentName, PipelineAgentName, QAAgentName, } from './constants';
|
package/dist/hooks/index.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
export { createAgentActivityHooks } from './agent-activity';
|
|
2
2
|
export { createCompactionCustomizerHook } from './compaction-customizer';
|
|
3
3
|
export { createContextBudgetHandler } from './context-budget';
|
|
4
|
-
export { createDelegationTrackerHook } from './delegation-tracker';
|
|
5
4
|
export { createDelegationGateHook } from './delegation-gate';
|
|
5
|
+
export { createDelegationTrackerHook } from './delegation-tracker';
|
|
6
6
|
export { extractCurrentPhase, extractCurrentPhaseFromPlan, extractCurrentTask, extractCurrentTaskFromPlan, extractDecisions, extractIncompleteTasks, extractIncompleteTasksFromPlan, extractPatterns, } from './extractors';
|
|
7
7
|
export { createGuardrailsHooks } from './guardrails';
|
|
8
8
|
export { createPipelineTrackerHook } from './pipeline-tracker';
|
|
9
9
|
export { createSystemEnhancerHook } from './system-enhancer';
|
|
10
|
+
export { createToolSummarizerHook, resetSummaryIdCounter, } from './tool-summarizer';
|
|
10
11
|
export { composeHandlers, estimateTokens, readSwarmFileAsync, safeHook, validateSwarmPath, } from './utils';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Output Summarizer Hook
|
|
3
|
+
*
|
|
4
|
+
* Intercepts oversized tool outputs in tool.execute.after,
|
|
5
|
+
* stores the full content to .swarm/summaries/, and replaces
|
|
6
|
+
* the output with a compact summary containing a retrieval ID.
|
|
7
|
+
*/
|
|
8
|
+
import type { SummaryConfig } from '../config/schema';
|
|
9
|
+
/**
|
|
10
|
+
* Reset the summary ID counter. Used for testing.
|
|
11
|
+
*/
|
|
12
|
+
export declare function resetSummaryIdCounter(): void;
|
|
13
|
+
/**
|
|
14
|
+
* Creates a tool.execute.after hook that summarizes oversized tool outputs.
|
|
15
|
+
*
|
|
16
|
+
* @param config - Summary configuration including enabled, thresholds, and limits
|
|
17
|
+
* @param directory - Base directory for storing full outputs
|
|
18
|
+
* @returns Async hook function for tool.execute.after
|
|
19
|
+
*/
|
|
20
|
+
export declare function createToolSummarizerHook(config: SummaryConfig, directory: string): (input: {
|
|
21
|
+
tool: string;
|
|
22
|
+
sessionID: string;
|
|
23
|
+
callID: string;
|
|
24
|
+
}, output: {
|
|
25
|
+
title: string;
|
|
26
|
+
output: string;
|
|
27
|
+
metadata: unknown;
|
|
28
|
+
}) => Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -13623,6 +13623,13 @@ var EvidenceConfigSchema = exports_external.object({
|
|
|
13623
13623
|
max_bundles: exports_external.number().min(10).max(1e4).default(1000),
|
|
13624
13624
|
auto_archive: exports_external.boolean().default(false)
|
|
13625
13625
|
});
|
|
13626
|
+
var SummaryConfigSchema = exports_external.object({
|
|
13627
|
+
enabled: exports_external.boolean().default(true),
|
|
13628
|
+
threshold_bytes: exports_external.number().min(1024).max(1048576).default(20480),
|
|
13629
|
+
max_summary_chars: exports_external.number().min(100).max(5000).default(1000),
|
|
13630
|
+
max_stored_bytes: exports_external.number().min(10240).max(104857600).default(10485760),
|
|
13631
|
+
retention_days: exports_external.number().min(1).max(365).default(7)
|
|
13632
|
+
});
|
|
13626
13633
|
var GuardrailsProfileSchema = exports_external.object({
|
|
13627
13634
|
max_tool_calls: exports_external.number().min(0).max(1000).optional(),
|
|
13628
13635
|
max_duration_minutes: exports_external.number().min(0).max(480).optional(),
|
|
@@ -13716,7 +13723,8 @@ var PluginConfigSchema = exports_external.object({
|
|
|
13716
13723
|
hooks: HooksConfigSchema.optional(),
|
|
13717
13724
|
context_budget: ContextBudgetConfigSchema.optional(),
|
|
13718
13725
|
guardrails: GuardrailsConfigSchema.optional(),
|
|
13719
|
-
evidence: EvidenceConfigSchema.optional()
|
|
13726
|
+
evidence: EvidenceConfigSchema.optional(),
|
|
13727
|
+
summaries: SummaryConfigSchema.optional()
|
|
13720
13728
|
});
|
|
13721
13729
|
|
|
13722
13730
|
// src/config/loader.ts
|
|
@@ -15014,6 +15022,282 @@ async function handleArchiveCommand(directory, args) {
|
|
|
15014
15022
|
`);
|
|
15015
15023
|
}
|
|
15016
15024
|
|
|
15025
|
+
// src/state.ts
|
|
15026
|
+
var swarmState = {
|
|
15027
|
+
activeToolCalls: new Map,
|
|
15028
|
+
toolAggregates: new Map,
|
|
15029
|
+
activeAgent: new Map,
|
|
15030
|
+
delegationChains: new Map,
|
|
15031
|
+
pendingEvents: 0,
|
|
15032
|
+
agentSessions: new Map
|
|
15033
|
+
};
|
|
15034
|
+
function startAgentSession(sessionId, agentName, staleDurationMs = 7200000) {
|
|
15035
|
+
const now = Date.now();
|
|
15036
|
+
const staleIds = [];
|
|
15037
|
+
for (const [id, session] of swarmState.agentSessions) {
|
|
15038
|
+
if (now - session.lastToolCallTime > staleDurationMs) {
|
|
15039
|
+
staleIds.push(id);
|
|
15040
|
+
}
|
|
15041
|
+
}
|
|
15042
|
+
for (const id of staleIds) {
|
|
15043
|
+
swarmState.agentSessions.delete(id);
|
|
15044
|
+
}
|
|
15045
|
+
const sessionState = {
|
|
15046
|
+
agentName,
|
|
15047
|
+
startTime: now,
|
|
15048
|
+
lastToolCallTime: now,
|
|
15049
|
+
toolCallCount: 0,
|
|
15050
|
+
consecutiveErrors: 0,
|
|
15051
|
+
recentToolCalls: [],
|
|
15052
|
+
warningIssued: false,
|
|
15053
|
+
warningReason: "",
|
|
15054
|
+
hardLimitHit: false,
|
|
15055
|
+
lastSuccessTime: now,
|
|
15056
|
+
delegationActive: false
|
|
15057
|
+
};
|
|
15058
|
+
swarmState.agentSessions.set(sessionId, sessionState);
|
|
15059
|
+
}
|
|
15060
|
+
function getAgentSession(sessionId) {
|
|
15061
|
+
return swarmState.agentSessions.get(sessionId);
|
|
15062
|
+
}
|
|
15063
|
+
function ensureAgentSession(sessionId, agentName) {
|
|
15064
|
+
const now = Date.now();
|
|
15065
|
+
let session = swarmState.agentSessions.get(sessionId);
|
|
15066
|
+
if (session) {
|
|
15067
|
+
if (agentName && agentName !== session.agentName) {
|
|
15068
|
+
session.agentName = agentName;
|
|
15069
|
+
session.startTime = now;
|
|
15070
|
+
session.toolCallCount = 0;
|
|
15071
|
+
session.consecutiveErrors = 0;
|
|
15072
|
+
session.recentToolCalls = [];
|
|
15073
|
+
session.warningIssued = false;
|
|
15074
|
+
session.warningReason = "";
|
|
15075
|
+
session.hardLimitHit = false;
|
|
15076
|
+
session.lastSuccessTime = now;
|
|
15077
|
+
session.delegationActive = false;
|
|
15078
|
+
}
|
|
15079
|
+
session.lastToolCallTime = now;
|
|
15080
|
+
return session;
|
|
15081
|
+
}
|
|
15082
|
+
startAgentSession(sessionId, agentName ?? "unknown");
|
|
15083
|
+
session = swarmState.agentSessions.get(sessionId);
|
|
15084
|
+
if (!session) {
|
|
15085
|
+
throw new Error(`Failed to create guardrail session for ${sessionId}`);
|
|
15086
|
+
}
|
|
15087
|
+
return session;
|
|
15088
|
+
}
|
|
15089
|
+
|
|
15090
|
+
// src/commands/benchmark.ts
|
|
15091
|
+
var CI = {
|
|
15092
|
+
review_pass_rate: 70,
|
|
15093
|
+
test_pass_rate: 80,
|
|
15094
|
+
max_agent_error_rate: 20,
|
|
15095
|
+
max_hard_limit_hits: 1
|
|
15096
|
+
};
|
|
15097
|
+
async function handleBenchmarkCommand(directory, args) {
|
|
15098
|
+
let cumulative = args.includes("--cumulative");
|
|
15099
|
+
if (args.includes("--ci-gate"))
|
|
15100
|
+
cumulative = true;
|
|
15101
|
+
const mode = cumulative ? "cumulative" : "in-memory";
|
|
15102
|
+
const agentMap = new Map;
|
|
15103
|
+
for (const [, s] of swarmState.agentSessions) {
|
|
15104
|
+
const e = agentMap.get(s.agentName) || {
|
|
15105
|
+
toolCalls: 0,
|
|
15106
|
+
hardLimits: 0,
|
|
15107
|
+
warnings: 0
|
|
15108
|
+
};
|
|
15109
|
+
e.toolCalls += s.toolCallCount;
|
|
15110
|
+
if (s.hardLimitHit)
|
|
15111
|
+
e.hardLimits++;
|
|
15112
|
+
if (s.warningIssued)
|
|
15113
|
+
e.warnings++;
|
|
15114
|
+
agentMap.set(s.agentName, e);
|
|
15115
|
+
}
|
|
15116
|
+
const agentHealth = Array.from(agentMap.entries()).map(([a, v]) => ({
|
|
15117
|
+
agent: a,
|
|
15118
|
+
...v
|
|
15119
|
+
}));
|
|
15120
|
+
const toolPerf = [];
|
|
15121
|
+
for (const [, a] of swarmState.toolAggregates) {
|
|
15122
|
+
const successRate = a.count ? a.successCount / a.count * 100 : 0;
|
|
15123
|
+
toolPerf.push({
|
|
15124
|
+
tool: a.tool,
|
|
15125
|
+
calls: a.count,
|
|
15126
|
+
successRate: Math.round(successRate * 10) / 10,
|
|
15127
|
+
avg: a.count ? Math.round(a.totalDuration / a.count) : 0
|
|
15128
|
+
});
|
|
15129
|
+
}
|
|
15130
|
+
toolPerf.sort((a, b) => b.calls - a.calls);
|
|
15131
|
+
let delegationCount = 0;
|
|
15132
|
+
for (const c of swarmState.delegationChains.values())
|
|
15133
|
+
delegationCount += c.length;
|
|
15134
|
+
let quality;
|
|
15135
|
+
if (cumulative) {
|
|
15136
|
+
let reviewPasses = 0, reviewFails = 0, testPasses = 0, testFails = 0, additions = 0, deletions = 0;
|
|
15137
|
+
for (const tid of await listEvidenceTaskIds(directory)) {
|
|
15138
|
+
const b = await loadEvidence(directory, tid);
|
|
15139
|
+
if (!b)
|
|
15140
|
+
continue;
|
|
15141
|
+
for (const e of b.entries) {
|
|
15142
|
+
if (e.type === "review") {
|
|
15143
|
+
if (e.verdict === "approved")
|
|
15144
|
+
reviewPasses++;
|
|
15145
|
+
else if (e.verdict === "rejected")
|
|
15146
|
+
reviewFails++;
|
|
15147
|
+
} else if (e.type === "test") {
|
|
15148
|
+
testPasses += e.tests_passed;
|
|
15149
|
+
testFails += e.tests_failed;
|
|
15150
|
+
} else if (e.type === "diff") {
|
|
15151
|
+
additions += e.additions;
|
|
15152
|
+
deletions += e.deletions;
|
|
15153
|
+
}
|
|
15154
|
+
}
|
|
15155
|
+
}
|
|
15156
|
+
const totalReviews = reviewPasses + reviewFails, totalTests = testPasses + testFails;
|
|
15157
|
+
quality = {
|
|
15158
|
+
reviewPassRate: totalReviews ? Math.round(reviewPasses / totalReviews * 1000) / 10 : null,
|
|
15159
|
+
testPassRate: totalTests ? Math.round(testPasses / totalTests * 1000) / 10 : null,
|
|
15160
|
+
totalReviews,
|
|
15161
|
+
testsPassed: testPasses,
|
|
15162
|
+
testsFailed: testFails,
|
|
15163
|
+
additions,
|
|
15164
|
+
deletions
|
|
15165
|
+
};
|
|
15166
|
+
}
|
|
15167
|
+
let ciGate;
|
|
15168
|
+
if (args.includes("--ci-gate")) {
|
|
15169
|
+
let totalCalls = 0, totalFailures = 0;
|
|
15170
|
+
for (const [, a] of swarmState.toolAggregates) {
|
|
15171
|
+
totalCalls += a.count;
|
|
15172
|
+
totalFailures += a.failureCount;
|
|
15173
|
+
}
|
|
15174
|
+
const agentErrorRate = totalCalls ? totalFailures / totalCalls * 100 : 0;
|
|
15175
|
+
let maxHardLimits = 0;
|
|
15176
|
+
for (const v of agentMap.values())
|
|
15177
|
+
if (v.hardLimits > maxHardLimits)
|
|
15178
|
+
maxHardLimits = v.hardLimits;
|
|
15179
|
+
const checks3 = [
|
|
15180
|
+
{
|
|
15181
|
+
name: "Review pass rate",
|
|
15182
|
+
value: quality?.reviewPassRate ?? 0,
|
|
15183
|
+
threshold: CI.review_pass_rate,
|
|
15184
|
+
operator: ">=",
|
|
15185
|
+
passed: (quality?.reviewPassRate ?? 0) >= CI.review_pass_rate
|
|
15186
|
+
},
|
|
15187
|
+
{
|
|
15188
|
+
name: "Test pass rate",
|
|
15189
|
+
value: quality?.testPassRate ?? 0,
|
|
15190
|
+
threshold: CI.test_pass_rate,
|
|
15191
|
+
operator: ">=",
|
|
15192
|
+
passed: (quality?.testPassRate ?? 0) >= CI.test_pass_rate
|
|
15193
|
+
},
|
|
15194
|
+
{
|
|
15195
|
+
name: "Agent error rate",
|
|
15196
|
+
value: Math.round(agentErrorRate * 10) / 10,
|
|
15197
|
+
threshold: CI.max_agent_error_rate,
|
|
15198
|
+
operator: "<=",
|
|
15199
|
+
passed: agentErrorRate <= CI.max_agent_error_rate
|
|
15200
|
+
},
|
|
15201
|
+
{
|
|
15202
|
+
name: "Hard limit hits",
|
|
15203
|
+
value: maxHardLimits,
|
|
15204
|
+
threshold: CI.max_hard_limit_hits,
|
|
15205
|
+
operator: "<=",
|
|
15206
|
+
passed: maxHardLimits <= CI.max_hard_limit_hits
|
|
15207
|
+
}
|
|
15208
|
+
];
|
|
15209
|
+
ciGate = { passed: checks3.every((c) => c.passed), checks: checks3 };
|
|
15210
|
+
}
|
|
15211
|
+
const lines = [
|
|
15212
|
+
`## Swarm Benchmark (mode: ${mode})`,
|
|
15213
|
+
"",
|
|
15214
|
+
"### Agent Health"
|
|
15215
|
+
];
|
|
15216
|
+
if (!agentHealth.length)
|
|
15217
|
+
lines.push("No agent sessions recorded");
|
|
15218
|
+
else
|
|
15219
|
+
for (const { agent, toolCalls, hardLimits, warnings } of agentHealth) {
|
|
15220
|
+
const parts = [`${toolCalls} tool calls`];
|
|
15221
|
+
if (warnings > 0)
|
|
15222
|
+
parts.push(`${warnings} warning${warnings > 1 ? "s" : ""}`);
|
|
15223
|
+
parts.push(hardLimits ? `${hardLimits} hard limit hit${hardLimits > 1 ? "s" : ""}` : "0 hard limits");
|
|
15224
|
+
lines.push(`- ${hardLimits ? "\u26A0\uFE0F" : "\u2705"} **${agent}**: ${parts.join(", ")}`);
|
|
15225
|
+
}
|
|
15226
|
+
lines.push("", "### Tool Performance");
|
|
15227
|
+
if (!toolPerf.length)
|
|
15228
|
+
lines.push("No tool data recorded");
|
|
15229
|
+
else {
|
|
15230
|
+
lines.push("| Tool | Calls | Success Rate | Avg Duration |", "|------|-------|-------------|-------------|");
|
|
15231
|
+
for (const { tool, calls, successRate, avg } of toolPerf)
|
|
15232
|
+
lines.push(`| ${tool} | ${calls} | ${successRate}% | ${avg}ms |`);
|
|
15233
|
+
}
|
|
15234
|
+
lines.push("", "### Delegations", delegationCount ? `Total: ${delegationCount} delegations` : "No delegations recorded", "");
|
|
15235
|
+
if (quality) {
|
|
15236
|
+
lines.push("### Quality Signals");
|
|
15237
|
+
if (!quality.totalReviews && !quality.testsPassed && !quality.additions)
|
|
15238
|
+
lines.push("No evidence data found");
|
|
15239
|
+
else {
|
|
15240
|
+
if (quality.reviewPassRate !== null)
|
|
15241
|
+
lines.push(`- Review pass rate: ${quality.reviewPassRate}% (${quality.totalReviews}) ${quality.reviewPassRate >= 70 ? "\u2705" : "\u274C"}`);
|
|
15242
|
+
else
|
|
15243
|
+
lines.push("- Review pass rate: N/A (no reviews)");
|
|
15244
|
+
if (quality.testPassRate !== null)
|
|
15245
|
+
lines.push(`- Test pass rate: ${quality.testPassRate}% (${quality.testsPassed}/${quality.testsPassed + quality.testsFailed}) ${quality.testPassRate >= 80 ? "\u2705" : "\u274C"}`);
|
|
15246
|
+
else
|
|
15247
|
+
lines.push("- Test pass rate: N/A (no tests)");
|
|
15248
|
+
lines.push(`- Code churn: +${quality.additions} / -${quality.deletions} lines`);
|
|
15249
|
+
}
|
|
15250
|
+
lines.push("");
|
|
15251
|
+
}
|
|
15252
|
+
if (ciGate) {
|
|
15253
|
+
lines.push("### CI Gate", ciGate.passed ? "\u2705 PASSED" : "\u274C FAILED");
|
|
15254
|
+
for (const c of ciGate.checks)
|
|
15255
|
+
lines.push(`- ${c.name}: ${c.value}% ${c.operator} ${c.threshold}% ${c.passed ? "\u2705" : "\u274C"}`);
|
|
15256
|
+
lines.push("");
|
|
15257
|
+
}
|
|
15258
|
+
const json2 = {
|
|
15259
|
+
mode,
|
|
15260
|
+
timestamp: new Date().toISOString(),
|
|
15261
|
+
agent_health: agentHealth.map((a) => ({
|
|
15262
|
+
agent: a.agent,
|
|
15263
|
+
tool_calls: a.toolCalls,
|
|
15264
|
+
hard_limit_hits: a.hardLimits,
|
|
15265
|
+
warnings: a.warnings
|
|
15266
|
+
})),
|
|
15267
|
+
tool_performance: toolPerf.map((t) => ({
|
|
15268
|
+
tool: t.tool,
|
|
15269
|
+
calls: t.calls,
|
|
15270
|
+
success_rate: t.successRate,
|
|
15271
|
+
avg_duration_ms: t.avg
|
|
15272
|
+
})),
|
|
15273
|
+
delegations: delegationCount
|
|
15274
|
+
};
|
|
15275
|
+
if (quality)
|
|
15276
|
+
json2.quality = {
|
|
15277
|
+
review_pass_rate: quality.reviewPassRate,
|
|
15278
|
+
test_pass_rate: quality.testPassRate,
|
|
15279
|
+
total_reviews: quality.totalReviews,
|
|
15280
|
+
total_tests_passed: quality.testsPassed,
|
|
15281
|
+
total_tests_failed: quality.testsFailed,
|
|
15282
|
+
additions: quality.additions,
|
|
15283
|
+
deletions: quality.deletions
|
|
15284
|
+
};
|
|
15285
|
+
if (ciGate)
|
|
15286
|
+
json2.ci_gate = {
|
|
15287
|
+
passed: ciGate.passed,
|
|
15288
|
+
checks: ciGate.checks.map((c) => ({
|
|
15289
|
+
name: c.name,
|
|
15290
|
+
value: c.value,
|
|
15291
|
+
threshold: c.threshold,
|
|
15292
|
+
operator: c.operator,
|
|
15293
|
+
passed: c.passed
|
|
15294
|
+
}))
|
|
15295
|
+
};
|
|
15296
|
+
lines.push("[BENCHMARK_JSON]", JSON.stringify(json2, null, 2), "[/BENCHMARK_JSON]");
|
|
15297
|
+
return lines.join(`
|
|
15298
|
+
`);
|
|
15299
|
+
}
|
|
15300
|
+
|
|
15017
15301
|
// src/commands/config.ts
|
|
15018
15302
|
import * as os2 from "os";
|
|
15019
15303
|
import * as path4 from "path";
|
|
@@ -15724,6 +16008,17 @@ async function handleResetCommand(directory, args) {
|
|
|
15724
16008
|
results.push(`- \u274C Failed to delete ${filename}`);
|
|
15725
16009
|
}
|
|
15726
16010
|
}
|
|
16011
|
+
try {
|
|
16012
|
+
const summariesPath = validateSwarmPath(directory, "summaries");
|
|
16013
|
+
if (fs2.existsSync(summariesPath)) {
|
|
16014
|
+
fs2.rmSync(summariesPath, { recursive: true, force: true });
|
|
16015
|
+
results.push("- \u2705 Deleted summaries/ directory");
|
|
16016
|
+
} else {
|
|
16017
|
+
results.push("- \u23ED\uFE0F summaries/ not found (skipped)");
|
|
16018
|
+
}
|
|
16019
|
+
} catch {
|
|
16020
|
+
results.push("- \u274C Failed to delete summaries/");
|
|
16021
|
+
}
|
|
15727
16022
|
return [
|
|
15728
16023
|
"## Swarm Reset Complete",
|
|
15729
16024
|
"",
|
|
@@ -15734,6 +16029,112 @@ async function handleResetCommand(directory, args) {
|
|
|
15734
16029
|
`);
|
|
15735
16030
|
}
|
|
15736
16031
|
|
|
16032
|
+
// src/summaries/manager.ts
|
|
16033
|
+
import { mkdirSync as mkdirSync2, readdirSync as readdirSync2, renameSync as renameSync2, rmSync as rmSync3, statSync as statSync3 } from "fs";
|
|
16034
|
+
import * as path6 from "path";
|
|
16035
|
+
var SUMMARY_ID_REGEX = /^S\d+$/;
|
|
16036
|
+
function sanitizeSummaryId(id) {
|
|
16037
|
+
if (!id || id.length === 0) {
|
|
16038
|
+
throw new Error("Invalid summary ID: empty string");
|
|
16039
|
+
}
|
|
16040
|
+
if (/\0/.test(id)) {
|
|
16041
|
+
throw new Error("Invalid summary ID: contains null bytes");
|
|
16042
|
+
}
|
|
16043
|
+
for (let i = 0;i < id.length; i++) {
|
|
16044
|
+
if (id.charCodeAt(i) < 32) {
|
|
16045
|
+
throw new Error("Invalid summary ID: contains control characters");
|
|
16046
|
+
}
|
|
16047
|
+
}
|
|
16048
|
+
if (id.includes("..") || id.includes("../") || id.includes("..\\")) {
|
|
16049
|
+
throw new Error("Invalid summary ID: path traversal detected");
|
|
16050
|
+
}
|
|
16051
|
+
if (!SUMMARY_ID_REGEX.test(id)) {
|
|
16052
|
+
throw new Error(`Invalid summary ID: must match pattern ^S\\d+$, got "${id}"`);
|
|
16053
|
+
}
|
|
16054
|
+
return id;
|
|
16055
|
+
}
|
|
16056
|
+
async function storeSummary(directory, id, fullOutput, summaryText, maxStoredBytes) {
|
|
16057
|
+
const sanitizedId = sanitizeSummaryId(id);
|
|
16058
|
+
const outputBytes = Buffer.byteLength(fullOutput, "utf8");
|
|
16059
|
+
if (outputBytes > maxStoredBytes) {
|
|
16060
|
+
throw new Error(`Summary fullOutput size (${outputBytes} bytes) exceeds maximum (${maxStoredBytes} bytes)`);
|
|
16061
|
+
}
|
|
16062
|
+
const relativePath = path6.join("summaries", `${sanitizedId}.json`);
|
|
16063
|
+
const summaryPath = validateSwarmPath(directory, relativePath);
|
|
16064
|
+
const summaryDir = path6.dirname(summaryPath);
|
|
16065
|
+
const entry = {
|
|
16066
|
+
id: sanitizedId,
|
|
16067
|
+
summaryText,
|
|
16068
|
+
fullOutput,
|
|
16069
|
+
timestamp: Date.now(),
|
|
16070
|
+
originalBytes: outputBytes
|
|
16071
|
+
};
|
|
16072
|
+
const entryJson = JSON.stringify(entry);
|
|
16073
|
+
mkdirSync2(summaryDir, { recursive: true });
|
|
16074
|
+
const tempPath = path6.join(summaryDir, `${sanitizedId}.json.tmp.${Date.now()}.${process.pid}`);
|
|
16075
|
+
try {
|
|
16076
|
+
await Bun.write(tempPath, entryJson);
|
|
16077
|
+
renameSync2(tempPath, summaryPath);
|
|
16078
|
+
} catch (error49) {
|
|
16079
|
+
try {
|
|
16080
|
+
rmSync3(tempPath, { force: true });
|
|
16081
|
+
} catch {}
|
|
16082
|
+
throw error49;
|
|
16083
|
+
}
|
|
16084
|
+
}
|
|
16085
|
+
async function loadFullOutput(directory, id) {
|
|
16086
|
+
const sanitizedId = sanitizeSummaryId(id);
|
|
16087
|
+
const relativePath = path6.join("summaries", `${sanitizedId}.json`);
|
|
16088
|
+
validateSwarmPath(directory, relativePath);
|
|
16089
|
+
const content = await readSwarmFileAsync(directory, relativePath);
|
|
16090
|
+
if (content === null) {
|
|
16091
|
+
return null;
|
|
16092
|
+
}
|
|
16093
|
+
try {
|
|
16094
|
+
const parsed = JSON.parse(content);
|
|
16095
|
+
if (typeof parsed.fullOutput === "string") {
|
|
16096
|
+
return parsed.fullOutput;
|
|
16097
|
+
}
|
|
16098
|
+
warn(`Summary entry ${sanitizedId} missing valid fullOutput field`);
|
|
16099
|
+
return null;
|
|
16100
|
+
} catch (error49) {
|
|
16101
|
+
warn(`Summary entry validation failed for ${sanitizedId}: ${error49 instanceof Error ? error49.message : String(error49)}`);
|
|
16102
|
+
return null;
|
|
16103
|
+
}
|
|
16104
|
+
}
|
|
16105
|
+
|
|
16106
|
+
// src/commands/retrieve.ts
|
|
16107
|
+
async function handleRetrieveCommand(directory, args) {
|
|
16108
|
+
const summaryId = args[0];
|
|
16109
|
+
if (!summaryId) {
|
|
16110
|
+
return [
|
|
16111
|
+
"## Swarm Retrieve",
|
|
16112
|
+
"",
|
|
16113
|
+
"Usage: `/swarm retrieve <id>`",
|
|
16114
|
+
"",
|
|
16115
|
+
"Example: `/swarm retrieve S1`",
|
|
16116
|
+
"",
|
|
16117
|
+
"Retrieves the full output that was replaced by a summary."
|
|
16118
|
+
].join(`
|
|
16119
|
+
`);
|
|
16120
|
+
}
|
|
16121
|
+
try {
|
|
16122
|
+
const fullOutput = await loadFullOutput(directory, summaryId);
|
|
16123
|
+
if (fullOutput === null) {
|
|
16124
|
+
return `## Summary Not Found
|
|
16125
|
+
|
|
16126
|
+
No stored output found for ID \`${summaryId}\`.
|
|
16127
|
+
|
|
16128
|
+
Use a valid summary ID (e.g., S1, S2, S3).`;
|
|
16129
|
+
}
|
|
16130
|
+
return fullOutput;
|
|
16131
|
+
} catch (error49) {
|
|
16132
|
+
return `## Retrieve Failed
|
|
16133
|
+
|
|
16134
|
+
${error49 instanceof Error ? error49.message : String(error49)}`;
|
|
16135
|
+
}
|
|
16136
|
+
}
|
|
16137
|
+
|
|
15737
16138
|
// src/hooks/extractors.ts
|
|
15738
16139
|
function extractCurrentPhase(planContent) {
|
|
15739
16140
|
if (!planContent) {
|
|
@@ -15982,8 +16383,10 @@ var HELP_TEXT = [
|
|
|
15982
16383
|
"- `/swarm evidence [taskId]` \u2014 Show evidence bundles",
|
|
15983
16384
|
"- `/swarm archive [--dry-run]` \u2014 Archive old evidence bundles",
|
|
15984
16385
|
"- `/swarm diagnose` \u2014 Run health check on swarm state",
|
|
16386
|
+
"- `/swarm benchmark [--cumulative] [--ci-gate]` \u2014 Show performance metrics",
|
|
15985
16387
|
"- `/swarm export` \u2014 Export plan and context as JSON",
|
|
15986
|
-
"- `/swarm reset --confirm` \u2014 Clear swarm state files"
|
|
16388
|
+
"- `/swarm reset --confirm` \u2014 Clear swarm state files",
|
|
16389
|
+
"- `/swarm retrieve <id>` \u2014 Retrieve full output from a summary"
|
|
15987
16390
|
].join(`
|
|
15988
16391
|
`);
|
|
15989
16392
|
function createSwarmCommandHandler(directory, agents) {
|
|
@@ -16022,12 +16425,18 @@ function createSwarmCommandHandler(directory, agents) {
|
|
|
16022
16425
|
case "diagnose":
|
|
16023
16426
|
text = await handleDiagnoseCommand(directory, args);
|
|
16024
16427
|
break;
|
|
16428
|
+
case "benchmark":
|
|
16429
|
+
text = await handleBenchmarkCommand(directory, args);
|
|
16430
|
+
break;
|
|
16025
16431
|
case "export":
|
|
16026
16432
|
text = await handleExportCommand(directory, args);
|
|
16027
16433
|
break;
|
|
16028
16434
|
case "reset":
|
|
16029
16435
|
text = await handleResetCommand(directory, args);
|
|
16030
16436
|
break;
|
|
16437
|
+
case "retrieve":
|
|
16438
|
+
text = await handleRetrieveCommand(directory, args);
|
|
16439
|
+
break;
|
|
16031
16440
|
default:
|
|
16032
16441
|
text = HELP_TEXT;
|
|
16033
16442
|
break;
|
|
@@ -16038,71 +16447,6 @@ function createSwarmCommandHandler(directory, agents) {
|
|
|
16038
16447
|
};
|
|
16039
16448
|
}
|
|
16040
16449
|
|
|
16041
|
-
// src/state.ts
|
|
16042
|
-
var swarmState = {
|
|
16043
|
-
activeToolCalls: new Map,
|
|
16044
|
-
toolAggregates: new Map,
|
|
16045
|
-
activeAgent: new Map,
|
|
16046
|
-
delegationChains: new Map,
|
|
16047
|
-
pendingEvents: 0,
|
|
16048
|
-
agentSessions: new Map
|
|
16049
|
-
};
|
|
16050
|
-
function startAgentSession(sessionId, agentName, staleDurationMs = 7200000) {
|
|
16051
|
-
const now = Date.now();
|
|
16052
|
-
const staleIds = [];
|
|
16053
|
-
for (const [id, session] of swarmState.agentSessions) {
|
|
16054
|
-
if (now - session.lastToolCallTime > staleDurationMs) {
|
|
16055
|
-
staleIds.push(id);
|
|
16056
|
-
}
|
|
16057
|
-
}
|
|
16058
|
-
for (const id of staleIds) {
|
|
16059
|
-
swarmState.agentSessions.delete(id);
|
|
16060
|
-
}
|
|
16061
|
-
const sessionState = {
|
|
16062
|
-
agentName,
|
|
16063
|
-
startTime: now,
|
|
16064
|
-
lastToolCallTime: now,
|
|
16065
|
-
toolCallCount: 0,
|
|
16066
|
-
consecutiveErrors: 0,
|
|
16067
|
-
recentToolCalls: [],
|
|
16068
|
-
warningIssued: false,
|
|
16069
|
-
warningReason: "",
|
|
16070
|
-
hardLimitHit: false,
|
|
16071
|
-
lastSuccessTime: now,
|
|
16072
|
-
delegationActive: false
|
|
16073
|
-
};
|
|
16074
|
-
swarmState.agentSessions.set(sessionId, sessionState);
|
|
16075
|
-
}
|
|
16076
|
-
function getAgentSession(sessionId) {
|
|
16077
|
-
return swarmState.agentSessions.get(sessionId);
|
|
16078
|
-
}
|
|
16079
|
-
function ensureAgentSession(sessionId, agentName) {
|
|
16080
|
-
const now = Date.now();
|
|
16081
|
-
let session = swarmState.agentSessions.get(sessionId);
|
|
16082
|
-
if (session) {
|
|
16083
|
-
if (agentName && agentName !== session.agentName) {
|
|
16084
|
-
session.agentName = agentName;
|
|
16085
|
-
session.startTime = now;
|
|
16086
|
-
session.toolCallCount = 0;
|
|
16087
|
-
session.consecutiveErrors = 0;
|
|
16088
|
-
session.recentToolCalls = [];
|
|
16089
|
-
session.warningIssued = false;
|
|
16090
|
-
session.warningReason = "";
|
|
16091
|
-
session.hardLimitHit = false;
|
|
16092
|
-
session.lastSuccessTime = now;
|
|
16093
|
-
session.delegationActive = false;
|
|
16094
|
-
}
|
|
16095
|
-
session.lastToolCallTime = now;
|
|
16096
|
-
return session;
|
|
16097
|
-
}
|
|
16098
|
-
startAgentSession(sessionId, agentName ?? "unknown");
|
|
16099
|
-
session = swarmState.agentSessions.get(sessionId);
|
|
16100
|
-
if (!session) {
|
|
16101
|
-
throw new Error(`Failed to create guardrail session for ${sessionId}`);
|
|
16102
|
-
}
|
|
16103
|
-
return session;
|
|
16104
|
-
}
|
|
16105
|
-
|
|
16106
16450
|
// src/hooks/agent-activity.ts
|
|
16107
16451
|
function createAgentActivityHooks(config2, directory) {
|
|
16108
16452
|
if (config2.hooks?.agent_activity === false) {
|
|
@@ -16171,8 +16515,8 @@ async function doFlush(directory) {
|
|
|
16171
16515
|
const activitySection = renderActivitySection();
|
|
16172
16516
|
const updated = replaceOrAppendSection(existing, "## Agent Activity", activitySection);
|
|
16173
16517
|
const flushedCount = swarmState.pendingEvents;
|
|
16174
|
-
const
|
|
16175
|
-
await Bun.write(
|
|
16518
|
+
const path7 = `${directory}/.swarm/context.md`;
|
|
16519
|
+
await Bun.write(path7, updated);
|
|
16176
16520
|
swarmState.pendingEvents = Math.max(0, swarmState.pendingEvents - flushedCount);
|
|
16177
16521
|
} catch (error49) {
|
|
16178
16522
|
warn("Agent activity flush failed:", error49);
|
|
@@ -16322,36 +16666,6 @@ function createContextBudgetHandler(config2) {
|
|
|
16322
16666
|
}
|
|
16323
16667
|
};
|
|
16324
16668
|
}
|
|
16325
|
-
// src/hooks/delegation-tracker.ts
|
|
16326
|
-
function createDelegationTrackerHook(config2) {
|
|
16327
|
-
return async (input, _output) => {
|
|
16328
|
-
if (!input.agent || input.agent === "") {
|
|
16329
|
-
const session2 = swarmState.agentSessions.get(input.sessionID);
|
|
16330
|
-
if (session2) {
|
|
16331
|
-
session2.delegationActive = false;
|
|
16332
|
-
}
|
|
16333
|
-
return;
|
|
16334
|
-
}
|
|
16335
|
-
const agentName = input.agent;
|
|
16336
|
-
const previousAgent = swarmState.activeAgent.get(input.sessionID);
|
|
16337
|
-
swarmState.activeAgent.set(input.sessionID, agentName);
|
|
16338
|
-
const session = ensureAgentSession(input.sessionID, agentName);
|
|
16339
|
-
session.delegationActive = true;
|
|
16340
|
-
if (config2.hooks?.delegation_tracker === true && previousAgent && previousAgent !== agentName) {
|
|
16341
|
-
const entry = {
|
|
16342
|
-
from: previousAgent,
|
|
16343
|
-
to: agentName,
|
|
16344
|
-
timestamp: Date.now()
|
|
16345
|
-
};
|
|
16346
|
-
if (!swarmState.delegationChains.has(input.sessionID)) {
|
|
16347
|
-
swarmState.delegationChains.set(input.sessionID, []);
|
|
16348
|
-
}
|
|
16349
|
-
const chain = swarmState.delegationChains.get(input.sessionID);
|
|
16350
|
-
chain?.push(entry);
|
|
16351
|
-
swarmState.pendingEvents++;
|
|
16352
|
-
}
|
|
16353
|
-
};
|
|
16354
|
-
}
|
|
16355
16669
|
// src/hooks/delegation-gate.ts
|
|
16356
16670
|
function createDelegationGateHook(config2) {
|
|
16357
16671
|
const enabled = config2.hooks?.delegation_gate !== false;
|
|
@@ -16416,6 +16730,38 @@ Split into smaller, atomic tasks for better results.]`;
|
|
|
16416
16730
|
${originalText}`;
|
|
16417
16731
|
};
|
|
16418
16732
|
}
|
|
16733
|
+
// src/hooks/delegation-tracker.ts
|
|
16734
|
+
function createDelegationTrackerHook(config2) {
|
|
16735
|
+
return async (input, _output) => {
|
|
16736
|
+
if (!input.agent || input.agent === "") {
|
|
16737
|
+
const session2 = swarmState.agentSessions.get(input.sessionID);
|
|
16738
|
+
if (session2) {
|
|
16739
|
+
session2.delegationActive = false;
|
|
16740
|
+
}
|
|
16741
|
+
swarmState.activeAgent.set(input.sessionID, ORCHESTRATOR_NAME);
|
|
16742
|
+
ensureAgentSession(input.sessionID, ORCHESTRATOR_NAME);
|
|
16743
|
+
return;
|
|
16744
|
+
}
|
|
16745
|
+
const agentName = input.agent;
|
|
16746
|
+
const previousAgent = swarmState.activeAgent.get(input.sessionID);
|
|
16747
|
+
swarmState.activeAgent.set(input.sessionID, agentName);
|
|
16748
|
+
const session = ensureAgentSession(input.sessionID, agentName);
|
|
16749
|
+
session.delegationActive = true;
|
|
16750
|
+
if (config2.hooks?.delegation_tracker === true && previousAgent && previousAgent !== agentName) {
|
|
16751
|
+
const entry = {
|
|
16752
|
+
from: previousAgent,
|
|
16753
|
+
to: agentName,
|
|
16754
|
+
timestamp: Date.now()
|
|
16755
|
+
};
|
|
16756
|
+
if (!swarmState.delegationChains.has(input.sessionID)) {
|
|
16757
|
+
swarmState.delegationChains.set(input.sessionID, []);
|
|
16758
|
+
}
|
|
16759
|
+
const chain = swarmState.delegationChains.get(input.sessionID);
|
|
16760
|
+
chain?.push(entry);
|
|
16761
|
+
swarmState.pendingEvents++;
|
|
16762
|
+
}
|
|
16763
|
+
};
|
|
16764
|
+
}
|
|
16419
16765
|
// src/hooks/guardrails.ts
|
|
16420
16766
|
function createGuardrailsHooks(config2) {
|
|
16421
16767
|
if (config2.enabled === false) {
|
|
@@ -16432,8 +16778,19 @@ function createGuardrailsHooks(config2) {
|
|
|
16432
16778
|
if (strippedAgent === ORCHESTRATOR_NAME) {
|
|
16433
16779
|
return;
|
|
16434
16780
|
}
|
|
16781
|
+
const existingSession = swarmState.agentSessions.get(input.sessionID);
|
|
16782
|
+
if (existingSession) {
|
|
16783
|
+
const sessionAgent = stripKnownSwarmPrefix(existingSession.agentName);
|
|
16784
|
+
if (sessionAgent === ORCHESTRATOR_NAME) {
|
|
16785
|
+
return;
|
|
16786
|
+
}
|
|
16787
|
+
}
|
|
16435
16788
|
const agentName = swarmState.activeAgent.get(input.sessionID);
|
|
16436
16789
|
const session = ensureAgentSession(input.sessionID, agentName);
|
|
16790
|
+
const resolvedName = stripKnownSwarmPrefix(session.agentName);
|
|
16791
|
+
if (resolvedName === ORCHESTRATOR_NAME) {
|
|
16792
|
+
return;
|
|
16793
|
+
}
|
|
16437
16794
|
const agentConfig = resolveGuardrailsConfig(config2, session.agentName);
|
|
16438
16795
|
if (session.hardLimitHit) {
|
|
16439
16796
|
throw new Error("\uD83D\uDED1 CIRCUIT BREAKER: Agent blocked. Hard limit was previously triggered. Stop making tool calls and return your progress summary.");
|
|
@@ -16762,6 +17119,7 @@ function createSystemEnhancerHook(config2, directory) {
|
|
|
16762
17119
|
}
|
|
16763
17120
|
}
|
|
16764
17121
|
}
|
|
17122
|
+
tryInject("[SWARM HINT] Large tool outputs may be auto-summarized. Use /swarm retrieve <id> to get the full content if needed.");
|
|
16765
17123
|
return;
|
|
16766
17124
|
}
|
|
16767
17125
|
const userScoringConfig = config2.context_budget?.scoring;
|
|
@@ -16887,6 +17245,154 @@ ${activitySection}`;
|
|
|
16887
17245
|
}
|
|
16888
17246
|
return contextSummary;
|
|
16889
17247
|
}
|
|
17248
|
+
// src/summaries/summarizer.ts
|
|
17249
|
+
var HYSTERESIS_FACTOR = 1.25;
|
|
17250
|
+
function detectContentType(output, toolName) {
|
|
17251
|
+
const trimmed = output.trim();
|
|
17252
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
17253
|
+
try {
|
|
17254
|
+
JSON.parse(trimmed);
|
|
17255
|
+
return "json";
|
|
17256
|
+
} catch {}
|
|
17257
|
+
}
|
|
17258
|
+
const codeToolNames = ["read", "cat", "grep", "bash"];
|
|
17259
|
+
const lowerToolName = toolName.toLowerCase();
|
|
17260
|
+
const toolSegments = lowerToolName.split(/[.\-_/]/);
|
|
17261
|
+
if (codeToolNames.some((name) => toolSegments.includes(name))) {
|
|
17262
|
+
return "code";
|
|
17263
|
+
}
|
|
17264
|
+
const codePatterns = [
|
|
17265
|
+
"function ",
|
|
17266
|
+
"const ",
|
|
17267
|
+
"import ",
|
|
17268
|
+
"export ",
|
|
17269
|
+
"class ",
|
|
17270
|
+
"def ",
|
|
17271
|
+
"return ",
|
|
17272
|
+
"=>"
|
|
17273
|
+
];
|
|
17274
|
+
const startsWithShebang = trimmed.startsWith("#!");
|
|
17275
|
+
if (codePatterns.some((pattern) => output.includes(pattern)) || startsWithShebang) {
|
|
17276
|
+
return "code";
|
|
17277
|
+
}
|
|
17278
|
+
const sampleSize = Math.min(1000, output.length);
|
|
17279
|
+
let nonPrintableCount = 0;
|
|
17280
|
+
for (let i = 0;i < sampleSize; i++) {
|
|
17281
|
+
const charCode = output.charCodeAt(i);
|
|
17282
|
+
if (charCode < 32 && charCode !== 9 && charCode !== 10 && charCode !== 13) {
|
|
17283
|
+
nonPrintableCount++;
|
|
17284
|
+
}
|
|
17285
|
+
}
|
|
17286
|
+
if (sampleSize > 0 && nonPrintableCount / sampleSize > 0.1) {
|
|
17287
|
+
return "binary";
|
|
17288
|
+
}
|
|
17289
|
+
return "text";
|
|
17290
|
+
}
|
|
17291
|
+
function shouldSummarize(output, thresholdBytes) {
|
|
17292
|
+
const byteLength = Buffer.byteLength(output, "utf8");
|
|
17293
|
+
return byteLength >= thresholdBytes * HYSTERESIS_FACTOR;
|
|
17294
|
+
}
|
|
17295
|
+
function formatBytes(bytes) {
|
|
17296
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
17297
|
+
let unitIndex = 0;
|
|
17298
|
+
let size = bytes;
|
|
17299
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
17300
|
+
size /= 1024;
|
|
17301
|
+
unitIndex++;
|
|
17302
|
+
}
|
|
17303
|
+
const formatted = unitIndex === 0 ? size.toString() : size.toFixed(1);
|
|
17304
|
+
return `${formatted} ${units[unitIndex]}`;
|
|
17305
|
+
}
|
|
17306
|
+
function createSummary(output, toolName, summaryId, maxSummaryChars) {
|
|
17307
|
+
const contentType = detectContentType(output, toolName);
|
|
17308
|
+
const lineCount = output.split(`
|
|
17309
|
+
`).length;
|
|
17310
|
+
const byteSize = Buffer.byteLength(output, "utf8");
|
|
17311
|
+
const formattedSize = formatBytes(byteSize);
|
|
17312
|
+
const headerLine = `[SUMMARY ${summaryId}] ${formattedSize} | ${contentType} | ${lineCount} lines`;
|
|
17313
|
+
const footerLine = `\u2192 Use /swarm retrieve ${summaryId} for full content`;
|
|
17314
|
+
const overhead = headerLine.length + 1 + footerLine.length + 1;
|
|
17315
|
+
const maxPreviewChars = maxSummaryChars - overhead;
|
|
17316
|
+
let preview;
|
|
17317
|
+
switch (contentType) {
|
|
17318
|
+
case "json": {
|
|
17319
|
+
try {
|
|
17320
|
+
const parsed = JSON.parse(output.trim());
|
|
17321
|
+
if (Array.isArray(parsed)) {
|
|
17322
|
+
preview = `[ ${parsed.length} items ]`;
|
|
17323
|
+
} else if (typeof parsed === "object" && parsed !== null) {
|
|
17324
|
+
const keys = Object.keys(parsed).slice(0, 3);
|
|
17325
|
+
preview = `{ ${keys.join(", ")}${Object.keys(parsed).length > 3 ? ", ..." : ""} }`;
|
|
17326
|
+
} else {
|
|
17327
|
+
const lines = output.split(`
|
|
17328
|
+
`).filter((line) => line.trim().length > 0).slice(0, 3);
|
|
17329
|
+
preview = lines.join(`
|
|
17330
|
+
`);
|
|
17331
|
+
}
|
|
17332
|
+
} catch {
|
|
17333
|
+
const lines = output.split(`
|
|
17334
|
+
`).filter((line) => line.trim().length > 0).slice(0, 3);
|
|
17335
|
+
preview = lines.join(`
|
|
17336
|
+
`);
|
|
17337
|
+
}
|
|
17338
|
+
break;
|
|
17339
|
+
}
|
|
17340
|
+
case "code": {
|
|
17341
|
+
const lines = output.split(`
|
|
17342
|
+
`).filter((line) => line.trim().length > 0).slice(0, 5);
|
|
17343
|
+
preview = lines.join(`
|
|
17344
|
+
`);
|
|
17345
|
+
break;
|
|
17346
|
+
}
|
|
17347
|
+
case "text": {
|
|
17348
|
+
const lines = output.split(`
|
|
17349
|
+
`).filter((line) => line.trim().length > 0).slice(0, 5);
|
|
17350
|
+
preview = lines.join(`
|
|
17351
|
+
`);
|
|
17352
|
+
break;
|
|
17353
|
+
}
|
|
17354
|
+
case "binary": {
|
|
17355
|
+
preview = `[Binary content - ${formattedSize}]`;
|
|
17356
|
+
break;
|
|
17357
|
+
}
|
|
17358
|
+
default: {
|
|
17359
|
+
const lines = output.split(`
|
|
17360
|
+
`).filter((line) => line.trim().length > 0).slice(0, 5);
|
|
17361
|
+
preview = lines.join(`
|
|
17362
|
+
`);
|
|
17363
|
+
}
|
|
17364
|
+
}
|
|
17365
|
+
if (preview.length > maxPreviewChars) {
|
|
17366
|
+
preview = preview.substring(0, maxPreviewChars - 3) + "...";
|
|
17367
|
+
}
|
|
17368
|
+
return `${headerLine}
|
|
17369
|
+
${preview}
|
|
17370
|
+
${footerLine}`;
|
|
17371
|
+
}
|
|
17372
|
+
|
|
17373
|
+
// src/hooks/tool-summarizer.ts
|
|
17374
|
+
var nextSummaryId = 1;
|
|
17375
|
+
function createToolSummarizerHook(config2, directory) {
|
|
17376
|
+
if (config2.enabled === false) {
|
|
17377
|
+
return async () => {};
|
|
17378
|
+
}
|
|
17379
|
+
return async (input, output) => {
|
|
17380
|
+
if (typeof output.output !== "string" || output.output.length === 0) {
|
|
17381
|
+
return;
|
|
17382
|
+
}
|
|
17383
|
+
if (!shouldSummarize(output.output, config2.threshold_bytes)) {
|
|
17384
|
+
return;
|
|
17385
|
+
}
|
|
17386
|
+
const summaryId = `S${nextSummaryId++}`;
|
|
17387
|
+
const summaryText = createSummary(output.output, input.tool, summaryId, config2.max_summary_chars);
|
|
17388
|
+
try {
|
|
17389
|
+
await storeSummary(directory, summaryId, output.output, summaryText, config2.max_stored_bytes);
|
|
17390
|
+
output.output = summaryText;
|
|
17391
|
+
} catch (error49) {
|
|
17392
|
+
warn(`Tool output summarization failed for ${summaryId}: ${error49 instanceof Error ? error49.message : String(error49)}`);
|
|
17393
|
+
}
|
|
17394
|
+
};
|
|
17395
|
+
}
|
|
16890
17396
|
// node_modules/@opencode-ai/plugin/node_modules/zod/v4/classic/external.js
|
|
16891
17397
|
var exports_external2 = {};
|
|
16892
17398
|
__export(exports_external2, {
|
|
@@ -17616,10 +18122,10 @@ function mergeDefs2(...defs) {
|
|
|
17616
18122
|
function cloneDef2(schema) {
|
|
17617
18123
|
return mergeDefs2(schema._zod.def);
|
|
17618
18124
|
}
|
|
17619
|
-
function getElementAtPath2(obj,
|
|
17620
|
-
if (!
|
|
18125
|
+
function getElementAtPath2(obj, path7) {
|
|
18126
|
+
if (!path7)
|
|
17621
18127
|
return obj;
|
|
17622
|
-
return
|
|
18128
|
+
return path7.reduce((acc, key) => acc?.[key], obj);
|
|
17623
18129
|
}
|
|
17624
18130
|
function promiseAllObject2(promisesObj) {
|
|
17625
18131
|
const keys = Object.keys(promisesObj);
|
|
@@ -17978,11 +18484,11 @@ function aborted2(x, startIndex = 0) {
|
|
|
17978
18484
|
}
|
|
17979
18485
|
return false;
|
|
17980
18486
|
}
|
|
17981
|
-
function prefixIssues2(
|
|
18487
|
+
function prefixIssues2(path7, issues) {
|
|
17982
18488
|
return issues.map((iss) => {
|
|
17983
18489
|
var _a2;
|
|
17984
18490
|
(_a2 = iss).path ?? (_a2.path = []);
|
|
17985
|
-
iss.path.unshift(
|
|
18491
|
+
iss.path.unshift(path7);
|
|
17986
18492
|
return iss;
|
|
17987
18493
|
});
|
|
17988
18494
|
}
|
|
@@ -18150,7 +18656,7 @@ function treeifyError2(error49, _mapper) {
|
|
|
18150
18656
|
return issue3.message;
|
|
18151
18657
|
};
|
|
18152
18658
|
const result = { errors: [] };
|
|
18153
|
-
const processError = (error50,
|
|
18659
|
+
const processError = (error50, path7 = []) => {
|
|
18154
18660
|
var _a2, _b;
|
|
18155
18661
|
for (const issue3 of error50.issues) {
|
|
18156
18662
|
if (issue3.code === "invalid_union" && issue3.errors.length) {
|
|
@@ -18160,7 +18666,7 @@ function treeifyError2(error49, _mapper) {
|
|
|
18160
18666
|
} else if (issue3.code === "invalid_element") {
|
|
18161
18667
|
processError({ issues: issue3.issues }, issue3.path);
|
|
18162
18668
|
} else {
|
|
18163
|
-
const fullpath = [...
|
|
18669
|
+
const fullpath = [...path7, ...issue3.path];
|
|
18164
18670
|
if (fullpath.length === 0) {
|
|
18165
18671
|
result.errors.push(mapper(issue3));
|
|
18166
18672
|
continue;
|
|
@@ -18192,8 +18698,8 @@ function treeifyError2(error49, _mapper) {
|
|
|
18192
18698
|
}
|
|
18193
18699
|
function toDotPath2(_path) {
|
|
18194
18700
|
const segs = [];
|
|
18195
|
-
const
|
|
18196
|
-
for (const seg of
|
|
18701
|
+
const path7 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
|
|
18702
|
+
for (const seg of path7) {
|
|
18197
18703
|
if (typeof seg === "number")
|
|
18198
18704
|
segs.push(`[${seg}]`);
|
|
18199
18705
|
else if (typeof seg === "symbol")
|
|
@@ -29389,7 +29895,7 @@ Use these as DOMAIN values when delegating to @sme.`;
|
|
|
29389
29895
|
});
|
|
29390
29896
|
// src/tools/file-extractor.ts
|
|
29391
29897
|
import * as fs3 from "fs";
|
|
29392
|
-
import * as
|
|
29898
|
+
import * as path7 from "path";
|
|
29393
29899
|
var EXT_MAP = {
|
|
29394
29900
|
python: ".py",
|
|
29395
29901
|
py: ".py",
|
|
@@ -29467,12 +29973,12 @@ var extract_code_blocks = tool({
|
|
|
29467
29973
|
if (prefix) {
|
|
29468
29974
|
filename = `${prefix}_${filename}`;
|
|
29469
29975
|
}
|
|
29470
|
-
let filepath =
|
|
29471
|
-
const base =
|
|
29472
|
-
const ext =
|
|
29976
|
+
let filepath = path7.join(targetDir, filename);
|
|
29977
|
+
const base = path7.basename(filepath, path7.extname(filepath));
|
|
29978
|
+
const ext = path7.extname(filepath);
|
|
29473
29979
|
let counter = 1;
|
|
29474
29980
|
while (fs3.existsSync(filepath)) {
|
|
29475
|
-
filepath =
|
|
29981
|
+
filepath = path7.join(targetDir, `${base}_${counter}${ext}`);
|
|
29476
29982
|
counter++;
|
|
29477
29983
|
}
|
|
29478
29984
|
try {
|
|
@@ -29596,6 +30102,8 @@ var OpenCodeSwarm = async (ctx) => {
|
|
|
29596
30102
|
const delegationGateHandler = createDelegationGateHook(config3);
|
|
29597
30103
|
const guardrailsConfig = GuardrailsConfigSchema.parse(config3.guardrails ?? {});
|
|
29598
30104
|
const guardrailsHooks = createGuardrailsHooks(guardrailsConfig);
|
|
30105
|
+
const summaryConfig = SummaryConfigSchema.parse(config3.summaries ?? {});
|
|
30106
|
+
const toolSummarizerHook = createToolSummarizerHook(summaryConfig, ctx.directory);
|
|
29599
30107
|
log("Plugin initialized", {
|
|
29600
30108
|
directory: ctx.directory,
|
|
29601
30109
|
maxIterations: config3.max_iterations,
|
|
@@ -29609,7 +30117,8 @@ var OpenCodeSwarm = async (ctx) => {
|
|
|
29609
30117
|
commands: true,
|
|
29610
30118
|
agentActivity: config3.hooks?.agent_activity !== false,
|
|
29611
30119
|
delegationTracker: config3.hooks?.delegation_tracker === true,
|
|
29612
|
-
guardrails: guardrailsConfig.enabled
|
|
30120
|
+
guardrails: guardrailsConfig.enabled,
|
|
30121
|
+
toolSummarizer: summaryConfig.enabled
|
|
29613
30122
|
}
|
|
29614
30123
|
});
|
|
29615
30124
|
return {
|
|
@@ -29653,14 +30162,20 @@ var OpenCodeSwarm = async (ctx) => {
|
|
|
29653
30162
|
}
|
|
29654
30163
|
const session = swarmState.agentSessions.get(input.sessionID);
|
|
29655
30164
|
const activeAgent = swarmState.activeAgent.get(input.sessionID);
|
|
29656
|
-
if (session && activeAgent && activeAgent !== ORCHESTRATOR_NAME
|
|
29657
|
-
|
|
29658
|
-
|
|
30165
|
+
if (session && activeAgent && activeAgent !== ORCHESTRATOR_NAME) {
|
|
30166
|
+
const stripActive = stripKnownSwarmPrefix(activeAgent);
|
|
30167
|
+
if (stripActive !== ORCHESTRATOR_NAME) {
|
|
30168
|
+
const staleDelegation = !session.delegationActive || Date.now() - session.lastToolCallTime > 60000;
|
|
30169
|
+
if (staleDelegation) {
|
|
30170
|
+
swarmState.activeAgent.set(input.sessionID, ORCHESTRATOR_NAME);
|
|
30171
|
+
ensureAgentSession(input.sessionID, ORCHESTRATOR_NAME);
|
|
30172
|
+
}
|
|
30173
|
+
}
|
|
29659
30174
|
}
|
|
29660
30175
|
await guardrailsHooks.toolBefore(input, output);
|
|
29661
30176
|
await safeHook(activityHooks.toolBefore)(input, output);
|
|
29662
30177
|
},
|
|
29663
|
-
"tool.execute.after": composeHandlers(activityHooks.toolAfter, guardrailsHooks.toolAfter),
|
|
30178
|
+
"tool.execute.after": composeHandlers(activityHooks.toolAfter, guardrailsHooks.toolAfter, toolSummarizerHook),
|
|
29664
30179
|
"chat.message": safeHook(delegationHandler)
|
|
29665
30180
|
};
|
|
29666
30181
|
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate and sanitize summary ID.
|
|
3
|
+
* Must match regex ^S\d+$ (e.g., S1, S2, S99)
|
|
4
|
+
* Rejects: empty string, null bytes, control characters, path traversal, non-matching patterns
|
|
5
|
+
* @throws Error with descriptive message on failure
|
|
6
|
+
*/
|
|
7
|
+
export declare function sanitizeSummaryId(id: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Store a summary entry to .swarm/summaries/{id}.json.
|
|
10
|
+
* Performs atomic write via temp file + rename.
|
|
11
|
+
* @throws Error if summary ID is invalid or size limit would be exceeded
|
|
12
|
+
*/
|
|
13
|
+
export declare function storeSummary(directory: string, id: string, fullOutput: string, summaryText: string, maxStoredBytes: number): Promise<void>;
|
|
14
|
+
/**
|
|
15
|
+
* Load fullOutput from a summary entry.
|
|
16
|
+
* Returns null if file doesn't exist or validation fails.
|
|
17
|
+
*/
|
|
18
|
+
export declare function loadFullOutput(directory: string, id: string): Promise<string | null>;
|
|
19
|
+
/**
|
|
20
|
+
* List all summary IDs that have summary entries.
|
|
21
|
+
* Returns sorted array of valid summary IDs.
|
|
22
|
+
* Returns empty array if summaries directory doesn't exist.
|
|
23
|
+
*/
|
|
24
|
+
export declare function listSummaries(directory: string): Promise<string[]>;
|
|
25
|
+
/**
|
|
26
|
+
* Delete summaries older than retentionDays.
|
|
27
|
+
* Returns array of deleted summary IDs.
|
|
28
|
+
*/
|
|
29
|
+
export declare function cleanupSummaries(directory: string, retentionDays: number): Promise<string[]>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Summarization engine for tool outputs.
|
|
3
|
+
* Provides content type detection, summarization decision logic, and structured summary creation.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Hysteresis factor to prevent churn for outputs near the threshold.
|
|
7
|
+
* An output must be 25% larger than the threshold to be summarized.
|
|
8
|
+
*/
|
|
9
|
+
export declare const HYSTERESIS_FACTOR = 1.25;
|
|
10
|
+
/**
|
|
11
|
+
* Content type classification for tool outputs.
|
|
12
|
+
*/
|
|
13
|
+
type ContentType = 'json' | 'code' | 'text' | 'binary';
|
|
14
|
+
/**
|
|
15
|
+
* Heuristic-based content type detection.
|
|
16
|
+
* @param output - The tool output string to analyze
|
|
17
|
+
* @param toolName - The name of the tool that produced the output
|
|
18
|
+
* @returns The detected content type: 'json', 'code', 'text', or 'binary'
|
|
19
|
+
*/
|
|
20
|
+
export declare function detectContentType(output: string, toolName: string): ContentType;
|
|
21
|
+
/**
|
|
22
|
+
* Determines whether output should be summarized based on size and hysteresis.
|
|
23
|
+
* Uses hysteresis to prevent repeated summarization decisions for outputs near the threshold.
|
|
24
|
+
* @param output - The tool output string to check
|
|
25
|
+
* @param thresholdBytes - The threshold in bytes
|
|
26
|
+
* @returns true if the output should be summarized
|
|
27
|
+
*/
|
|
28
|
+
export declare function shouldSummarize(output: string, thresholdBytes: number): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Creates a structured summary string from tool output.
|
|
31
|
+
* @param output - The full tool output string
|
|
32
|
+
* @param toolName - The name of the tool that produced the output
|
|
33
|
+
* @param summaryId - Unique identifier for this summary
|
|
34
|
+
* @param maxSummaryChars - Maximum characters allowed for the preview
|
|
35
|
+
* @returns Formatted summary string
|
|
36
|
+
*/
|
|
37
|
+
export declare function createSummary(output: string, toolName: string, summaryId: string, maxSummaryChars: number): string;
|
|
38
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-swarm",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.4",
|
|
4
4
|
"description": "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|