@poncho-ai/harness 0.59.1 → 0.59.3
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 +28 -0
- package/dist/index.d.ts +39 -118
- package/dist/index.js +37 -327
- package/package.json +1 -1
- package/src/index.ts +3 -11
- package/src/orchestrator/entries-dual-write.ts +20 -209
- package/src/orchestrator/index.ts +1 -6
- package/src/orchestrator/orchestrator.ts +22 -115
- package/src/orchestrator/run-conversation-turn.ts +0 -108
- package/src/state.ts +3 -1
- package/src/storage/entries.ts +47 -182
- package/src/storage/memory-engine.ts +2 -2
- package/src/storage/sql-dialect.ts +27 -7
- package/test/entries-dual-write.test.ts +21 -144
- package/test/entries-store.test.ts +43 -53
- package/test/entries.test.ts +37 -105
|
@@ -4,33 +4,24 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { InMemoryConversationStore } from "../src/state.js";
|
|
6
6
|
import type { ConversationStore } from "../src/state.js";
|
|
7
|
-
import type {
|
|
7
|
+
import type { NewConversationEntry } from "../src/storage/entries.js";
|
|
8
8
|
import { SqliteEngine } from "../src/storage/sqlite-engine.js";
|
|
9
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
10
|
|
|
14
11
|
// Entry factories (without seq/createdAt — those are assigned by the store).
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
const harnessEntry = (id: string, content: string): NewConversationEntry => ({
|
|
23
|
-
type: "harness_message",
|
|
12
|
+
// The queue carries exactly two entry types; the engine-level append/read
|
|
13
|
+
// semantics tested here (seq assignment, ordering, filters, isolation) are
|
|
14
|
+
// type-agnostic.
|
|
15
|
+
const resultEntry = (id: string, subagentId: string): NewConversationEntry => ({
|
|
16
|
+
type: "subagent_result",
|
|
24
17
|
id,
|
|
25
|
-
|
|
26
|
-
turnId: "t1",
|
|
18
|
+
result: { subagentId, task: "t", status: "completed", timestamp: 1 },
|
|
27
19
|
});
|
|
28
20
|
|
|
29
|
-
const
|
|
30
|
-
type: "
|
|
21
|
+
const consumedEntry = (id: string, seqs: number[]): NewConversationEntry => ({
|
|
22
|
+
type: "callback_started",
|
|
31
23
|
id,
|
|
32
|
-
|
|
33
|
-
firstKeptSeq: 2,
|
|
24
|
+
consumedSeqs: seqs,
|
|
34
25
|
});
|
|
35
26
|
|
|
36
27
|
// Shared suite run against both InMemory and SQLite-backed stores.
|
|
@@ -51,55 +42,55 @@ function runSuite(name: string, factory: () => Promise<{ store: ConversationStor
|
|
|
51
42
|
|
|
52
43
|
it("assigns consecutive per-conversation seqs starting at 1", async () => {
|
|
53
44
|
const stored = await store.appendEntries("c1", "agent", null, [
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
45
|
+
resultEntry("r1", "s1"),
|
|
46
|
+
resultEntry("r2", "s2"),
|
|
47
|
+
consumedEntry("cb1", [1]),
|
|
57
48
|
]);
|
|
58
49
|
expect(stored.map((e) => e.seq)).toEqual([1, 2, 3]);
|
|
59
50
|
expect(stored.every((e) => typeof e.createdAt === "number")).toBe(true);
|
|
60
51
|
});
|
|
61
52
|
|
|
62
53
|
it("continues seq across multiple appendEntries calls", async () => {
|
|
63
|
-
await store.appendEntries("c1", "agent", null, [
|
|
54
|
+
await store.appendEntries("c1", "agent", null, [resultEntry("r1", "s1")]);
|
|
64
55
|
const second = await store.appendEntries("c1", "agent", null, [
|
|
65
|
-
|
|
66
|
-
|
|
56
|
+
resultEntry("r2", "s2"),
|
|
57
|
+
consumedEntry("cb1", [1]),
|
|
67
58
|
]);
|
|
68
59
|
expect(second.map((e) => e.seq)).toEqual([2, 3]);
|
|
69
60
|
});
|
|
70
61
|
|
|
71
62
|
it("keeps seq spaces independent per conversation", async () => {
|
|
72
|
-
await store.appendEntries("c1", "agent", null, [
|
|
73
|
-
const other = await store.appendEntries("c2", "agent", null, [
|
|
63
|
+
await store.appendEntries("c1", "agent", null, [resultEntry("r1", "s1")]);
|
|
64
|
+
const other = await store.appendEntries("c2", "agent", null, [resultEntry("r2", "s2")]);
|
|
74
65
|
expect(other[0].seq).toBe(1);
|
|
75
66
|
});
|
|
76
67
|
|
|
77
68
|
it("reads entries ordered by seq ascending", async () => {
|
|
78
69
|
await store.appendEntries("c1", "agent", null, [
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
70
|
+
resultEntry("r1", "s1"),
|
|
71
|
+
resultEntry("r2", "s2"),
|
|
72
|
+
consumedEntry("cb1", [1]),
|
|
82
73
|
]);
|
|
83
74
|
const all = await store.readEntries("c1");
|
|
84
75
|
expect(all.map((e) => e.seq)).toEqual([1, 2, 3]);
|
|
85
|
-
expect(all.map((e) => e.id)).toEqual(["
|
|
76
|
+
expect(all.map((e) => e.id)).toEqual(["r1", "r2", "cb1"]);
|
|
86
77
|
});
|
|
87
78
|
|
|
88
79
|
it("filters by type", async () => {
|
|
89
80
|
await store.appendEntries("c1", "agent", null, [
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
81
|
+
resultEntry("r1", "s1"),
|
|
82
|
+
consumedEntry("cb1", [1]),
|
|
83
|
+
resultEntry("r2", "s2"),
|
|
93
84
|
]);
|
|
94
|
-
const
|
|
95
|
-
expect(
|
|
85
|
+
const resultsOnly = await store.readEntries("c1", { types: ["subagent_result"] });
|
|
86
|
+
expect(resultsOnly.map((e) => e.id)).toEqual(["r1", "r2"]);
|
|
96
87
|
});
|
|
97
88
|
|
|
98
89
|
it("filters by afterSeq", async () => {
|
|
99
90
|
await store.appendEntries("c1", "agent", null, [
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
91
|
+
resultEntry("r1", "s1"),
|
|
92
|
+
resultEntry("r2", "s2"),
|
|
93
|
+
resultEntry("r3", "s3"),
|
|
103
94
|
]);
|
|
104
95
|
const after1 = await store.readEntries("c1", { afterSeq: 1 });
|
|
105
96
|
expect(after1.map((e) => e.seq)).toEqual([2, 3]);
|
|
@@ -107,30 +98,29 @@ function runSuite(name: string, factory: () => Promise<{ store: ConversationStor
|
|
|
107
98
|
|
|
108
99
|
it("respects limit", async () => {
|
|
109
100
|
await store.appendEntries("c1", "agent", null, [
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
101
|
+
resultEntry("r1", "s1"),
|
|
102
|
+
resultEntry("r2", "s2"),
|
|
103
|
+
resultEntry("r3", "s3"),
|
|
113
104
|
]);
|
|
114
105
|
const limited = await store.readEntries("c1", { limit: 2 });
|
|
115
106
|
expect(limited.map((e) => e.seq)).toEqual([1, 2]);
|
|
116
107
|
});
|
|
117
108
|
|
|
118
|
-
it("round-trips
|
|
109
|
+
it("round-trips both entry types with their payloads intact", async () => {
|
|
119
110
|
await store.appendEntries("c1", "agent", null, [
|
|
120
|
-
|
|
121
|
-
|
|
111
|
+
resultEntry("r1", "sub-42"),
|
|
112
|
+
consumedEntry("cb1", [1, 7]),
|
|
122
113
|
]);
|
|
123
114
|
const all = await store.readEntries("c1");
|
|
124
115
|
|
|
125
|
-
const
|
|
126
|
-
expect(
|
|
127
|
-
expect(
|
|
128
|
-
expect(
|
|
116
|
+
const result = all.find((e) => e.id === "r1");
|
|
117
|
+
expect(result?.type).toBe("subagent_result");
|
|
118
|
+
expect(result?.type === "subagent_result" && result.result.subagentId).toBe("sub-42");
|
|
119
|
+
expect(result?.type === "subagent_result" && result.result.status).toBe("completed");
|
|
129
120
|
|
|
130
|
-
const
|
|
131
|
-
expect(
|
|
132
|
-
expect(
|
|
133
|
-
expect(cmp?.type === "compaction" && cmp.summaryMessage.content).toBe("summary so far");
|
|
121
|
+
const consumed = all.find((e) => e.id === "cb1");
|
|
122
|
+
expect(consumed?.type).toBe("callback_started");
|
|
123
|
+
expect(consumed?.type === "callback_started" && consumed.consumedSeqs).toEqual([1, 7]);
|
|
134
124
|
});
|
|
135
125
|
|
|
136
126
|
it("returns an empty array for an unknown conversation", async () => {
|
package/test/entries.test.ts
CHANGED
|
@@ -1,125 +1,57 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
import {
|
|
3
|
-
buildLlmContext,
|
|
4
|
-
buildDisplaySnapshot,
|
|
5
3
|
getPendingSubagentResults,
|
|
6
4
|
type ConversationEntry,
|
|
7
5
|
} from "../src/storage/entries.js";
|
|
8
|
-
import type { Message } from "@poncho-ai/sdk";
|
|
9
6
|
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const harness = (content: string, turnId = "t1"): ConversationEntry => ({
|
|
17
|
-
type: "harness_message", id: `h${seq + 1}`, seq: next(), createdAt: 0,
|
|
18
|
-
message: msg("assistant", content), turnId,
|
|
19
|
-
});
|
|
20
|
-
const user = (content: string, opts: { hidden?: boolean } = {}): ConversationEntry => ({
|
|
21
|
-
type: "user_message", id: `u${seq + 1}`, seq: next(), createdAt: 0,
|
|
22
|
-
message: msg("user", content), turnId: "t1", hidden: opts.hidden,
|
|
23
|
-
});
|
|
24
|
-
const assistant = (id: string, content: string): ConversationEntry => ({
|
|
25
|
-
type: "assistant_message", id, seq: next(), createdAt: 0,
|
|
26
|
-
message: msg("assistant", content), turnId: "t1", runId: "r1",
|
|
7
|
+
const result = (seq: number, subagentId: string): ConversationEntry => ({
|
|
8
|
+
type: "subagent_result",
|
|
9
|
+
id: `r${seq}`,
|
|
10
|
+
seq,
|
|
11
|
+
createdAt: seq,
|
|
12
|
+
result: { subagentId, task: "t", status: "completed", timestamp: seq },
|
|
27
13
|
});
|
|
28
14
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
it("applies a compaction overlay: summary + messages from firstKeptSeq", () => {
|
|
37
|
-
reset();
|
|
38
|
-
const h1 = harness("old1"); // seq 1
|
|
39
|
-
const h2 = harness("old2"); // seq 2
|
|
40
|
-
const h3 = harness("kept3"); // seq 3
|
|
41
|
-
const h4 = harness("kept4"); // seq 4
|
|
42
|
-
const compaction: ConversationEntry = {
|
|
43
|
-
type: "compaction", id: "c1", seq: next(), createdAt: 0,
|
|
44
|
-
summaryMessage: msg("user", "[summary]"), firstKeptSeq: 3,
|
|
45
|
-
};
|
|
46
|
-
const ctx = buildLlmContext([h1, h2, h3, h4, compaction]);
|
|
47
|
-
expect(ctx.map((m) => m.content)).toEqual(["[summary]", "kept3", "kept4"]);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("uses the LATEST compaction when several exist (layered)", () => {
|
|
51
|
-
reset();
|
|
52
|
-
const h1 = harness("a");
|
|
53
|
-
const c1: ConversationEntry = { type: "compaction", id: "c1", seq: next(), createdAt: 0, summaryMessage: msg("user", "[sum1]"), firstKeptSeq: 1 };
|
|
54
|
-
const h2 = harness("b"); // seq 3
|
|
55
|
-
const c2: ConversationEntry = { type: "compaction", id: "c2", seq: next(), createdAt: 0, summaryMessage: msg("user", "[sum2]"), firstKeptSeq: 3 };
|
|
56
|
-
const ctx = buildLlmContext([h1, c1, h2, c2]);
|
|
57
|
-
expect(ctx.map((m) => m.content)).toEqual(["[sum2]", "b"]);
|
|
58
|
-
});
|
|
15
|
+
const consumed = (seq: number, consumedSeqs: number[]): ConversationEntry => ({
|
|
16
|
+
type: "callback_started",
|
|
17
|
+
id: `cb${seq}`,
|
|
18
|
+
seq,
|
|
19
|
+
createdAt: seq,
|
|
20
|
+
consumedSeqs,
|
|
59
21
|
});
|
|
60
22
|
|
|
61
|
-
describe("
|
|
62
|
-
it("
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
user("hidden-framed", { hidden: true }),
|
|
66
|
-
user("hello"),
|
|
67
|
-
assistant("a1", "hi"),
|
|
68
|
-
user("again"),
|
|
69
|
-
assistant("a2", "yo"),
|
|
70
|
-
];
|
|
71
|
-
const snap = buildDisplaySnapshot(entries, 10);
|
|
72
|
-
expect(snap.messages.map((m) => m.content)).toEqual(["hello", "hi", "again", "yo"]);
|
|
73
|
-
expect(snap.totalMessages).toBe(4);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("folds amendments into their target assistant message", () => {
|
|
77
|
-
reset();
|
|
78
|
-
const a = assistant("a1", "part1");
|
|
79
|
-
const amend: ConversationEntry = {
|
|
80
|
-
type: "assistant_amendment", id: "am1", seq: next(), createdAt: 0,
|
|
81
|
-
targetEntryId: "a1", appendText: " + part2",
|
|
82
|
-
};
|
|
83
|
-
const snap = buildDisplaySnapshot([user("q"), a, amend], 10);
|
|
84
|
-
expect(snap.messages.map((m) => m.content)).toEqual(["q", "part1 + part2"]);
|
|
23
|
+
describe("getPendingSubagentResults", () => {
|
|
24
|
+
it("returns every result when nothing is consumed", () => {
|
|
25
|
+
const pending = getPendingSubagentResults([result(1, "a"), result(2, "b")]);
|
|
26
|
+
expect(pending.map((r) => r.subagentId)).toEqual(["a", "b"]);
|
|
85
27
|
});
|
|
86
28
|
|
|
87
|
-
it("
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
29
|
+
it("excludes results consumed by a callback_started entry", () => {
|
|
30
|
+
const pending = getPendingSubagentResults([
|
|
31
|
+
result(1, "a"),
|
|
32
|
+
result(2, "b"),
|
|
33
|
+
consumed(3, [1]),
|
|
34
|
+
]);
|
|
35
|
+
expect(pending.map((r) => r.subagentId)).toEqual(["b"]);
|
|
93
36
|
});
|
|
94
|
-
});
|
|
95
37
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
38
|
+
it("supports multiple consumption entries", () => {
|
|
39
|
+
const pending = getPendingSubagentResults([
|
|
40
|
+
result(1, "a"),
|
|
41
|
+
consumed(2, [1]),
|
|
42
|
+
result(3, "b"),
|
|
43
|
+
result(4, "c"),
|
|
44
|
+
consumed(5, [3, 4]),
|
|
45
|
+
]);
|
|
46
|
+
expect(pending).toEqual([]);
|
|
100
47
|
});
|
|
101
48
|
|
|
102
|
-
it("
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const r2 = result("s2"); // seq 2
|
|
106
|
-
const callback: ConversationEntry = {
|
|
107
|
-
type: "callback_started", id: "cb1", seq: next(), createdAt: 0,
|
|
108
|
-
consumedSeqs: [1],
|
|
109
|
-
};
|
|
110
|
-
const r3 = result("s3"); // seq 4
|
|
111
|
-
const pending = getPendingSubagentResults([r1, r2, callback, r3]);
|
|
112
|
-
// s1 consumed; s2 + s3 still pending
|
|
113
|
-
expect(pending.map((p) => p.subagentId)).toEqual(["s2", "s3"]);
|
|
49
|
+
it("ignores consumption of unknown seqs", () => {
|
|
50
|
+
const pending = getPendingSubagentResults([result(1, "a"), consumed(2, [99])]);
|
|
51
|
+
expect(pending.map((r) => r.subagentId)).toEqual(["a"]);
|
|
114
52
|
});
|
|
115
53
|
|
|
116
|
-
it("returns
|
|
117
|
-
|
|
118
|
-
const r1 = result("s1");
|
|
119
|
-
const callback: ConversationEntry = {
|
|
120
|
-
type: "callback_started", id: "cb1", seq: next(), createdAt: 0,
|
|
121
|
-
consumedSeqs: [1],
|
|
122
|
-
};
|
|
123
|
-
expect(getPendingSubagentResults([r1, callback])).toEqual([]);
|
|
54
|
+
it("returns [] for an empty log", () => {
|
|
55
|
+
expect(getPendingSubagentResults([])).toEqual([]);
|
|
124
56
|
});
|
|
125
57
|
});
|