@poncho-ai/harness 0.55.0 → 0.57.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +5 -5
- package/CHANGELOG.md +38 -0
- package/dist/index.d.ts +210 -2
- package/dist/index.js +569 -19
- package/package.json +1 -1
- package/src/index.ts +18 -0
- package/src/orchestrator/entries-dual-write.ts +265 -0
- package/src/orchestrator/index.ts +7 -0
- package/src/orchestrator/orchestrator.ts +179 -13
- package/src/orchestrator/run-conversation-turn.ts +108 -0
- package/src/state.ts +56 -0
- package/src/storage/engine.ts +18 -0
- package/src/storage/entries.ts +13 -0
- package/src/storage/memory-engine.ts +40 -0
- package/src/storage/schema.ts +30 -0
- package/src/storage/sql-dialect.ts +112 -0
- package/src/storage/store-adapters.ts +8 -0
- package/test/entries-dual-write.test.ts +172 -0
- package/test/entries-store.test.ts +165 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { InMemoryConversationStore } from "../src/state.js";
|
|
6
|
+
import type { ConversationStore } from "../src/state.js";
|
|
7
|
+
import type { ConversationEntry, NewConversationEntry } from "../src/storage/entries.js";
|
|
8
|
+
import { SqliteEngine } from "../src/storage/sqlite-engine.js";
|
|
9
|
+
import { createConversationStoreFromEngine } from "../src/storage/store-adapters.js";
|
|
10
|
+
import type { Message } from "@poncho-ai/sdk";
|
|
11
|
+
|
|
12
|
+
const msg = (role: Message["role"], content: string): Message => ({ role, content });
|
|
13
|
+
|
|
14
|
+
// Entry factories (without seq/createdAt — those are assigned by the store).
|
|
15
|
+
const userEntry = (id: string, content: string): NewConversationEntry => ({
|
|
16
|
+
type: "user_message",
|
|
17
|
+
id,
|
|
18
|
+
message: msg("user", content),
|
|
19
|
+
turnId: "t1",
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const harnessEntry = (id: string, content: string): NewConversationEntry => ({
|
|
23
|
+
type: "harness_message",
|
|
24
|
+
id,
|
|
25
|
+
message: msg("assistant", content),
|
|
26
|
+
turnId: "t1",
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const compactionEntry = (id: string): NewConversationEntry => ({
|
|
30
|
+
type: "compaction",
|
|
31
|
+
id,
|
|
32
|
+
summaryMessage: msg("user", "summary so far"),
|
|
33
|
+
firstKeptSeq: 2,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Shared suite run against both InMemory and SQLite-backed stores.
|
|
37
|
+
function runSuite(name: string, factory: () => Promise<{ store: ConversationStore; cleanup: () => Promise<void> }>) {
|
|
38
|
+
describe(name, () => {
|
|
39
|
+
let store: ConversationStore;
|
|
40
|
+
let cleanup: () => Promise<void>;
|
|
41
|
+
|
|
42
|
+
beforeEach(async () => {
|
|
43
|
+
const ctx = await factory();
|
|
44
|
+
store = ctx.store;
|
|
45
|
+
cleanup = ctx.cleanup;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
afterEach(async () => {
|
|
49
|
+
await cleanup();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("assigns consecutive per-conversation seqs starting at 1", async () => {
|
|
53
|
+
const stored = await store.appendEntries("c1", "agent", null, [
|
|
54
|
+
userEntry("u1", "hi"),
|
|
55
|
+
harnessEntry("h1", "hello"),
|
|
56
|
+
harnessEntry("h2", "how can I help?"),
|
|
57
|
+
]);
|
|
58
|
+
expect(stored.map((e) => e.seq)).toEqual([1, 2, 3]);
|
|
59
|
+
expect(stored.every((e) => typeof e.createdAt === "number")).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("continues seq across multiple appendEntries calls", async () => {
|
|
63
|
+
await store.appendEntries("c1", "agent", null, [userEntry("u1", "a")]);
|
|
64
|
+
const second = await store.appendEntries("c1", "agent", null, [
|
|
65
|
+
harnessEntry("h1", "b"),
|
|
66
|
+
harnessEntry("h2", "c"),
|
|
67
|
+
]);
|
|
68
|
+
expect(second.map((e) => e.seq)).toEqual([2, 3]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("keeps seq spaces independent per conversation", async () => {
|
|
72
|
+
await store.appendEntries("c1", "agent", null, [userEntry("u1", "a")]);
|
|
73
|
+
const other = await store.appendEntries("c2", "agent", null, [userEntry("u2", "b")]);
|
|
74
|
+
expect(other[0].seq).toBe(1);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("reads entries ordered by seq ascending", async () => {
|
|
78
|
+
await store.appendEntries("c1", "agent", null, [
|
|
79
|
+
userEntry("u1", "one"),
|
|
80
|
+
harnessEntry("h1", "two"),
|
|
81
|
+
harnessEntry("h2", "three"),
|
|
82
|
+
]);
|
|
83
|
+
const all = await store.readEntries("c1");
|
|
84
|
+
expect(all.map((e) => e.seq)).toEqual([1, 2, 3]);
|
|
85
|
+
expect(all.map((e) => e.id)).toEqual(["u1", "h1", "h2"]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("filters by type", async () => {
|
|
89
|
+
await store.appendEntries("c1", "agent", null, [
|
|
90
|
+
userEntry("u1", "one"),
|
|
91
|
+
harnessEntry("h1", "two"),
|
|
92
|
+
harnessEntry("h2", "three"),
|
|
93
|
+
]);
|
|
94
|
+
const harnessOnly = await store.readEntries("c1", { types: ["harness_message"] });
|
|
95
|
+
expect(harnessOnly.map((e) => e.id)).toEqual(["h1", "h2"]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("filters by afterSeq", async () => {
|
|
99
|
+
await store.appendEntries("c1", "agent", null, [
|
|
100
|
+
userEntry("u1", "one"),
|
|
101
|
+
harnessEntry("h1", "two"),
|
|
102
|
+
harnessEntry("h2", "three"),
|
|
103
|
+
]);
|
|
104
|
+
const after1 = await store.readEntries("c1", { afterSeq: 1 });
|
|
105
|
+
expect(after1.map((e) => e.seq)).toEqual([2, 3]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("respects limit", async () => {
|
|
109
|
+
await store.appendEntries("c1", "agent", null, [
|
|
110
|
+
userEntry("u1", "one"),
|
|
111
|
+
harnessEntry("h1", "two"),
|
|
112
|
+
harnessEntry("h2", "three"),
|
|
113
|
+
]);
|
|
114
|
+
const limited = await store.readEntries("c1", { limit: 2 });
|
|
115
|
+
expect(limited.map((e) => e.seq)).toEqual([1, 2]);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("round-trips distinct entry types with their payloads intact", async () => {
|
|
119
|
+
await store.appendEntries("c1", "agent", null, [
|
|
120
|
+
userEntry("u1", "question"),
|
|
121
|
+
compactionEntry("cmp1"),
|
|
122
|
+
]);
|
|
123
|
+
const all = await store.readEntries("c1");
|
|
124
|
+
|
|
125
|
+
const user = all.find((e) => e.id === "u1");
|
|
126
|
+
expect(user?.type).toBe("user_message");
|
|
127
|
+
expect(user?.type === "user_message" && user.message.content).toBe("question");
|
|
128
|
+
expect(user?.type === "user_message" && user.turnId).toBe("t1");
|
|
129
|
+
|
|
130
|
+
const cmp = all.find((e) => e.id === "cmp1");
|
|
131
|
+
expect(cmp?.type).toBe("compaction");
|
|
132
|
+
expect(cmp?.type === "compaction" && cmp.firstKeptSeq).toBe(2);
|
|
133
|
+
expect(cmp?.type === "compaction" && cmp.summaryMessage.content).toBe("summary so far");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("returns an empty array for an unknown conversation", async () => {
|
|
137
|
+
expect(await store.readEntries("nope")).toEqual([]);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("treats an empty append as a no-op", async () => {
|
|
141
|
+
const stored = await store.appendEntries("c1", "agent", null, []);
|
|
142
|
+
expect(stored).toEqual([]);
|
|
143
|
+
expect(await store.readEntries("c1")).toEqual([]);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
runSuite("InMemoryConversationStore entries", async () => {
|
|
149
|
+
const store = new InMemoryConversationStore();
|
|
150
|
+
return { store, cleanup: async () => {} };
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
runSuite("SqliteEngine entries (via adapter)", async () => {
|
|
154
|
+
const dir = await mkdtemp(join(tmpdir(), "entries-store-"));
|
|
155
|
+
const engine = new SqliteEngine({ workingDir: dir, agentId: "agent", dbPath: join(dir, "test.db") });
|
|
156
|
+
await engine.initialize();
|
|
157
|
+
const store = createConversationStoreFromEngine(engine);
|
|
158
|
+
return {
|
|
159
|
+
store,
|
|
160
|
+
cleanup: async () => {
|
|
161
|
+
await engine.close();
|
|
162
|
+
await rm(dir, { recursive: true, force: true });
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
});
|