opencode-swarm-plugin 0.39.1 → 0.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.hive/analysis/eval-failure-analysis-2025-12-25.md +331 -0
- package/.hive/analysis/session-data-quality-audit.md +320 -0
- package/.hive/eval-results.json +481 -24
- package/.hive/issues.jsonl +76 -11
- package/.hive/memories.jsonl +159 -1
- package/.opencode/eval-history.jsonl +315 -0
- package/.turbo/turbo-build.log +5 -5
- package/CHANGELOG.md +207 -0
- package/README.md +2 -0
- package/SCORER-ANALYSIS.md +598 -0
- package/bin/eval-gate.test.ts +158 -0
- package/bin/eval-gate.ts +74 -0
- package/bin/swarm.test.ts +1054 -719
- package/bin/swarm.ts +577 -0
- package/dist/compaction-hook.d.ts +10 -1
- package/dist/compaction-hook.d.ts.map +1 -1
- package/dist/compaction-observability.d.ts +173 -0
- package/dist/compaction-observability.d.ts.map +1 -0
- package/dist/compaction-prompt-scoring.d.ts +1 -0
- package/dist/compaction-prompt-scoring.d.ts.map +1 -1
- package/dist/eval-capture.d.ts +93 -0
- package/dist/eval-capture.d.ts.map +1 -1
- package/dist/eval-runner.d.ts +134 -0
- package/dist/eval-runner.d.ts.map +1 -0
- package/dist/hive.d.ts.map +1 -1
- package/dist/index.d.ts +65 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +84043 -28070
- package/dist/memory-tools.d.ts +70 -2
- package/dist/memory-tools.d.ts.map +1 -1
- package/dist/memory.d.ts +37 -0
- package/dist/memory.d.ts.map +1 -1
- package/dist/observability-tools.d.ts +64 -0
- package/dist/observability-tools.d.ts.map +1 -1
- package/dist/plugin.js +83570 -27466
- package/dist/schemas/task.d.ts +3 -3
- package/dist/swarm-orchestrate.d.ts.map +1 -1
- package/dist/swarm-prompts.d.ts +32 -1
- package/dist/swarm-prompts.d.ts.map +1 -1
- package/docs/planning/ADR-009-oh-my-opencode-patterns.md +353 -0
- package/evals/ARCHITECTURE.md +1189 -0
- package/evals/README.md +113 -0
- package/evals/example.eval.ts +3 -4
- package/evals/fixtures/compaction-prompt-cases.ts +6 -0
- package/evals/scorers/coordinator-discipline.evalite-test.ts +163 -0
- package/evals/scorers/coordinator-discipline.ts +82 -2
- package/evals/scorers/index.test.ts +146 -0
- package/evals/scorers/index.ts +104 -0
- package/evals/swarm-decomposition.eval.ts +13 -4
- package/examples/commands/swarm.md +291 -21
- package/package.json +4 -3
- package/src/compaction-hook.ts +258 -110
- package/src/compaction-observability.integration.test.ts +139 -0
- package/src/compaction-observability.test.ts +187 -0
- package/src/compaction-observability.ts +324 -0
- package/src/compaction-prompt-scorers.test.ts +10 -9
- package/src/compaction-prompt-scoring.ts +7 -5
- package/src/eval-capture.test.ts +204 -1
- package/src/eval-capture.ts +194 -2
- package/src/eval-runner.test.ts +223 -0
- package/src/eval-runner.ts +402 -0
- package/src/hive.ts +57 -22
- package/src/index.ts +54 -1
- package/src/memory-tools.test.ts +84 -0
- package/src/memory-tools.ts +68 -3
- package/src/memory.test.ts +2 -2
- package/src/memory.ts +122 -49
- package/src/observability-tools.test.ts +13 -0
- package/src/observability-tools.ts +277 -0
- package/src/swarm-orchestrate.test.ts +162 -0
- package/src/swarm-orchestrate.ts +7 -5
- package/src/swarm-prompts.test.ts +168 -4
- package/src/swarm-prompts.ts +228 -7
- package/.env +0 -2
- package/.turbo/turbo-test.log +0 -481
- package/.turbo/turbo-typecheck.log +0 -1
- package/dist/beads.d.ts +0 -386
- package/dist/beads.d.ts.map +0 -1
- package/dist/schemas/bead-events.d.ts +0 -698
- package/dist/schemas/bead-events.d.ts.map +0 -1
- package/dist/schemas/bead.d.ts +0 -255
- package/dist/schemas/bead.d.ts.map +0 -1
package/src/hive.ts
CHANGED
|
@@ -741,30 +741,65 @@ export const hive_create_epic = tool({
|
|
|
741
741
|
};
|
|
742
742
|
|
|
743
743
|
// Emit DecompositionGeneratedEvent for learning system
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
744
|
+
// Always emit using projectKey (from getHiveWorkingDirectory), not args.project_key
|
|
745
|
+
// This fixes the bug where events weren't emitted when callers didn't pass project_key
|
|
746
|
+
const effectiveProjectKey = args.project_key || projectKey;
|
|
747
|
+
try {
|
|
748
|
+
const event = createEvent("decomposition_generated", {
|
|
749
|
+
project_key: effectiveProjectKey,
|
|
750
|
+
epic_id: epic.id,
|
|
751
|
+
task: args.task || validated.epic_title,
|
|
752
|
+
context: validated.epic_description,
|
|
753
|
+
strategy: args.strategy || "feature-based",
|
|
754
|
+
epic_title: validated.epic_title,
|
|
755
|
+
subtasks: validated.subtasks.map((st) => ({
|
|
756
|
+
title: st.title,
|
|
757
|
+
files: st.files || [],
|
|
758
|
+
priority: st.priority,
|
|
759
|
+
})),
|
|
760
|
+
recovery_context: args.recovery_context,
|
|
761
|
+
});
|
|
762
|
+
await appendEvent(event, effectiveProjectKey);
|
|
763
|
+
} catch (error) {
|
|
764
|
+
// Non-fatal - log and continue
|
|
765
|
+
console.warn(
|
|
766
|
+
"[hive_create_epic] Failed to emit DecompositionGeneratedEvent:",
|
|
767
|
+
error,
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Capture decomposition_complete event for eval scoring
|
|
772
|
+
try {
|
|
773
|
+
const { captureCoordinatorEvent } = await import("./eval-capture.js");
|
|
774
|
+
|
|
775
|
+
// Build files_per_subtask map (indexed by subtask index)
|
|
776
|
+
const filesPerSubtask: Record<number, string[]> = {};
|
|
777
|
+
validated.subtasks.forEach((subtask, index) => {
|
|
778
|
+
if (subtask.files && subtask.files.length > 0) {
|
|
779
|
+
filesPerSubtask[index] = subtask.files;
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
captureCoordinatorEvent({
|
|
784
|
+
session_id: ctx.sessionID || "unknown",
|
|
748
785
|
epic_id: epic.id,
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
786
|
+
timestamp: new Date().toISOString(),
|
|
787
|
+
event_type: "DECISION",
|
|
788
|
+
decision_type: "decomposition_complete",
|
|
789
|
+
payload: {
|
|
790
|
+
subtask_count: validated.subtasks.length,
|
|
791
|
+
strategy_used: args.strategy || "feature-based",
|
|
792
|
+
files_per_subtask: filesPerSubtask,
|
|
793
|
+
epic_title: validated.epic_title,
|
|
794
|
+
task: args.task,
|
|
795
|
+
},
|
|
759
796
|
});
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
);
|
|
767
|
-
}
|
|
797
|
+
} catch (error) {
|
|
798
|
+
// Non-fatal - log and continue
|
|
799
|
+
console.warn(
|
|
800
|
+
"[hive_create_epic] Failed to capture decomposition_complete event:",
|
|
801
|
+
error,
|
|
802
|
+
);
|
|
768
803
|
}
|
|
769
804
|
|
|
770
805
|
// Sync cells to JSONL so spawned workers can see them immediately
|
package/src/index.ts
CHANGED
|
@@ -49,6 +49,7 @@ import { mandateTools } from "./mandates";
|
|
|
49
49
|
import { memoryTools } from "./memory-tools";
|
|
50
50
|
import { observabilityTools } from "./observability-tools";
|
|
51
51
|
import { researchTools } from "./swarm-research";
|
|
52
|
+
import { evalTools } from "./eval-runner";
|
|
52
53
|
import {
|
|
53
54
|
guardrailOutput,
|
|
54
55
|
DEFAULT_GUARDRAIL_CONFIG,
|
|
@@ -175,6 +176,7 @@ const SwarmPlugin: Plugin = async (
|
|
|
175
176
|
...memoryTools,
|
|
176
177
|
...observabilityTools,
|
|
177
178
|
...researchTools,
|
|
179
|
+
...evalTools,
|
|
178
180
|
},
|
|
179
181
|
|
|
180
182
|
/**
|
|
@@ -729,6 +731,8 @@ export {
|
|
|
729
731
|
* Includes:
|
|
730
732
|
* - SWARM_COMPACTION_CONTEXT - Prompt text for swarm state preservation
|
|
731
733
|
* - createCompactionHook - Factory function for the compaction hook
|
|
734
|
+
* - scanSessionMessages - Scan session for swarm state
|
|
735
|
+
* - ScannedSwarmState - Scanned state interface
|
|
732
736
|
*
|
|
733
737
|
* Usage:
|
|
734
738
|
* ```typescript
|
|
@@ -739,7 +743,56 @@ export {
|
|
|
739
743
|
* };
|
|
740
744
|
* ```
|
|
741
745
|
*/
|
|
742
|
-
export {
|
|
746
|
+
export {
|
|
747
|
+
SWARM_COMPACTION_CONTEXT,
|
|
748
|
+
createCompactionHook,
|
|
749
|
+
scanSessionMessages,
|
|
750
|
+
type ScannedSwarmState,
|
|
751
|
+
} from "./compaction-hook";
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Re-export compaction-observability module
|
|
755
|
+
*
|
|
756
|
+
* Includes:
|
|
757
|
+
* - CompactionPhase - Enum of compaction phases
|
|
758
|
+
* - createMetricsCollector - Create a metrics collector
|
|
759
|
+
* - recordPhaseStart, recordPhaseComplete - Phase timing
|
|
760
|
+
* - recordPatternExtracted, recordPatternSkipped - Pattern tracking
|
|
761
|
+
* - getMetricsSummary - Get metrics summary
|
|
762
|
+
*
|
|
763
|
+
* Types:
|
|
764
|
+
* - CompactionMetrics - Mutable metrics collector
|
|
765
|
+
* - CompactionMetricsSummary - Read-only summary snapshot
|
|
766
|
+
*
|
|
767
|
+
* Features:
|
|
768
|
+
* - Phase timing breakdown (START, GATHER, DETECT, INJECT, COMPLETE)
|
|
769
|
+
* - Pattern extraction tracking with reasons
|
|
770
|
+
* - Success rate calculation
|
|
771
|
+
* - Debug mode for verbose details
|
|
772
|
+
* - JSON serializable for persistence
|
|
773
|
+
*
|
|
774
|
+
* Usage:
|
|
775
|
+
* ```typescript
|
|
776
|
+
* import { createMetricsCollector, CompactionPhase, recordPhaseStart } from "opencode-swarm-plugin";
|
|
777
|
+
*
|
|
778
|
+
* const metrics = createMetricsCollector({ session_id: "abc123" });
|
|
779
|
+
* recordPhaseStart(metrics, CompactionPhase.DETECT);
|
|
780
|
+
* // ... work ...
|
|
781
|
+
* recordPhaseComplete(metrics, CompactionPhase.DETECT);
|
|
782
|
+
* const summary = getMetricsSummary(metrics);
|
|
783
|
+
* ```
|
|
784
|
+
*/
|
|
785
|
+
export {
|
|
786
|
+
CompactionPhase,
|
|
787
|
+
createMetricsCollector,
|
|
788
|
+
recordPhaseStart,
|
|
789
|
+
recordPhaseComplete,
|
|
790
|
+
recordPatternExtracted,
|
|
791
|
+
recordPatternSkipped,
|
|
792
|
+
getMetricsSummary,
|
|
793
|
+
type CompactionMetrics,
|
|
794
|
+
type CompactionMetricsSummary,
|
|
795
|
+
} from "./compaction-observability";
|
|
743
796
|
|
|
744
797
|
/**
|
|
745
798
|
* Re-export memory module
|
package/src/memory-tools.test.ts
CHANGED
|
@@ -24,6 +24,7 @@ describe("memory tools integration", () => {
|
|
|
24
24
|
expect(toolNames).toContain("semantic-memory_list");
|
|
25
25
|
expect(toolNames).toContain("semantic-memory_stats");
|
|
26
26
|
expect(toolNames).toContain("semantic-memory_check");
|
|
27
|
+
expect(toolNames).toContain("semantic-memory_upsert");
|
|
27
28
|
});
|
|
28
29
|
|
|
29
30
|
test("tools have execute functions", () => {
|
|
@@ -108,4 +109,87 @@ describe("memory tools integration", () => {
|
|
|
108
109
|
expect(typeof parsed.ollama).toBe("boolean");
|
|
109
110
|
});
|
|
110
111
|
});
|
|
112
|
+
|
|
113
|
+
describe("semantic-memory_upsert", () => {
|
|
114
|
+
test("returns valid ADD operation result", async () => {
|
|
115
|
+
const tool = memoryTools["semantic-memory_upsert"];
|
|
116
|
+
const result = await tool.execute(
|
|
117
|
+
{
|
|
118
|
+
information: "Test memory for plugin tool",
|
|
119
|
+
tags: "test,plugin",
|
|
120
|
+
},
|
|
121
|
+
{ sessionID: "test-session" } as any,
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const parsed = JSON.parse(result);
|
|
125
|
+
|
|
126
|
+
expect(parsed.operation).toBe("ADD");
|
|
127
|
+
expect(parsed.reason).toBeDefined();
|
|
128
|
+
expect(parsed.memoryId).toBeDefined();
|
|
129
|
+
expect(parsed.memoryId).toMatch(/^mem_/);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("includes autoTags when enabled", async () => {
|
|
133
|
+
const tool = memoryTools["semantic-memory_upsert"];
|
|
134
|
+
const result = await tool.execute(
|
|
135
|
+
{
|
|
136
|
+
information: "TypeScript is a typed superset of JavaScript",
|
|
137
|
+
autoTag: true,
|
|
138
|
+
},
|
|
139
|
+
{ sessionID: "test-session" } as any,
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const parsed = JSON.parse(result);
|
|
143
|
+
|
|
144
|
+
expect(parsed.autoTags).toBeDefined();
|
|
145
|
+
expect(parsed.autoTags.tags).toBeInstanceOf(Array);
|
|
146
|
+
expect(parsed.autoTags.keywords).toBeInstanceOf(Array);
|
|
147
|
+
expect(parsed.autoTags.category).toBe("general");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("includes linksCreated when autoLink enabled", async () => {
|
|
151
|
+
const tool = memoryTools["semantic-memory_upsert"];
|
|
152
|
+
const result = await tool.execute(
|
|
153
|
+
{
|
|
154
|
+
information: "React hooks enable functional components to use state",
|
|
155
|
+
autoLink: true,
|
|
156
|
+
},
|
|
157
|
+
{ sessionID: "test-session" } as any,
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const parsed = JSON.parse(result);
|
|
161
|
+
|
|
162
|
+
expect(parsed.linksCreated).toBeDefined();
|
|
163
|
+
expect(typeof parsed.linksCreated).toBe("number");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("includes entitiesExtracted when extractEntities enabled", async () => {
|
|
167
|
+
const tool = memoryTools["semantic-memory_upsert"];
|
|
168
|
+
const result = await tool.execute(
|
|
169
|
+
{
|
|
170
|
+
information: "Next.js 15 was released by Vercel in October 2024",
|
|
171
|
+
extractEntities: true,
|
|
172
|
+
},
|
|
173
|
+
{ sessionID: "test-session" } as any,
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const parsed = JSON.parse(result);
|
|
177
|
+
|
|
178
|
+
expect(parsed.entitiesExtracted).toBeDefined();
|
|
179
|
+
expect(typeof parsed.entitiesExtracted).toBe("number");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("throws error when information is missing", async () => {
|
|
183
|
+
const tool = memoryTools["semantic-memory_upsert"];
|
|
184
|
+
|
|
185
|
+
await expect(async () => {
|
|
186
|
+
await tool.execute(
|
|
187
|
+
{
|
|
188
|
+
tags: "test",
|
|
189
|
+
} as any,
|
|
190
|
+
{ sessionID: "test-session" } as any,
|
|
191
|
+
);
|
|
192
|
+
}).toThrow("information is required");
|
|
193
|
+
});
|
|
194
|
+
});
|
|
111
195
|
});
|
package/src/memory-tools.ts
CHANGED
|
@@ -27,6 +27,9 @@ import {
|
|
|
27
27
|
type StatsResult,
|
|
28
28
|
type HealthResult,
|
|
29
29
|
type OperationResult,
|
|
30
|
+
type UpsertArgs,
|
|
31
|
+
type UpsertResult,
|
|
32
|
+
type AutoTags,
|
|
30
33
|
} from "./memory";
|
|
31
34
|
|
|
32
35
|
// Re-export types for external use
|
|
@@ -41,7 +44,10 @@ export type {
|
|
|
41
44
|
StatsResult,
|
|
42
45
|
HealthResult,
|
|
43
46
|
OperationResult,
|
|
44
|
-
|
|
47
|
+
UpsertArgs,
|
|
48
|
+
UpsertResult,
|
|
49
|
+
AutoTags,
|
|
50
|
+
} from "./memory";
|
|
45
51
|
|
|
46
52
|
// ============================================================================
|
|
47
53
|
// Types
|
|
@@ -65,7 +71,7 @@ let cachedProjectPath: string | null = null;
|
|
|
65
71
|
* @param projectPath - Project path (uses CWD if not provided)
|
|
66
72
|
* @returns Memory adapter instance
|
|
67
73
|
*/
|
|
68
|
-
async function getMemoryAdapter(
|
|
74
|
+
export async function getMemoryAdapter(
|
|
69
75
|
projectPath?: string,
|
|
70
76
|
): Promise<MemoryAdapter> {
|
|
71
77
|
const path = projectPath || process.cwd();
|
|
@@ -106,7 +112,7 @@ export { createMemoryAdapter };
|
|
|
106
112
|
*/
|
|
107
113
|
export const semantic_memory_store = tool({
|
|
108
114
|
description:
|
|
109
|
-
"Store a memory with semantic embedding. Memories are searchable by semantic similarity and can be organized into collections. Confidence affects decay rate: high confidence (1.0) = 135 day half-life, low confidence (0.0) = 45 day half-life.",
|
|
115
|
+
"Store a memory with semantic embedding. Memories are searchable by semantic similarity and can be organized into collections. Confidence affects decay rate: high confidence (1.0) = 135 day half-life, low confidence (0.0) = 45 day half-life. Supports auto-tagging, auto-linking, and entity extraction via LLM.",
|
|
110
116
|
args: {
|
|
111
117
|
information: tool.schema
|
|
112
118
|
.string()
|
|
@@ -127,6 +133,18 @@ export const semantic_memory_store = tool({
|
|
|
127
133
|
.number()
|
|
128
134
|
.optional()
|
|
129
135
|
.describe("Confidence level (0.0-1.0) affecting decay rate. Higher = slower decay. Default 0.7"),
|
|
136
|
+
autoTag: tool.schema
|
|
137
|
+
.boolean()
|
|
138
|
+
.optional()
|
|
139
|
+
.describe("Auto-generate tags using LLM. Default false"),
|
|
140
|
+
autoLink: tool.schema
|
|
141
|
+
.boolean()
|
|
142
|
+
.optional()
|
|
143
|
+
.describe("Auto-link to related memories. Default false"),
|
|
144
|
+
extractEntities: tool.schema
|
|
145
|
+
.boolean()
|
|
146
|
+
.optional()
|
|
147
|
+
.describe("Extract entities (people, places, technologies). Default false"),
|
|
130
148
|
},
|
|
131
149
|
async execute(args, ctx: ToolContext) {
|
|
132
150
|
const adapter = await getMemoryAdapter();
|
|
@@ -258,6 +276,52 @@ export const semantic_memory_check = tool({
|
|
|
258
276
|
},
|
|
259
277
|
});
|
|
260
278
|
|
|
279
|
+
/**
|
|
280
|
+
* Smart upsert - ADD, UPDATE, DELETE, or NOOP based on existing memories
|
|
281
|
+
*/
|
|
282
|
+
export const semantic_memory_upsert = tool({
|
|
283
|
+
description:
|
|
284
|
+
"Smart memory storage that decides whether to ADD, UPDATE, DELETE, or skip (NOOP) based on existing memories. Uses LLM to detect duplicates, refinements, and contradictions. Auto-generates tags, links, and entities when enabled.",
|
|
285
|
+
args: {
|
|
286
|
+
information: tool.schema
|
|
287
|
+
.string()
|
|
288
|
+
.describe("The information to store (required)"),
|
|
289
|
+
collection: tool.schema
|
|
290
|
+
.string()
|
|
291
|
+
.optional()
|
|
292
|
+
.describe("Collection name (defaults to 'default')"),
|
|
293
|
+
tags: tool.schema
|
|
294
|
+
.string()
|
|
295
|
+
.optional()
|
|
296
|
+
.describe("Comma-separated tags (e.g., 'auth,tokens,oauth')"),
|
|
297
|
+
metadata: tool.schema
|
|
298
|
+
.string()
|
|
299
|
+
.optional()
|
|
300
|
+
.describe("JSON string with additional metadata"),
|
|
301
|
+
confidence: tool.schema
|
|
302
|
+
.number()
|
|
303
|
+
.optional()
|
|
304
|
+
.describe("Confidence level (0.0-1.0) affecting decay rate. Higher = slower decay. Default 0.7"),
|
|
305
|
+
autoTag: tool.schema
|
|
306
|
+
.boolean()
|
|
307
|
+
.optional()
|
|
308
|
+
.describe("Auto-generate tags using LLM. Default true"),
|
|
309
|
+
autoLink: tool.schema
|
|
310
|
+
.boolean()
|
|
311
|
+
.optional()
|
|
312
|
+
.describe("Auto-link to related memories. Default true"),
|
|
313
|
+
extractEntities: tool.schema
|
|
314
|
+
.boolean()
|
|
315
|
+
.optional()
|
|
316
|
+
.describe("Extract entities (people, places, technologies). Default false"),
|
|
317
|
+
},
|
|
318
|
+
async execute(args, ctx: ToolContext) {
|
|
319
|
+
const adapter = await getMemoryAdapter();
|
|
320
|
+
const result = await adapter.upsert(args);
|
|
321
|
+
return JSON.stringify(result, null, 2);
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
|
|
261
325
|
// ============================================================================
|
|
262
326
|
// Tool Registry
|
|
263
327
|
// ============================================================================
|
|
@@ -276,4 +340,5 @@ export const memoryTools = {
|
|
|
276
340
|
"semantic-memory_list": semantic_memory_list,
|
|
277
341
|
"semantic-memory_stats": semantic_memory_stats,
|
|
278
342
|
"semantic-memory_check": semantic_memory_check,
|
|
343
|
+
"semantic-memory_upsert": semantic_memory_upsert,
|
|
279
344
|
} as const;
|
package/src/memory.test.ts
CHANGED
|
@@ -45,7 +45,7 @@ describe("memory adapter", () => {
|
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
expect(result.id).toBeDefined();
|
|
48
|
-
expect(result.id).toMatch(/^
|
|
48
|
+
expect(result.id).toMatch(/^mem-/); // Real swarm-mail adapter uses 'mem-' prefix
|
|
49
49
|
expect(result.message).toContain("Stored memory");
|
|
50
50
|
});
|
|
51
51
|
|
|
@@ -55,7 +55,7 @@ describe("memory adapter", () => {
|
|
|
55
55
|
collection: "project-alpha",
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
expect(result.id).toMatch(/^
|
|
58
|
+
expect(result.id).toMatch(/^mem-/); // Real swarm-mail adapter uses 'mem-' prefix
|
|
59
59
|
expect(result.message).toContain("collection: project-alpha");
|
|
60
60
|
});
|
|
61
61
|
});
|
package/src/memory.ts
CHANGED
|
@@ -39,6 +39,8 @@ import {
|
|
|
39
39
|
legacyDatabaseExists,
|
|
40
40
|
migrateLegacyMemories,
|
|
41
41
|
toSwarmDb,
|
|
42
|
+
createMemoryAdapter as createSwarmMailAdapter,
|
|
43
|
+
type MemoryConfig,
|
|
42
44
|
} from "swarm-mail";
|
|
43
45
|
|
|
44
46
|
// ============================================================================
|
|
@@ -71,6 +73,12 @@ export interface StoreArgs {
|
|
|
71
73
|
readonly metadata?: string;
|
|
72
74
|
/** Confidence level (0.0-1.0) affecting decay rate. Higher = slower decay. Default 0.7 */
|
|
73
75
|
readonly confidence?: number;
|
|
76
|
+
/** Auto-generate tags using LLM. Default false */
|
|
77
|
+
readonly autoTag?: boolean;
|
|
78
|
+
/** Auto-link to related memories. Default false */
|
|
79
|
+
readonly autoLink?: boolean;
|
|
80
|
+
/** Extract entities (people, places, technologies). Default false */
|
|
81
|
+
readonly extractEntities?: boolean;
|
|
74
82
|
}
|
|
75
83
|
|
|
76
84
|
/** Arguments for find operation */
|
|
@@ -129,6 +137,39 @@ export interface OperationResult {
|
|
|
129
137
|
readonly message?: string;
|
|
130
138
|
}
|
|
131
139
|
|
|
140
|
+
/** Arguments for upsert operation */
|
|
141
|
+
export interface UpsertArgs {
|
|
142
|
+
readonly information: string;
|
|
143
|
+
readonly collection?: string;
|
|
144
|
+
readonly tags?: string;
|
|
145
|
+
readonly metadata?: string;
|
|
146
|
+
readonly confidence?: number;
|
|
147
|
+
/** Auto-generate tags using LLM. Default true */
|
|
148
|
+
readonly autoTag?: boolean;
|
|
149
|
+
/** Auto-link to related memories. Default true */
|
|
150
|
+
readonly autoLink?: boolean;
|
|
151
|
+
/** Extract entities (people, places, technologies). Default false */
|
|
152
|
+
readonly extractEntities?: boolean;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Auto-generated tags result */
|
|
156
|
+
export interface AutoTags {
|
|
157
|
+
readonly tags: string[];
|
|
158
|
+
readonly keywords: string[];
|
|
159
|
+
readonly category: string;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Result from upsert operation */
|
|
163
|
+
export interface UpsertResult {
|
|
164
|
+
readonly operation: "ADD" | "UPDATE" | "DELETE" | "NOOP";
|
|
165
|
+
readonly reason: string;
|
|
166
|
+
readonly memoryId?: string;
|
|
167
|
+
readonly affectedMemoryIds?: string[];
|
|
168
|
+
readonly autoTags?: AutoTags;
|
|
169
|
+
readonly linksCreated?: number;
|
|
170
|
+
readonly entitiesExtracted?: number;
|
|
171
|
+
}
|
|
172
|
+
|
|
132
173
|
// ============================================================================
|
|
133
174
|
// Auto-Migration Logic
|
|
134
175
|
// ============================================================================
|
|
@@ -206,6 +247,7 @@ export interface MemoryAdapter {
|
|
|
206
247
|
readonly list: (args: ListArgs) => Promise<Memory[]>;
|
|
207
248
|
readonly stats: () => Promise<StatsResult>;
|
|
208
249
|
readonly checkHealth: () => Promise<HealthResult>;
|
|
250
|
+
readonly upsert: (args: UpsertArgs) => Promise<UpsertResult>;
|
|
209
251
|
}
|
|
210
252
|
|
|
211
253
|
/**
|
|
@@ -236,10 +278,24 @@ export async function createMemoryAdapter(
|
|
|
236
278
|
await maybeAutoMigrate(db);
|
|
237
279
|
}
|
|
238
280
|
|
|
239
|
-
// Convert DatabaseAdapter to SwarmDb (Drizzle client) for
|
|
281
|
+
// Convert DatabaseAdapter to SwarmDb (Drizzle client) for real swarm-mail adapter
|
|
240
282
|
const drizzleDb = toSwarmDb(db);
|
|
241
|
-
const store = createMemoryStore(drizzleDb);
|
|
242
283
|
const config = getDefaultConfig();
|
|
284
|
+
|
|
285
|
+
// Create real swarm-mail adapter with Wave 1-3 features
|
|
286
|
+
const realAdapter = createSwarmMailAdapter(drizzleDb, config);
|
|
287
|
+
|
|
288
|
+
// DEBUG: Check if upsert exists
|
|
289
|
+
if (!realAdapter || typeof realAdapter.upsert !== 'function') {
|
|
290
|
+
console.warn('[memory] realAdapter.upsert is not available:', {
|
|
291
|
+
hasAdapter: !!realAdapter,
|
|
292
|
+
upsertType: typeof realAdapter?.upsert,
|
|
293
|
+
methods: realAdapter ? Object.keys(realAdapter) : []
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// For backward compatibility, keep legacy adapter for methods not yet in real adapter
|
|
298
|
+
const store = createMemoryStore(drizzleDb);
|
|
243
299
|
const ollamaLayer = makeOllamaLive(config);
|
|
244
300
|
|
|
245
301
|
/**
|
|
@@ -272,59 +328,39 @@ export async function createMemoryAdapter(
|
|
|
272
328
|
|
|
273
329
|
return {
|
|
274
330
|
/**
|
|
275
|
-
* Store a memory with embedding
|
|
331
|
+
* Store a memory with embedding and optional auto-features
|
|
332
|
+
*
|
|
333
|
+
* Delegates to real swarm-mail adapter which supports:
|
|
334
|
+
* - autoTag: LLM-powered tag generation
|
|
335
|
+
* - autoLink: Semantic linking to related memories
|
|
336
|
+
* - extractEntities: Entity extraction and knowledge graph building
|
|
276
337
|
*/
|
|
277
338
|
async store(args: StoreArgs): Promise<StoreResult> {
|
|
278
|
-
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
} catch {
|
|
288
|
-
metadata = { raw: args.metadata };
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Add tags to metadata
|
|
293
|
-
if (tags.length > 0) {
|
|
294
|
-
metadata.tags = tags;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// Clamp confidence to valid range [0.0, 1.0]
|
|
298
|
-
const clampConfidence = (c: number | undefined): number => {
|
|
299
|
-
if (c === undefined) return 0.7;
|
|
300
|
-
return Math.max(0.0, Math.min(1.0, c));
|
|
301
|
-
};
|
|
302
|
-
|
|
303
|
-
const memory: Memory = {
|
|
304
|
-
id,
|
|
305
|
-
content: args.information,
|
|
306
|
-
metadata,
|
|
307
|
-
collection,
|
|
308
|
-
createdAt: new Date(),
|
|
309
|
-
confidence: clampConfidence(args.confidence),
|
|
310
|
-
};
|
|
311
|
-
|
|
312
|
-
// Generate embedding
|
|
313
|
-
const program = Effect.gen(function* () {
|
|
314
|
-
const ollama = yield* Ollama;
|
|
315
|
-
return yield* ollama.embed(args.information);
|
|
339
|
+
// Delegate to real swarm-mail adapter
|
|
340
|
+
const result = await realAdapter.store(args.information, {
|
|
341
|
+
collection: args.collection,
|
|
342
|
+
tags: args.tags,
|
|
343
|
+
metadata: args.metadata,
|
|
344
|
+
confidence: args.confidence,
|
|
345
|
+
autoTag: args.autoTag,
|
|
346
|
+
autoLink: args.autoLink,
|
|
347
|
+
extractEntities: args.extractEntities,
|
|
316
348
|
});
|
|
317
349
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
350
|
+
// Build user-facing message
|
|
351
|
+
let message = `Stored memory ${result.id} in collection: ${args.collection ?? "default"}`;
|
|
352
|
+
|
|
353
|
+
if (result.autoTags) {
|
|
354
|
+
message += `\nAuto-tags: ${result.autoTags.tags.join(", ")}`;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (result.links && result.links.length > 0) {
|
|
358
|
+
message += `\nLinked to ${result.links.length} related memor${result.links.length === 1 ? "y" : "ies"}`;
|
|
359
|
+
}
|
|
324
360
|
|
|
325
361
|
return {
|
|
326
|
-
id,
|
|
327
|
-
message
|
|
362
|
+
id: result.id,
|
|
363
|
+
message,
|
|
328
364
|
};
|
|
329
365
|
},
|
|
330
366
|
|
|
@@ -450,5 +486,42 @@ export async function createMemoryAdapter(
|
|
|
450
486
|
};
|
|
451
487
|
}
|
|
452
488
|
},
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Smart upsert - uses LLM to decide ADD, UPDATE, DELETE, or NOOP
|
|
492
|
+
*
|
|
493
|
+
* Delegates to real swarm-mail adapter with Mem0 pattern:
|
|
494
|
+
* - Finds semantically similar memories
|
|
495
|
+
* - LLM analyzes and decides operation
|
|
496
|
+
* - Executes with graceful degradation on LLM failures
|
|
497
|
+
*/
|
|
498
|
+
async upsert(args: UpsertArgs): Promise<UpsertResult> {
|
|
499
|
+
// Validate required fields
|
|
500
|
+
if (!args.information) {
|
|
501
|
+
throw new Error("information is required for upsert");
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Delegate to real swarm-mail adapter with useSmartOps enabled
|
|
505
|
+
const result = await realAdapter.upsert(args.information, {
|
|
506
|
+
collection: args.collection,
|
|
507
|
+
tags: args.tags,
|
|
508
|
+
metadata: args.metadata,
|
|
509
|
+
confidence: args.confidence,
|
|
510
|
+
useSmartOps: true, // Enable LLM-powered decision making
|
|
511
|
+
autoTag: args.autoTag,
|
|
512
|
+
autoLink: args.autoLink,
|
|
513
|
+
extractEntities: args.extractEntities,
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
// Map real adapter result to plugin UpsertResult format
|
|
517
|
+
return {
|
|
518
|
+
operation: result.operation,
|
|
519
|
+
reason: result.reason,
|
|
520
|
+
memoryId: result.id,
|
|
521
|
+
affectedMemoryIds: [result.id],
|
|
522
|
+
// Note: Real adapter doesn't return autoTags/links from upsert yet
|
|
523
|
+
// Those are only on store(). This is consistent with current behavior.
|
|
524
|
+
};
|
|
525
|
+
},
|
|
453
526
|
};
|
|
454
527
|
}
|
|
@@ -343,4 +343,17 @@ describe("observability-tools", () => {
|
|
|
343
343
|
}
|
|
344
344
|
});
|
|
345
345
|
});
|
|
346
|
+
|
|
347
|
+
describe("CLI Stats Helpers", () => {
|
|
348
|
+
// These helpers will be exported for use in bin/swarm.ts
|
|
349
|
+
// They format analytics data for beautiful CLI output
|
|
350
|
+
|
|
351
|
+
describe("formatSwarmStatsBox", () => {
|
|
352
|
+
test("formats stats in a beautiful box", () => {
|
|
353
|
+
// This will be implemented in observability-tools.ts
|
|
354
|
+
// Just defining the test structure for now
|
|
355
|
+
expect(true).toBe(true);
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
});
|
|
346
359
|
});
|