@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.
@@ -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 { ConversationEntry, NewConversationEntry } from "../src/storage/entries.js";
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
- 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",
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
- message: msg("assistant", content),
26
- turnId: "t1",
18
+ result: { subagentId, task: "t", status: "completed", timestamp: 1 },
27
19
  });
28
20
 
29
- const compactionEntry = (id: string): NewConversationEntry => ({
30
- type: "compaction",
21
+ const consumedEntry = (id: string, seqs: number[]): NewConversationEntry => ({
22
+ type: "callback_started",
31
23
  id,
32
- summaryMessage: msg("user", "summary so far"),
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
- userEntry("u1", "hi"),
55
- harnessEntry("h1", "hello"),
56
- harnessEntry("h2", "how can I help?"),
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, [userEntry("u1", "a")]);
54
+ await store.appendEntries("c1", "agent", null, [resultEntry("r1", "s1")]);
64
55
  const second = await store.appendEntries("c1", "agent", null, [
65
- harnessEntry("h1", "b"),
66
- harnessEntry("h2", "c"),
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, [userEntry("u1", "a")]);
73
- const other = await store.appendEntries("c2", "agent", null, [userEntry("u2", "b")]);
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
- userEntry("u1", "one"),
80
- harnessEntry("h1", "two"),
81
- harnessEntry("h2", "three"),
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(["u1", "h1", "h2"]);
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
- userEntry("u1", "one"),
91
- harnessEntry("h1", "two"),
92
- harnessEntry("h2", "three"),
81
+ resultEntry("r1", "s1"),
82
+ consumedEntry("cb1", [1]),
83
+ resultEntry("r2", "s2"),
93
84
  ]);
94
- const harnessOnly = await store.readEntries("c1", { types: ["harness_message"] });
95
- expect(harnessOnly.map((e) => e.id)).toEqual(["h1", "h2"]);
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
- userEntry("u1", "one"),
101
- harnessEntry("h1", "two"),
102
- harnessEntry("h2", "three"),
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
- userEntry("u1", "one"),
111
- harnessEntry("h1", "two"),
112
- harnessEntry("h2", "three"),
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 distinct entry types with their payloads intact", async () => {
109
+ it("round-trips both entry types with their payloads intact", async () => {
119
110
  await store.appendEntries("c1", "agent", null, [
120
- userEntry("u1", "question"),
121
- compactionEntry("cmp1"),
111
+ resultEntry("r1", "sub-42"),
112
+ consumedEntry("cb1", [1, 7]),
122
113
  ]);
123
114
  const all = await store.readEntries("c1");
124
115
 
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");
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 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");
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 () => {
@@ -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 msg = (role: Message["role"], content: string): Message => ({ role, content });
11
-
12
- let seq = 0;
13
- const reset = () => { seq = 0; };
14
- const next = () => ++seq;
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
- describe("buildLlmContext", () => {
30
- it("returns all harness messages in order with no compaction", () => {
31
- reset();
32
- const entries = [harness("a"), harness("b"), harness("c")];
33
- expect(buildLlmContext(entries).map((m) => m.content)).toEqual(["a", "b", "c"]);
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("buildDisplaySnapshot", () => {
62
- it("drops hidden user messages and returns the tail", () => {
63
- reset();
64
- const entries = [
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("returns only the trailing tailN messages", () => {
88
- reset();
89
- const entries = [user("1"), assistant("a", "2"), user("3"), assistant("b", "4")];
90
- const snap = buildDisplaySnapshot(entries, 2);
91
- expect(snap.messages.map((m) => m.content)).toEqual(["3", "4"]);
92
- expect(snap.totalMessages).toBe(4);
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
- describe("getPendingSubagentResults", () => {
97
- const result = (subagentId: string): ConversationEntry => ({
98
- type: "subagent_result", id: `sr-${subagentId}`, seq: next(), createdAt: 0,
99
- result: { subagentId, task: "t", status: "completed", timestamp: 0 },
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("returns results not yet consumed by a callback", () => {
103
- reset();
104
- const r1 = result("s1"); // seq 1
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 empty when all consumed", () => {
117
- reset();
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
  });