@cueloop/daemon 0.1.0-alpha.0 → 0.1.0-alpha.100
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 +43 -16
- package/src/agent-tools.ts +98 -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 +1425 -89
- 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 +189 -0
- package/src/client.test.ts +37 -0
- package/src/client.ts +820 -94
- package/src/curate.ts +109 -0
- package/src/daemon-sdk-effect.ts +113 -0
- package/src/daemon-sdk.test.ts +306 -0
- package/src/daemon-sdk.ts +397 -0
- package/src/daemon-version.test.ts +142 -0
- package/src/diff-hot-reload.test.ts +419 -0
- package/src/diff-watcher.test.ts +117 -0
- package/src/diff-watcher.ts +326 -0
- package/src/examples/daemon-sdk.ts +55 -0
- package/src/gh.test.ts +59 -0
- package/src/gh.ts +74 -0
- package/src/ghostty-thread-surface-store.test.ts +49 -0
- package/src/ghostty-thread-surface-store.ts +101 -0
- package/src/ghostty-thread-surface.test.ts +398 -0
- package/src/ghostty-thread-surface.ts +267 -0
- package/src/harness-delivery.test.ts +269 -0
- package/src/harness-diagnostics.test.ts +26 -0
- package/src/harness-diagnostics.ts +30 -0
- package/src/harness-state-store.test.ts +93 -0
- package/src/harness-state-store.ts +213 -0
- package/src/herdr-split.test.ts +106 -0
- package/src/herdr-split.ts +85 -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 +378 -0
- package/src/index.ts +8 -8
- package/src/interruptible-wait.test.ts +127 -0
- package/src/interruptible-wait.ts +66 -0
- package/src/jj-working-tree.ts +131 -0
- package/src/main.ts +4 -1
- package/src/operation-receipts.test.ts +111 -0
- package/src/operation-receipts.ts +91 -0
- package/src/paths.ts +49 -0
- package/src/pr-poller.test.ts +102 -0
- package/src/pr-poller.ts +73 -0
- package/src/project-files.test.ts +134 -0
- package/src/project-files.ts +68 -0
- package/src/protocol.ts +78 -7
- package/src/retention.test.ts +191 -0
- package/src/retention.ts +123 -0
- package/src/sdk-identities.ts +35 -0
- package/src/server.test.ts +465 -40
- package/src/server.ts +601 -88
- package/src/share-blob.test.ts +119 -0
- package/src/share-blob.ts +57 -0
- package/src/single-instance.test.ts +162 -13
- package/src/store.conformance.test.ts +41 -0
- package/src/store.jsonl.test.ts +173 -0
- package/src/store.ts +267 -34
- package/src/testing/store-conformance.ts +192 -0
- package/src/thread-agent-validation.ts +119 -0
- package/src/thread-agent.test.ts +412 -0
- package/src/thread-agent.ts +713 -0
- package/src/thread-review.test.ts +483 -0
- package/src/thread-review.ts +382 -0
- package/src/thread-subscription.test.ts +174 -0
- package/src/thread-subscription.ts +129 -0
- package/src/thread-surface-config.test.ts +63 -0
- package/src/thread-surface-config.ts +61 -0
- package/src/validate.test.ts +366 -21
- package/src/validate.ts +458 -47
- package/src/vcs-source.test.ts +180 -0
- package/src/vcs-source.ts +229 -0
- package/src/version.ts +6 -0
- package/src/working-tree.ts +150 -0
package/README.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# @cueloop/daemon
|
|
2
|
+
|
|
3
|
+
The local cueloop daemon owns Thread mutations, persisted review history, harness
|
|
4
|
+
bindings and socket subscriptions. Integrations use its typed NDJSON client.
|
|
5
|
+
|
|
6
|
+
See the [daemon SDK contract](SDK.md) for explicit capabilities, safe mutation
|
|
7
|
+
retries, reconnecting subscriptions and the optional scoped Effect service.
|
package/SDK.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Daemon SDK
|
|
2
|
+
|
|
3
|
+
The SDK is a facade over the existing NDJSON socket client. It lives at
|
|
4
|
+
`@cueloop/daemon/sdk`; the optional Effect service is at
|
|
5
|
+
`@cueloop/daemon/sdk-effect`. Both use the same daemon, Thread records and harness
|
|
6
|
+
adapter. Importing the Promise SDK does not load Effect.
|
|
7
|
+
|
|
8
|
+
## Capabilities
|
|
9
|
+
|
|
10
|
+
`connectOwnerSdk({ home, autostart })` authenticates with the local owner token.
|
|
11
|
+
It exposes Thread creation, review comments, review messages and embedded agent
|
|
12
|
+
operations. Agent methods still require `[experimental] thread_agent = true` in
|
|
13
|
+
the daemon's workspace configuration. Shared collaborators cannot invoke them.
|
|
14
|
+
|
|
15
|
+
`connectReviewSdk({ home, role: "collaborator" | "agent", author })` exposes
|
|
16
|
+
Thread reads and review comments only. An `agent` role here means a review-side
|
|
17
|
+
agent, not the daemon-owned embedded harness. The daemon enforces authorization;
|
|
18
|
+
TypeScript narrows the available operations but does not grant authority.
|
|
19
|
+
The SDK brands Thread, operation and submission IDs separately. Thread reads
|
|
20
|
+
return branded identities; `sdk.ids.thread`, `sdk.ids.operation` and
|
|
21
|
+
`sdk.ids.submission` parse external strings for incremental adoption.
|
|
22
|
+
|
|
23
|
+
## Acceptance, completion and retries
|
|
24
|
+
|
|
25
|
+
`sessions.sendMessage` and `agents.prompt` require a caller-chosen `operationId`
|
|
26
|
+
(up to 128 characters). Persist that ID with the normalized input if a workflow
|
|
27
|
+
needs to resume. Receipts are scoped to a Thread and method. The same ID and
|
|
28
|
+
payload return the original accepted message or submission batch after a lost
|
|
29
|
+
response, disconnect or daemon restart. Different payloads reject with
|
|
30
|
+
`operation_conflict`. Submission acceptance does not imply agent completion.
|
|
31
|
+
|
|
32
|
+
Receipts commit in the same persisted record as their mutation. Each method
|
|
33
|
+
retains up to 128 receipts for the lifetime of the Thread; it rejects new IDs with
|
|
34
|
+
`operation_capacity` rather than evicting receipts and risking duplicate work.
|
|
35
|
+
Deleting or retaining out an old Thread removes its receipts. A missing Thread
|
|
36
|
+
rejects retries. Private receipt metadata is omitted from share blobs.
|
|
37
|
+
|
|
38
|
+
`agents.wait(accepted)` waits for that exact batch and returns `completed`,
|
|
39
|
+
`failed`, or `cancelled`. Terminal outcomes stay fixed even when a failed
|
|
40
|
+
submission is explicitly retried with a new operation ID. Queued or running
|
|
41
|
+
submissions recovered after a daemon restart become failed; they are never
|
|
42
|
+
silently re-executed.
|
|
43
|
+
|
|
44
|
+
There is no automatic mutation replay. A transport failure after writing has
|
|
45
|
+
`certainty: "unknown"`; work may already have committed. Explicitly retry the two
|
|
46
|
+
receipt-backed methods with the same operation ID and payload. Thread creation,
|
|
47
|
+
comment replies and other legacy methods have no acceptance receipts: do not
|
|
48
|
+
retry an ambiguous write automatically. Comments accept caller-chosen annotation
|
|
49
|
+
IDs for upsert semantics, which are separate from mutation acceptance receipts.
|
|
50
|
+
|
|
51
|
+
## Failures and cancellation
|
|
52
|
+
|
|
53
|
+
`DaemonClientError` carries a daemon rejection `code` (including `forbidden`,
|
|
54
|
+
`resolved`, `operation_conflict` and `operation_capacity`). A rejection is not a
|
|
55
|
+
blanket rollback guarantee: a daemon internal error can follow a commit.
|
|
56
|
+
|
|
57
|
+
`DaemonTransportError` carries `kind`: `protocol`, `connection`, `timeout` or
|
|
58
|
+
`cancelled`, plus `certainty`: `not_sent` or `unknown`. A malformed result rejects
|
|
59
|
+
its request promptly. A malformed outer frame closes the connection and rejects
|
|
60
|
+
all outstanding requests. Pending requests remove timers and abort listeners on
|
|
61
|
+
every completion or failure path.
|
|
62
|
+
|
|
63
|
+
Requests accept `{ timeoutMs, signal }`, defaulting to a 30-second local deadline.
|
|
64
|
+
Cancelling a request or wait stops local observation; it does not undo a mutation
|
|
65
|
+
or stop a harness. `agents.cancel(threadId)` explicitly requests cancellation of
|
|
66
|
+
the active turn and preserves partial output. It does not clear queued inputs.
|
|
67
|
+
Closing the owner SDK also cancels its outstanding local waits.
|
|
68
|
+
|
|
69
|
+
## Subscriptions
|
|
70
|
+
|
|
71
|
+
`subscribeThreadState` owns an observation connection, subscribes before reading,
|
|
72
|
+
refreshes after matching event hints and reconnects after connection loss. Every
|
|
73
|
+
reconnect reads authoritative state, including changes made while disconnected.
|
|
74
|
+
It never replays a write or treats event notifications as a durable log.
|
|
75
|
+
|
|
76
|
+
Generation and request counters prevent late reads from overwriting newer state.
|
|
77
|
+
Disposal removes listeners, cancels reconnect timers, aborts reads and closes the
|
|
78
|
+
observation connection. Protocol errors and authorization/version rejections
|
|
79
|
+
stop reconnection. Transient connection failures retry at the configured delay.
|
|
80
|
+
The Thread-agent UI uses this shared lifecycle.
|
|
81
|
+
|
|
82
|
+
## Effect and example
|
|
83
|
+
|
|
84
|
+
`CueloopSdk.ownerLayer` acquires the owner connection with `Effect.acquireRelease`.
|
|
85
|
+
Its operations have a typed `DaemonSdkError` channel. `threads.watch(id)` is a
|
|
86
|
+
scoped stream of refreshed snapshots with a one-item sliding buffer. It coalesces
|
|
87
|
+
state updates rather than promising delivery of every event. Effect interruption is passed
|
|
88
|
+
through an AbortSignal, including to completion waits. Scope finalization closes
|
|
89
|
+
the connection. The layer does not replace the daemon or rewrite UI state in
|
|
90
|
+
Effect.
|
|
91
|
+
|
|
92
|
+
With the experimental flag enabled and a harness configured, run:
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
bun run packages/daemon/src/examples/daemon-sdk.ts
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The example creates and reads a Thread, submits a prompt, waits for its terminal
|
|
99
|
+
outcome, lists and replies to a comment, then sends approval. It creates a real
|
|
100
|
+
Thread and invokes the configured model. IDs are generated once per invocation;
|
|
101
|
+
a resumable application should save them rather than generate replacements when
|
|
102
|
+
retrying.
|
package/package.json
CHANGED
|
@@ -1,29 +1,56 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cueloop/daemon",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.100",
|
|
4
|
+
"description": "The cueloop session daemon and its client: socket protocol, atomic store, resumable waits",
|
|
5
|
+
"homepage": "https://github.com/mmurakaru/cueloop#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/mmurakaru/cueloop/issues"
|
|
8
|
+
},
|
|
4
9
|
"license": "Apache-2.0",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/mmurakaru/cueloop.git",
|
|
13
|
+
"directory": "packages/daemon"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src",
|
|
17
|
+
"README.md",
|
|
18
|
+
"SDK.md"
|
|
19
|
+
],
|
|
5
20
|
"type": "module",
|
|
6
21
|
"exports": {
|
|
7
22
|
".": "./src/index.ts",
|
|
8
|
-
"./client": "./src/client.ts"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
"
|
|
12
|
-
"
|
|
23
|
+
"./client": "./src/client.ts",
|
|
24
|
+
"./thread-review": "./src/thread-review.ts",
|
|
25
|
+
"./vcs-source": "./src/vcs-source.ts",
|
|
26
|
+
"./api": "./src/api.ts",
|
|
27
|
+
"./working-tree": "./src/working-tree.ts",
|
|
28
|
+
"./store": "./src/store.ts",
|
|
29
|
+
"./validate": "./src/validate.ts",
|
|
30
|
+
"./paths": "./src/paths.ts",
|
|
31
|
+
"./retention": "./src/retention.ts",
|
|
32
|
+
"./herdr-thread-surface": "./src/herdr-thread-surface.ts",
|
|
33
|
+
"./ghostty-thread-surface": "./src/ghostty-thread-surface.ts",
|
|
34
|
+
"./thread-surface-config": "./src/thread-surface-config.ts",
|
|
35
|
+
"./herdr-split": "./src/herdr-split.ts",
|
|
36
|
+
"./share-blob": "./src/share-blob.ts",
|
|
37
|
+
"./curate": "./src/curate.ts",
|
|
38
|
+
"./sdk": "./src/daemon-sdk.ts",
|
|
39
|
+
"./sdk-effect": "./src/daemon-sdk-effect.ts",
|
|
40
|
+
"./thread-subscription": "./src/thread-subscription.ts"
|
|
13
41
|
},
|
|
14
42
|
"publishConfig": {
|
|
15
43
|
"access": "public"
|
|
16
44
|
},
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@cueloop/extension-api": "0.1.0-alpha.100",
|
|
47
|
+
"@cueloop/schema": "0.1.0-alpha.100",
|
|
48
|
+
"@pierre/diffs": "^1.5.1",
|
|
49
|
+
"effect": "^4.0.0",
|
|
50
|
+
"smol-toml": "^1.9.0",
|
|
51
|
+
"valibot": "^1.5.0"
|
|
52
|
+
},
|
|
21
53
|
"engines": {
|
|
22
54
|
"bun": ">=1.3.0"
|
|
23
|
-
},
|
|
24
|
-
"repository": {
|
|
25
|
-
"type": "git",
|
|
26
|
-
"url": "git+https://github.com/mmurakaru/cueloop.git",
|
|
27
|
-
"directory": "packages/daemon"
|
|
28
55
|
}
|
|
29
|
-
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import * as v from "valibot";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { MESSAGE_OUTCOMES, type Thread } from "@cueloop/schema";
|
|
4
|
+
import type { MethodName } from "./validate";
|
|
5
|
+
|
|
6
|
+
/** Tool arguments are validated before entering the owner's daemon API. */
|
|
7
|
+
export function parseAgentToolInput(name: string, serialized: string) {
|
|
8
|
+
const input: unknown = JSON.parse(serialized);
|
|
9
|
+
|
|
10
|
+
if (name === "reply_to_comment") {
|
|
11
|
+
const args = v.parse(
|
|
12
|
+
v.object({
|
|
13
|
+
id: v.string(),
|
|
14
|
+
commentId: v.string(),
|
|
15
|
+
body: v.pipe(v.string(), v.minLength(1), v.maxLength(32768)),
|
|
16
|
+
}),
|
|
17
|
+
input,
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
return { kind: "reply" as const, ...args };
|
|
21
|
+
}
|
|
22
|
+
if (name === "send_message") {
|
|
23
|
+
const args = v.parse(
|
|
24
|
+
v.object({
|
|
25
|
+
id: v.string(),
|
|
26
|
+
outcome: v.picklist(MESSAGE_OUTCOMES),
|
|
27
|
+
summary: v.optional(v.string(), ""),
|
|
28
|
+
}),
|
|
29
|
+
input,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
return { kind: "api" as const, method: "session.sendMessage", params: args };
|
|
33
|
+
}
|
|
34
|
+
if (name !== "cueloop_api") throw new Error("Thread agent tool name is unavailable");
|
|
35
|
+
const args = v.parse(v.object({ method: v.string(), params: v.unknown() }), input);
|
|
36
|
+
|
|
37
|
+
return { kind: "api" as const, ...args };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const THREAD_AGENT_SESSION_METHODS = new Set<string>([
|
|
41
|
+
"session.get",
|
|
42
|
+
"session.wait",
|
|
43
|
+
"session.annotate",
|
|
44
|
+
"session.comment",
|
|
45
|
+
"session.removeAnnotation",
|
|
46
|
+
"session.setParticipantName",
|
|
47
|
+
"session.setWorkingCopy",
|
|
48
|
+
"session.cutBlock",
|
|
49
|
+
"session.navigate",
|
|
50
|
+
"session.branch",
|
|
51
|
+
"session.switch",
|
|
52
|
+
"session.label",
|
|
53
|
+
"session.restoreBlock",
|
|
54
|
+
"session.curate",
|
|
55
|
+
"session.setViewed",
|
|
56
|
+
"session.setTitle",
|
|
57
|
+
"session.projectFiles",
|
|
58
|
+
"session.fileContents",
|
|
59
|
+
"session.refreshDiff",
|
|
60
|
+
"session.sendMessage",
|
|
61
|
+
"session.submitRevision",
|
|
62
|
+
] satisfies MethodName[]);
|
|
63
|
+
const THREAD_AGENT_REPO_METHODS = new Set<string>([
|
|
64
|
+
"repo.files",
|
|
65
|
+
"repo.fileContents",
|
|
66
|
+
"repo.changes",
|
|
67
|
+
"repo.diff",
|
|
68
|
+
] satisfies MethodName[]);
|
|
69
|
+
|
|
70
|
+
/** Harness tools are bound to their originating Thread; model arguments cannot widen that scope. */
|
|
71
|
+
export function assertAgentToolScope(
|
|
72
|
+
thread: Thread,
|
|
73
|
+
input: ReturnType<typeof parseAgentToolInput>,
|
|
74
|
+
): void {
|
|
75
|
+
if (input.kind === "reply") {
|
|
76
|
+
if (input.id !== thread.id) throw new Error("Thread agent tool cannot access another Thread");
|
|
77
|
+
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (input.method === "session.list") return;
|
|
81
|
+
const params = v.parse(v.record(v.string(), v.unknown()), input.params);
|
|
82
|
+
|
|
83
|
+
if (THREAD_AGENT_SESSION_METHODS.has(input.method)) {
|
|
84
|
+
if (params.id !== thread.id) throw new Error("Thread agent tool cannot access another Thread");
|
|
85
|
+
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (THREAD_AGENT_REPO_METHODS.has(input.method)) {
|
|
89
|
+
const { cwd } = v.parse(v.object({ cwd: v.string() }), params);
|
|
90
|
+
|
|
91
|
+
if (resolve(cwd) !== resolve(thread.workspace.repoRoot))
|
|
92
|
+
throw new Error("Thread agent tool cannot access another repository");
|
|
93
|
+
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
throw new Error("Thread agent tool API method is unavailable");
|
|
98
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { stepAgentTurn, type AgentTurn } from "./agent-turn";
|
|
3
|
+
|
|
4
|
+
test("an initialization stop settles before accepting a fresh turn", () => {
|
|
5
|
+
const starting = stepAgentTurn({ kind: "idle" }, "start");
|
|
6
|
+
const stopped = stepAgentTurn(starting, "stop");
|
|
7
|
+
const ready = stepAgentTurn(stopped, "ready");
|
|
8
|
+
|
|
9
|
+
expect(ready).toEqual({ kind: "idle" });
|
|
10
|
+
expect(stepAgentTurn(ready, "start")).toEqual({ kind: "starting", cancelled: false });
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("an active stop waits for completion before accepting another turn", () => {
|
|
14
|
+
const running: AgentTurn = { kind: "running" };
|
|
15
|
+
const stopped = stepAgentTurn(running, "stop");
|
|
16
|
+
|
|
17
|
+
expect(stopped).toEqual({ kind: "stopping" });
|
|
18
|
+
expect(stepAgentTurn(stopped, "start")).toEqual(stopped);
|
|
19
|
+
expect(stepAgentTurn(stopped, "ready")).toEqual(stopped);
|
|
20
|
+
expect(stepAgentTurn(stepAgentTurn(stopped, "finished"), "start")).toEqual({
|
|
21
|
+
kind: "starting",
|
|
22
|
+
cancelled: false,
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** A harness turn distinguishes initialization cancellation from a running stop request. */
|
|
2
|
+
export type AgentTurn =
|
|
3
|
+
| { kind: "idle" }
|
|
4
|
+
| { kind: "starting"; cancelled: boolean }
|
|
5
|
+
| { kind: "running" }
|
|
6
|
+
| { kind: "stopping" };
|
|
7
|
+
|
|
8
|
+
/** Lifecycle events are independent of the harness wire protocol. */
|
|
9
|
+
export type AgentTurnEvent = "start" | "ready" | "stop" | "finished";
|
|
10
|
+
|
|
11
|
+
/** Start is accepted only at idle; readiness consumes an initialization cancellation. */
|
|
12
|
+
export function stepAgentTurn(state: AgentTurn, event: AgentTurnEvent): AgentTurn {
|
|
13
|
+
switch (event) {
|
|
14
|
+
case "start":
|
|
15
|
+
return state.kind === "idle" ? { kind: "starting", cancelled: false } : state;
|
|
16
|
+
case "ready":
|
|
17
|
+
return state.kind === "starting"
|
|
18
|
+
? state.cancelled
|
|
19
|
+
? { kind: "idle" }
|
|
20
|
+
: { kind: "running" }
|
|
21
|
+
: state;
|
|
22
|
+
case "stop":
|
|
23
|
+
if (state.kind === "starting") return { kind: "starting", cancelled: true };
|
|
24
|
+
|
|
25
|
+
return state.kind === "running" ? { kind: "stopping" } : state;
|
|
26
|
+
case "finished":
|
|
27
|
+
return { kind: "idle" };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Pending stop requests suppress later tool and permission events from this turn. */
|
|
32
|
+
export function agentTurnCancelled(state: AgentTurn): boolean {
|
|
33
|
+
return state.kind === "stopping" || (state.kind === "starting" && state.cancelled);
|
|
34
|
+
}
|