@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,186 @@
1
+ /**
2
+ * Tests for AgentMemory, SimpleMemory, and AdvancedMemory.
3
+ */
4
+
5
+ import { test, describe } from "node:test";
6
+ import assert from "node:assert";
7
+ import type { ChatDriver, ChatMessage, ChatOutput } from "../src/drivers/types.js";
8
+ import { SimpleMemory } from "../src/memory/simple-memory.js";
9
+ import { AdvancedMemory } from "../src/memory/advanced-memory.js";
10
+
11
+ /** Mock driver that returns a canned summary. */
12
+ function mockDriver(response = "Summary bullet point."): ChatDriver {
13
+ return {
14
+ async chat(_msgs: ChatMessage[], _opts?: any): Promise<ChatOutput> {
15
+ return { text: response, toolCalls: [] };
16
+ },
17
+ };
18
+ }
19
+
20
+ /** Helper to build a message. */
21
+ function msg(role: string, content: string, from?: string): ChatMessage {
22
+ return { role, content, from: from ?? role };
23
+ }
24
+
25
+ describe("SimpleMemory", () => {
26
+ test("stores messages in order", async () => {
27
+ const mem = new SimpleMemory();
28
+ await mem.add(msg("user", "hello"));
29
+ await mem.add(msg("assistant", "hi"));
30
+ const msgs = mem.messages();
31
+ assert.strictEqual(msgs.length, 2);
32
+ assert.strictEqual(msgs[0].content, "hello");
33
+ assert.strictEqual(msgs[1].content, "hi");
34
+ });
35
+
36
+ test("prepends system prompt", async () => {
37
+ const mem = new SimpleMemory("You are helpful.");
38
+ await mem.add(msg("user", "hello"));
39
+ const msgs = mem.messages();
40
+ assert.strictEqual(msgs.length, 2);
41
+ assert.strictEqual(msgs[0].role, "system");
42
+ assert.strictEqual(msgs[0].content, "You are helpful.");
43
+ assert.strictEqual(msgs[1].content, "hello");
44
+ });
45
+
46
+ test("empty system prompt is not added", async () => {
47
+ const mem = new SimpleMemory(" ");
48
+ const msgs = mem.messages();
49
+ assert.strictEqual(msgs.length, 0);
50
+ });
51
+
52
+ test("addIfNotExists deduplicates", async () => {
53
+ const mem = new SimpleMemory();
54
+ await mem.add(msg("user", "hello"));
55
+ await mem.addIfNotExists(msg("user", "hello"));
56
+ await mem.addIfNotExists(msg("user", "world"));
57
+ const msgs = mem.messages();
58
+ assert.strictEqual(msgs.length, 2);
59
+ assert.strictEqual(msgs[1].content, "world");
60
+ });
61
+
62
+ test("messages() returns a copy", async () => {
63
+ const mem = new SimpleMemory();
64
+ await mem.add(msg("user", "hello"));
65
+ const msgs = mem.messages();
66
+ msgs.push(msg("user", "injected"));
67
+ assert.strictEqual(mem.messages().length, 1);
68
+ });
69
+
70
+ test("load and save are no-ops", async () => {
71
+ const mem = new SimpleMemory();
72
+ await mem.load("test-id");
73
+ await mem.save("test-id");
74
+ // No error thrown
75
+ });
76
+ });
77
+
78
+ describe("AdvancedMemory", () => {
79
+ test("stores messages when under budget", async () => {
80
+ const mem = new AdvancedMemory({
81
+ driver: mockDriver(),
82
+ model: "test-model",
83
+ contextTokens: 100_000,
84
+ });
85
+ await mem.add(msg("user", "hello"));
86
+ await mem.add(msg("assistant", "hi"));
87
+ const msgs = mem.messages();
88
+ assert.strictEqual(msgs.length, 2);
89
+ });
90
+
91
+ test("includes system prompt", async () => {
92
+ const mem = new AdvancedMemory({
93
+ driver: mockDriver(),
94
+ model: "test-model",
95
+ systemPrompt: "Be helpful.",
96
+ });
97
+ await mem.add(msg("user", "hello"));
98
+ const msgs = mem.messages();
99
+ assert.strictEqual(msgs[0].role, "system");
100
+ assert.strictEqual(msgs[0].content, "Be helpful.");
101
+ });
102
+
103
+ test("triggers compaction when budget exceeded", async () => {
104
+ const mem = new AdvancedMemory({
105
+ driver: mockDriver(),
106
+ model: "test-model",
107
+ contextTokens: 4096,
108
+ reserveHeaderTokens: 200,
109
+ reserveResponseTokens: 200,
110
+ avgCharsPerToken: 4,
111
+ highRatio: 0.70,
112
+ lowRatio: 0.50,
113
+ keepRecentPerLane: 2,
114
+ });
115
+
116
+ // Add enough messages to exceed the high watermark.
117
+ // Budget = 3696 tokens. High = 2587 tokens = ~10348 chars.
118
+ // Each msg pair ~200 chars + 64 overhead = ~264 chars.
119
+ // Need ~40 pairs to exceed.
120
+ for (let i = 0; i < 60; i++) {
121
+ await mem.add(msg("user", `Message number ${i} with padding text to use up token budget quickly.`));
122
+ await mem.add(msg("assistant", `Reply number ${i} acknowledging the user's message with detail.`));
123
+ }
124
+
125
+ // After compaction (summarization or pruning), buffer should be smaller
126
+ const msgs = mem.messages();
127
+ assert.ok(msgs.length < 120, `Expected compaction to reduce messages, got ${msgs.length}`);
128
+ // Should still have some messages (not empty)
129
+ assert.ok(msgs.length > 0, "Buffer should not be empty after compaction");
130
+ });
131
+
132
+ test("preserves system prompt during summarization", async () => {
133
+ const mem = new AdvancedMemory({
134
+ driver: mockDriver(),
135
+ model: "test-model",
136
+ systemPrompt: "Critical instruction.",
137
+ contextTokens: 2048,
138
+ avgCharsPerToken: 4,
139
+ keepRecentPerLane: 1,
140
+ });
141
+
142
+ for (let i = 0; i < 30; i++) {
143
+ await mem.add(msg("user", `User msg ${i} with padding to fill the budget up.`));
144
+ await mem.add(msg("assistant", `Assistant reply ${i} also with padding text.`));
145
+ }
146
+
147
+ const msgs = mem.messages();
148
+ const systemMsgs = msgs.filter(m => m.role === "system");
149
+ assert.ok(systemMsgs.length > 0, "System messages should be preserved after summarization");
150
+ });
151
+
152
+ test("handles tool messages", async () => {
153
+ const mem = new AdvancedMemory({
154
+ driver: mockDriver(),
155
+ model: "test-model",
156
+ contextTokens: 100_000,
157
+ keepRecentTools: 2,
158
+ });
159
+
160
+ await mem.add(msg("user", "Run the tool"));
161
+ await mem.add({ role: "tool", content: "tool result 1", from: "tool", tool_call_id: "tc1" });
162
+ await mem.add({ role: "tool", content: "tool result 2", from: "tool", tool_call_id: "tc2" });
163
+ await mem.add(msg("assistant", "Got it"));
164
+
165
+ const msgs = mem.messages();
166
+ const toolMsgs = msgs.filter(m => m.role === "tool");
167
+ assert.strictEqual(toolMsgs.length, 2);
168
+ });
169
+
170
+ test("config clamps extreme values", async () => {
171
+ // This shouldn't throw — extreme values get clamped
172
+ const mem = new AdvancedMemory({
173
+ driver: mockDriver(),
174
+ model: "test-model",
175
+ contextTokens: 100, // gets clamped to 2048
176
+ highRatio: 99, // clamped to 0.95
177
+ lowRatio: -5, // clamped to 0.35
178
+ summaryRatio: 999, // clamped to 0.50
179
+ avgCharsPerToken: 0.1, // clamped to 1.5
180
+ keepRecentPerLane: -1, // clamped to 1
181
+ keepRecentTools: -10, // clamped to 0
182
+ });
183
+ await mem.add(msg("user", "still works"));
184
+ assert.strictEqual(mem.messages().length, 1);
185
+ });
186
+ });
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Tests for RateLimiter.
3
+ */
4
+
5
+ import { test, describe } from "node:test";
6
+ import assert from "node:assert";
7
+ import { rateLimiter } from "../src/utils/rate-limiter.js";
8
+
9
+ describe("RateLimiter", () => {
10
+ test("first call returns immediately", async () => {
11
+ rateLimiter.reset("test-immediate");
12
+ const start = Date.now();
13
+ await rateLimiter.limit("test-immediate", 10);
14
+ const elapsed = Date.now() - start;
15
+ assert.ok(elapsed < 200, `First call should be near-instant, took ${elapsed}ms`);
16
+ });
17
+
18
+ test("second call is delayed", async () => {
19
+ rateLimiter.reset("test-delay");
20
+ await rateLimiter.limit("test-delay", 2); // 2 per second = 500ms interval
21
+ const start = Date.now();
22
+ await rateLimiter.limit("test-delay", 2);
23
+ const elapsed = Date.now() - start;
24
+ assert.ok(elapsed >= 400, `Second call should wait ~500ms, took ${elapsed}ms`);
25
+ assert.ok(elapsed < 800, `Should not wait too long, took ${elapsed}ms`);
26
+ });
27
+
28
+ test("different keys are independent", async () => {
29
+ rateLimiter.reset();
30
+ await rateLimiter.limit("key-a", 1);
31
+ const start = Date.now();
32
+ await rateLimiter.limit("key-b", 1); // different key, no wait
33
+ const elapsed = Date.now() - start;
34
+ assert.ok(elapsed < 200, `Different key should not wait, took ${elapsed}ms`);
35
+ });
36
+
37
+ test("throws on invalid throughput", async () => {
38
+ await assert.rejects(
39
+ () => rateLimiter.limit("bad", 0),
40
+ /positive finite number/
41
+ );
42
+ await assert.rejects(
43
+ () => rateLimiter.limit("bad", -1),
44
+ /positive finite number/
45
+ );
46
+ await assert.rejects(
47
+ () => rateLimiter.limit("bad", Infinity),
48
+ /positive finite number/
49
+ );
50
+ await assert.rejects(
51
+ () => rateLimiter.limit("bad", NaN),
52
+ /positive finite number/
53
+ );
54
+ });
55
+
56
+ test("reset clears state", async () => {
57
+ rateLimiter.reset("test-reset");
58
+ await rateLimiter.limit("test-reset", 1); // first call
59
+ rateLimiter.reset("test-reset"); // reset
60
+ const start = Date.now();
61
+ await rateLimiter.limit("test-reset", 1); // should be instant after reset
62
+ const elapsed = Date.now() - start;
63
+ assert.ok(elapsed < 200, `After reset should be instant, took ${elapsed}ms`);
64
+ });
65
+
66
+ test("reset() without args clears all keys", async () => {
67
+ await rateLimiter.limit("x1", 1);
68
+ await rateLimiter.limit("x2", 1);
69
+ rateLimiter.reset();
70
+ const start = Date.now();
71
+ await rateLimiter.limit("x1", 1);
72
+ await rateLimiter.limit("x2", 1);
73
+ const elapsed = Date.now() - start;
74
+ assert.ok(elapsed < 200, `After global reset both should be instant, took ${elapsed}ms`);
75
+ });
76
+ });
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Tests for retry utilities.
3
+ *
4
+ * Covers: isRetryable, retryDelay, sleep, MAX_RETRIES, RETRY_BASE_MS
5
+ */
6
+
7
+ import { test, describe } from "node:test";
8
+ import assert from "node:assert";
9
+ import { isRetryable, retryDelay, sleep, MAX_RETRIES, RETRY_BASE_MS } from "../src/utils/retry.js";
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // isRetryable
13
+ // ---------------------------------------------------------------------------
14
+
15
+ describe("isRetryable", () => {
16
+ test("429 (rate limited) is retryable", () => {
17
+ assert.strictEqual(isRetryable(429), true);
18
+ });
19
+
20
+ test("502 (bad gateway) is retryable", () => {
21
+ assert.strictEqual(isRetryable(502), true);
22
+ });
23
+
24
+ test("503 (service unavailable) is retryable", () => {
25
+ assert.strictEqual(isRetryable(503), true);
26
+ });
27
+
28
+ test("529 (overloaded) is retryable", () => {
29
+ assert.strictEqual(isRetryable(529), true);
30
+ });
31
+
32
+ test("200 is not retryable", () => {
33
+ assert.strictEqual(isRetryable(200), false);
34
+ });
35
+
36
+ test("400 (bad request) is not retryable", () => {
37
+ assert.strictEqual(isRetryable(400), false);
38
+ });
39
+
40
+ test("401 (unauthorized) is not retryable", () => {
41
+ assert.strictEqual(isRetryable(401), false);
42
+ });
43
+
44
+ test("403 (forbidden) is not retryable", () => {
45
+ assert.strictEqual(isRetryable(403), false);
46
+ });
47
+
48
+ test("404 (not found) is not retryable", () => {
49
+ assert.strictEqual(isRetryable(404), false);
50
+ });
51
+
52
+ test("500 (internal server error) is not retryable", () => {
53
+ assert.strictEqual(isRetryable(500), false);
54
+ });
55
+ });
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // retryDelay
59
+ // ---------------------------------------------------------------------------
60
+
61
+ describe("retryDelay", () => {
62
+ test("attempt 0 returns delay in [1000, 1500) range", () => {
63
+ // base = 1000 * 2^0 = 1000, jitter = [0, 500)
64
+ for (let i = 0; i < 100; i++) {
65
+ const delay = retryDelay(0);
66
+ assert.ok(delay >= 1000, `delay ${delay} should be >= 1000`);
67
+ assert.ok(delay < 1500, `delay ${delay} should be < 1500`);
68
+ }
69
+ });
70
+
71
+ test("attempt 1 returns delay in [2000, 3000) range", () => {
72
+ // base = 1000 * 2^1 = 2000, jitter = [0, 1000)
73
+ for (let i = 0; i < 100; i++) {
74
+ const delay = retryDelay(1);
75
+ assert.ok(delay >= 2000, `delay ${delay} should be >= 2000`);
76
+ assert.ok(delay < 3000, `delay ${delay} should be < 3000`);
77
+ }
78
+ });
79
+
80
+ test("attempt 2 returns delay in [4000, 6000) range", () => {
81
+ // base = 1000 * 2^2 = 4000, jitter = [0, 2000)
82
+ for (let i = 0; i < 100; i++) {
83
+ const delay = retryDelay(2);
84
+ assert.ok(delay >= 4000, `delay ${delay} should be >= 4000`);
85
+ assert.ok(delay < 6000, `delay ${delay} should be < 6000`);
86
+ }
87
+ });
88
+
89
+ test("delay increases with attempt number (exponential)", () => {
90
+ // Run enough samples that minimum of higher attempt > maximum of lower attempt (statistically)
91
+ const delays0 = Array.from({ length: 50 }, () => retryDelay(0));
92
+ const delays2 = Array.from({ length: 50 }, () => retryDelay(2));
93
+ const max0 = Math.max(...delays0);
94
+ const min2 = Math.min(...delays2);
95
+ assert.ok(min2 > max0, `min attempt-2 delay (${min2}) should exceed max attempt-0 delay (${max0})`);
96
+ });
97
+
98
+ test("includes jitter (not always the same value)", () => {
99
+ const delays = Array.from({ length: 20 }, () => retryDelay(0));
100
+ const unique = new Set(delays);
101
+ assert.ok(unique.size > 1, "delays should vary due to jitter");
102
+ });
103
+ });
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // sleep
107
+ // ---------------------------------------------------------------------------
108
+
109
+ describe("sleep", () => {
110
+ test("resolves after approximately the specified duration", async () => {
111
+ const start = Date.now();
112
+ await sleep(50);
113
+ const elapsed = Date.now() - start;
114
+ assert.ok(elapsed >= 40, `should wait at least ~50ms, took ${elapsed}ms`);
115
+ assert.ok(elapsed < 200, `should not wait too long, took ${elapsed}ms`);
116
+ });
117
+
118
+ test("sleep(0) resolves nearly immediately", async () => {
119
+ const start = Date.now();
120
+ await sleep(0);
121
+ const elapsed = Date.now() - start;
122
+ assert.ok(elapsed < 50, `sleep(0) should be near-instant, took ${elapsed}ms`);
123
+ });
124
+ });
125
+
126
+ // ---------------------------------------------------------------------------
127
+ // Constants
128
+ // ---------------------------------------------------------------------------
129
+
130
+ describe("retry constants", () => {
131
+ test("MAX_RETRIES is 3", () => {
132
+ assert.strictEqual(MAX_RETRIES, 3);
133
+ });
134
+
135
+ test("RETRY_BASE_MS is 1000", () => {
136
+ assert.strictEqual(RETRY_BASE_MS, 1000);
137
+ });
138
+ });
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Tests for timedFetch.
3
+ */
4
+
5
+ import { test, describe } from "node:test";
6
+ import assert from "node:assert";
7
+ import http from "http";
8
+ import { timedFetch } from "../src/utils/timed-fetch.js";
9
+
10
+ function startServer(handler: http.RequestListener): Promise<{ port: number; close: () => Promise<void> }> {
11
+ return new Promise((resolve) => {
12
+ const server = http.createServer(handler);
13
+ server.listen(0, () => {
14
+ const port = (server.address() as any).port;
15
+ resolve({
16
+ port,
17
+ close: () => new Promise<void>((r) => server.close(() => r())),
18
+ });
19
+ });
20
+ });
21
+ }
22
+
23
+ describe("timedFetch", () => {
24
+ test("successful GET", async () => {
25
+ const srv = await startServer((_req, res) => {
26
+ res.writeHead(200, { "content-type": "application/json" });
27
+ res.end(JSON.stringify({ ok: true }));
28
+ });
29
+
30
+ try {
31
+ const res = await timedFetch(`http://localhost:${srv.port}/`, {
32
+ where: "test",
33
+ });
34
+ assert.strictEqual(res.status, 200);
35
+ const data = await res.json() as any;
36
+ assert.strictEqual(data.ok, true);
37
+ } finally {
38
+ await srv.close();
39
+ }
40
+ });
41
+
42
+ test("successful POST with body", async () => {
43
+ const srv = await startServer((req, res) => {
44
+ let body = "";
45
+ req.on("data", (d: Buffer) => (body += d.toString()));
46
+ req.on("end", () => {
47
+ res.writeHead(200, { "content-type": "application/json" });
48
+ res.end(JSON.stringify({ echo: body }));
49
+ });
50
+ });
51
+
52
+ try {
53
+ const res = await timedFetch(`http://localhost:${srv.port}/`, {
54
+ method: "POST",
55
+ body: "hello",
56
+ where: "test-post",
57
+ });
58
+ assert.strictEqual(res.status, 200);
59
+ const data = await res.json() as any;
60
+ assert.strictEqual(data.echo, "hello");
61
+ } finally {
62
+ await srv.close();
63
+ }
64
+ });
65
+
66
+ test("timeout triggers abort", async () => {
67
+ const srv = await startServer((_req, _res) => {
68
+ // Never respond — simulate hang
69
+ });
70
+
71
+ try {
72
+ await assert.rejects(
73
+ () =>
74
+ timedFetch(`http://localhost:${srv.port}/`, {
75
+ timeoutMs: 100,
76
+ where: "test-timeout",
77
+ }),
78
+ (err: any) => {
79
+ assert.ok(err.message.includes("fetch timeout"), `Expected timeout error, got: ${err.message}`);
80
+ return true;
81
+ }
82
+ );
83
+ } finally {
84
+ await srv.close();
85
+ }
86
+ });
87
+
88
+ test("no timeout when timeoutMs is 0", async () => {
89
+ const srv = await startServer((_req, res) => {
90
+ res.writeHead(200);
91
+ res.end("ok");
92
+ });
93
+
94
+ try {
95
+ const res = await timedFetch(`http://localhost:${srv.port}/`, {
96
+ timeoutMs: 0,
97
+ where: "test-no-timeout",
98
+ });
99
+ assert.strictEqual(res.status, 200);
100
+ } finally {
101
+ await srv.close();
102
+ }
103
+ });
104
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2021",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "resolveJsonModule": true,
10
+ "outDir": "dist"
11
+ },
12
+ "include": ["src/**/*"]
13
+ }