opencode-swarm 5.1.1 → 5.1.3

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.
@@ -0,0 +1 @@
1
+ export declare function handleBenchmarkCommand(directory: string, args: string[]): Promise<string>;
@@ -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.
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Handles the /swarm retrieve command.
3
+ * Loads full tool output from .swarm/summaries/{id}.json and returns it.
4
+ */
5
+ export declare function handleRetrieveCommand(directory: string, args: string[]): Promise<string>;
@@ -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';
@@ -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 path6 = `${directory}/.swarm/context.md`;
16175
- await Bun.write(path6, updated);
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,36 @@ 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
+ return;
16742
+ }
16743
+ const agentName = input.agent;
16744
+ const previousAgent = swarmState.activeAgent.get(input.sessionID);
16745
+ swarmState.activeAgent.set(input.sessionID, agentName);
16746
+ const session = ensureAgentSession(input.sessionID, agentName);
16747
+ session.delegationActive = true;
16748
+ if (config2.hooks?.delegation_tracker === true && previousAgent && previousAgent !== agentName) {
16749
+ const entry = {
16750
+ from: previousAgent,
16751
+ to: agentName,
16752
+ timestamp: Date.now()
16753
+ };
16754
+ if (!swarmState.delegationChains.has(input.sessionID)) {
16755
+ swarmState.delegationChains.set(input.sessionID, []);
16756
+ }
16757
+ const chain = swarmState.delegationChains.get(input.sessionID);
16758
+ chain?.push(entry);
16759
+ swarmState.pendingEvents++;
16760
+ }
16761
+ };
16762
+ }
16419
16763
  // src/hooks/guardrails.ts
16420
16764
  function createGuardrailsHooks(config2) {
16421
16765
  if (config2.enabled === false) {
@@ -16434,6 +16778,10 @@ function createGuardrailsHooks(config2) {
16434
16778
  }
16435
16779
  const agentName = swarmState.activeAgent.get(input.sessionID);
16436
16780
  const session = ensureAgentSession(input.sessionID, agentName);
16781
+ const resolvedName = stripKnownSwarmPrefix(session.agentName);
16782
+ if (resolvedName === ORCHESTRATOR_NAME) {
16783
+ return;
16784
+ }
16437
16785
  const agentConfig = resolveGuardrailsConfig(config2, session.agentName);
16438
16786
  if (session.hardLimitHit) {
16439
16787
  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 +17110,7 @@ function createSystemEnhancerHook(config2, directory) {
16762
17110
  }
16763
17111
  }
16764
17112
  }
17113
+ tryInject("[SWARM HINT] Large tool outputs may be auto-summarized. Use /swarm retrieve <id> to get the full content if needed.");
16765
17114
  return;
16766
17115
  }
16767
17116
  const userScoringConfig = config2.context_budget?.scoring;
@@ -16887,6 +17236,154 @@ ${activitySection}`;
16887
17236
  }
16888
17237
  return contextSummary;
16889
17238
  }
17239
+ // src/summaries/summarizer.ts
17240
+ var HYSTERESIS_FACTOR = 1.25;
17241
+ function detectContentType(output, toolName) {
17242
+ const trimmed = output.trim();
17243
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
17244
+ try {
17245
+ JSON.parse(trimmed);
17246
+ return "json";
17247
+ } catch {}
17248
+ }
17249
+ const codeToolNames = ["read", "cat", "grep", "bash"];
17250
+ const lowerToolName = toolName.toLowerCase();
17251
+ const toolSegments = lowerToolName.split(/[.\-_/]/);
17252
+ if (codeToolNames.some((name) => toolSegments.includes(name))) {
17253
+ return "code";
17254
+ }
17255
+ const codePatterns = [
17256
+ "function ",
17257
+ "const ",
17258
+ "import ",
17259
+ "export ",
17260
+ "class ",
17261
+ "def ",
17262
+ "return ",
17263
+ "=>"
17264
+ ];
17265
+ const startsWithShebang = trimmed.startsWith("#!");
17266
+ if (codePatterns.some((pattern) => output.includes(pattern)) || startsWithShebang) {
17267
+ return "code";
17268
+ }
17269
+ const sampleSize = Math.min(1000, output.length);
17270
+ let nonPrintableCount = 0;
17271
+ for (let i = 0;i < sampleSize; i++) {
17272
+ const charCode = output.charCodeAt(i);
17273
+ if (charCode < 32 && charCode !== 9 && charCode !== 10 && charCode !== 13) {
17274
+ nonPrintableCount++;
17275
+ }
17276
+ }
17277
+ if (sampleSize > 0 && nonPrintableCount / sampleSize > 0.1) {
17278
+ return "binary";
17279
+ }
17280
+ return "text";
17281
+ }
17282
+ function shouldSummarize(output, thresholdBytes) {
17283
+ const byteLength = Buffer.byteLength(output, "utf8");
17284
+ return byteLength >= thresholdBytes * HYSTERESIS_FACTOR;
17285
+ }
17286
+ function formatBytes(bytes) {
17287
+ const units = ["B", "KB", "MB", "GB"];
17288
+ let unitIndex = 0;
17289
+ let size = bytes;
17290
+ while (size >= 1024 && unitIndex < units.length - 1) {
17291
+ size /= 1024;
17292
+ unitIndex++;
17293
+ }
17294
+ const formatted = unitIndex === 0 ? size.toString() : size.toFixed(1);
17295
+ return `${formatted} ${units[unitIndex]}`;
17296
+ }
17297
+ function createSummary(output, toolName, summaryId, maxSummaryChars) {
17298
+ const contentType = detectContentType(output, toolName);
17299
+ const lineCount = output.split(`
17300
+ `).length;
17301
+ const byteSize = Buffer.byteLength(output, "utf8");
17302
+ const formattedSize = formatBytes(byteSize);
17303
+ const headerLine = `[SUMMARY ${summaryId}] ${formattedSize} | ${contentType} | ${lineCount} lines`;
17304
+ const footerLine = `\u2192 Use /swarm retrieve ${summaryId} for full content`;
17305
+ const overhead = headerLine.length + 1 + footerLine.length + 1;
17306
+ const maxPreviewChars = maxSummaryChars - overhead;
17307
+ let preview;
17308
+ switch (contentType) {
17309
+ case "json": {
17310
+ try {
17311
+ const parsed = JSON.parse(output.trim());
17312
+ if (Array.isArray(parsed)) {
17313
+ preview = `[ ${parsed.length} items ]`;
17314
+ } else if (typeof parsed === "object" && parsed !== null) {
17315
+ const keys = Object.keys(parsed).slice(0, 3);
17316
+ preview = `{ ${keys.join(", ")}${Object.keys(parsed).length > 3 ? ", ..." : ""} }`;
17317
+ } else {
17318
+ const lines = output.split(`
17319
+ `).filter((line) => line.trim().length > 0).slice(0, 3);
17320
+ preview = lines.join(`
17321
+ `);
17322
+ }
17323
+ } catch {
17324
+ const lines = output.split(`
17325
+ `).filter((line) => line.trim().length > 0).slice(0, 3);
17326
+ preview = lines.join(`
17327
+ `);
17328
+ }
17329
+ break;
17330
+ }
17331
+ case "code": {
17332
+ const lines = output.split(`
17333
+ `).filter((line) => line.trim().length > 0).slice(0, 5);
17334
+ preview = lines.join(`
17335
+ `);
17336
+ break;
17337
+ }
17338
+ case "text": {
17339
+ const lines = output.split(`
17340
+ `).filter((line) => line.trim().length > 0).slice(0, 5);
17341
+ preview = lines.join(`
17342
+ `);
17343
+ break;
17344
+ }
17345
+ case "binary": {
17346
+ preview = `[Binary content - ${formattedSize}]`;
17347
+ break;
17348
+ }
17349
+ default: {
17350
+ const lines = output.split(`
17351
+ `).filter((line) => line.trim().length > 0).slice(0, 5);
17352
+ preview = lines.join(`
17353
+ `);
17354
+ }
17355
+ }
17356
+ if (preview.length > maxPreviewChars) {
17357
+ preview = preview.substring(0, maxPreviewChars - 3) + "...";
17358
+ }
17359
+ return `${headerLine}
17360
+ ${preview}
17361
+ ${footerLine}`;
17362
+ }
17363
+
17364
+ // src/hooks/tool-summarizer.ts
17365
+ var nextSummaryId = 1;
17366
+ function createToolSummarizerHook(config2, directory) {
17367
+ if (config2.enabled === false) {
17368
+ return async () => {};
17369
+ }
17370
+ return async (input, output) => {
17371
+ if (typeof output.output !== "string" || output.output.length === 0) {
17372
+ return;
17373
+ }
17374
+ if (!shouldSummarize(output.output, config2.threshold_bytes)) {
17375
+ return;
17376
+ }
17377
+ const summaryId = `S${nextSummaryId++}`;
17378
+ const summaryText = createSummary(output.output, input.tool, summaryId, config2.max_summary_chars);
17379
+ try {
17380
+ await storeSummary(directory, summaryId, output.output, summaryText, config2.max_stored_bytes);
17381
+ output.output = summaryText;
17382
+ } catch (error49) {
17383
+ warn(`Tool output summarization failed for ${summaryId}: ${error49 instanceof Error ? error49.message : String(error49)}`);
17384
+ }
17385
+ };
17386
+ }
16890
17387
  // node_modules/@opencode-ai/plugin/node_modules/zod/v4/classic/external.js
16891
17388
  var exports_external2 = {};
16892
17389
  __export(exports_external2, {
@@ -17616,10 +18113,10 @@ function mergeDefs2(...defs) {
17616
18113
  function cloneDef2(schema) {
17617
18114
  return mergeDefs2(schema._zod.def);
17618
18115
  }
17619
- function getElementAtPath2(obj, path6) {
17620
- if (!path6)
18116
+ function getElementAtPath2(obj, path7) {
18117
+ if (!path7)
17621
18118
  return obj;
17622
- return path6.reduce((acc, key) => acc?.[key], obj);
18119
+ return path7.reduce((acc, key) => acc?.[key], obj);
17623
18120
  }
17624
18121
  function promiseAllObject2(promisesObj) {
17625
18122
  const keys = Object.keys(promisesObj);
@@ -17978,11 +18475,11 @@ function aborted2(x, startIndex = 0) {
17978
18475
  }
17979
18476
  return false;
17980
18477
  }
17981
- function prefixIssues2(path6, issues) {
18478
+ function prefixIssues2(path7, issues) {
17982
18479
  return issues.map((iss) => {
17983
18480
  var _a2;
17984
18481
  (_a2 = iss).path ?? (_a2.path = []);
17985
- iss.path.unshift(path6);
18482
+ iss.path.unshift(path7);
17986
18483
  return iss;
17987
18484
  });
17988
18485
  }
@@ -18150,7 +18647,7 @@ function treeifyError2(error49, _mapper) {
18150
18647
  return issue3.message;
18151
18648
  };
18152
18649
  const result = { errors: [] };
18153
- const processError = (error50, path6 = []) => {
18650
+ const processError = (error50, path7 = []) => {
18154
18651
  var _a2, _b;
18155
18652
  for (const issue3 of error50.issues) {
18156
18653
  if (issue3.code === "invalid_union" && issue3.errors.length) {
@@ -18160,7 +18657,7 @@ function treeifyError2(error49, _mapper) {
18160
18657
  } else if (issue3.code === "invalid_element") {
18161
18658
  processError({ issues: issue3.issues }, issue3.path);
18162
18659
  } else {
18163
- const fullpath = [...path6, ...issue3.path];
18660
+ const fullpath = [...path7, ...issue3.path];
18164
18661
  if (fullpath.length === 0) {
18165
18662
  result.errors.push(mapper(issue3));
18166
18663
  continue;
@@ -18192,8 +18689,8 @@ function treeifyError2(error49, _mapper) {
18192
18689
  }
18193
18690
  function toDotPath2(_path) {
18194
18691
  const segs = [];
18195
- const path6 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
18196
- for (const seg of path6) {
18692
+ const path7 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
18693
+ for (const seg of path7) {
18197
18694
  if (typeof seg === "number")
18198
18695
  segs.push(`[${seg}]`);
18199
18696
  else if (typeof seg === "symbol")
@@ -29389,7 +29886,7 @@ Use these as DOMAIN values when delegating to @sme.`;
29389
29886
  });
29390
29887
  // src/tools/file-extractor.ts
29391
29888
  import * as fs3 from "fs";
29392
- import * as path6 from "path";
29889
+ import * as path7 from "path";
29393
29890
  var EXT_MAP = {
29394
29891
  python: ".py",
29395
29892
  py: ".py",
@@ -29467,12 +29964,12 @@ var extract_code_blocks = tool({
29467
29964
  if (prefix) {
29468
29965
  filename = `${prefix}_${filename}`;
29469
29966
  }
29470
- let filepath = path6.join(targetDir, filename);
29471
- const base = path6.basename(filepath, path6.extname(filepath));
29472
- const ext = path6.extname(filepath);
29967
+ let filepath = path7.join(targetDir, filename);
29968
+ const base = path7.basename(filepath, path7.extname(filepath));
29969
+ const ext = path7.extname(filepath);
29473
29970
  let counter = 1;
29474
29971
  while (fs3.existsSync(filepath)) {
29475
- filepath = path6.join(targetDir, `${base}_${counter}${ext}`);
29972
+ filepath = path7.join(targetDir, `${base}_${counter}${ext}`);
29476
29973
  counter++;
29477
29974
  }
29478
29975
  try {
@@ -29596,6 +30093,8 @@ var OpenCodeSwarm = async (ctx) => {
29596
30093
  const delegationGateHandler = createDelegationGateHook(config3);
29597
30094
  const guardrailsConfig = GuardrailsConfigSchema.parse(config3.guardrails ?? {});
29598
30095
  const guardrailsHooks = createGuardrailsHooks(guardrailsConfig);
30096
+ const summaryConfig = SummaryConfigSchema.parse(config3.summaries ?? {});
30097
+ const toolSummarizerHook = createToolSummarizerHook(summaryConfig, ctx.directory);
29599
30098
  log("Plugin initialized", {
29600
30099
  directory: ctx.directory,
29601
30100
  maxIterations: config3.max_iterations,
@@ -29609,7 +30108,8 @@ var OpenCodeSwarm = async (ctx) => {
29609
30108
  commands: true,
29610
30109
  agentActivity: config3.hooks?.agent_activity !== false,
29611
30110
  delegationTracker: config3.hooks?.delegation_tracker === true,
29612
- guardrails: guardrailsConfig.enabled
30111
+ guardrails: guardrailsConfig.enabled,
30112
+ toolSummarizer: summaryConfig.enabled
29613
30113
  }
29614
30114
  });
29615
30115
  return {
@@ -29653,14 +30153,20 @@ var OpenCodeSwarm = async (ctx) => {
29653
30153
  }
29654
30154
  const session = swarmState.agentSessions.get(input.sessionID);
29655
30155
  const activeAgent = swarmState.activeAgent.get(input.sessionID);
29656
- if (session && activeAgent && activeAgent !== ORCHESTRATOR_NAME && session.delegationActive === false) {
29657
- swarmState.activeAgent.set(input.sessionID, ORCHESTRATOR_NAME);
29658
- ensureAgentSession(input.sessionID, ORCHESTRATOR_NAME);
30156
+ if (session && activeAgent && activeAgent !== ORCHESTRATOR_NAME) {
30157
+ const stripActive = stripKnownSwarmPrefix(activeAgent);
30158
+ if (stripActive !== ORCHESTRATOR_NAME) {
30159
+ const staleDelegation = !session.delegationActive || Date.now() - session.lastToolCallTime > 60000;
30160
+ if (staleDelegation) {
30161
+ swarmState.activeAgent.set(input.sessionID, ORCHESTRATOR_NAME);
30162
+ ensureAgentSession(input.sessionID, ORCHESTRATOR_NAME);
30163
+ }
30164
+ }
29659
30165
  }
29660
30166
  await guardrailsHooks.toolBefore(input, output);
29661
30167
  await safeHook(activityHooks.toolBefore)(input, output);
29662
30168
  },
29663
- "tool.execute.after": composeHandlers(activityHooks.toolAfter, guardrailsHooks.toolAfter),
30169
+ "tool.execute.after": composeHandlers(activityHooks.toolAfter, guardrailsHooks.toolAfter, toolSummarizerHook),
29664
30170
  "chat.message": safeHook(delegationHandler)
29665
30171
  };
29666
30172
  };
@@ -0,0 +1,2 @@
1
+ export { cleanupSummaries, listSummaries, loadFullOutput, storeSummary, } from './manager';
2
+ export { createSummary, detectContentType, HYSTERESIS_FACTOR, shouldSummarize, } from './summarizer';
@@ -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.1",
3
+ "version": "5.1.3",
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",