@alook/daemon 0.1.15 → 0.1.16

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.
Files changed (4) hide show
  1. package/README.md +119 -130
  2. package/dist/cli/index.js +7774 -3743
  3. package/dist/index.js +6812 -2950
  4. package/package.json +3 -1
package/README.md CHANGED
@@ -1,22 +1,17 @@
1
1
  # @alook/daemon
2
2
 
3
- A **pure, host-neutral agent-runtime "driver" abstraction** in TypeScript.
4
-
5
- It documents — as compilable code — how a host adapts many different AI coding
6
- runtimes (Claude Code, Codex, Gemini, Kimi, Pi, Copilot, Cursor, OpenCode,
7
- Antigravity) behind one uniform interface, and how it delivers messages into a
8
- running agent (the "steering" mechanism).
9
-
10
- The backend hardcodes **no specific host platform**. The agent always invokes a
11
- stable CLI name (`alook`); `cliTransport.ts` writes a small wrapper by that
12
- name into a per-launch state dir, prepends it to `PATH`, and forwards to the
13
- host's real `targetCommand` **decoupling the agent-facing name from the host's
14
- real binary** (the host can rename/relocate its CLI without touching the agent
15
- surface). It also injects an `ALOOK_*` env contract. The default
16
- `MOCK_CLI_CONFIG` wires no `targetCommand` (the wrapper errors if invoked); a
17
- real deployment passes its own `CliTransportConfig` (CLI name, env prefix,
18
- `targetCommand`) and its own system-prompt communication guide. The abstraction
19
- is what's reusable — plug in whatever host you like.
3
+ Alook's host daemon: control-plane connectivity, agent scheduling, credentials,
4
+ diagnostics, and lifecycle orchestration.
5
+
6
+ Runtime execution lives in the independently buildable `@alook/agent-driver`
7
+ package under `agent-driver/`. That package adapts Claude Code, Codex, Cursor,
8
+ OpenCode, and Pi behind one public `AgentSession` contract. The daemon consumes
9
+ that contract and does not speak vendor process or SDK protocols.
10
+
11
+ The driver package remains host-neutral. Its CLI transport creates a per-launch
12
+ state directory, exposes a stable `alook` command through `PATH`, and composes
13
+ explicit environment layers supplied by the host. Vendor adapters never import
14
+ daemon credentials or control-plane code.
20
15
 
21
16
  > **Provenance.** The shapes, protocols, flag strings, and control flow here were
22
17
  > derived by studying how production agent-runtime daemons drive these CLIs, then
@@ -61,67 +56,60 @@ into logs, bug reports, or chat.
61
56
 
62
57
  ## The big idea
63
58
 
64
- The daemon never speaks a runtime's native protocol directly. Each runtime is
65
- wrapped by a **`Driver`** (`src/types.ts`) that knows how to:
66
-
67
- 1. **`spawn`** (or `createSession`) — launch the runtime,
68
- 2. **`encodeStdinMessage`** — encode an outgoing user message for the runtime's
69
- input channel,
70
- 3. **`parseLine`** — normalize the runtime's output into a tiny shared event
71
- vocabulary (`ParsedEvent`: `session_init`, `thinking`, `text`, `tool_call`,
72
- `tool_output`, `compaction_*`, `turn_end`, `error`, `telemetry`).
73
-
74
- A generic **session host** then runs the process and fans `ParsedEvent`s out to
75
- the daemon:
59
+ The daemon creates an `AgentDriverSdk`, probes or opens a selected backend, and
60
+ then works only with an `AgentSession`:
76
61
 
77
- - `ChildProcessRuntimeSession` (`src/runtime/runtimeSession.ts`) for CLI runtimes.
78
- - `SdkRuntimeSession` (`src/runtime/sdkRuntimeSession.ts`) for in-process SDK
79
- runtimes (pi).
62
+ - `start(message)` admits the first command.
63
+ - `send(message)` accepts, queues, or rejects a later command with a typed receipt.
64
+ - `interrupt(...)` and `stop(...)` provide bounded lifecycle control.
65
+ - `snapshot()` exposes logical state without leaking process/SDK objects.
66
+ - `events` is an async stream of normalized `AgentEvent` values.
67
+ - `closed` settles exactly once with terminal and host-cleanup facts.
80
68
 
81
- Because everything downstream consumes the same `ParsedEvent` stream and the
82
- same `Driver` capability flags, the rest of the daemon is transport-agnostic.
69
+ Inside `@alook/agent-driver`, a backend adapter declares its execution model and
70
+ normalizes vendor output. `LogicalAgentSession` owns admission, FIFO queueing,
71
+ safe-boundary delivery, terminal ordering, and cleanup. Internal `ProcessLane`
72
+ and `SdkLane` hosts make child-process and in-process SDK transports look alike;
73
+ they are deliberately not public daemon primitives.
83
74
 
84
75
  ---
85
76
 
86
- ## Two delivery models (the heart of it)
77
+ ## Persistent delivery models (the heart of it)
87
78
 
88
79
  When a message arrives, what happens depends on the runtime's lifecycle:
89
80
 
90
- ### Persistent runtimes (claude, codex, kimi, pi)
91
- One long-lived process spans many turns. A new message is **written onto the
92
- still-open input channel** no restart. This is "steering."
93
-
94
- The busy-delivery mode is **derived from `lifecycle`** (`busyDeliveryModeOf` /
95
- `supportsStdinNotificationOf` in `types.ts`) — `lifecycle.stdin` is the single
96
- source of truth, so a driver's mode can't drift from it (it used to: a driver
97
- could declare `lifecycle.stdin: "gated"` yet `busyDeliveryMode: "direct"`).
98
- `persistent+direct → direct`, `persistent+gated → gated`, `per_turn → none`.
99
-
100
- - **`direct`** (kimi, pi): write immediately; the runtime tolerates injection
101
- any time.
102
- - **`gated`** (claude, codex): a raw write mid-stream could collide with an active
103
- signed thinking block, so writes are **held until a safe boundary**,
104
- implemented by:
105
- - `apmStateMachine` (`src/runtime/apmStateMachine.ts`) — the policy reducers
106
- that decide *when* to flush queued inbox notices and emit the concrete
107
- `notify_stdin` / `deliver_stdin` effects (clause `SMR-002`). Flushing is
108
- blocked while `outstanding_tool_uses > 0`, while `compacting`, while
109
- `reviewing` (Codex review mode), or when `tool_boundary_flush_disabled`.
110
- Compaction *and* review exit are treated as safe flush boundaries.
81
+ ### Safe-boundary runtimes (Claude and Codex)
82
+
83
+ One child process spans many turns. Both adapters declare `lifetime: "session"`;
84
+ their public capability is `midTurnDelivery: "safe_boundary_queue"`. A message arriving during tool use,
85
+ compaction, or review is queued by `LogicalAgentSession` and receives a later
86
+ `command_accepted` or `command_failed` event. The daemon does not implement that
87
+ protocol or queue.
111
88
 
112
89
  For Claude specifically, the input channel is **stream-json**: the process is
113
90
  launched with `--input-format stream-json --output-format stream-json
114
91
  --include-partial-messages`, and each message is one NDJSON line
115
92
  `{"type":"user","message":{"role":"user","content":[{"type":"text","text":…}]}}`.
116
93
 
117
- ### Per-turn runtimes (gemini, copilot, cursor, opencode, antigravity)
118
- The process handles exactly one turn and exits. `supportsStdinNotification` is
119
- `false` and `encodeStdinMessage` returns `null`. A new message means a **brand-new
120
- process**; the agent re-checks the inbox each wake. This lifecycle
121
- (`lifecycle.kind: "per_turn"`) drives the `## Message Notifications` section
122
- generated by `buildCliSystemPrompt` — no driver hand-writes this reminder.
123
- `opencode` is a special per-turn case: it defers spawning until a concrete
124
- message and terminates the process on turn end.
94
+ ### In-process SDK runtime (Pi)
95
+
96
+ Pi declares the `in_process_sdk` transport and
97
+ `midTurnDelivery: "steer"`. Its lane delegates prompt, steer, abort, and dispose
98
+ to the SDK while preserving the same receipts, events, and terminal contract.
99
+
100
+ ### Persistent queued runtime (Cursor)
101
+
102
+ Cursor keeps one `cursor-agent acp` process and ACP session for the logical
103
+ session. Each idle command is a `session/prompt` request; while one is active,
104
+ later commands remain in the logical next-turn FIFO until its correlated
105
+ response arrives. Interrupt sends `session/cancel` without killing the process.
106
+
107
+ ### Persistent service runtime (OpenCode)
108
+
109
+ OpenCode starts one authenticated, loopback-only v2 service per logical session.
110
+ Root prompts and busy steers share the same service and vendor session. Durable
111
+ session SSE is replayed by event id and sequence across reconnects; a separate
112
+ live stream handles permissions.
125
113
 
126
114
  ---
127
115
 
@@ -129,67 +117,48 @@ message and terminates the process on turn end.
129
117
 
130
118
  | Runtime | Lifecycle | Transport / protocol | Steering | Initial input | Output format |
131
119
  |---|---|---|---|---|---|
132
- | **claude** | persistent | child process, stream-json NDJSON | `gated` | `{type:"user",…}` line on stdin | stream-json |
133
- | **codex** | persistent | child process, JSON-RPC 2.0 (`app-server --listen stdio://`) | `gated` | `initialize` → `thread/start`/`resume` | JSON-RPC notifications |
134
- | **kimi** | persistent | child process, JSON-RPC "wire" | `direct` (`steer`) | `initialize` → `prompt` | JSON-RPC events |
135
- | **pi** | persistent | in-process SDK (`@earendil-works/pi-coding-agent`), multi-provider | `direct` | `session.prompt()` | SDK event callback |
136
- | **gemini** | per-turn | child process, stream-json | none | prompt on stdin, then close | stream-json |
137
- | **copilot** | per-turn | child process, JSON | none | prompt as `-p` arg | JSON events |
138
- | **cursor** | per-turn | child process, stream-json | none | prompt as trailing arg | stream-json |
139
- | **opencode** | per-turn (defer-spawn, terminate-on-end) | child process, JSON | none | prompt as `-- <arg>` | JSON events |
140
- | **antigravity** | per-turn | child process, **plain text** | none | prompt on stdin, then close | plain text lines |
141
-
142
- (Each driver's exact launch flags live in its file under `src/drivers/`.)
120
+ | **claude** | persistent session | stream-json NDJSON | `safe_boundary_queue` | stdin user-message line | stream-json |
121
+ | **codex** | persistent session | JSON-RPC 2.0 (`app-server --listen stdio://`) | `safe_boundary_queue` | `initialize` → `thread/start`/`resume` | JSON-RPC notifications |
122
+ | **pi** | persistent session | `@earendil-works/pi-coding-agent` | `steer` | `session.prompt()` | SDK callback |
123
+ | **cursor** | persistent session | ACP JSON-RPC 2.0 (`cursor-agent acp`) | `next_turn_queue` | `session/prompt` | `session/update` + correlated prompt response |
124
+ | **opencode** | persistent session | authenticated HTTP + SSE (`opencode serve --pure`) | `steer` | v2 session prompt API | durable + live SSE |
125
+
126
+ (Exact launch flags live in `agent-driver/src/adapters/<backend>/`.)
143
127
 
144
128
  ---
145
129
 
146
130
  ## Layout
147
131
 
148
132
  ```
133
+ agent-driver/
134
+ src/
135
+ contract.ts # public SDK, session, receipt, event, and result types
136
+ sdk.ts # createAgentDriverSdk
137
+ registry.ts # built-in ids and capabilities
138
+ controller/
139
+ logical-session.ts # admission, queueing, turns, stop, cleanup
140
+ process-host.ts # internal child-process lane
141
+ sdk-host.ts # internal in-process SDK lane
142
+ adapters/<backend>/ # vendor launch + normalization
143
+ host/default-host.ts # standalone host implementation
144
+ internal/ # adapter-only transport/config/process helpers
145
+ testing/ # public conformance fixtures
149
146
  src/
150
- types.ts # Driver interface + ParsedEvent + lifecycle/capability types
151
- index.ts # public entry point
152
- logger.ts # tiny structured logger (ALOOK_LOG_LEVEL); wired through
153
- # wsControlChannel.ts/agentRouter.ts/managerRuntime.ts too, not just cli/daemonStart.ts
154
- drivers/
155
- index.ts # getDriver(runtimeId) registry ← start here
156
- cliTransport.ts # shared: state dir, token, decoupling cliName wrapper -> targetCommand, env
157
- systemPrompt.ts # shared: standing-prompt assembly
158
- probe.ts # CLI/binary detection + version
159
- claude.ts # Claude Code driver
160
- claudeLaunch.ts # args / command / spawn-spec / prompt-file
161
- claudeProviderIsolation.ts # custom-provider HOME/config isolation
162
- claudeEventNormalizer.ts # stream-json → ParsedEvent
163
- codex.ts / codexEventNormalizer.ts / codexTelemetrySidecar.ts
164
- gemini.ts copilot.ts cursor.ts opencode.ts antigravity.ts
165
- kimi.ts # child-process Kimi (JSON-RPC wire)
166
- pi.ts # in-process Pi SDK (multi-provider)
167
- runtime/
168
- runtimeSession.ts # ChildProcessRuntimeSession + descriptor
169
- sdkRuntimeSession.ts # SdkRuntimeSession (in-process)
170
- apmStateMachine.ts # gated-delivery policy reducers (SMR-002)
171
- progressState.ts # liveness / stall detection
172
- notificationState.ts # inbox-notice de-dup + batching
173
- errorDiagnostics.ts # runtime-error classification + scrubbing
174
- inbox/
175
- projection.ts # bucket pending messages by target → notice snapshots
176
- stateMachine.ts # pre-action freshness guard (forward / hold / bypass)
147
+ index.ts # daemon exports; re-exports the driver public API
148
+ drivers/index.ts # daemon runtime lookup/probe facade only
149
+ logger.ts # structured daemon logging
150
+ runtime/ # manager liveness, notification, error helpers
151
+ inbox/ # unread projection and freshness policy
177
152
  manager/
178
- managerPolicy.ts # pure reducer: single-flight, wake/sleep, queue+coalesce, stalled
179
- managerRuntime.ts # AgentProcessManager: applies effects to real sessions
180
- server/
181
- contract.ts # ServerApi (data) + EnrollmentApi (machine→runner key) + HostControlChannel (control) + AdminApi
182
- wsControlServer.ts # server end of the control plane over ws (machine-key authed)
183
- wsControlChannel.ts # host end: WebSocket HostControlChannel (injectable socket, reconnect + heartbeat + resync)
184
- daemon/
185
- createDaemon.ts # runtime-agnostic daemon factory (driver/sessionFactory/runtimes INJECTED; no test code)
186
- cli/
187
- index.ts # the `alook` agent CLI skeleton (message send / inbox pull)
188
- proxyServerApi.ts # agent data-plane client: voucher → proxy → server (identity from the voucher)
189
- credentials/
190
- credentialProxy.ts # CredentialBroker (mint/revoke/check per-voucher vouchers) + local key-swapping proxy
153
+ agentDriverHost.ts # host resource/env/CLI preparation
154
+ managerPolicy.ts # pure scheduling reducer
155
+ managerRuntime.ts # applies effects through AgentSession
156
+ server/ # data/control-plane contracts and WebSockets
157
+ daemon/createDaemon.ts # runtime-agnostic daemon composition
158
+ cli/ # lifecycle CLI and proxy data-plane client
159
+ credentials/ # voucher broker and key-swapping proxy
191
160
  scripts/
192
- daemon.ts # local entry: delegates to the canonical `alook daemon` parser
161
+ daemon.ts # local daemon entry
193
162
  ```
194
163
 
195
164
  ## Host orchestration (manager + server)
@@ -361,22 +330,42 @@ posting a message in a channel it's a member of — the real wake-producer path
361
330
  (`src/web` → `src/wake-worker` → `src/ws-do`) delivers `agent:wake` over the
362
331
  daemon's real `WsControlChannel`.
363
332
 
364
- > **Run the source, not `dist/` (yet).** `pnpm run build` emits JS, but the
365
- > current Bundler-resolution emit produces extensionless relative imports that
366
- > Node's native ESM rejects (`Cannot find module './types'`). Until that's
367
- > switched to NodeNext or bundled, consume the library through a TS toolchain
368
- > (tsx / ts-node / vite / esbuild / webpack).
333
+ `pnpm --filter @alook/daemon build` emits a self-contained daemon bundle. The
334
+ published tarball includes the runtime-driver implementation, while
335
+ `@alook/agent-driver` is also independently buildable and publishable for hosts
336
+ that want only the logical-session SDK.
369
337
 
370
338
  ## Usage sketch
371
339
 
372
340
  ```ts
373
- import { getDriver, createChildProcessRuntimeSession } from "@alook/daemon";
374
-
375
- const driver = getDriver("claude");
376
- const session = createChildProcessRuntimeSession(driver, ctx);
377
- session.on("runtime_event", (e) => handleParsedEvent(e));
378
- await session.start({ text: initialPrompt });
379
-
380
- // Later, a message arrives while the agent is working:
381
- session.send({ text: "new message from #general", mode: "busy" }); // steer
341
+ import { createAgentDriverSdk } from "@alook/agent-driver";
342
+
343
+ const sdk = createAgentDriverSdk();
344
+ const opened = await sdk.open({
345
+ backend: "codex",
346
+ launch: {
347
+ workingDirectory: ".",
348
+ instructions: { format: "markdown", content: "Be concise." },
349
+ launchId: "launch-example",
350
+ },
351
+ config: {
352
+ model: { kind: "default" },
353
+ mode: "default",
354
+ },
355
+ });
356
+ if (!opened.ok) throw new Error(opened.error.message);
357
+
358
+ const { session } = opened;
359
+ const eventsDone = (async () => {
360
+ for await (const event of session.events) {
361
+ if (event.type === "text_delta") process.stdout.write(event.text);
362
+ }
363
+ })();
364
+
365
+ await session.start({ id: "command-1", kind: "user", text: "Inspect this repository." });
366
+ await session.send({ id: "command-2", kind: "user", text: "Now summarize the risks." });
367
+ await session.stop({ reason: "owner_request", forceAfterMs: 5_000 });
368
+ const result = await session.closed;
369
+ await eventsDone;
370
+ void result;
382
371
  ```