@cueloop/daemon 0.1.0-alpha.11 → 0.1.0-alpha.111
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/README.md +7 -0
- package/SDK.md +102 -0
- package/package.json +44 -22
- package/src/agent-tools.ts +104 -0
- package/src/agent-turn.test.ts +24 -0
- package/src/agent-turn.ts +34 -0
- package/src/api.test.ts +1081 -65
- package/src/api.ts +1483 -90
- package/src/capabilities.test.ts +57 -0
- package/src/capabilities.ts +95 -0
- package/src/client-errors.ts +24 -0
- package/src/client-reliability.test.ts +191 -0
- package/src/client.test.ts +37 -0
- package/src/client.ts +844 -95
- package/src/curate.ts +111 -0
- package/src/daemon-sdk-effect.ts +113 -0
- package/src/daemon-sdk.test.ts +306 -0
- package/src/daemon-sdk.ts +405 -0
- package/src/daemon-version.test.ts +142 -0
- package/src/diff-hot-reload.test.ts +423 -0
- package/src/diff-watcher.test.ts +165 -0
- package/src/diff-watcher.ts +363 -0
- package/src/examples/daemon-sdk.ts +57 -0
- package/src/gh.test.ts +59 -0
- package/src/gh.ts +78 -0
- package/src/ghostty-thread-surface-store.test.ts +49 -0
- package/src/ghostty-thread-surface-store.ts +104 -0
- package/src/ghostty-thread-surface.test.ts +400 -0
- package/src/ghostty-thread-surface.ts +273 -0
- package/src/harness-delivery.test.ts +269 -0
- package/src/harness-diagnostics.test.ts +26 -0
- package/src/harness-diagnostics.ts +31 -0
- package/src/harness-handoff.test.ts +725 -0
- package/src/harness-handoff.ts +27 -0
- package/src/harness-state-store.test.ts +93 -0
- package/src/harness-state-store.ts +218 -0
- package/src/herdr-split.test.ts +106 -0
- package/src/herdr-split.ts +90 -0
- package/src/herdr-thread-surface-store.test.ts +81 -0
- package/src/herdr-thread-surface-store.ts +66 -0
- package/src/herdr-thread-surface.test.ts +465 -0
- package/src/herdr-thread-surface.ts +392 -0
- package/src/index.ts +8 -9
- package/src/interruptible-wait.test.ts +127 -0
- package/src/interruptible-wait.ts +69 -0
- package/src/jj-working-tree.ts +140 -0
- package/src/main.ts +5 -1
- package/src/operation-receipts.test.ts +111 -0
- package/src/operation-receipts.ts +104 -0
- package/src/paths.ts +49 -0
- package/src/pr-poller.test.ts +102 -0
- package/src/pr-poller.ts +80 -0
- package/src/project-files.test.ts +134 -0
- package/src/project-files.ts +72 -0
- package/src/protocol.ts +82 -7
- package/src/retention.test.ts +191 -0
- package/src/retention.ts +131 -0
- package/src/sdk-identities.ts +35 -0
- package/src/server.test.ts +502 -42
- package/src/server.ts +660 -88
- package/src/share-blob.test.ts +142 -0
- package/src/share-blob.ts +57 -0
- package/src/shared-agent-protocol.ts +30 -0
- package/src/single-instance.test.ts +165 -13
- package/src/store.conformance.test.ts +41 -0
- package/src/store.jsonl.test.ts +173 -0
- package/src/store.ts +278 -34
- package/src/testing/store-conformance.ts +192 -0
- package/src/thread-agent-validation.ts +141 -0
- package/src/thread-agent.test.ts +732 -0
- package/src/thread-agent.ts +1171 -0
- package/src/thread-review.test.ts +487 -0
- package/src/thread-review.ts +394 -0
- package/src/thread-subscription.test.ts +177 -0
- package/src/thread-subscription.ts +138 -0
- package/src/thread-surface-config.test.ts +63 -0
- package/src/thread-surface-config.ts +61 -0
- package/src/validate.test.ts +290 -37
- package/src/validate.ts +427 -33
- package/src/vcs-source.test.ts +180 -0
- package/src/vcs-source.ts +235 -0
- package/src/version.ts +6 -0
- package/src/working-tree.ts +154 -0
- package/src/review.test.ts +0 -167
- package/src/review.ts +0 -174
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session repository contract as executable cases. Every adapter runs the
|
|
3
|
+
* same suite: what a store returns, in which order, what survives a restart,
|
|
4
|
+
* what a bad record does, and how a record without a history reads.
|
|
5
|
+
*
|
|
6
|
+
* `restart` gives back a fresh adapter over the same persisted state (or the
|
|
7
|
+
* same seed) so recovery is tested through the contract, never past it.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, test } from "bun:test";
|
|
11
|
+
import { SCHEMA_VERSION, type Thread } from "@cueloop/schema";
|
|
12
|
+
import type { ThreadRepository } from "../store";
|
|
13
|
+
|
|
14
|
+
export interface StoreHarness {
|
|
15
|
+
open: (records: unknown[]) => ThreadRepository;
|
|
16
|
+
restart: (store: ThreadRepository) => ThreadRepository;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function createTestThreadRecord(
|
|
20
|
+
id: string,
|
|
21
|
+
createdAt: string,
|
|
22
|
+
overrides: Partial<Thread> = {},
|
|
23
|
+
): Thread {
|
|
24
|
+
return {
|
|
25
|
+
schemaVersion: SCHEMA_VERSION,
|
|
26
|
+
id,
|
|
27
|
+
workspace: { repoRoot: "/repo", branch: "main" },
|
|
28
|
+
artifact: { type: "plan", content: `# ${id}`, meta: {} },
|
|
29
|
+
revisions: [{ revision: 1, content: `# ${id}`, submittedAt: createdAt }],
|
|
30
|
+
annotations: [],
|
|
31
|
+
message: null,
|
|
32
|
+
status: "pending",
|
|
33
|
+
createdAt,
|
|
34
|
+
...overrides,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function runThreadStoreConformance(name: string, harness: StoreHarness): void {
|
|
39
|
+
describe(`${name} - session repository contract`, () => {
|
|
40
|
+
test("a stored session reads back whole and lists oldest first", () => {
|
|
41
|
+
// Arrange
|
|
42
|
+
const store = harness.open([]);
|
|
43
|
+
|
|
44
|
+
store.recover();
|
|
45
|
+
const newer = createTestThreadRecord("ses_b", "2026-09-02T00:00:00.000Z");
|
|
46
|
+
const older = createTestThreadRecord("ses_a", "2026-09-01T00:00:00.000Z");
|
|
47
|
+
|
|
48
|
+
// Act
|
|
49
|
+
store.upsert(newer);
|
|
50
|
+
store.upsert(older);
|
|
51
|
+
|
|
52
|
+
// Assert
|
|
53
|
+
expect(store.get("ses_a")).toEqual(older);
|
|
54
|
+
expect(store.list().map((session) => session.id)).toEqual(["ses_a", "ses_b"]);
|
|
55
|
+
expect(store.get("ses_zzz")).toBeUndefined();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("upsert replaces by id and delete reports whether anything was removed", () => {
|
|
59
|
+
// Arrange
|
|
60
|
+
const store = harness.open([]);
|
|
61
|
+
|
|
62
|
+
store.recover();
|
|
63
|
+
store.upsert(createTestThreadRecord("ses_a", "2026-09-01T00:00:00.000Z"));
|
|
64
|
+
|
|
65
|
+
// Act
|
|
66
|
+
store.upsert(
|
|
67
|
+
createTestThreadRecord("ses_a", "2026-09-01T00:00:00.000Z", { status: "resolved" }),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// Assert
|
|
71
|
+
expect(store.list()).toHaveLength(1);
|
|
72
|
+
expect(store.get("ses_a")?.status).toBe("resolved");
|
|
73
|
+
expect(store.delete("ses_a")).toBe(true);
|
|
74
|
+
expect(store.delete("ses_a")).toBe(false);
|
|
75
|
+
expect(store.list()).toEqual([]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("what was upserted survives a restart, deletions included", () => {
|
|
79
|
+
// Arrange
|
|
80
|
+
const store = harness.open([]);
|
|
81
|
+
|
|
82
|
+
store.recover();
|
|
83
|
+
store.upsert(createTestThreadRecord("ses_keep", "2026-09-01T00:00:00.000Z"));
|
|
84
|
+
store.upsert(createTestThreadRecord("ses_gone", "2026-09-02T00:00:00.000Z"));
|
|
85
|
+
store.delete("ses_gone");
|
|
86
|
+
|
|
87
|
+
// Act
|
|
88
|
+
const reopened = harness.restart(store);
|
|
89
|
+
const report = reopened.recover();
|
|
90
|
+
|
|
91
|
+
// Assert
|
|
92
|
+
expect(report.recovered).toEqual(["ses_keep"]);
|
|
93
|
+
expect(reopened.get("ses_keep")?.artifact.content).toBe("# ses_keep");
|
|
94
|
+
expect(reopened.get("ses_gone")).toBeUndefined();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("a record without a history reads as a one-branch tree whose head is the artifact", () => {
|
|
98
|
+
// Arrange: written before histories existed, with two revisions and a comment
|
|
99
|
+
const legacy = createTestThreadRecord("ses_old", "2026-09-01T00:00:00.000Z", {
|
|
100
|
+
artifact: { type: "plan", content: "v2", meta: {} },
|
|
101
|
+
revisions: [
|
|
102
|
+
{ revision: 1, content: "v1", submittedAt: "2026-09-01T00:00:00.000Z" },
|
|
103
|
+
{ revision: 2, content: "v2", submittedAt: "2026-09-01T02:00:00.000Z" },
|
|
104
|
+
],
|
|
105
|
+
annotations: [
|
|
106
|
+
{
|
|
107
|
+
id: "a1",
|
|
108
|
+
kind: "comment",
|
|
109
|
+
anchor: { quote: "v1", prefix: "", suffix: "" },
|
|
110
|
+
body: "why",
|
|
111
|
+
createdAt: "2026-09-01T01:00:00.000Z",
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
});
|
|
115
|
+
const store = harness.open([legacy]);
|
|
116
|
+
|
|
117
|
+
// Act
|
|
118
|
+
const report = store.recover();
|
|
119
|
+
const history = store.get("ses_old")?.history;
|
|
120
|
+
|
|
121
|
+
// Assert
|
|
122
|
+
expect(report.recovered).toEqual(["ses_old"]);
|
|
123
|
+
expect(history?.branch).toBe("main");
|
|
124
|
+
expect(history?.entries.map((entry) => entry.type)).toEqual([
|
|
125
|
+
"revision",
|
|
126
|
+
"comment",
|
|
127
|
+
"revision",
|
|
128
|
+
]);
|
|
129
|
+
expect(history?.entries.at(-1)).toMatchObject({ type: "revision", content: "v2" });
|
|
130
|
+
expect(history?.tips.main).toBe(history?.entries.at(-1)?.id);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("a record with no revision still recovers, without a history", () => {
|
|
134
|
+
// Arrange
|
|
135
|
+
const store = harness.open([
|
|
136
|
+
createTestThreadRecord("ses_empty", "2026-09-01T00:00:00.000Z", { revisions: [] }),
|
|
137
|
+
]);
|
|
138
|
+
|
|
139
|
+
// Act
|
|
140
|
+
const report = store.recover();
|
|
141
|
+
|
|
142
|
+
// Assert
|
|
143
|
+
expect(report.recovered).toEqual(["ses_empty"]);
|
|
144
|
+
expect(store.get("ses_empty")?.history).toBeUndefined();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("a record that already carries a history keeps it untouched", () => {
|
|
148
|
+
// Arrange
|
|
149
|
+
const withTree = createTestThreadRecord("ses_tree", "2026-09-01T00:00:00.000Z", {
|
|
150
|
+
history: {
|
|
151
|
+
entries: [
|
|
152
|
+
{
|
|
153
|
+
id: "root",
|
|
154
|
+
parentId: null,
|
|
155
|
+
type: "revision",
|
|
156
|
+
by: "agent",
|
|
157
|
+
content: "# ses_tree",
|
|
158
|
+
createdAt: "2026-09-01T00:00:00.000Z",
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
tips: { main: "root", ramble: "root" },
|
|
162
|
+
branch: "ramble",
|
|
163
|
+
labels: { root: "start" },
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
const store = harness.open([withTree]);
|
|
167
|
+
|
|
168
|
+
// Act
|
|
169
|
+
store.recover();
|
|
170
|
+
|
|
171
|
+
// Assert
|
|
172
|
+
expect(store.get("ses_tree")?.history).toEqual(withTree.history);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("a record that fails validation is skipped and reported, never dropped from the report", () => {
|
|
176
|
+
// Arrange
|
|
177
|
+
const store = harness.open([
|
|
178
|
+
createTestThreadRecord("ses_good", "2026-09-01T00:00:00.000Z"),
|
|
179
|
+
{ schemaVersion: SCHEMA_VERSION, id: "ses_bad" },
|
|
180
|
+
]);
|
|
181
|
+
|
|
182
|
+
// Act
|
|
183
|
+
const report = store.recover();
|
|
184
|
+
|
|
185
|
+
// Assert
|
|
186
|
+
expect(report.recovered).toEqual(["ses_good"]);
|
|
187
|
+
expect(report.skipped).toHaveLength(1);
|
|
188
|
+
expect(report.skipped[0]!.error).toContain("invalid record");
|
|
189
|
+
expect(store.get("ses_bad")).toBeUndefined();
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import * as v from "valibot";
|
|
2
|
+
import type { ThreadAgentState } from "@cueloop/schema";
|
|
3
|
+
|
|
4
|
+
const AgentAnchorSchema = v.object({
|
|
5
|
+
quote: v.pipe(v.string(), v.minLength(1), v.maxLength(32_768)),
|
|
6
|
+
prefix: v.string(),
|
|
7
|
+
suffix: v.string(),
|
|
8
|
+
blockIndex: v.number(),
|
|
9
|
+
start: v.number(),
|
|
10
|
+
end: v.number(),
|
|
11
|
+
endBlockIndex: v.optional(v.number()),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
/** Validate agent comments at the socket and persisted transcript boundaries. */
|
|
15
|
+
export const AgentCommentSchema = v.object({
|
|
16
|
+
id: v.string(),
|
|
17
|
+
messageId: v.string(),
|
|
18
|
+
anchor: AgentAnchorSchema,
|
|
19
|
+
body: v.pipe(v.string(), v.minLength(1), v.maxLength(32_768)),
|
|
20
|
+
sent: v.boolean(),
|
|
21
|
+
replyTo: v.optional(v.string()),
|
|
22
|
+
author: v.optional(v.string()),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/** Validate persisted agent transcripts independently of submitted artifacts. */
|
|
26
|
+
const StoredAgentSchema = v.object({
|
|
27
|
+
threadId: v.string(),
|
|
28
|
+
fxSessionId: v.optional(v.string()),
|
|
29
|
+
harness: v.optional(
|
|
30
|
+
v.object({ id: v.string(), label: v.string(), sessionId: v.optional(v.string()) }),
|
|
31
|
+
),
|
|
32
|
+
phase: v.variant("kind", [
|
|
33
|
+
v.object({ kind: v.literal("idle") }),
|
|
34
|
+
v.object({ kind: v.literal("running") }),
|
|
35
|
+
v.object({ kind: v.literal("offline") }),
|
|
36
|
+
v.object({ kind: v.literal("failed"), error: v.string() }),
|
|
37
|
+
v.object({
|
|
38
|
+
kind: v.literal("permission"),
|
|
39
|
+
permission: v.object({
|
|
40
|
+
id: v.string(),
|
|
41
|
+
title: v.string(),
|
|
42
|
+
options: v.array(
|
|
43
|
+
v.object({
|
|
44
|
+
optionId: v.string(),
|
|
45
|
+
name: v.string(),
|
|
46
|
+
kind: v.string(),
|
|
47
|
+
}),
|
|
48
|
+
),
|
|
49
|
+
}),
|
|
50
|
+
}),
|
|
51
|
+
]),
|
|
52
|
+
messages: v.array(
|
|
53
|
+
v.object({
|
|
54
|
+
id: v.string(),
|
|
55
|
+
role: v.picklist(["user", "agent"]),
|
|
56
|
+
text: v.string(),
|
|
57
|
+
complete: v.boolean(),
|
|
58
|
+
revision: v.number(),
|
|
59
|
+
submissionId: v.optional(v.string()),
|
|
60
|
+
}),
|
|
61
|
+
),
|
|
62
|
+
tools: v.array(
|
|
63
|
+
v.object({
|
|
64
|
+
id: v.string(),
|
|
65
|
+
turnId: v.string(),
|
|
66
|
+
title: v.string(),
|
|
67
|
+
kind: v.string(),
|
|
68
|
+
status: v.picklist(["pending", "in_progress", "completed", "failed", "cancelled"]),
|
|
69
|
+
output: v.string(),
|
|
70
|
+
locations: v.array(v.object({ path: v.string(), line: v.optional(v.number()) })),
|
|
71
|
+
}),
|
|
72
|
+
),
|
|
73
|
+
comments: v.array(AgentCommentSchema),
|
|
74
|
+
submissions: v.optional(
|
|
75
|
+
v.array(
|
|
76
|
+
v.object({
|
|
77
|
+
id: v.string(),
|
|
78
|
+
commentId: v.optional(v.string()),
|
|
79
|
+
messageId: v.optional(v.string()),
|
|
80
|
+
prompt: v.string(),
|
|
81
|
+
harnessPrompt: v.optional(v.string()),
|
|
82
|
+
attemptId: v.optional(v.string()),
|
|
83
|
+
quote: v.optional(v.string()),
|
|
84
|
+
context: v.optional(v.string()),
|
|
85
|
+
status: v.picklist(["queued", "running", "completed", "failed"]),
|
|
86
|
+
cancelled: v.optional(v.boolean()),
|
|
87
|
+
}),
|
|
88
|
+
),
|
|
89
|
+
),
|
|
90
|
+
promptOperations: v.optional(
|
|
91
|
+
v.array(
|
|
92
|
+
v.object({
|
|
93
|
+
operationId: v.string(),
|
|
94
|
+
fingerprint: v.string(),
|
|
95
|
+
result: v.array(v.string()),
|
|
96
|
+
outcome: v.optional(v.picklist(["completed", "failed", "cancelled"])),
|
|
97
|
+
}),
|
|
98
|
+
),
|
|
99
|
+
),
|
|
100
|
+
continuation: v.optional(v.string()),
|
|
101
|
+
handoff: v.optional(
|
|
102
|
+
v.intersect([
|
|
103
|
+
v.object({
|
|
104
|
+
target: v.string(),
|
|
105
|
+
requestedTarget: v.optional(v.string()),
|
|
106
|
+
operationId: v.string(),
|
|
107
|
+
prompt: v.optional(v.string()),
|
|
108
|
+
source: v.optional(
|
|
109
|
+
v.object({ id: v.string(), label: v.string(), sessionId: v.optional(v.string()) }),
|
|
110
|
+
),
|
|
111
|
+
}),
|
|
112
|
+
v.variant("status", [
|
|
113
|
+
v.object({ status: v.literal("queued") }),
|
|
114
|
+
v.object({ status: v.literal("summarizing") }),
|
|
115
|
+
v.object({ status: v.literal("prepared"), text: v.string() }),
|
|
116
|
+
]),
|
|
117
|
+
]),
|
|
118
|
+
),
|
|
119
|
+
configOptions: v.optional(
|
|
120
|
+
v.array(
|
|
121
|
+
v.object({
|
|
122
|
+
id: v.string(),
|
|
123
|
+
name: v.string(),
|
|
124
|
+
category: v.optional(v.string()),
|
|
125
|
+
currentValue: v.string(),
|
|
126
|
+
options: v.array(v.object({ value: v.string(), name: v.string() })),
|
|
127
|
+
}),
|
|
128
|
+
),
|
|
129
|
+
),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
/** Read the first fx prototype's records without discarding their session identity. */
|
|
133
|
+
export const ThreadAgentSchema: v.GenericSchema<ThreadAgentState> = v.pipe(
|
|
134
|
+
StoredAgentSchema,
|
|
135
|
+
v.transform(({ fxSessionId, ...state }) => ({
|
|
136
|
+
...state,
|
|
137
|
+
harness:
|
|
138
|
+
state.harness ??
|
|
139
|
+
(fxSessionId ? { id: "fx", label: "fx", sessionId: fxSessionId } : undefined),
|
|
140
|
+
})),
|
|
141
|
+
);
|