@anteros/core 0.0.41 → 0.0.44
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/database/audit.ts +9 -59
- package/database/collection.ts +3 -1
- package/database/deleteLog.ts +67 -3
- package/database/file.ts +4 -3
- package/database/mongodbadapter.ts +103 -42
- package/database/replication.ts +724 -58
- package/database/ttl.ts +68 -0
- package/database/workflow.ts +466 -240
- package/index.ts +6 -0
- package/lib/agent.ts +1567 -0
- package/lib/agentMemory.ts +689 -0
- package/lib/agents.ts +180 -0
- package/lib/ai.ts +0 -0
- package/lib/api.ts +246 -0
- package/lib/attachment.ts +351 -0
- package/lib/auditResult.ts +13 -0
- package/lib/define.ts +43 -1
- package/lib/endpoints.ts +80 -0
- package/lib/files.ts +2 -1
- package/lib/jsonSchema.ts +183 -0
- package/lib/lifecycle.ts +7 -1
- package/lib/load.ts +17 -0
- package/lib/mcp.ts +3 -2
- package/lib/mcpClient.ts +128 -0
- package/lib/messages.ts +299 -0
- package/lib/providers.ts +970 -0
- package/lib/routes.ts +14 -1
- package/lib/scriptWorker.ts +4 -1
- package/lib/scripts.ts +6 -1
- package/lib/skills.ts +481 -0
- package/lib/sockets.ts +3 -1
- package/lib/workflow.ts +209 -37
- package/lib/zip.ts +109 -0
- package/package.json +1 -1
- package/server/access.ts +96 -0
- package/server/agents.ts +472 -0
- package/server/api.ts +80 -65
- package/server/boot.ts +11 -0
- package/server/hono.ts +8 -3
- package/server/mcp.ts +17 -63
- package/server/routes.ts +19 -24
- package/tests/agent-mcp.test.ts +116 -0
- package/tests/agent-memory.test.ts +617 -0
- package/tests/agent-provider.test.ts +288 -0
- package/tests/agent-runtime.test.ts +369 -0
- package/tests/agent-skills.test.ts +177 -0
- package/tests/agent-summarize.test.ts +124 -0
- package/tests/agent-working-memory.test.ts +404 -0
- package/tests/agent.test.ts +1241 -0
- package/tests/agents-api.test.ts +593 -0
- package/tests/api-private-fields.test.ts +196 -0
- package/tests/api.test.ts +51 -0
- package/tests/attachment.test.ts +241 -0
- package/tests/audit-operations.test.ts +1 -1
- package/tests/audit-redact.test.ts +29 -3
- package/tests/audit-run-action.test.ts +1 -1
- package/tests/endpoints.test.ts +80 -0
- package/tests/file-collection-replication.test.ts +2 -0
- package/tests/fixtures/agent-tenant/agents/support.agent.ts +9 -0
- package/tests/fixtures/agent-tenant/agents/weather.agent.ts +21 -0
- package/tests/fixtures/agents-api/agents/assistant.agent.ts +46 -0
- package/tests/fixtures/agents-api/agents/plain.agent.ts +23 -0
- package/tests/fixtures/agents-api/agents/private.agent.ts +15 -0
- package/tests/fixtures/agents-api/collections/notes.model.ts +26 -0
- package/tests/fixtures/agents-api/routes/probe.route.ts +21 -0
- package/tests/fixtures/agents-api/services/probe.service.ts +19 -0
- package/tests/fixtures/attachment/data.csv +2 -0
- package/tests/fixtures/fake-provider.ts +128 -0
- package/tests/fixtures/repl-only/app.ts +3 -0
- package/tests/fixtures/replication-meta/agents/chat.agent.ts +16 -0
- package/tests/fixtures/replication-meta/collections/counters.model.ts +13 -0
- package/tests/fixtures/replication-meta/collections/orders.model.ts +9 -0
- package/tests/fixtures/replication-meta/workflows/run.workflow.ts +8 -0
- package/tests/fixtures/skills-tenant/agents/docs.agent.ts +23 -0
- package/tests/fixtures/skills-tenant/agents/incomplete.agent.ts +14 -0
- package/tests/fixtures/skills-tenant/broken/bad-name.md +6 -0
- package/tests/fixtures/skills-tenant/broken/dup-a.md +6 -0
- package/tests/fixtures/skills-tenant/broken/dup-b.md +6 -0
- package/tests/fixtures/skills-tenant/broken/empty-body.md +4 -0
- package/tests/fixtures/skills-tenant/broken/long-compat.md +7 -0
- package/tests/fixtures/skills-tenant/broken/long-description.md +6 -0
- package/tests/fixtures/skills-tenant/broken/mismatch/SKILL.md +6 -0
- package/tests/fixtures/skills-tenant/broken/no-description.md +5 -0
- package/tests/fixtures/skills-tenant/broken/no-name.md +5 -0
- package/tests/fixtures/skills-tenant/broken/plain.md +4 -0
- package/tests/fixtures/skills-tenant/broken/unknown-key.md +9 -0
- package/tests/fixtures/skills-tenant/docs/pdf-forms.md +18 -0
- package/tests/fixtures/skills-tenant/guide/ml.md +9 -0
- package/tests/fixtures/skills-tenant/guides/pdf/SKILL.md +9 -0
- package/tests/fixtures/skills-tenant/runbooks/incident.md +9 -0
- package/tests/fixtures/skills-tenant/runbooks/notes.txt +2 -0
- package/tests/fixtures/vars-tenant/vars/private.var.ts +4 -1
- package/tests/fixtures/workflow-engine/workflows/failing.workflow.ts +34 -0
- package/tests/fixtures/workflow-engine/workflows/flaky.workflow.ts +23 -0
- package/tests/fixtures/workflow-engine/workflows/hang.workflow.ts +17 -0
- package/tests/fixtures/workflow-engine/workflows/simple.workflow.ts +19 -0
- package/tests/fixtures/workflow-engine/workflows/slow.workflow.ts +23 -0
- package/tests/fixtures/workflow-engine/workflows/versioned.workflow.ts +19 -0
- package/tests/fixtures/workflow-tenant/workflows/broken.workflow.ts +3 -0
- package/tests/fixtures/workflow-tenant/workflows/disabled.workflow.ts +8 -0
- package/tests/fixtures/workflow-tenant/workflows/invoice.workflow.ts +21 -0
- package/tests/fixtures/workflow-tenant/workflows/no-id.workflow.ts +7 -0
- package/tests/fixtures/workflow-tenant/workflows/order-duplicate.workflow.ts +13 -0
- package/tests/fixtures/workflow-tenant/workflows/order.workflow.ts +17 -0
- package/tests/hono.test.ts +6 -3
- package/tests/locks.test.ts +120 -0
- package/tests/mcp.test.ts +11 -11
- package/tests/messages.test.ts +258 -0
- package/tests/replication-change-stream.test.ts +240 -0
- package/tests/replication-control.test.ts +4 -2
- package/tests/replication-initial-sync.test.ts +2 -0
- package/tests/replication-meta.test.ts +246 -0
- package/tests/replication.test.ts +2 -0
- package/tests/upload.test.ts +44 -12
- package/tests/vars-api.test.ts +1 -1
- package/tests/vars-replication.test.ts +5 -2
- package/tests/workflow-engine.test.ts +340 -0
- package/tests/workflow-retention.test.ts +103 -0
- package/tests/workflow.test.ts +186 -0
- package/types/agent.d.ts +1067 -0
- package/types/config.d.ts +16 -0
- package/types/file.d.ts +7 -7
- package/types/hook.d.ts +6 -0
- package/types/lifecycle.d.ts +6 -0
- package/types/mcp.d.ts +11 -1
- package/types/replication.d.ts +70 -4
- package/types/route.d.ts +7 -1
- package/types/scripts.d.ts +6 -0
- package/types/service.d.ts +7 -1
- package/types/tenant.d.ts +13 -0
- package/types/vars.d.ts +2 -2
- package/types/websocket.d.ts +6 -0
- package/types/workflow.d.ts +50 -3
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent **skills** — the documents an agent carries in its system prompt.
|
|
3
|
+
*
|
|
4
|
+
* The format is the one every skill file shares (`name`, `description`, `license`,
|
|
5
|
+
* `compatibility`, `metadata`, `allowed-tools`), and the rules that matter are:
|
|
6
|
+
* a missing `name`/`description` refuses the agent (a typo must never change the
|
|
7
|
+
* prompt silently), a non-markdown file is skipped, and the same file matched by
|
|
8
|
+
* two patterns is one skill.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, expect, beforeAll, afterAll } from "bun:test";
|
|
11
|
+
import { cfg, formatConfig } from "../server/config";
|
|
12
|
+
import { syncAgents, agents, createAgents } from "../lib/agents";
|
|
13
|
+
import { resolveSkills, skillsBlock, MAX_DESCRIPTION_CHARS } from "../lib/skills";
|
|
14
|
+
import { fakeProvider, anthropicText, request, type Fake } from "./fixtures/fake-provider";
|
|
15
|
+
|
|
16
|
+
const TENANT = "skills";
|
|
17
|
+
const FIXTURE = "packages/core/tests/fixtures/skills-tenant";
|
|
18
|
+
|
|
19
|
+
const resolve = (sources: any[]) => resolveSkills(sources, { baseDir: FIXTURE, agentId: "test" });
|
|
20
|
+
const names = (skills: any[]) => skills.map((skill) => skill.name);
|
|
21
|
+
|
|
22
|
+
describe("resolveSkills", () => {
|
|
23
|
+
it("walks the tenant for a glob, a file, a regex and a `SKILL.md` — one skill per file", async () => {
|
|
24
|
+
const skills = await resolve([
|
|
25
|
+
"./docs/*.md",
|
|
26
|
+
"./guide/ml.md",
|
|
27
|
+
/^docs\/.*\.md$/, // the same file as the glob above: not injected twice
|
|
28
|
+
/^runbooks\/.*$/, // catches a `.txt` too — skipped, see below
|
|
29
|
+
"./guides/**/SKILL.md",
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
expect(names(skills)).toEqual(["incident-response", "ml-conventions", "pdf", "pdf-forms"]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("parses every field of the frontmatter, and strips it from the body", async () => {
|
|
36
|
+
const [skill] = await resolve(["./docs/pdf-forms.md"]);
|
|
37
|
+
|
|
38
|
+
expect(skill).toMatchObject({
|
|
39
|
+
name: "pdf-forms",
|
|
40
|
+
license: "MIT",
|
|
41
|
+
compatibility: "Needs the `pdftk` binary on the host.",
|
|
42
|
+
metadata: { owner: "platform", reviewed: "2026-09-24" },
|
|
43
|
+
allowedTools: ["read_pdf", "write_pdf"],
|
|
44
|
+
source: "docs/pdf-forms.md",
|
|
45
|
+
});
|
|
46
|
+
expect(skill!.content.startsWith("# Filling a form")).toBe(true);
|
|
47
|
+
expect(skill!.content).toContain("dump_data_fields");
|
|
48
|
+
expect(skill!.content).not.toContain("pdftk` binary");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("skips a file that is not markdown instead of refusing the agent", async () => {
|
|
52
|
+
const skills = await resolve([/^runbooks\/.*$/]);
|
|
53
|
+
|
|
54
|
+
// `runbooks/notes.txt` matched the regex and was left out (logged)
|
|
55
|
+
expect(names(skills)).toEqual(["incident-response"]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("keeps an unknown frontmatter key out of the skill", async () => {
|
|
59
|
+
const [skill] = await resolve(["./broken/unknown-key.md"]);
|
|
60
|
+
|
|
61
|
+
expect(skill!.name).toBe("unknown-keys");
|
|
62
|
+
expect((skill as any).version).toBeUndefined();
|
|
63
|
+
expect((skill as any).author).toBeUndefined();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("refuses what the format requires, naming the file", async () => {
|
|
67
|
+
await expect(resolve(["./broken/plain.md"])).rejects.toThrow(/`name` is required/);
|
|
68
|
+
await expect(resolve(["./broken/no-name.md"])).rejects.toMatchObject({ code: "AGENT_SKILL_INVALID" });
|
|
69
|
+
await expect(resolve(["./broken/no-description.md"])).rejects.toThrow(/`description` is required/);
|
|
70
|
+
await expect(resolve(["./broken/bad-name.md"])).rejects.toThrow(/lowercase alphanumeric with hyphens/);
|
|
71
|
+
await expect(resolve(["./broken/mismatch/SKILL.md"])).rejects.toThrow(/must match the parent directory/);
|
|
72
|
+
// A skill *is* its body: frontmatter alone injects nothing
|
|
73
|
+
await expect(resolve(["./broken/empty-body.md"])).rejects.toThrow(/`content` is required/);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("refuses a field over its limit", async () => {
|
|
77
|
+
await expect(resolve(["./broken/long-description.md"])).rejects.toThrow(new RegExp(`over the ${MAX_DESCRIPTION_CHARS} limit`));
|
|
78
|
+
await expect(resolve(["./broken/long-compat.md"])).rejects.toThrow(/`compatibility` is 510 characters/);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("refuses two skills that share a name", async () => {
|
|
82
|
+
await expect(resolve(["./broken/dup-a.md", "./broken/dup-b.md"]))
|
|
83
|
+
.rejects.toMatchObject({ code: "AGENT_SKILL_DUPLICATE" });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("takes no source as no skills", async () => {
|
|
87
|
+
expect(await resolve([])).toEqual([]);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("skillsBlock", () => {
|
|
92
|
+
it("renders one section per skill, description first", async () => {
|
|
93
|
+
const block = skillsBlock(await resolve(["./guide/ml.md"]));
|
|
94
|
+
|
|
95
|
+
expect(block.startsWith("## Skills")).toBe(true);
|
|
96
|
+
expect(block).toContain("### ml-conventions");
|
|
97
|
+
expect(block).toContain("feature naming, training splits");
|
|
98
|
+
expect(block).toContain("Features are `snake_case`");
|
|
99
|
+
// the frontmatter never reaches the prompt
|
|
100
|
+
expect(block).not.toContain("description:");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("is empty without a skill", () => {
|
|
104
|
+
expect(skillsBlock([])).toBe("");
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("agent skills", () => {
|
|
109
|
+
let provider: Fake;
|
|
110
|
+
|
|
111
|
+
beforeAll(async () => {
|
|
112
|
+
formatConfig({
|
|
113
|
+
server: { port: 4000 },
|
|
114
|
+
tenants: [{ id: TENANT, dir: FIXTURE, database: { uri: "mongodb://localhost:27017/none" } }],
|
|
115
|
+
});
|
|
116
|
+
await syncAgents();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
afterAll(() => {
|
|
120
|
+
cfg.agents = [];
|
|
121
|
+
try { provider?.stop(); } catch (_) {}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("resolves the declared skills once, at load", () => {
|
|
125
|
+
const agent = agents.get(TENANT, "docs");
|
|
126
|
+
|
|
127
|
+
expect(names(agent!.getSkills())).toEqual(["incident-response", "ml-conventions", "pdf", "pdf-forms"]);
|
|
128
|
+
|
|
129
|
+
// The copy bound to a calling `rest` shares the declaration, skills included
|
|
130
|
+
const bound = createAgents(TENANT, { tenant_id: TENANT } as any).get("docs");
|
|
131
|
+
expect(names(bound!.getSkills())).toEqual(names(agent!.getSkills()));
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("refuses the agent whose skill is malformed, and keeps the others", () => {
|
|
135
|
+
expect(agents.has(TENANT, "incomplete")).toBe(false);
|
|
136
|
+
expect(agents.has(TENANT, "docs")).toBe(true);
|
|
137
|
+
expect(agents.ids(TENANT)).toEqual(["docs"]);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("injects them into the system prompt of a run", async () => {
|
|
141
|
+
provider = fakeProvider(() => anthropicText("ok"));
|
|
142
|
+
const agent = agents.get(TENANT, "docs")!;
|
|
143
|
+
agent.setProvider({ baseUrl: provider.url });
|
|
144
|
+
|
|
145
|
+
await agent.generate("How do I fill a form?");
|
|
146
|
+
|
|
147
|
+
const system: string = request(provider, 0).body.system;
|
|
148
|
+
expect(system.startsWith("Answer from the documentation you were given.")).toBe(true);
|
|
149
|
+
expect(system).toContain("## Skills");
|
|
150
|
+
expect(system).toContain("### pdf-forms");
|
|
151
|
+
expect(system).toContain("Fill, flatten and merge PDF forms.");
|
|
152
|
+
expect(system).toContain("dump_data_fields");
|
|
153
|
+
expect(system).toContain("### incident-response");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("keeps the skills when the caller overrides the instructions", async () => {
|
|
157
|
+
const agent = agents.get(TENANT, "docs")!;
|
|
158
|
+
agent.setProvider({ baseUrl: provider.url });
|
|
159
|
+
|
|
160
|
+
await agent.generate("hi", { instructions: "You are a pirate." });
|
|
161
|
+
|
|
162
|
+
const system: string = request(provider, 1).body.system;
|
|
163
|
+
expect(system.startsWith("You are a pirate.")).toBe(true);
|
|
164
|
+
expect(system).toContain("### pdf");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("serves name and description on info — never the body", () => {
|
|
168
|
+
const info = agents.get(TENANT, "docs")!.toJSON();
|
|
169
|
+
|
|
170
|
+
expect(info.skills).toHaveLength(4);
|
|
171
|
+
expect(info.skills[0]).toEqual({
|
|
172
|
+
name: "incident-response",
|
|
173
|
+
description: "What to do when production breaks — paging, comms, post-mortem.",
|
|
174
|
+
});
|
|
175
|
+
expect(JSON.stringify(info)).not.toContain("Acknowledge the page");
|
|
176
|
+
});
|
|
177
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* History compaction — `agents.memory.summarize()`.
|
|
3
|
+
*
|
|
4
|
+
* A thread that grew past a threshold is compacted **for the prompt**: the oldest
|
|
5
|
+
* turns become one summary written by a model you name. The summary is cached in the
|
|
6
|
+
* store's state for the thread it belongs to, so a long conversation costs one
|
|
7
|
+
* summary per `keep` turns instead of one per turn — and the prompt prefix stays put
|
|
8
|
+
* between refreshes.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, expect, afterAll } from "bun:test";
|
|
11
|
+
import { Agent, agents } from "../index";
|
|
12
|
+
import { InMemoryAgentMemory } from "../lib/agent";
|
|
13
|
+
import { fakeProvider, openaiText, request, type Fake } from "./fixtures/fake-provider";
|
|
14
|
+
|
|
15
|
+
const servers: Fake[] = [];
|
|
16
|
+
function track(fake: Fake): Fake {
|
|
17
|
+
servers.push(fake);
|
|
18
|
+
return fake;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
afterAll(() => {
|
|
22
|
+
for (const server of servers) server.stop();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/** The summariser answers `SUMMARY`, the agent under test answers `answer`. */
|
|
26
|
+
function route(body: any): Response {
|
|
27
|
+
return body.messages[0].content === "You condense." ? openaiText("SUMMARY") : openaiText("answer");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function seed(count: number): any[] {
|
|
31
|
+
return Array.from({ length: count }, (_, index) => ({
|
|
32
|
+
role: index % 2 === 0 ? "user" : "assistant",
|
|
33
|
+
content: `m${index}`,
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe("agents.memory.summarize", () => {
|
|
38
|
+
const build = (provider: Fake, options: Record<string, any> = {}) => {
|
|
39
|
+
const summarizer = new Agent({
|
|
40
|
+
id: "compact",
|
|
41
|
+
description: "Condenses a conversation.",
|
|
42
|
+
instructions: "You condense.",
|
|
43
|
+
provider: { model: "test-model", apiKey: "test-key", baseUrl: provider.url, options: { retries: 0 } },
|
|
44
|
+
} as any);
|
|
45
|
+
|
|
46
|
+
const memory = new InMemoryAgentMemory();
|
|
47
|
+
const agent = new Agent({
|
|
48
|
+
id: "test",
|
|
49
|
+
description: "A test agent.",
|
|
50
|
+
instructions: "You are a test agent.",
|
|
51
|
+
provider: { model: "test-model", apiKey: "test-key", baseUrl: provider.url, options: { retries: 0 } },
|
|
52
|
+
memory,
|
|
53
|
+
processors: [agents.memory.summarize({ agent: summarizer, keep: 10, threshold: 12, ...options })],
|
|
54
|
+
} as any);
|
|
55
|
+
|
|
56
|
+
return { agent, memory };
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
it("compacts the oldest turns for the prompt, and leaves the thread alone", async () => {
|
|
60
|
+
const provider = track(fakeProvider(route));
|
|
61
|
+
const { agent, memory } = build(provider);
|
|
62
|
+
const seeded = seed(20);
|
|
63
|
+
await memory.save("t1", seeded as any);
|
|
64
|
+
|
|
65
|
+
await agent.generate("next?", { thread: "t1" });
|
|
66
|
+
|
|
67
|
+
// The summariser is called first, then the run
|
|
68
|
+
const prompt = request(provider, 1).body.messages;
|
|
69
|
+
expect(prompt[0].role).toBe("system");
|
|
70
|
+
expect(prompt[1].content).toContain("[Summary of the conversation so far]");
|
|
71
|
+
expect(prompt[1].content).toContain("SUMMARY");
|
|
72
|
+
// …then the last 10 turns verbatim (m11…m19 and the question), then the system
|
|
73
|
+
expect(prompt).toHaveLength(12);
|
|
74
|
+
expect(prompt[2].content).toBe("m11");
|
|
75
|
+
expect(prompt[10].content).toBe("m19");
|
|
76
|
+
expect(prompt[11].content).toBe("next?");
|
|
77
|
+
|
|
78
|
+
// The store keeps everything: the compaction shapes the prompt, never the thread
|
|
79
|
+
expect(await memory.get("t1")).toHaveLength(22);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("caches the summary in the store's state, one per window", async () => {
|
|
83
|
+
const provider = track(fakeProvider(route));
|
|
84
|
+
const { agent, memory } = build(provider);
|
|
85
|
+
await memory.save("t1", seed(20) as any);
|
|
86
|
+
|
|
87
|
+
const summarised = () => provider.requests.filter((entry) => entry.body.messages[0].content === "You condense.").length;
|
|
88
|
+
|
|
89
|
+
await agent.generate("first?", { thread: "t1" });
|
|
90
|
+
expect(summarised()).toBe(1);
|
|
91
|
+
|
|
92
|
+
// The next turn moves the window by two messages — far from `keep`: reused
|
|
93
|
+
await agent.generate("second?", { thread: "t1" });
|
|
94
|
+
expect(summarised()).toBe(1);
|
|
95
|
+
|
|
96
|
+
// The cache lives in the store, per thread
|
|
97
|
+
const state = await (agent.getMemory() as any).getState("summary:t1", {});
|
|
98
|
+
expect((state.value as any).summary).toBe("SUMMARY");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("does nothing below the threshold", async () => {
|
|
102
|
+
const provider = track(fakeProvider(route));
|
|
103
|
+
const { agent, memory } = build(provider);
|
|
104
|
+
await memory.save("t1", seed(4) as any);
|
|
105
|
+
|
|
106
|
+
await agent.generate("next?", { thread: "t1" });
|
|
107
|
+
|
|
108
|
+
const prompt = request(provider, 0).body.messages;
|
|
109
|
+
expect(prompt).toHaveLength(6); // system + the 4 stored turns + the question
|
|
110
|
+
expect(JSON.stringify(prompt)).not.toContain("[Summary");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("refuses a summary the model did not write", async () => {
|
|
114
|
+
const provider = track(fakeProvider((body) =>
|
|
115
|
+
body.messages[0].content === "You condense." ? openaiText(" ") : openaiText("answer")));
|
|
116
|
+
const { agent, memory } = build(provider);
|
|
117
|
+
await memory.save("t1", seed(20) as any);
|
|
118
|
+
|
|
119
|
+
await agent.generate("next?", { thread: "t1" });
|
|
120
|
+
|
|
121
|
+
// An empty summary compacts nothing rather than dropping the conversation
|
|
122
|
+
expect(JSON.stringify(request(provider, 1).body.messages)).not.toContain("[Summary");
|
|
123
|
+
});
|
|
124
|
+
});
|