@tjamescouch/gro 1.3.6 → 1.3.7
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/dist/drivers/anthropic.js +256 -0
- package/dist/drivers/index.js +2 -0
- package/dist/drivers/streaming-openai.js +262 -0
- package/dist/drivers/types.js +1 -0
- package/dist/errors.js +79 -0
- package/dist/logger.js +30 -0
- package/dist/main.js +867 -0
- package/dist/mcp/client.js +130 -0
- package/dist/mcp/index.js +1 -0
- package/dist/memory/advanced-memory.js +210 -0
- package/dist/memory/agent-memory.js +52 -0
- package/dist/memory/agenthnsw.js +86 -0
- package/{src/memory/index.ts → dist/memory/index.js} +0 -1
- package/dist/memory/simple-memory.js +34 -0
- package/dist/memory/vector-index.js +7 -0
- package/dist/package.json +22 -0
- package/dist/session.js +110 -0
- package/dist/tools/agentpatch.js +91 -0
- package/dist/tools/bash.js +61 -0
- package/dist/tools/version.js +76 -0
- package/dist/utils/rate-limiter.js +46 -0
- package/{src/utils/retry.ts → dist/utils/retry.js} +8 -12
- package/dist/utils/timed-fetch.js +25 -0
- package/package.json +11 -3
- package/.github/workflows/ci.yml +0 -20
- package/src/drivers/anthropic.ts +0 -281
- package/src/drivers/index.ts +0 -5
- package/src/drivers/streaming-openai.ts +0 -258
- package/src/drivers/types.ts +0 -39
- package/src/errors.ts +0 -97
- package/src/logger.ts +0 -28
- package/src/main.ts +0 -905
- package/src/mcp/client.ts +0 -163
- package/src/mcp/index.ts +0 -2
- package/src/memory/advanced-memory.ts +0 -263
- package/src/memory/agent-memory.ts +0 -61
- package/src/memory/agenthnsw.ts +0 -122
- package/src/memory/simple-memory.ts +0 -41
- package/src/memory/vector-index.ts +0 -30
- package/src/session.ts +0 -150
- package/src/tools/agentpatch.ts +0 -89
- package/src/tools/bash.ts +0 -61
- package/src/tools/version.ts +0 -98
- package/src/utils/rate-limiter.ts +0 -60
- package/src/utils/timed-fetch.ts +0 -29
- package/tests/errors.test.ts +0 -246
- package/tests/memory.test.ts +0 -186
- package/tests/rate-limiter.test.ts +0 -76
- package/tests/retry.test.ts +0 -138
- package/tests/timed-fetch.test.ts +0 -104
- package/tsconfig.json +0 -13
package/tests/errors.test.ts
DELETED
|
@@ -1,246 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for structured error types (GroError).
|
|
3
|
-
*
|
|
4
|
-
* Covers: groError, asError, isGroError, errorLogFields
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { test, describe } from "node:test";
|
|
8
|
-
import assert from "node:assert";
|
|
9
|
-
import { groError, asError, isGroError, errorLogFields } from "../src/errors.js";
|
|
10
|
-
import type { GroError } from "../src/errors.js";
|
|
11
|
-
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
|
-
// groError factory
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
|
|
16
|
-
describe("groError", () => {
|
|
17
|
-
test("creates an Error with kind and retryable fields", () => {
|
|
18
|
-
const e = groError("provider_error", "API failed");
|
|
19
|
-
assert.ok(e instanceof Error);
|
|
20
|
-
assert.strictEqual(e.kind, "provider_error");
|
|
21
|
-
assert.strictEqual(e.message, "API failed");
|
|
22
|
-
assert.strictEqual(e.retryable, false); // default
|
|
23
|
-
assert.ok(e.stack, "should have a stack trace");
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test("retryable defaults to false", () => {
|
|
27
|
-
const e = groError("tool_error", "boom");
|
|
28
|
-
assert.strictEqual(e.retryable, false);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test("retryable can be set to true", () => {
|
|
32
|
-
const e = groError("provider_error", "429", { retryable: true });
|
|
33
|
-
assert.strictEqual(e.retryable, true);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
test("optional fields are set when provided", () => {
|
|
37
|
-
const e = groError("provider_error", "fail", {
|
|
38
|
-
provider: "anthropic",
|
|
39
|
-
model: "claude-sonnet-4-20250514",
|
|
40
|
-
request_id: "req_123",
|
|
41
|
-
latency_ms: 450,
|
|
42
|
-
cause: new Error("underlying"),
|
|
43
|
-
});
|
|
44
|
-
assert.strictEqual(e.provider, "anthropic");
|
|
45
|
-
assert.strictEqual(e.model, "claude-sonnet-4-20250514");
|
|
46
|
-
assert.strictEqual(e.request_id, "req_123");
|
|
47
|
-
assert.strictEqual(e.latency_ms, 450);
|
|
48
|
-
assert.ok(e.cause instanceof Error);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test("optional fields are omitted when not provided", () => {
|
|
52
|
-
const e = groError("config_error", "bad config");
|
|
53
|
-
assert.strictEqual(e.provider, undefined);
|
|
54
|
-
assert.strictEqual(e.model, undefined);
|
|
55
|
-
assert.strictEqual(e.request_id, undefined);
|
|
56
|
-
assert.strictEqual(e.latency_ms, undefined);
|
|
57
|
-
assert.strictEqual(e.cause, undefined);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
test("all GroErrorKind values are accepted", () => {
|
|
61
|
-
const kinds = [
|
|
62
|
-
"provider_error",
|
|
63
|
-
"tool_error",
|
|
64
|
-
"config_error",
|
|
65
|
-
"mcp_error",
|
|
66
|
-
"timeout_error",
|
|
67
|
-
"session_error",
|
|
68
|
-
] as const;
|
|
69
|
-
for (const kind of kinds) {
|
|
70
|
-
const e = groError(kind, `test ${kind}`);
|
|
71
|
-
assert.strictEqual(e.kind, kind);
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
test("latency_ms of 0 is preserved (not treated as falsy)", () => {
|
|
76
|
-
const e = groError("provider_error", "fast fail", { latency_ms: 0 });
|
|
77
|
-
assert.strictEqual(e.latency_ms, 0);
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
// ---------------------------------------------------------------------------
|
|
82
|
-
// asError
|
|
83
|
-
// ---------------------------------------------------------------------------
|
|
84
|
-
|
|
85
|
-
describe("asError", () => {
|
|
86
|
-
test("returns Error instances unchanged", () => {
|
|
87
|
-
const original = new Error("original");
|
|
88
|
-
const result = asError(original);
|
|
89
|
-
assert.strictEqual(result, original);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
test("wraps strings into Error", () => {
|
|
93
|
-
const result = asError("something broke");
|
|
94
|
-
assert.ok(result instanceof Error);
|
|
95
|
-
assert.strictEqual(result.message, "something broke");
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
test("truncates long strings to 1024 chars", () => {
|
|
99
|
-
const long = "x".repeat(2000);
|
|
100
|
-
const result = asError(long);
|
|
101
|
-
assert.strictEqual(result.message.length, 1024);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
test("handles null", () => {
|
|
105
|
-
const result = asError(null);
|
|
106
|
-
assert.ok(result instanceof Error);
|
|
107
|
-
assert.strictEqual(result.message, "Unknown error");
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
test("handles undefined", () => {
|
|
111
|
-
const result = asError(undefined);
|
|
112
|
-
assert.ok(result instanceof Error);
|
|
113
|
-
assert.strictEqual(result.message, "Unknown error");
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
test("handles objects via String()", () => {
|
|
117
|
-
const result = asError({ code: 42 });
|
|
118
|
-
assert.ok(result instanceof Error);
|
|
119
|
-
assert.ok(result.message.includes("[object Object]"));
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
test("handles numbers", () => {
|
|
123
|
-
const result = asError(42);
|
|
124
|
-
assert.ok(result instanceof Error);
|
|
125
|
-
assert.strictEqual(result.message, "42");
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
test("truncates long stringified values with ellipsis", () => {
|
|
129
|
-
// Use a value whose String() representation exceeds 1024 chars
|
|
130
|
-
const longStr = "z".repeat(2000);
|
|
131
|
-
const result = asError(longStr);
|
|
132
|
-
// String input goes through the string branch (slice to 1024, no ellipsis)
|
|
133
|
-
assert.strictEqual(result.message.length, 1024);
|
|
134
|
-
|
|
135
|
-
// For non-string values, String() > 1024 gets ellipsis
|
|
136
|
-
const longToString = { toString: () => "w".repeat(2000) };
|
|
137
|
-
const result2 = asError(longToString);
|
|
138
|
-
assert.ok(result2.message.endsWith("..."));
|
|
139
|
-
assert.strictEqual(result2.message.length, 1027); // 1024 + "..."
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
// ---------------------------------------------------------------------------
|
|
144
|
-
// isGroError
|
|
145
|
-
// ---------------------------------------------------------------------------
|
|
146
|
-
|
|
147
|
-
describe("isGroError", () => {
|
|
148
|
-
test("returns true for groError instances", () => {
|
|
149
|
-
const e = groError("provider_error", "test");
|
|
150
|
-
assert.strictEqual(isGroError(e), true);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
test("returns false for plain Error", () => {
|
|
154
|
-
assert.strictEqual(isGroError(new Error("nope")), false);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
test("returns false for non-Error objects", () => {
|
|
158
|
-
assert.strictEqual(isGroError({ kind: "provider_error", retryable: true }), false);
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
test("returns false for null", () => {
|
|
162
|
-
assert.strictEqual(isGroError(null), false);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
test("returns false for undefined", () => {
|
|
166
|
-
assert.strictEqual(isGroError(undefined), false);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
test("returns false for strings", () => {
|
|
170
|
-
assert.strictEqual(isGroError("error"), false);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
test("returns true for Error manually augmented with kind and retryable", () => {
|
|
174
|
-
const e = new Error("manual") as any;
|
|
175
|
-
e.kind = "tool_error";
|
|
176
|
-
e.retryable = false;
|
|
177
|
-
assert.strictEqual(isGroError(e), true);
|
|
178
|
-
});
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
// ---------------------------------------------------------------------------
|
|
182
|
-
// errorLogFields
|
|
183
|
-
// ---------------------------------------------------------------------------
|
|
184
|
-
|
|
185
|
-
describe("errorLogFields", () => {
|
|
186
|
-
test("returns basic fields for minimal GroError", () => {
|
|
187
|
-
const e = groError("config_error", "bad");
|
|
188
|
-
const fields = errorLogFields(e);
|
|
189
|
-
assert.strictEqual(fields.kind, "config_error");
|
|
190
|
-
assert.strictEqual(fields.message, "bad");
|
|
191
|
-
assert.strictEqual(fields.retryable, false);
|
|
192
|
-
assert.ok(fields.stack, "should include stack");
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
test("includes optional fields when present", () => {
|
|
196
|
-
const e = groError("provider_error", "fail", {
|
|
197
|
-
provider: "anthropic",
|
|
198
|
-
model: "claude-sonnet-4-20250514",
|
|
199
|
-
request_id: "req_abc",
|
|
200
|
-
latency_ms: 100,
|
|
201
|
-
});
|
|
202
|
-
const fields = errorLogFields(e);
|
|
203
|
-
assert.strictEqual(fields.provider, "anthropic");
|
|
204
|
-
assert.strictEqual(fields.model, "claude-sonnet-4-20250514");
|
|
205
|
-
assert.strictEqual(fields.request_id, "req_abc");
|
|
206
|
-
assert.strictEqual(fields.latency_ms, 100);
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
test("omits optional fields when not set", () => {
|
|
210
|
-
const e = groError("tool_error", "boom");
|
|
211
|
-
const fields = errorLogFields(e);
|
|
212
|
-
assert.strictEqual(fields.provider, undefined);
|
|
213
|
-
assert.strictEqual(fields.model, undefined);
|
|
214
|
-
assert.strictEqual(fields.request_id, undefined);
|
|
215
|
-
assert.strictEqual(fields.latency_ms, undefined);
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
test("resolves cause into cause_message and cause_stack", () => {
|
|
219
|
-
const cause = new Error("root cause");
|
|
220
|
-
const e = groError("mcp_error", "mcp failed", { cause });
|
|
221
|
-
const fields = errorLogFields(e);
|
|
222
|
-
assert.strictEqual(fields.cause_message, "root cause");
|
|
223
|
-
assert.ok(fields.cause_stack);
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
test("resolves non-Error cause via asError", () => {
|
|
227
|
-
const e = groError("provider_error", "fail", { cause: "string cause" });
|
|
228
|
-
const fields = errorLogFields(e);
|
|
229
|
-
assert.strictEqual(fields.cause_message, "string cause");
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
test("result is JSON-serializable", () => {
|
|
233
|
-
const e = groError("provider_error", "test", {
|
|
234
|
-
provider: "openai",
|
|
235
|
-
model: "gpt-4o",
|
|
236
|
-
request_id: "req_1",
|
|
237
|
-
latency_ms: 200,
|
|
238
|
-
cause: new Error("inner"),
|
|
239
|
-
});
|
|
240
|
-
const fields = errorLogFields(e);
|
|
241
|
-
const json = JSON.stringify(fields);
|
|
242
|
-
assert.ok(json.length > 0);
|
|
243
|
-
const parsed = JSON.parse(json);
|
|
244
|
-
assert.strictEqual(parsed.kind, "provider_error");
|
|
245
|
-
});
|
|
246
|
-
});
|
package/tests/memory.test.ts
DELETED
|
@@ -1,186 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,76 +0,0 @@
|
|
|
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
|
-
});
|
package/tests/retry.test.ts
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
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
|
-
});
|