@controlflow-ai/daemon 0.1.0
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 +360 -0
- package/bin/console.js +2 -0
- package/bin/daemon.js +2 -0
- package/bin/pal.js +2 -0
- package/bin/server.js +2 -0
- package/package.json +31 -0
- package/src/agent-runtime.ts +285 -0
- package/src/app.ts +745 -0
- package/src/args.ts +54 -0
- package/src/artifacts.ts +85 -0
- package/src/cli.ts +284 -0
- package/src/client.ts +310 -0
- package/src/coco.ts +52 -0
- package/src/codex.ts +41 -0
- package/src/coding-agent-runtime.ts +20 -0
- package/src/config.ts +106 -0
- package/src/console.ts +349 -0
- package/src/daemon-client.ts +91 -0
- package/src/daemon.ts +580 -0
- package/src/db.ts +2830 -0
- package/src/failure-message.ts +17 -0
- package/src/format.ts +13 -0
- package/src/http.ts +55 -0
- package/src/lark/agent-runtime.ts +142 -0
- package/src/lark/cli.ts +549 -0
- package/src/lark/credentials.ts +105 -0
- package/src/lark/daemon-integration.ts +108 -0
- package/src/lark/dispatcher.ts +374 -0
- package/src/lark/event-router.ts +329 -0
- package/src/lark/inbound-events.ts +131 -0
- package/src/lark/server-integration.ts +445 -0
- package/src/lark/setup.ts +326 -0
- package/src/lark/ws-daemon.ts +224 -0
- package/src/lark-fixture-diagnostics.ts +56 -0
- package/src/lark-fixture.ts +277 -0
- package/src/local-api.ts +155 -0
- package/src/local-auth.ts +45 -0
- package/src/migrations/001_initial.ts +61 -0
- package/src/migrations/002_daemon_deliveries.ts +52 -0
- package/src/migrations/003_sessions_runs.ts +49 -0
- package/src/migrations/004_message_idempotency.ts +21 -0
- package/src/migrations/005_artifacts.ts +24 -0
- package/src/migrations/006_lark_channel_foundation.ts +119 -0
- package/src/migrations/007_agents_a0.ts +17 -0
- package/src/migrations/008_b0_chat_history.ts +31 -0
- package/src/migrations/009_b0_transcript_ingest_seq.ts +35 -0
- package/src/migrations/010_b0_transcript_shadow_external_ids.ts +32 -0
- package/src/migrations/011_b0_channel_conversation_audit_only.ts +27 -0
- package/src/migrations/012_b0_cross_conversation_invariant.ts +45 -0
- package/src/migrations/013_b1_0_eng_inbound_raw_events.ts +56 -0
- package/src/migrations/014_agents_runtime.ts +10 -0
- package/src/migrations/015_agent_runtime_sessions.ts +15 -0
- package/src/migrations/016_room_participants.ts +27 -0
- package/src/migrations/017_unified_room_delivery.ts +203 -0
- package/src/migrations/018_room_display_names.ts +36 -0
- package/src/migrations/019_computer_connections.ts +63 -0
- package/src/migrations/020_computer_agent_assignments.ts +20 -0
- package/src/migrations/021_provider_identity_bindings.ts +32 -0
- package/src/migrations.ts +85 -0
- package/src/neeko.ts +23 -0
- package/src/provider-identity.ts +40 -0
- package/src/runtime-registry.ts +41 -0
- package/src/server-auth.ts +13 -0
- package/src/server.ts +63 -0
- package/src/token-file.ts +57 -0
- package/src/types.ts +408 -0
- package/src/web.ts +565 -0
package/README.md
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
# pal
|
|
2
|
+
|
|
3
|
+
Small Bun demo for the Lock idea: a local message server, a daemon that connects a computer, and CLI surfaces for humans, agents, and Lark bots.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
| Component | Script | Role | Default endpoint |
|
|
8
|
+
|-----------|--------|------|------------------|
|
|
9
|
+
| **Server** | `bun run server` | Message platform, control API, web workbench, Lark websocket host | `http://127.0.0.1:4127` |
|
|
10
|
+
| **Daemon** | `bun run daemon` | Connects one computer, claims deliveries for assigned agents, runs coding-agent CLIs | local API `http://127.0.0.1:4137` |
|
|
11
|
+
| **Console** | `bun run console -- ...` | Admin CLI that talks directly to the Server | Server API |
|
|
12
|
+
| **CLI** | `bun run cli -- ...` | Agent/human CLI that talks through the local Daemon | Daemon local API |
|
|
13
|
+
|
|
14
|
+
The current preferred model is computer-first: provision a computer, start one daemon with the provisioned API key, then assign agents to that computer. The daemon can run with zero agents and periodically reconciles assignments from the server, so newly assigned agents start without a daemon restart and removed agents stop being registered for new work.
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
### 1. Install Dependencies
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
bun install
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
For disposable local smoke tests, keep state isolated:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
export PAL_HOME=/tmp/pal-demo
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### 2. Start Server
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
bun run server
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Server listens on `http://127.0.0.1:4127/` by default. Change the bind address with `PAL_HOST` and the port with `PAL_PORT`:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
PAL_HOST=0.0.0.0 PAL_PORT=4127 bun run server
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
When clients or daemons connect through a non-default address, pass `--server <url>` or set `PAL_SERVER` / `LOCK_SERVER_URL` to the reachable URL.
|
|
43
|
+
|
|
44
|
+
### 3. Provision a Computer
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
curl -s -X POST http://127.0.0.1:4127/api/computers/provision \
|
|
48
|
+
-H 'content-type: application/json' \
|
|
49
|
+
-d '{"name":"Local Demo","server_url":"http://127.0.0.1:4127"}'
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The response includes a `computer.id`, an `api_key`, and a packaged daemon command. For a source checkout, start the local Bun daemon with the returned key:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
bun run daemon -- --server http://127.0.0.1:4127 --api-key sk_machine_...
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
If no agents are assigned yet, the daemon stays connected, heartbeats the computer, and waits for assignments.
|
|
59
|
+
|
|
60
|
+
### 4. Create and Assign an Agent
|
|
61
|
+
|
|
62
|
+
Use `agents onboard` to create/update an agent and optionally assign it to the provisioned computer:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
bun run console -- agents onboard \
|
|
66
|
+
--key codex \
|
|
67
|
+
--name "Codex Agent" \
|
|
68
|
+
--runtime codex \
|
|
69
|
+
--computer-id machine_...
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
If the daemon is already running with the same API key, it will discover the `codex` assignment on a later heartbeat and begin claiming deliveries for it. Restarting is not required.
|
|
73
|
+
|
|
74
|
+
You can also do the two steps separately:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
bun run console -- agents create --key codex --name "Codex Agent" --runtime codex --desc "Main coding agent"
|
|
78
|
+
curl -s -X POST http://127.0.0.1:4127/api/agents/codex/assignment \
|
|
79
|
+
-H 'content-type: application/json' \
|
|
80
|
+
-d '{"computer_id":"machine_..."}'
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 5. Send a Message
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# Through the local daemon
|
|
87
|
+
bun run cli -- send --room general --from alice --to codex "hello"
|
|
88
|
+
|
|
89
|
+
# Directly to the server
|
|
90
|
+
bun run console -- send --chat general --from alice --to codex "hello"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Console Commands
|
|
94
|
+
|
|
95
|
+
Console connects to the server directly and is intended for administration.
|
|
96
|
+
|
|
97
|
+
### Agents
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
# List agents
|
|
101
|
+
bun run console -- agents list [--json]
|
|
102
|
+
|
|
103
|
+
# Create or update an agent record
|
|
104
|
+
bun run console -- agents create --key <agent-key> --name <display-name> [--runtime neeko|coco|codex] [--desc <description>]
|
|
105
|
+
|
|
106
|
+
# Create/update an agent and optionally assign it to a computer
|
|
107
|
+
bun run console -- agents onboard --key <agent-key> --name <display-name> [--runtime codex] [--desc <description>] [--computer-id <machine-id>]
|
|
108
|
+
|
|
109
|
+
# Update only the runtime
|
|
110
|
+
bun run console -- agents update --key <agent-key> --runtime neeko|coco|codex
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`codex` is the validated runtime for the current demo environment. Do not use `neeko` or `coco` until their binaries and adapters are available on the runtime host.
|
|
114
|
+
|
|
115
|
+
### Lark Bots
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
# Configure a bot, resolve its bot open_id, bind it to an agent, and ask the running server to reload Lark
|
|
119
|
+
bun run console -- lark setup --app-id <app-id> --app-secret <app-secret> --label <name> --agent <agent-key>
|
|
120
|
+
|
|
121
|
+
# Configure a bot and create/update the bound agent in one command
|
|
122
|
+
bun run console -- lark setup \
|
|
123
|
+
--app-id <app-id> \
|
|
124
|
+
--app-secret <app-secret> \
|
|
125
|
+
--label <name> \
|
|
126
|
+
--agent codex \
|
|
127
|
+
--create-agent \
|
|
128
|
+
--agent-name "Codex" \
|
|
129
|
+
--runtime codex \
|
|
130
|
+
--computer-id <machine-id>
|
|
131
|
+
|
|
132
|
+
# List configured bots; secrets are redacted
|
|
133
|
+
bun run console -- lark list
|
|
134
|
+
|
|
135
|
+
# View recent raw Lark events
|
|
136
|
+
bun run console -- lark events --limit 20
|
|
137
|
+
|
|
138
|
+
# Send a message through a configured bot
|
|
139
|
+
bun run console -- lark send --app-id <app-id> --to <receive_id> [--to-type chat_id|open_id|union_id|email|user_id] "Hello"
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`lark setup` writes `~/.pal/lark.json` or `PAL_LARK_CONFIG` with mode `0600`. By default it calls `POST /api/lark/reload` on the running server after a successful write. Use `--no-reload` when you want to edit or validate the file first, then trigger reload manually:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
curl -s -X POST http://127.0.0.1:4127/api/lark/reload
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Reload scans the config explicitly. If the file is invalid, the server keeps existing Lark websocket listeners running and returns an error instead of dropping them.
|
|
149
|
+
|
|
150
|
+
The older `bun run console -- lark daemon ...` command still exists for standalone Lark testing, but normal server operation now hosts Lark websocket clients inside `bun run server`.
|
|
151
|
+
|
|
152
|
+
### Messages and Runs
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
# Health check
|
|
156
|
+
bun run console -- health
|
|
157
|
+
|
|
158
|
+
# List rooms/chats
|
|
159
|
+
bun run console -- chats [--json]
|
|
160
|
+
|
|
161
|
+
# View messages
|
|
162
|
+
bun run console -- messages [--chat general] [--parent 1] [--after 0] [--limit 50] [--q text] [--json]
|
|
163
|
+
|
|
164
|
+
# View an agent inbox
|
|
165
|
+
bun run console -- inbox --agent codex [--after 0] [--limit 50] [--json]
|
|
166
|
+
|
|
167
|
+
# List runs
|
|
168
|
+
bun run console -- runs [--json]
|
|
169
|
+
|
|
170
|
+
# Control a run
|
|
171
|
+
bun run console -- run-action <run-id> kill
|
|
172
|
+
bun run console -- run-action <run-id> restart
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## CLI Commands
|
|
176
|
+
|
|
177
|
+
CLI connects through the local daemon. It is the surface used by coding agents and local operators once the daemon is running.
|
|
178
|
+
|
|
179
|
+
### Rooms, Topics, and Messages
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
# Send a message
|
|
183
|
+
bun run cli -- send --room general --from alice [--to codex] "Task completed"
|
|
184
|
+
|
|
185
|
+
# Reply to an existing message
|
|
186
|
+
bun run cli -- send --room general --from codex --parent 1 "Reply content"
|
|
187
|
+
|
|
188
|
+
# Send file content as a message
|
|
189
|
+
bun run cli -- send --room general --from codex --file /path/to/message.txt
|
|
190
|
+
|
|
191
|
+
# List rooms and members
|
|
192
|
+
bun run cli -- rooms list [--json]
|
|
193
|
+
bun run cli -- rooms members --room general [--json]
|
|
194
|
+
|
|
195
|
+
# Invite an agent to a room with a delivery mode
|
|
196
|
+
bun run cli -- rooms invite --room general --agent codex [--mode mentions|all|periodic|muted|off]
|
|
197
|
+
|
|
198
|
+
# Create a topic inside a room
|
|
199
|
+
bun run cli -- topics create --room general "Investigation"
|
|
200
|
+
|
|
201
|
+
# List or show messages
|
|
202
|
+
bun run cli -- messages list [--room general] [--topic 1] [--after 0] [--limit 50] [--q text] [--json]
|
|
203
|
+
bun run cli -- messages show <message-id> [--json]
|
|
204
|
+
|
|
205
|
+
# Start a simple agent-to-agent debate workflow
|
|
206
|
+
bun run cli -- debate start --chat general --a codex --b reviewer [--turns 6] "Topic"
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Artifacts
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
# Upload an HTML file through the daemon to the server
|
|
213
|
+
bun run cli -- http file /path/to/report.html --mime text/html --title "Report"
|
|
214
|
+
|
|
215
|
+
# Upload with a TTL in seconds
|
|
216
|
+
bun run cli -- http file /path/to/data.json --mime application/json --ttl 3600
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Query and Control
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
bun run cli -- health
|
|
223
|
+
bun run cli -- chats [--json]
|
|
224
|
+
bun run cli -- runs [--json]
|
|
225
|
+
bun run cli -- run-action <run-id> kill
|
|
226
|
+
bun run cli -- run-action <run-id> restart
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Daemon Commands
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
# Preferred computer-first startup
|
|
233
|
+
bun run daemon -- --server http://127.0.0.1:4127 --api-key sk_machine_...
|
|
234
|
+
|
|
235
|
+
# Equivalent env-var startup
|
|
236
|
+
PAL_API_KEY=sk_machine_... bun run daemon -- --server http://127.0.0.1:4127
|
|
237
|
+
|
|
238
|
+
# Custom polling interval and daemon local API port
|
|
239
|
+
bun run daemon -- --server http://127.0.0.1:4127 --api-key sk_machine_... --interval 3000 --local-port 4137
|
|
240
|
+
|
|
241
|
+
# Dry run: claim and record runs without executing the agent runtime
|
|
242
|
+
bun run daemon -- --server http://127.0.0.1:4127 --api-key sk_machine_... --dry-run
|
|
243
|
+
|
|
244
|
+
# Legacy explicit-agent startup using a computer id/secret instead of an API key
|
|
245
|
+
bun run daemon -- --agent codex --computer-id hm-media-demo --computer-secret pal-demo-computer-secret --cwd /path/to/workspace
|
|
246
|
+
|
|
247
|
+
# One-shot processing for tests or scripts
|
|
248
|
+
bun run daemon -- --server http://127.0.0.1:4127 --api-key sk_machine_... --once
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
The daemon creates one local API for CLI calls, connects the computer to the server, heartbeats the connection, reconciles assigned agents, claims pending deliveries for currently assigned agents, and starts one runtime process per claimed delivery. It can stay online with no assigned agents. Runs have no default timeout; use run actions to kill or restart them.
|
|
252
|
+
|
|
253
|
+
## Environment Variables
|
|
254
|
+
|
|
255
|
+
| Variable | Default | Description |
|
|
256
|
+
|----------|---------|-------------|
|
|
257
|
+
| `PAL_HOME` | `~/.pal` | Runtime home for DB, daemon state, Lark config, and agent homes |
|
|
258
|
+
| `PAL_DB` | `$PAL_HOME/lock.sqlite` | SQLite database path |
|
|
259
|
+
| `PAL_HOST` | `127.0.0.1` | Server bind host used by `bun run server`; set `0.0.0.0` to listen on all interfaces |
|
|
260
|
+
| `PAL_PORT` | `4127` | Server bind port used by `bun run server` |
|
|
261
|
+
| `PAL_SERVER` | `http://127.0.0.1:4127` | Reachable server URL for console/daemon clients; update this when `PAL_HOST`/`PAL_PORT` change client access |
|
|
262
|
+
| `LOCK_SERVER_URL` | - | Alternate server URL env read before `PAL_SERVER` |
|
|
263
|
+
| `PAL_API_KEY` | - | Provisioned computer API key for daemon startup |
|
|
264
|
+
| `PAL_AGENT` | - | Optional legacy explicit agent for daemon startup |
|
|
265
|
+
| `PAL_COMPUTER_ID` | - | Legacy computer id when not using `PAL_API_KEY` |
|
|
266
|
+
| `PAL_COMPUTER_SECRET` | - | Legacy computer secret when not using `PAL_API_KEY` |
|
|
267
|
+
| `LOCK_DAEMON_URL` | `http://127.0.0.1:4137` | Local daemon API URL used by `bun run cli` |
|
|
268
|
+
| `LOCK_DAEMON_TOKEN` | token file fallback | Local daemon API bearer token |
|
|
269
|
+
| `PAL_LARK_CONFIG` | `$PAL_HOME/lark.json` | Lark bot credential file |
|
|
270
|
+
| `PAL_OWNER_LARK_UNION_ID` | - | Optional Lark sender allowlist for business ingest |
|
|
271
|
+
| `PAL_LARK_ACTION_REACTION_EMOJI` | `Typing` | Reaction added when Lark delivery is created |
|
|
272
|
+
|
|
273
|
+
## Runtime Semantics
|
|
274
|
+
|
|
275
|
+
- Rooms are the primary conversation container; `chat` remains as a compatibility alias in several APIs.
|
|
276
|
+
- Agents receive deliveries through room subscriptions, direct recipients, or Lark bot bindings.
|
|
277
|
+
- A daemon starts one agent run for each claimed delivery.
|
|
278
|
+
- Runs do not have a default timeout. Agents can work as long as needed.
|
|
279
|
+
- A visible chat reply is just a message, not run completion.
|
|
280
|
+
- A run completes only when the agent exits normally, fails when it exits non-zero, or is explicitly killed/restarted.
|
|
281
|
+
- Restart kills the current process and starts a fresh run for the same message.
|
|
282
|
+
|
|
283
|
+
## HTTP API
|
|
284
|
+
|
|
285
|
+
Common read and room APIs:
|
|
286
|
+
|
|
287
|
+
- `GET /`
|
|
288
|
+
- `GET /health`
|
|
289
|
+
- `GET /api/chats`
|
|
290
|
+
- `GET /api/rooms`
|
|
291
|
+
- `POST /api/rooms`
|
|
292
|
+
- `GET /api/rooms/<room-id-or-name>/members`
|
|
293
|
+
- `GET /api/rooms/<room-id-or-name>/mentionables`
|
|
294
|
+
- `POST /api/rooms/<room-id-or-name>/agents`
|
|
295
|
+
- `POST /api/rooms/<room-id-or-name>/topics`
|
|
296
|
+
- `GET /api/messages?chat=general&after=0&limit=50`
|
|
297
|
+
- `GET /api/messages/<id>`
|
|
298
|
+
- `POST /api/messages`
|
|
299
|
+
- `GET /api/inbox?agent=codex&after=0&limit=50`
|
|
300
|
+
|
|
301
|
+
Computer, daemon, and delivery APIs:
|
|
302
|
+
|
|
303
|
+
- `POST /api/computers/provision`
|
|
304
|
+
- `POST /api/computers/connect`
|
|
305
|
+
- `POST /api/computers/<computer-id>/heartbeat`
|
|
306
|
+
- `POST /api/daemons`
|
|
307
|
+
- `GET /api/deliveries?agent=codex&status=pending&limit=50`
|
|
308
|
+
- `POST /api/deliveries`
|
|
309
|
+
- `POST /api/deliveries/<delivery-id>/claim`
|
|
310
|
+
- `POST /api/deliveries/<delivery-id>/ack`
|
|
311
|
+
- `POST /api/deliveries/<delivery-id>/fail`
|
|
312
|
+
|
|
313
|
+
Agent, session, run, artifact, and Lark APIs:
|
|
314
|
+
|
|
315
|
+
- `GET /api/agents`
|
|
316
|
+
- `POST /api/agents`
|
|
317
|
+
- `POST /api/agents/onboard`
|
|
318
|
+
- `PATCH /api/agents/<key>`
|
|
319
|
+
- `POST /api/agents/<key>/assignment`
|
|
320
|
+
- `GET /api/sessions`
|
|
321
|
+
- `POST /api/sessions`
|
|
322
|
+
- `GET /api/sessions/<session-id>/runs`
|
|
323
|
+
- `POST /api/sessions/<session-id>/runtime-session`
|
|
324
|
+
- `GET /api/runs`
|
|
325
|
+
- `POST /api/runs`
|
|
326
|
+
- `GET /api/runs/<id>`
|
|
327
|
+
- `POST /api/runs/<id>/pid`
|
|
328
|
+
- `POST /api/runs/<id>/finish`
|
|
329
|
+
- `POST /api/runs/<id>/kill`
|
|
330
|
+
- `POST /api/runs/<id>/restart`
|
|
331
|
+
- `GET /api/artifacts`
|
|
332
|
+
- `POST /api/artifacts`
|
|
333
|
+
- `POST /api/artifacts/cleanup`
|
|
334
|
+
- `POST /api/artifacts/<artifact-id>/revoke`
|
|
335
|
+
- `GET /api/workbench/artifacts`
|
|
336
|
+
- `POST /api/lark/reload`
|
|
337
|
+
|
|
338
|
+
Example message body:
|
|
339
|
+
|
|
340
|
+
```json
|
|
341
|
+
{
|
|
342
|
+
"room": "general",
|
|
343
|
+
"sender": "alice",
|
|
344
|
+
"recipient": "codex",
|
|
345
|
+
"content": "hello"
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Example agent onboarding body:
|
|
350
|
+
|
|
351
|
+
```json
|
|
352
|
+
{
|
|
353
|
+
"agent_key": "codex",
|
|
354
|
+
"display_name": "Codex Agent",
|
|
355
|
+
"runtime": "codex",
|
|
356
|
+
"computer_id": "machine_..."
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
This is intentionally not a production architecture. It is a single-node demo to validate the workflow before adding richer routing, auth, WebSocket delivery, UI, or multi-agent adapters.
|
package/bin/console.js
ADDED
package/bin/daemon.js
ADDED
package/bin/pal.js
ADDED
package/bin/server.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@controlflow-ai/daemon",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"server": "bun run src/server.ts",
|
|
7
|
+
"daemon": "bun run src/daemon.ts",
|
|
8
|
+
"cli": "bun run src/cli.ts",
|
|
9
|
+
"console": "bun run src/console.ts",
|
|
10
|
+
"typecheck": "tsc --noEmit",
|
|
11
|
+
"test": "bun test",
|
|
12
|
+
"check": "bun run typecheck && bun test"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@types/bun": "latest",
|
|
16
|
+
"typescript": "^5.9.3"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@larksuiteoapi/node-sdk": "^1.59.0"
|
|
20
|
+
},
|
|
21
|
+
"bin": {
|
|
22
|
+
"daemon": "bin/daemon.js",
|
|
23
|
+
"pal-daemon": "bin/daemon.js",
|
|
24
|
+
"pal": "bin/pal.js",
|
|
25
|
+
"pal-server": "bin/server.js",
|
|
26
|
+
"pal-console": "bin/console.js"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import type { Message, RoomParticipant, RunAction } from './types.js';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { sanitizeProviderIds } from './provider-identity.js';
|
|
4
|
+
|
|
5
|
+
export interface AgentRuntimeRunInput {
|
|
6
|
+
agent: string;
|
|
7
|
+
serverUrl: string;
|
|
8
|
+
message: Message;
|
|
9
|
+
cwd: string;
|
|
10
|
+
agentHome?: string;
|
|
11
|
+
projectCwd?: string;
|
|
12
|
+
extraArgs: string[];
|
|
13
|
+
localDaemonUrl?: string;
|
|
14
|
+
localDaemonToken?: string;
|
|
15
|
+
runtimeSessionId?: string | null;
|
|
16
|
+
roomParticipants?: RoomParticipant[];
|
|
17
|
+
recentMessages?: Message[];
|
|
18
|
+
dryRun?: boolean;
|
|
19
|
+
privateCliBinDir?: string;
|
|
20
|
+
palCliCommand?: string;
|
|
21
|
+
signal?: AbortSignal;
|
|
22
|
+
onStart?: (pid: number | null) => Promise<void>;
|
|
23
|
+
getAction?: () => Promise<RunAction | null>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function formatRoomParticipantContext(participants: RoomParticipant[] | undefined): string {
|
|
27
|
+
if (!participants || participants.length === 0) {
|
|
28
|
+
return '\nRoom participants: no local participant snapshot is available yet.';
|
|
29
|
+
}
|
|
30
|
+
const lines = participants.slice(0, 50).map((p) => {
|
|
31
|
+
const participant = sanitizeProviderIds(p.participant_id);
|
|
32
|
+
const name = p.display_name ? ` (${sanitizeProviderIds(p.display_name)})` : '';
|
|
33
|
+
return `- ${p.kind}: ${participant}${name}, source=${p.source}`;
|
|
34
|
+
});
|
|
35
|
+
const suffix = participants.length > 50 ? `\n- ... ${participants.length - 50} more participants omitted` : '';
|
|
36
|
+
return `\nRoom participants:\n${lines.join('\n')}${suffix}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function formatRecentMessageContext(messages: Message[] | undefined, currentMessageId: number): string {
|
|
40
|
+
if (!messages || messages.length === 0) {
|
|
41
|
+
return '\nRecent room history: no local message history is available yet.';
|
|
42
|
+
}
|
|
43
|
+
const lines = messages.slice(-12).map((message) => {
|
|
44
|
+
const marker = message.id === currentMessageId ? ' current' : '';
|
|
45
|
+
const parent = message.parent_id === null ? '' : ` parent=${message.parent_id}`;
|
|
46
|
+
const recipient = message.recipient ? ` to=@${sanitizeProviderIds(message.recipient)}` : '';
|
|
47
|
+
const sender = sanitizeProviderIds(message.sender);
|
|
48
|
+
const content = sanitizeProviderIds(message.content).replace(/\s+/g, ' ').trim().slice(0, 500);
|
|
49
|
+
return `- [${message.id}${marker}] @${sender}${recipient}${parent}: ${content}`;
|
|
50
|
+
});
|
|
51
|
+
const omitted = messages.length > 12 ? `\n- ... ${messages.length - 12} older messages omitted` : '';
|
|
52
|
+
return `\nRecent room history:\n${lines.join('\n')}${omitted}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function runtimeCwd(input: AgentRuntimeRunInput): string {
|
|
56
|
+
return input.agentHome ?? input.cwd;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function runtimeProjectCwd(input: AgentRuntimeRunInput): string {
|
|
60
|
+
return input.projectCwd ?? input.cwd;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function buildPalPrompt(input: AgentRuntimeRunInput): string {
|
|
64
|
+
const palCli = input.palCliCommand ?? 'pal';
|
|
65
|
+
const thread = input.message.parent_id === null ? '' : `\nTopic parent message id: ${input.message.parent_id}`;
|
|
66
|
+
const recipient = input.message.recipient ? `\nRecipient: @${sanitizeProviderIds(input.message.recipient)}` : '';
|
|
67
|
+
const agentHome = runtimeCwd(input);
|
|
68
|
+
const projectCwd = runtimeProjectCwd(input);
|
|
69
|
+
const chatName = sanitizeProviderIds(input.message.chat_name);
|
|
70
|
+
const sender = sanitizeProviderIds(input.message.sender);
|
|
71
|
+
const content = sanitizeProviderIds(input.message.content);
|
|
72
|
+
|
|
73
|
+
return `You are ${input.agent}, a long-running PAL coding agent connected to the pal chat server.
|
|
74
|
+
|
|
75
|
+
PAL is a multi-agent chat platform where humans and agents collaborate through rooms and topics.
|
|
76
|
+
|
|
77
|
+
Workspace contract:
|
|
78
|
+
- Your current working directory is your persistent PAL agent home: ${agentHome}
|
|
79
|
+
- Your cwd is for identity, memory, recovery state, scratch files, and durable local notes.
|
|
80
|
+
- Your cwd is not the project repository.
|
|
81
|
+
- The project workspace for source inspection, code changes, tests, and Git work is: ${projectCwd}
|
|
82
|
+
|
|
83
|
+
Startup recovery:
|
|
84
|
+
- Read MEMORY.md in your cwd first when it exists.
|
|
85
|
+
- Follow its recovery order and read only the referenced state/kb files you need.
|
|
86
|
+
- Keep durable agent memory in your agent home, not in the project repository.
|
|
87
|
+
|
|
88
|
+
A new chat message arrived.
|
|
89
|
+
|
|
90
|
+
Message id: ${input.message.id}
|
|
91
|
+
Chat: #${chatName}${thread}
|
|
92
|
+
Sender: @${sender}${recipient}
|
|
93
|
+
${formatRoomParticipantContext(input.roomParticipants)}
|
|
94
|
+
${formatRecentMessageContext(input.recentMessages, input.message.id)}
|
|
95
|
+
Content:
|
|
96
|
+
${content}
|
|
97
|
+
|
|
98
|
+
Communication rules:
|
|
99
|
+
- Use the PAL CLI for any reply that should be visible in chat.
|
|
100
|
+
- Reply to the same chat/topic unless the user explicitly asks otherwise.
|
|
101
|
+
- Avoid acknowledgment-only replies; send a visible message only when it provides a result, blocker, decision request, ownership signal, or useful progress.
|
|
102
|
+
- Report only work you actually did.
|
|
103
|
+
- Store PAL handles such as user:pal_user_ab12cd34 in durable memory. Never store provider-native IDs such as Feishu/Lark open_id, chat_id, message_id, or app_id values.
|
|
104
|
+
|
|
105
|
+
Commands:
|
|
106
|
+
- Send a reply to the same chat: ${palCli} send --chat ${chatName} --from ${input.agent} <message>
|
|
107
|
+
- Send a message to another agent and trigger that agent's runtime: ${palCli} send --chat ${chatName} --from ${input.agent} --to <agent-key> <message>
|
|
108
|
+
- Reply in this topic: ${palCli} send --chat ${chatName} --parent ${input.message.parent_id ?? input.message.id} --from ${input.agent} <message>
|
|
109
|
+
- List rooms: ${palCli} rooms list
|
|
110
|
+
- View room members: ${palCli} rooms members --room ${chatName}
|
|
111
|
+
- Read recent chat messages: ${palCli} messages list --room ${chatName}
|
|
112
|
+
- Show one message: ${palCli} messages show <message-id>
|
|
113
|
+
|
|
114
|
+
Finish naturally when the task is done. There is no fixed timeout for this run.`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface AgentRuntimeRunResult {
|
|
118
|
+
output: string;
|
|
119
|
+
exitCode: number | null;
|
|
120
|
+
stoppedByAction: RunAction | null;
|
|
121
|
+
runtimeSessionId?: string | null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export type AgentRuntimeProtocol = 'json-stream' | 'acp';
|
|
125
|
+
|
|
126
|
+
export interface AgentRuntimeCapabilities {
|
|
127
|
+
protocol: AgentRuntimeProtocol;
|
|
128
|
+
resume: 'runtime-session-id' | 'adapter-session-id' | 'none';
|
|
129
|
+
busyDeliveryMode: 'queue' | 'direct' | 'notification' | 'none';
|
|
130
|
+
supportsMcp: boolean;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface AgentRuntime {
|
|
134
|
+
readonly name: string;
|
|
135
|
+
readonly capabilities: AgentRuntimeCapabilities;
|
|
136
|
+
/** Build the prompt text fed to the agent CLI. */
|
|
137
|
+
buildPrompt(input: AgentRuntimeRunInput): string;
|
|
138
|
+
/** Build the CLI argument array (excluding the command name itself). */
|
|
139
|
+
buildArgs(input: AgentRuntimeRunInput): string[];
|
|
140
|
+
/** The executable command name (e.g. "neeko", "coco", "codex"). */
|
|
141
|
+
readonly command: string;
|
|
142
|
+
/** Return the working directory for the agent process. Defaults to input.agentHome ?? input.cwd. */
|
|
143
|
+
buildCwd?(input: AgentRuntimeRunInput): string;
|
|
144
|
+
/** Parse stdout/stderr from the agent process. Defaults to trimmed stdout + stderr. */
|
|
145
|
+
parseOutput?(input: { stdout: string; stderr: string; input: AgentRuntimeRunInput }): { output: string; runtimeSessionId?: string | null };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function sleep(ms: number): Promise<void> {
|
|
149
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function processGroupSignal(pid: number, signal: NodeJS.Signals): void {
|
|
153
|
+
try {
|
|
154
|
+
process.kill(-pid, signal);
|
|
155
|
+
} catch {
|
|
156
|
+
try { process.kill(pid, signal); } catch { /* already exited */ }
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function stopProcess(proc: ReturnType<typeof Bun.spawn>): Promise<number | null> {
|
|
161
|
+
processGroupSignal(proc.pid, 'SIGTERM');
|
|
162
|
+
const graceful = await Promise.race([
|
|
163
|
+
proc.exited.then((exitCode) => ({ exited: true as const, exitCode })),
|
|
164
|
+
sleep(2000).then(() => ({ exited: false as const })),
|
|
165
|
+
]);
|
|
166
|
+
|
|
167
|
+
if (graceful.exited) return graceful.exitCode;
|
|
168
|
+
|
|
169
|
+
processGroupSignal(proc.pid, 'SIGKILL');
|
|
170
|
+
proc.kill('SIGKILL');
|
|
171
|
+
return await Promise.race([
|
|
172
|
+
proc.exited,
|
|
173
|
+
sleep(500).then(() => null),
|
|
174
|
+
]);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function waitForExitOrAction(
|
|
178
|
+
proc: ReturnType<typeof Bun.spawn>,
|
|
179
|
+
input: AgentRuntimeRunInput,
|
|
180
|
+
): Promise<Pick<AgentRuntimeRunResult, 'exitCode' | 'stoppedByAction'>> {
|
|
181
|
+
const pollMs = 1000;
|
|
182
|
+
|
|
183
|
+
while (true) {
|
|
184
|
+
const waiters: Array<Promise<{ type: 'exit'; exitCode: number } | { type: 'abort' } | { type: 'tick' }>> = [
|
|
185
|
+
proc.exited.then((exitCode) => ({ type: 'exit' as const, exitCode })),
|
|
186
|
+
sleep(pollMs).then(() => ({ type: 'tick' as const })),
|
|
187
|
+
];
|
|
188
|
+
if (input.signal) {
|
|
189
|
+
waiters.push(new Promise<{ type: 'abort' }>((resolve) => {
|
|
190
|
+
if (input.signal!.aborted) {
|
|
191
|
+
resolve({ type: 'abort' });
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
input.signal!.addEventListener('abort', () => resolve({ type: 'abort' }), { once: true });
|
|
195
|
+
}));
|
|
196
|
+
}
|
|
197
|
+
const result = await Promise.race(waiters);
|
|
198
|
+
|
|
199
|
+
if (result.type === 'exit') {
|
|
200
|
+
return { exitCode: result.exitCode, stoppedByAction: null };
|
|
201
|
+
}
|
|
202
|
+
if (result.type === 'abort') {
|
|
203
|
+
return { exitCode: await stopProcess(proc), stoppedByAction: 'kill' };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const action = await input.getAction?.();
|
|
207
|
+
if (action) {
|
|
208
|
+
return { exitCode: await stopProcess(proc), stoppedByAction: action };
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Spawn an agent runtime process and manage its lifecycle. */
|
|
214
|
+
export async function runAgentRuntime(
|
|
215
|
+
runtime: AgentRuntime,
|
|
216
|
+
input: AgentRuntimeRunInput,
|
|
217
|
+
): Promise<AgentRuntimeRunResult> {
|
|
218
|
+
const prompt = runtime.buildPrompt(input);
|
|
219
|
+
const args = runtime.buildArgs(input);
|
|
220
|
+
const procCwd = runtime.buildCwd?.(input) ?? runtimeCwd(input);
|
|
221
|
+
console.log(`[${runtime.name}] run agent=${input.agent} chat=${input.message.chat_name} message=${input.message.id} cwd=${procCwd} projectCwd=${runtimeProjectCwd(input)} dryRun=${input.dryRun} extraArgs=[${input.extraArgs.join(', ')}]`);
|
|
222
|
+
|
|
223
|
+
if (input.dryRun) {
|
|
224
|
+
console.log(`[${runtime.name}] dry-run mode, skipping spawn`);
|
|
225
|
+
const [subcommand, ...restArgs] = args;
|
|
226
|
+
const renderedArgs = [
|
|
227
|
+
subcommand,
|
|
228
|
+
...restArgs.map((arg) => JSON.stringify(arg)),
|
|
229
|
+
].filter(Boolean).join(' ');
|
|
230
|
+
await input.onStart?.(null);
|
|
231
|
+
return {
|
|
232
|
+
output: `[dry-run] ${runtime.command}${renderedArgs ? ` ${renderedArgs}` : ''}`,
|
|
233
|
+
exitCode: 0,
|
|
234
|
+
stoppedByAction: null,
|
|
235
|
+
runtimeSessionId: input.runtimeSessionId ?? null,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const env = {
|
|
240
|
+
...process.env,
|
|
241
|
+
PAL_SERVER: input.serverUrl,
|
|
242
|
+
PAL_AGENT: input.agent,
|
|
243
|
+
...(input.localDaemonUrl ? { LOCK_DAEMON_URL: input.localDaemonUrl } : {}),
|
|
244
|
+
...(input.localDaemonToken ? { LOCK_DAEMON_TOKEN: input.localDaemonToken } : {}),
|
|
245
|
+
...(input.privateCliBinDir ? { PAL_CLI: join(input.privateCliBinDir, 'pal'), PATH: `${input.privateCliBinDir}:${process.env.PATH ?? ''}` } : {}),
|
|
246
|
+
};
|
|
247
|
+
console.log(`[${runtime.name}] spawning: ${runtime.command} ${args.map((a) => JSON.stringify(a)).join(' ')}`);
|
|
248
|
+
const proc = Bun.spawn([runtime.command, ...args], {
|
|
249
|
+
cwd: procCwd,
|
|
250
|
+
stdout: 'pipe',
|
|
251
|
+
stderr: 'pipe',
|
|
252
|
+
detached: true,
|
|
253
|
+
env,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
console.log(`[${runtime.name}] spawned pid=${proc.pid}`);
|
|
257
|
+
await input.onStart?.(proc.pid);
|
|
258
|
+
const status = await waitForExitOrAction(proc, input);
|
|
259
|
+
console.log(`[${runtime.name}] process exited exitCode=${status.exitCode} stoppedByAction=${status.stoppedByAction ?? '-'}`);
|
|
260
|
+
|
|
261
|
+
const [stdout, stderr] = await Promise.all([
|
|
262
|
+
new Response(proc.stdout).text(),
|
|
263
|
+
new Response(proc.stderr).text(),
|
|
264
|
+
]);
|
|
265
|
+
|
|
266
|
+
const parsed = runtime.parseOutput?.({ stdout, stderr, input }) ?? {
|
|
267
|
+
output: [stdout.trim(), stderr.trim()].filter(Boolean).join('\n'),
|
|
268
|
+
runtimeSessionId: input.runtimeSessionId ?? null,
|
|
269
|
+
};
|
|
270
|
+
const output = parsed.output;
|
|
271
|
+
console.log(`[${runtime.name}] output length=${output.length}`);
|
|
272
|
+
return { output, runtimeSessionId: parsed.runtimeSessionId ?? input.runtimeSessionId ?? null, ...status };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export type RuntimeDriver = AgentRuntime;
|
|
276
|
+
|
|
277
|
+
/** @deprecated Use AgentRuntimeRunInput instead. */
|
|
278
|
+
export type CodingAgentRunInput = AgentRuntimeRunInput;
|
|
279
|
+
/** @deprecated Use AgentRuntimeRunResult instead. */
|
|
280
|
+
export type CodingAgentRunResult = AgentRuntimeRunResult;
|
|
281
|
+
/** @deprecated Use AgentRuntime instead. */
|
|
282
|
+
export type CodingAgentRuntime = AgentRuntime;
|
|
283
|
+
|
|
284
|
+
/** @deprecated Use runAgentRuntime instead. */
|
|
285
|
+
export const runCodingAgent = runAgentRuntime;
|