@tjamescouch/gro 1.3.2

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.
Files changed (44) hide show
  1. package/.github/workflows/ci.yml +20 -0
  2. package/README.md +218 -0
  3. package/_base.md +44 -0
  4. package/gro +198 -0
  5. package/owl/behaviors/agentic-turn.md +43 -0
  6. package/owl/components/cli.md +37 -0
  7. package/owl/components/drivers.md +29 -0
  8. package/owl/components/mcp.md +33 -0
  9. package/owl/components/memory.md +35 -0
  10. package/owl/components/session.md +35 -0
  11. package/owl/constraints.md +32 -0
  12. package/owl/product.md +28 -0
  13. package/owl/proposals/cooperative-scheduler.md +106 -0
  14. package/package.json +22 -0
  15. package/providers/claude.sh +50 -0
  16. package/providers/gemini.sh +36 -0
  17. package/providers/openai.py +85 -0
  18. package/src/drivers/anthropic.ts +215 -0
  19. package/src/drivers/index.ts +5 -0
  20. package/src/drivers/streaming-openai.ts +245 -0
  21. package/src/drivers/types.ts +33 -0
  22. package/src/errors.ts +97 -0
  23. package/src/logger.ts +28 -0
  24. package/src/main.ts +827 -0
  25. package/src/mcp/client.ts +147 -0
  26. package/src/mcp/index.ts +2 -0
  27. package/src/memory/advanced-memory.ts +263 -0
  28. package/src/memory/agent-memory.ts +61 -0
  29. package/src/memory/agenthnsw.ts +122 -0
  30. package/src/memory/index.ts +6 -0
  31. package/src/memory/simple-memory.ts +41 -0
  32. package/src/memory/vector-index.ts +30 -0
  33. package/src/session.ts +150 -0
  34. package/src/tools/agentpatch.ts +89 -0
  35. package/src/tools/bash.ts +61 -0
  36. package/src/utils/rate-limiter.ts +60 -0
  37. package/src/utils/retry.ts +32 -0
  38. package/src/utils/timed-fetch.ts +29 -0
  39. package/tests/errors.test.ts +246 -0
  40. package/tests/memory.test.ts +186 -0
  41. package/tests/rate-limiter.test.ts +76 -0
  42. package/tests/retry.test.ts +138 -0
  43. package/tests/timed-fetch.test.ts +104 -0
  44. package/tsconfig.json +13 -0
@@ -0,0 +1,147 @@
1
+ /**
2
+ * MCP client — connects to MCP servers, discovers tools, routes tool calls.
3
+ * Compatible with Claude Code's ~/.claude/settings.json mcpServers config.
4
+ */
5
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
6
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
7
+ import { Logger } from "../logger.js";
8
+ import { groError, asError, errorLogFields } from "../errors.js";
9
+
10
+ export interface McpServerConfig {
11
+ command: string;
12
+ args?: string[];
13
+ env?: Record<string, string>;
14
+ cwd?: string;
15
+ }
16
+
17
+ export interface McpTool {
18
+ name: string;
19
+ description?: string;
20
+ inputSchema: any;
21
+ /** Which MCP server provides this tool. */
22
+ serverName: string;
23
+ }
24
+
25
+ interface ConnectedServer {
26
+ name: string;
27
+ client: Client;
28
+ transport: StdioClientTransport;
29
+ tools: McpTool[];
30
+ }
31
+
32
+ export class McpManager {
33
+ private servers = new Map<string, ConnectedServer>();
34
+
35
+ /**
36
+ * Connect to all configured MCP servers and discover their tools.
37
+ */
38
+ async connectAll(configs: Record<string, McpServerConfig>): Promise<void> {
39
+ const entries = Object.entries(configs);
40
+ if (entries.length === 0) return;
41
+
42
+ Logger.debug(`Connecting to ${entries.length} MCP server(s)...`);
43
+
44
+ await Promise.all(
45
+ entries.map(([name, cfg]) => this.connectOne(name, cfg).catch((e: unknown) => {
46
+ const ge = groError("mcp_error", `MCP server "${name}" failed to connect: ${asError(e).message}`, {
47
+ retryable: true,
48
+ cause: e,
49
+ });
50
+ Logger.warn(ge.message, errorLogFields(ge));
51
+ }))
52
+ );
53
+ }
54
+
55
+ private async connectOne(name: string, cfg: McpServerConfig): Promise<void> {
56
+ const transport = new StdioClientTransport({
57
+ command: cfg.command,
58
+ args: cfg.args ?? [],
59
+ env: { ...process.env, ...cfg.env } as Record<string, string>,
60
+ cwd: cfg.cwd,
61
+ stderr: "pipe",
62
+ });
63
+
64
+ const client = new Client(
65
+ { name: "gro", version: "0.2.0" },
66
+ { capabilities: {} }
67
+ );
68
+
69
+ await client.connect(transport);
70
+
71
+ // Discover tools
72
+ const toolsResult = await client.listTools();
73
+ const tools: McpTool[] = (toolsResult.tools ?? []).map((t: any) => ({
74
+ name: t.name,
75
+ description: t.description,
76
+ inputSchema: t.inputSchema,
77
+ serverName: name,
78
+ }));
79
+
80
+ this.servers.set(name, { name, client, transport, tools });
81
+ Logger.debug(`MCP "${name}": ${tools.length} tool(s) available`);
82
+ }
83
+
84
+ /**
85
+ * Get all discovered tools in OpenAI function-calling format.
86
+ */
87
+ getToolDefinitions(): any[] {
88
+ const defs: any[] = [];
89
+ for (const server of this.servers.values()) {
90
+ for (const tool of server.tools) {
91
+ defs.push({
92
+ type: "function",
93
+ function: {
94
+ name: tool.name,
95
+ description: tool.description ?? "",
96
+ parameters: tool.inputSchema ?? { type: "object", properties: {} },
97
+ },
98
+ });
99
+ }
100
+ }
101
+ return defs;
102
+ }
103
+
104
+ /**
105
+ * Execute a tool call by routing it to the correct MCP server.
106
+ */
107
+ async callTool(name: string, args: Record<string, any>): Promise<string> {
108
+ // Find which server provides this tool
109
+ for (const server of this.servers.values()) {
110
+ const tool = server.tools.find(t => t.name === name);
111
+ if (tool) {
112
+ const result = await server.client.callTool({ name, arguments: args }, undefined, { timeout: 5 * 60 * 1000 });
113
+ // Extract text content from result
114
+ if (Array.isArray(result.content)) {
115
+ return result.content
116
+ .map((c: any) => {
117
+ if (c.type === "text") return c.text;
118
+ return JSON.stringify(c);
119
+ })
120
+ .join("\n");
121
+ }
122
+ return JSON.stringify(result);
123
+ }
124
+ }
125
+ throw new Error(`No MCP server provides tool "${name}"`);
126
+ }
127
+
128
+ /**
129
+ * Check if a tool name is provided by any connected MCP server.
130
+ */
131
+ hasTool(name: string): boolean {
132
+ for (const server of this.servers.values()) {
133
+ if (server.tools.some(t => t.name === name)) return true;
134
+ }
135
+ return false;
136
+ }
137
+
138
+ /**
139
+ * Disconnect all MCP servers.
140
+ */
141
+ async disconnectAll(): Promise<void> {
142
+ for (const server of this.servers.values()) {
143
+ try { await server.client.close(); } catch {}
144
+ }
145
+ this.servers.clear();
146
+ }
147
+ }
@@ -0,0 +1,2 @@
1
+ export { McpManager } from "./client.js";
2
+ export type { McpServerConfig, McpTool } from "./client.js";
@@ -0,0 +1,263 @@
1
+ import type { ChatDriver, ChatMessage } from "../drivers/types.js";
2
+ import { AgentMemory } from "./agent-memory.js";
3
+ import { saveSession, loadSession, ensureGroDir } from "../session.js";
4
+
5
+ /**
6
+ * AdvancedMemory — swim-lane summarization with token budgeting.
7
+ *
8
+ * Maintains three lanes (assistant / system / user) and summarizes independently.
9
+ * Uses character-based token estimation with high/low watermark hysteresis.
10
+ * Background summarization never blocks the caller.
11
+ */
12
+ export class AdvancedMemory extends AgentMemory {
13
+ private readonly driver: ChatDriver;
14
+ private readonly model: string;
15
+ private readonly summarizerDriver: ChatDriver;
16
+ private readonly summarizerModel: string;
17
+
18
+ private readonly contextTokens: number;
19
+ private readonly reserveHeaderTokens: number;
20
+ private readonly reserveResponseTokens: number;
21
+ private readonly highRatio: number;
22
+ private readonly lowRatio: number;
23
+ private readonly summaryRatio: number;
24
+ private readonly avgCharsPerToken: number;
25
+ private readonly keepRecentPerLane: number;
26
+ private readonly keepRecentTools: number;
27
+
28
+ constructor(args: {
29
+ driver: ChatDriver;
30
+ model: string;
31
+ summarizerDriver?: ChatDriver;
32
+ summarizerModel?: string;
33
+ systemPrompt?: string;
34
+ contextTokens?: number;
35
+ reserveHeaderTokens?: number;
36
+ reserveResponseTokens?: number;
37
+ highRatio?: number;
38
+ lowRatio?: number;
39
+ summaryRatio?: number;
40
+ avgCharsPerToken?: number;
41
+ keepRecentPerLane?: number;
42
+ keepRecentTools?: number;
43
+ }) {
44
+ super(args.systemPrompt);
45
+ this.driver = args.driver;
46
+ this.model = args.model;
47
+ this.summarizerDriver = args.summarizerDriver ?? args.driver;
48
+ this.summarizerModel = args.summarizerModel ?? args.model;
49
+
50
+ this.contextTokens = Math.max(2048, Math.floor(args.contextTokens ?? 8192));
51
+ this.reserveHeaderTokens = Math.max(0, Math.floor(args.reserveHeaderTokens ?? 1200));
52
+ this.reserveResponseTokens = Math.max(0, Math.floor(args.reserveResponseTokens ?? 800));
53
+ this.highRatio = Math.min(0.95, Math.max(0.55, args.highRatio ?? 0.70));
54
+ this.lowRatio = Math.min(this.highRatio - 0.05, Math.max(0.35, args.lowRatio ?? 0.50));
55
+ this.summaryRatio = Math.min(0.50, Math.max(0.15, args.summaryRatio ?? 0.35));
56
+ this.avgCharsPerToken = Math.max(1.5, Number(args.avgCharsPerToken ?? 4));
57
+ this.keepRecentPerLane = Math.max(1, Math.floor(args.keepRecentPerLane ?? 4));
58
+ this.keepRecentTools = Math.max(0, Math.floor(args.keepRecentTools ?? 3));
59
+ }
60
+
61
+ async load(id: string): Promise<void> {
62
+ const session = loadSession(id);
63
+ if (session) {
64
+ this.messagesBuffer = session.messages;
65
+ }
66
+ }
67
+
68
+ async save(id: string): Promise<void> {
69
+ ensureGroDir();
70
+ saveSession(id, this.messagesBuffer, {
71
+ id,
72
+ provider: "unknown",
73
+ model: this.model,
74
+ createdAt: new Date().toISOString(),
75
+ });
76
+ }
77
+
78
+ protected async onAfterAdd(): Promise<void> {
79
+ const budget = this.budgetTokens();
80
+ const estTok = this.estimateTokens(this.messagesBuffer);
81
+ if (estTok <= Math.floor(this.highRatio * budget)) return;
82
+
83
+ await this.runOnce(async () => {
84
+ const budget2 = this.budgetTokens();
85
+ const estTok2 = this.estimateTokens(this.messagesBuffer);
86
+ if (estTok2 <= Math.floor(this.highRatio * budget2)) return;
87
+
88
+ const { firstSystemIndex, assistant, user, system, tool, other } = this.partition();
89
+ const tailN = this.keepRecentPerLane;
90
+
91
+ const olderAssistant = assistant.slice(0, Math.max(0, assistant.length - tailN));
92
+ const keepAssistant = assistant.slice(Math.max(0, assistant.length - tailN));
93
+
94
+ const sysHead = firstSystemIndex === 0 ? [this.messagesBuffer[0]] : [];
95
+ const remainingSystem = firstSystemIndex === 0 ? system.slice(1) : system.slice(0);
96
+ const olderSystem = remainingSystem.slice(0, Math.max(0, remainingSystem.length - tailN));
97
+ const keepSystem = remainingSystem.slice(Math.max(0, remainingSystem.length - tailN));
98
+
99
+ const olderUser = user.slice(0, Math.max(0, user.length - tailN));
100
+ const keepUser = user.slice(Math.max(0, user.length - tailN));
101
+
102
+ const keepTools = tool.slice(Math.max(0, tool.length - this.keepRecentTools));
103
+
104
+ const preserved: ChatMessage[] = [
105
+ ...sysHead, ...keepAssistant, ...keepSystem, ...keepUser, ...keepTools, ...other,
106
+ ];
107
+ const preservedTok = this.estimateTokens(preserved);
108
+ const lowTarget = Math.floor(this.lowRatio * budget2);
109
+ const maxSummaryTok = Math.floor(this.summaryRatio * budget2);
110
+
111
+ if (preservedTok <= lowTarget) {
112
+ const rebuilt = this.ordered([], sysHead, keepAssistant, keepSystem, keepUser, keepTools, other);
113
+ this.messagesBuffer.splice(0, this.messagesBuffer.length, ...rebuilt);
114
+ return;
115
+ }
116
+
117
+ const removedCharA = this.totalChars(olderAssistant);
118
+ const removedCharS = this.totalChars(olderSystem);
119
+ const removedCharU = this.totalChars(olderUser);
120
+ const removedTotal = Math.max(1, removedCharA + removedCharS + removedCharU);
121
+
122
+ const totalSummaryBudget = Math.max(64, Math.min(maxSummaryTok, lowTarget - preservedTok));
123
+ const budgetA = Math.max(48, Math.floor(totalSummaryBudget * (removedCharA / removedTotal)));
124
+ const budgetS = Math.max(48, Math.floor(totalSummaryBudget * (removedCharS / removedTotal)));
125
+ const budgetU = Math.max(48, Math.floor(totalSummaryBudget * (removedCharU / removedTotal)));
126
+
127
+ const [sumA, sumS, sumU] = await Promise.all([
128
+ olderAssistant.length ? this.summarizeLane("assistant", olderAssistant, budgetA) : "",
129
+ olderSystem.length ? this.summarizeLane("system", olderSystem, budgetS) : "",
130
+ olderUser.length ? this.summarizeLane("user", olderUser, budgetU) : "",
131
+ ]);
132
+
133
+ const summaries: ChatMessage[] = [];
134
+ if (sumA) summaries.push({ from: "Me", role: "assistant", content: `ASSISTANT SUMMARY:\n${sumA}` });
135
+ if (sumS) summaries.push({ from: "System", role: "system", content: `SYSTEM SUMMARY:\n${sumS}` });
136
+ if (sumU) summaries.push({ from: "Memory", role: "user", content: `USER SUMMARY:\n${sumU}` });
137
+
138
+ const rebuilt = this.ordered(summaries, sysHead, keepAssistant, keepSystem, keepUser, keepTools, other);
139
+ this.messagesBuffer.splice(0, this.messagesBuffer.length, ...rebuilt);
140
+
141
+ // Final clamp
142
+ let finalTok = this.estimateTokens(this.messagesBuffer);
143
+ if (finalTok > lowTarget) {
144
+ const pruned: ChatMessage[] = [];
145
+ for (const m of this.messagesBuffer) {
146
+ pruned.push(m);
147
+ finalTok = this.estimateTokens(pruned);
148
+ if (finalTok > lowTarget && m.role !== "system") {
149
+ pruned.pop();
150
+ }
151
+ }
152
+ this.messagesBuffer.splice(0, this.messagesBuffer.length, ...pruned);
153
+ }
154
+ });
155
+ }
156
+
157
+ private budgetTokens(): number {
158
+ return Math.max(512, this.contextTokens - this.reserveHeaderTokens - this.reserveResponseTokens);
159
+ }
160
+
161
+ private estimateTokens(msgs: ChatMessage[]): number {
162
+ return Math.ceil(this.totalChars(msgs) / this.avgCharsPerToken);
163
+ }
164
+
165
+ private totalChars(msgs: ChatMessage[]): number {
166
+ let c = 0;
167
+ for (const m of msgs) {
168
+ const s = String((m as any).content ?? "");
169
+ if (m.role === "tool" && s.length > 24_000) c += 24_000;
170
+ else c += s.length;
171
+ c += 32;
172
+ }
173
+ return c;
174
+ }
175
+
176
+ private partition() {
177
+ const assistant: ChatMessage[] = [];
178
+ const user: ChatMessage[] = [];
179
+ const system: ChatMessage[] = [];
180
+ const tool: ChatMessage[] = [];
181
+ const other: ChatMessage[] = [];
182
+
183
+ for (const m of this.messagesBuffer) {
184
+ switch (m.role) {
185
+ case "assistant": assistant.push(m); break;
186
+ case "user": user.push(m); break;
187
+ case "system": system.push(m); break;
188
+ case "tool": tool.push(m); break;
189
+ default: other.push(m); break;
190
+ }
191
+ }
192
+ const firstSystemIndex = this.messagesBuffer.findIndex(x => x.role === "system");
193
+ return { firstSystemIndex, assistant, user, system, tool, other };
194
+ }
195
+
196
+ private ordered(
197
+ summaries: ChatMessage[],
198
+ sysHead: ChatMessage[],
199
+ keepA: ChatMessage[],
200
+ keepS: ChatMessage[],
201
+ keepU: ChatMessage[],
202
+ keepT: ChatMessage[],
203
+ other: ChatMessage[],
204
+ ): ChatMessage[] {
205
+ const keepSet = new Set([...sysHead, ...keepA, ...keepS, ...keepU, ...keepT, ...other]);
206
+ const rest: ChatMessage[] = [];
207
+ for (const m of this.messagesBuffer) {
208
+ if (keepSet.has(m)) rest.push(m);
209
+ }
210
+ const orderedSummaries = [
211
+ ...summaries.filter(s => s.role === "assistant"),
212
+ ...summaries.filter(s => s.role === "system"),
213
+ ...summaries.filter(s => s.role === "user"),
214
+ ];
215
+ return [...orderedSummaries, ...rest];
216
+ }
217
+
218
+ private async summarizeLane(
219
+ laneName: "assistant" | "system" | "user",
220
+ messages: ChatMessage[],
221
+ tokenBudget: number,
222
+ ): Promise<string> {
223
+ if (messages.length === 0 || tokenBudget <= 0) return "";
224
+ const approxChars = Math.max(120, Math.floor(tokenBudget * this.avgCharsPerToken));
225
+
226
+ const header = (() => {
227
+ switch (laneName) {
228
+ case "assistant": return "Summarize prior ASSISTANT replies (decisions, plans, code edits, shell commands and outcomes).";
229
+ case "system": return "Summarize SYSTEM instructions (rules, goals, constraints) without changing their intent.";
230
+ case "user": return "Summarize USER requests, feedback, constraints, and acceptance criteria.";
231
+ }
232
+ })();
233
+
234
+ let acc = "";
235
+ for (const m of messages) {
236
+ let c = String((m as any).content ?? "");
237
+ if (c.length > 4000) c = c.slice(0, 4000) + "\n…(truncated)…";
238
+ const next = `- ${laneName.toUpperCase()}: ${c}\n\n`;
239
+ if (acc.length + next.length > approxChars * 3) break;
240
+ acc += next;
241
+ }
242
+
243
+ const sys: ChatMessage = {
244
+ role: "system",
245
+ from: "System",
246
+ content: [
247
+ "You are a precise summarizer.",
248
+ "Output concise bullet points; preserve facts, tasks, file paths, commands, constraints.",
249
+ `Hard limit: ~${approxChars} characters total.`,
250
+ "Avoid fluff; keep actionable details.",
251
+ ].join(" "),
252
+ };
253
+
254
+ const usr: ChatMessage = {
255
+ role: "user",
256
+ from: "User",
257
+ content: `${header}\n\nTranscript:\n${acc}`,
258
+ };
259
+
260
+ const out = await this.summarizerDriver.chat([sys, usr], { model: this.summarizerModel });
261
+ return String((out as any)?.text ?? "").trim();
262
+ }
263
+ }
@@ -0,0 +1,61 @@
1
+ import type { ChatMessage } from "../drivers/types.js";
2
+
3
+ /**
4
+ * Base class for agent memory with background summarization support.
5
+ * Subclasses call `runOnce` to serialize/queue summarization so callers never block.
6
+ */
7
+ export abstract class AgentMemory {
8
+ protected messagesBuffer: ChatMessage[] = [];
9
+
10
+ private summarizing = false;
11
+ private pending = false;
12
+
13
+ constructor(systemPrompt?: string) {
14
+ if (systemPrompt && systemPrompt.trim().length > 0) {
15
+ this.messagesBuffer.push({ role: "system", content: systemPrompt, from: "System" });
16
+ }
17
+ }
18
+
19
+ abstract load(id: string): Promise<void>;
20
+ abstract save(id: string): Promise<void>;
21
+
22
+ async add(msg: ChatMessage): Promise<void> {
23
+ this.messagesBuffer.push(msg);
24
+ await this.onAfterAdd();
25
+ }
26
+
27
+ async addIfNotExists(msg: ChatMessage): Promise<void> {
28
+ const exists = this.messagesBuffer.some(m => m.content === msg.content && m.role === msg.role);
29
+ if (!exists) {
30
+ this.messagesBuffer.push(msg);
31
+ await this.onAfterAdd();
32
+ }
33
+ }
34
+
35
+ protected abstract onAfterAdd(): Promise<void>;
36
+
37
+ messages(): ChatMessage[] {
38
+ return [...this.messagesBuffer];
39
+ }
40
+
41
+ protected nonSystemCount(): number {
42
+ if (this.messagesBuffer.length === 0) return 0;
43
+ return this.messagesBuffer[0].role === "system"
44
+ ? this.messagesBuffer.length - 1
45
+ : this.messagesBuffer.length;
46
+ }
47
+
48
+ protected async runOnce(task: () => Promise<void>): Promise<void> {
49
+ if (this.summarizing) { this.pending = true; return; }
50
+ this.summarizing = true;
51
+ try {
52
+ await task();
53
+ } finally {
54
+ this.summarizing = false;
55
+ if (this.pending) {
56
+ this.pending = false;
57
+ void this.runOnce(task);
58
+ }
59
+ }
60
+ }
61
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * agenthnsw adapter — optional dynamic import.
3
+ *
4
+ * The "agenthnsw" package is an optional dependency. We load it at
5
+ * runtime via a dynamic `import()` so that TypeScript compilation and
6
+ * Docker builds succeed even when the package is not installed.
7
+ *
8
+ * If agenthnsw is missing at runtime the factory function
9
+ * `createAgentHnswIndex()` will throw a clear error.
10
+ */
11
+
12
+ import type { VectorIndex, VectorRecord, VectorSearchResult, Vector } from "./vector-index.js";
13
+
14
+ // ── Dynamic loader ──────────────────────────────────────────────────────────
15
+
16
+ interface InMemoryLinearIndexLike {
17
+ upsert(record: { id: string; vector: Vector; metadata?: unknown }): Promise<void>;
18
+ upsertMany(records: { id: string; vector: Vector; metadata?: unknown }[]): Promise<void>;
19
+ search(query: Vector, k: number): Promise<{ id: string; score: number; metadata?: unknown }[]>;
20
+ delete(id: string): Promise<void>;
21
+ save(dir: string): Promise<void>;
22
+ load(dir: string): Promise<void>;
23
+ stats(): Promise<{ count: number; dims?: number }>;
24
+ }
25
+
26
+ interface AgentHnswModule {
27
+ InMemoryLinearIndex: new (opts?: { metric?: "cosine" | "l2" }) => InMemoryLinearIndexLike;
28
+ }
29
+
30
+ /**
31
+ * Dynamically import "agenthnsw".
32
+ *
33
+ * We use `eval("(m) => import(m)")` to prevent the TypeScript compiler
34
+ * and bundlers from resolving the specifier at compile time.
35
+ */
36
+ async function importAgentHnsw(): Promise<AgentHnswModule> {
37
+ try {
38
+ const dynImport: (m: string) => Promise<AgentHnswModule> = eval("(m) => import(m)");
39
+ return await dynImport("agenthnsw");
40
+ } catch (err: unknown) {
41
+ throw new Error(
42
+ `Optional dependency "agenthnsw" is not installed. ` +
43
+ `Install it with: npm install agenthnsw\n` +
44
+ `Original error: ${err instanceof Error ? err.message : String(err)}`
45
+ );
46
+ }
47
+ }
48
+
49
+ // ── AgentHnswIndex class ────────────────────────────────────────────────────
50
+
51
+ export class AgentHnswIndex implements VectorIndex {
52
+ private idx: InMemoryLinearIndexLike | null = null;
53
+ private readonly metric: "cosine" | "l2" | undefined;
54
+
55
+ constructor(opts?: { metric?: "cosine" | "l2" }) {
56
+ this.metric = opts?.metric;
57
+ }
58
+
59
+ /** Lazily initialise the underlying index on first use. */
60
+ private async ensureIndex(): Promise<InMemoryLinearIndexLike> {
61
+ if (!this.idx) {
62
+ const mod = await importAgentHnsw();
63
+ this.idx = new mod.InMemoryLinearIndex({ metric: this.metric });
64
+ }
65
+ return this.idx;
66
+ }
67
+
68
+ async upsert(record: VectorRecord): Promise<void> {
69
+ const idx = await this.ensureIndex();
70
+ await idx.upsert({ id: record.id, vector: record.vector, metadata: record.metadata });
71
+ }
72
+
73
+ async upsertMany(records: VectorRecord[]): Promise<void> {
74
+ const idx = await this.ensureIndex();
75
+ await idx.upsertMany(
76
+ records.map((r: VectorRecord) => ({ id: r.id, vector: r.vector, metadata: r.metadata }))
77
+ );
78
+ }
79
+
80
+ async search(query: Vector, k: number): Promise<VectorSearchResult[]> {
81
+ const idx = await this.ensureIndex();
82
+ const res = await idx.search(query, k);
83
+ return res.map((r: { id: string; score: number; metadata?: unknown }) => ({
84
+ id: r.id,
85
+ score: r.score,
86
+ metadata: r.metadata,
87
+ }));
88
+ }
89
+
90
+ async delete(id: string): Promise<void> {
91
+ const idx = await this.ensureIndex();
92
+ await idx.delete(id);
93
+ }
94
+
95
+ async save(dir: string): Promise<void> {
96
+ const idx = await this.ensureIndex();
97
+ await idx.save(dir);
98
+ }
99
+
100
+ async load(dir: string): Promise<void> {
101
+ const idx = await this.ensureIndex();
102
+ await idx.load(dir);
103
+ }
104
+
105
+ async stats(): Promise<{ count: number; dims?: number }> {
106
+ const idx = await this.ensureIndex();
107
+ return await idx.stats();
108
+ }
109
+ }
110
+
111
+ // ── Factory ─────────────────────────────────────────────────────────────────
112
+
113
+ /**
114
+ * Create an AgentHnswIndex.
115
+ *
116
+ * The underlying "agenthnsw" package is loaded lazily — this function
117
+ * itself never throws even when the package is absent. The first call
118
+ * to any index method will attempt the dynamic import.
119
+ */
120
+ export function createAgentHnswIndex(opts?: { metric?: "cosine" | "l2" }): AgentHnswIndex {
121
+ return new AgentHnswIndex(opts);
122
+ }
@@ -0,0 +1,6 @@
1
+ export * from "./vector-index.js";
2
+ export * from "./agenthnsw.js";
3
+
4
+ export { AgentMemory } from "./agent-memory.js";
5
+ export { AdvancedMemory } from "./advanced-memory.js";
6
+ export { SimpleMemory } from "./simple-memory.js";
@@ -0,0 +1,41 @@
1
+ import type { ChatMessage } from "../drivers/types.js";
2
+ import { AgentMemory } from "./agent-memory.js";
3
+ import { saveSession, loadSession, ensureGroDir } from "../session.js";
4
+
5
+ /**
6
+ * SimpleMemory — unbounded message buffer.
7
+ * No summarization, no token budgeting. Useful for short conversations
8
+ * or when the caller manages context externally.
9
+ */
10
+ export class SimpleMemory extends AgentMemory {
11
+ private provider = "";
12
+ private model = "";
13
+
14
+ constructor(systemPrompt?: string) {
15
+ super(systemPrompt);
16
+ }
17
+
18
+ setMeta(provider: string, model: string): void {
19
+ this.provider = provider;
20
+ this.model = model;
21
+ }
22
+
23
+ async load(id: string): Promise<void> {
24
+ const session = loadSession(id);
25
+ if (session) {
26
+ this.messagesBuffer = session.messages;
27
+ }
28
+ }
29
+
30
+ async save(id: string): Promise<void> {
31
+ ensureGroDir();
32
+ saveSession(id, this.messagesBuffer, {
33
+ id,
34
+ provider: this.provider,
35
+ model: this.model,
36
+ createdAt: new Date().toISOString(),
37
+ });
38
+ }
39
+
40
+ protected async onAfterAdd(): Promise<void> {}
41
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Pluggable vector index interface for retrieval-backed memory.
3
+ *
4
+ * This is intentionally small so backends can live in separate packages
5
+ * (e.g. agenthnsw) and be imported by gro.
6
+ */
7
+
8
+ export type Vector = Float32Array | number[];
9
+
10
+ export interface VectorRecord {
11
+ id: string;
12
+ vector: Vector;
13
+ metadata?: unknown;
14
+ }
15
+
16
+ export interface VectorSearchResult {
17
+ id: string;
18
+ score: number;
19
+ metadata?: unknown;
20
+ }
21
+
22
+ export interface VectorIndex {
23
+ upsert(record: VectorRecord): Promise<void>;
24
+ upsertMany(records: VectorRecord[]): Promise<void>;
25
+ search(query: Vector, k: number): Promise<VectorSearchResult[]>;
26
+ delete(id: string): Promise<void>;
27
+ save(dir: string): Promise<void>;
28
+ load(dir: string): Promise<void>;
29
+ stats(): Promise<{ count: number; dims?: number }>;
30
+ }