opencode-courier 0.7.3 → 0.8.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 +109 -135
- package/dist/index.js +23 -4
- package/dist/memory/domain.d.ts +119 -0
- package/dist/memory/domain.js +84 -0
- package/dist/memory/events.d.ts +26 -0
- package/dist/memory/events.js +46 -0
- package/dist/memory/forget.d.ts +10 -0
- package/dist/memory/forget.js +20 -0
- package/dist/memory/index.d.ts +10 -0
- package/dist/memory/index.js +11 -0
- package/dist/memory/note.d.ts +37 -0
- package/dist/memory/note.js +29 -0
- package/dist/memory/ports.d.ts +44 -0
- package/dist/memory/ports.js +1 -0
- package/dist/memory/recall.d.ts +27 -0
- package/dist/memory/recall.js +48 -0
- package/dist/memory/record.d.ts +7 -0
- package/dist/memory/record.js +92 -0
- package/dist/memory/rpc-handlers.d.ts +134 -0
- package/dist/memory/rpc-handlers.js +23 -0
- package/dist/memory/rpc.d.ts +227 -0
- package/dist/memory/rpc.js +60 -0
- package/dist/memory/store.d.ts +10 -0
- package/dist/memory/store.js +90 -0
- package/dist/memory/sweep.d.ts +4 -0
- package/dist/memory/sweep.js +12 -0
- package/dist/memory/text.d.ts +27 -0
- package/dist/memory/text.js +95 -0
- package/dist/memory/tools.d.ts +16 -0
- package/dist/memory/tools.js +78 -0
- package/dist/memory/tui.d.ts +73 -0
- package/dist/person/rpc.d.ts +4 -4
- package/dist/platform/json.d.ts +1 -0
- package/dist/platform/json.js +1 -0
- package/dist/rpc.d.ts +1 -0
- package/dist/rpc.js +1 -0
- package/dist/skill.js +10 -3
- package/dist/tree/context.d.ts +2 -2
- package/dist/tree/context.js +4 -3
- package/dist/tree/index.d.ts +6 -5
- package/dist/tree/index.js +5 -4
- package/dist/tree/ports.d.ts +4 -2
- package/dist/tree/roster.d.ts +17 -0
- package/dist/tree/roster.js +24 -0
- package/dist/tree/rpc-handlers.d.ts +18 -18
- package/dist/tree/rpc.d.ts +18 -18
- package/dist/tree/status.d.ts +5 -5
- package/dist/tree/subtree.d.ts +2 -2
- package/dist/tree/text.js +2 -1
- package/dist/tree/tui.d.ts +11 -3
- package/dist/tui.js +544 -121
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,49 +38,44 @@ If OpenCode asks to allow a child's command, the parent passes the question to y
|
|
|
38
38
|
sometimes stall on a request; if nothing moves for a few minutes, send the prompt again. Next:
|
|
39
39
|
[Using it](#using-it).
|
|
40
40
|
|
|
41
|
-
|
|
42
41
|
## OpenCode V2 native
|
|
43
42
|
|
|
44
43
|
Courier is built on OpenCode V2's plugin API (`@opencode/plugin`), not the V1 one
|
|
45
|
-
(`@opencode-ai/plugin`)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
plugin shows up there first. Older releases and their versions: [every release](docs/reference.md#the-opencode-version).
|
|
44
|
+
(`@opencode-ai/plugin`). Each release is pinned to one V2 version and tested against it end to end;
|
|
45
|
+
the current one is tested on **OpenCode 2.0.32**. CI also runs the live suite on the newest OpenCode
|
|
46
|
+
release, on `main` and nightly. Older releases: [every release](docs/reference.md#the-opencode-version).
|
|
49
47
|
|
|
50
48
|
## Courier or the built-in subagent tool?
|
|
51
49
|
|
|
52
|
-
OpenCode V2
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
a self-contained task that needs nothing from you on the way. Use courier when the work needs more:
|
|
50
|
+
OpenCode V2's own [`subagent` tool](https://opencode.ai/v2/docs/agents) starts an agent with fresh
|
|
51
|
+
context in a child session; the parent waits for its final answer or, with `background: true`, is
|
|
52
|
+
notified when the child finishes. Use it when one result back is all you need. Use courier when the
|
|
53
|
+
work needs more:
|
|
57
54
|
|
|
58
|
-
- **Messages from the child, mid-run:** a child reports progress
|
|
55
|
+
- **Messages from the child, mid-run:** a child reports progress or asks its parent something
|
|
59
56
|
whenever it likes with `courier_send`, and any session can message any other.
|
|
60
57
|
- **Waking an idle session from anywhere:** a message from a child, a schedule (`courier_later`) or
|
|
61
|
-
a [webhook](#webhooks)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
a
|
|
68
|
-
model, and your answers go back; a form only you can answer is pointed out to you.
|
|
58
|
+
a [webhook](#webhooks) starts a new turn in a session that has ended its own.
|
|
59
|
+
- **Worktree isolation:** `isolate: true` gives a child its own git worktree, so children edit files
|
|
60
|
+
in parallel; each commits on `courier/<its session ID>` and reports that branch for its parent to
|
|
61
|
+
merge ([nested isolation](docs/reference.md#nested-isolation)).
|
|
62
|
+
- **Relaying to you:** a child's permission requests and questions reach you through the parent. A
|
|
63
|
+
request is approved in OpenCode's own permission prompt in the parent's session, never by its
|
|
64
|
+
model; a form only you can answer is pointed out to you.
|
|
69
65
|
|
|
70
66
|
## How it works
|
|
71
67
|
|
|
72
|
-
A parent
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
idle); `queue: true` waits until the current turn ends.
|
|
68
|
+
A parent calls `courier_spawn`, gets a session id back at once and ends its turn. The child works
|
|
69
|
+
on its own and, when done or stuck, calls `courier_send` with the parent's id, a status and what it
|
|
70
|
+
produced. The message lands in the parent's inbox through OpenCode's own API, the way OpenCode's
|
|
71
|
+
background subagents report, and starts a new turn if the parent is idle. Delivery is `steer` by
|
|
72
|
+
default (into the target's running turn, or a new one if idle); `queue: true` waits until the current
|
|
73
|
+
turn ends.
|
|
79
74
|
|
|
80
75
|
## Demos
|
|
81
76
|
|
|
82
|
-
The video at the top (1:46, narrated) is the TUI driven in `tmux` on the
|
|
83
|
-
|
|
77
|
+
The video at the top (1:46, narrated) is the TUI driven in `tmux` on the live test's scripted
|
|
78
|
+
stand-in model, so each scene is real plugin behaviour:
|
|
84
79
|
|
|
85
80
|
- **A join group:** one member reports and its report is held with the group, while the other waits
|
|
86
81
|
on a permission request, so the footer says one is waiting on you. You allow it from the asks inbox
|
|
@@ -95,11 +90,10 @@ the live test, with `e2e/tui.sh`'s setup, so each scene is real plugin behaviour
|
|
|
95
90
|
## Recursive orchestration
|
|
96
91
|
|
|
97
92
|
A session can split a goal across child sessions, each child deciding in turn whether to split its
|
|
98
|
-
part again, with the results checked and integrated at every level on the way back up. The
|
|
99
|
-
ships
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
reference](docs/reference.md#the-courier-orchestrate-skill).
|
|
93
|
+
part again, with the results checked and integrated at every level on the way back up. The playbook
|
|
94
|
+
ships as the OpenCode skill `courier-orchestrate`, listed in every session: models load it at the
|
|
95
|
+
start of an implementation task that may have independent parts, and `/skills` in the TUI attaches
|
|
96
|
+
it to your prompt ([what it says](docs/reference.md#the-courier-orchestrate-skill)).
|
|
103
97
|
|
|
104
98
|
## Install
|
|
105
99
|
|
|
@@ -107,14 +101,14 @@ reference](docs/reference.md#the-courier-orchestrate-skill).
|
|
|
107
101
|
|
|
108
102
|
Requires OpenCode V2: the `opencode` command from `@opencode/cli` (also installed as `opencode2`).
|
|
109
103
|
Each release is built and tested against exactly one OpenCode V2 version; a row lists the releases
|
|
110
|
-
pinned to its version. OpenCode never checks it:
|
|
111
|
-
|
|
112
|
-
|
|
104
|
+
pinned to its version. OpenCode never checks it: the plugin loads on any V2 host, and on another
|
|
105
|
+
version than the pinned one writes one line to the server log
|
|
106
|
+
([more](docs/reference.md#the-opencode-version)).
|
|
113
107
|
|
|
114
108
|
<!-- compat-table:start rows=3: generated by script/compat-table.ts from the release tags -->
|
|
115
109
|
| opencode-courier | OpenCode V2 (`opencode` and `@opencode/plugin`) |
|
|
116
110
|
|---|---|
|
|
117
|
-
| 0.7.3 | 2.0.32 |
|
|
111
|
+
| 0.7.3, 0.8.0 | 2.0.32 |
|
|
118
112
|
| 0.7.2 | 2.0.31 |
|
|
119
113
|
| 0.7.1 | 2.0.30 |
|
|
120
114
|
<!-- compat-table:end -->
|
|
@@ -122,7 +116,7 @@ line to the server log naming both ([more](docs/reference.md#the-opencode-versio
|
|
|
122
116
|
Older releases and their versions: [every release](docs/reference.md#the-opencode-version).
|
|
123
117
|
|
|
124
118
|
The [terminal part](#in-the-terminal) relies on the host to provide its UI modules and is tested on
|
|
125
|
-
OpenCode 2.0.32 only; the server part works
|
|
119
|
+
OpenCode 2.0.32 only; the server part works on older V2 hosts from 2.0.4 on.
|
|
126
120
|
|
|
127
121
|
Check yours with `opencode --version`, and install the matching CLI with:
|
|
128
122
|
|
|
@@ -139,9 +133,8 @@ opencode plugin add opencode-courier
|
|
|
139
133
|
This installs the package from npm and adds `"opencode-courier"` to `plugins` in the global
|
|
140
134
|
configuration (`~/.config/opencode/opencode.json`); its options ([session trees](#session-trees),
|
|
141
135
|
[webhooks](#webhooks)) go in the object form of that entry. Install it by name, without a version:
|
|
142
|
-
OpenCode only checks plugins for updates when their entry is not an exact version
|
|
143
|
-
|
|
144
|
-
entry](docs/reference.md#updating-the-plugin)).
|
|
136
|
+
OpenCode only checks plugins for updates when their entry is not an exact version
|
|
137
|
+
([updating](docs/reference.md#updating-the-plugin)).
|
|
145
138
|
|
|
146
139
|
From a local clone, `bun install && npm run build`, then list its `dist` **directory** in
|
|
147
140
|
`opencode.json` (V2 uses `plugins`, plural; a path to a file is ignored with a warning):
|
|
@@ -155,14 +148,15 @@ From a local clone, `bun install && npm run build`, then list its `dist` **direc
|
|
|
155
148
|
## Using it
|
|
156
149
|
|
|
157
150
|
1. Keep the background server running so sessions can be woken while you are away (`opencode
|
|
158
|
-
service start`), and run one OpenCode server per data directory where you can: a second one
|
|
159
|
-
|
|
151
|
+
service start`), and run one OpenCode server per data directory where you can: a second one
|
|
152
|
+
cannot pass your answer on to a child the other runs
|
|
160
153
|
([why](docs/reference.md#two-servers-on-one-data-directory)).
|
|
161
154
|
2. Give the agents that run children the permissions their work needs: a child that hits an approval
|
|
162
|
-
prompt waits until you answer OpenCode's prompt for it in the parent's session. Let them use the
|
|
163
|
-
should be able to ask you. Choose a web search provider before children
|
|
164
|
-
web search in your own session, or use OpenCode's "Third-party search"
|
|
165
|
-
child to search shows that prompt in its own session, which only you can
|
|
155
|
+
prompt waits until you answer OpenCode's prompt for it in the parent's session. Let them use the
|
|
156
|
+
`question` tool if they should be able to ask you. Choose a web search provider before children
|
|
157
|
+
search the web (run one web search in your own session, or use OpenCode's "Third-party search"
|
|
158
|
+
setting), or the first child to search shows that prompt in its own session, which only you can
|
|
159
|
+
answer there.
|
|
166
160
|
3. Your own session's prompts are relayed nowhere, and each one stalls the whole tree until you
|
|
167
161
|
answer it. Before an unattended run, allow the orchestrating session's toolchain up front: one rule
|
|
168
162
|
on `bun run *` or `npm run *` (the skill has checks run through the project's scripts), `git`, and
|
|
@@ -191,38 +185,33 @@ From a local clone, `bun install && npm run build`, then list its `dist` **direc
|
|
|
191
185
|
| `courier_cancel` | Drops a message scheduled with `courier_later`. |
|
|
192
186
|
| `courier_subscribe` | Subscribes a session to webhook deliveries for a topic (`owner/repo`, `owner/repo#12` or a name). |
|
|
193
187
|
| `courier_unsubscribe` | Drops one topic, or all of a session's. |
|
|
188
|
+
| `courier_recall` | One look at what earlier session trees in this project did and the lesson each left; with `sessionID`, one tree's [record](#memory) in full. |
|
|
189
|
+
| `courier_note` | Writes this tree's one lesson on its [record](#memory), for the next orchestrator in the project: one line of at most 300 characters, naming the job and the reason. A longer one is refused, a second call replaces the first. From the top or a sub-orchestrator, never a leaf. |
|
|
194
190
|
|
|
195
191
|
Each tool's parameters: [the reference](docs/reference.md#the-tools).
|
|
196
192
|
|
|
197
193
|
## How children reach you
|
|
198
194
|
|
|
199
195
|
- **A child reports once, with a status**: `done`, `partial`, `blocked` (it needs a decision from
|
|
200
|
-
its parent, and says what) or `failed`, with its artifacts
|
|
201
|
-
|
|
202
|
-
[More](docs/reference.md#a-childs-report).
|
|
196
|
+
its parent, and says what) or `failed`, with its artifacts in the message. A message without a
|
|
197
|
+
status is progress, not the report. [More](docs/reference.md#a-childs-report).
|
|
203
198
|
- **A join group wakes you once.** Start a batch of sessions with the same `group` name, and their
|
|
204
|
-
`done`, `partial` and `failed` reports are held until the last one is in
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
- **A child that fails, or ends its turn without reporting, is reported** to its parent
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
(
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
prompt
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
and under its rules, before anything is passed on: its model cannot approve, only propose. For a
|
|
221
|
-
question, it asks you and passes your answer back, or links its own question to the child's so your
|
|
222
|
-
pick goes straight to it. Whichever answer comes first counts, and the other is withdrawn.
|
|
223
|
-
A request starts a turn in the top session if it is idle; if you answer it somewhere else, the
|
|
224
|
-
note that it is settled waits for that session's next turn, usually the child's report or its
|
|
225
|
-
silence, instead of starting one of its own. [More](docs/reference.md#a-child-that-asks-for-permission).
|
|
199
|
+
`done`, `partial` and `failed` reports are held until the last one is in, then delivered in one
|
|
200
|
+
message; a `blocked` report, a failure, a question or a permission request still reaches you at
|
|
201
|
+
once. A member that will not report holds the others back: drop it with `courier_cleanup`.
|
|
202
|
+
[More](docs/reference.md#join-groups).
|
|
203
|
+
- **A child that fails, or ends its turn without reporting, is reported** to its parent, with the
|
|
204
|
+
error or its last reply, unless it waits on a request, its own children, a scheduled message or a
|
|
205
|
+
webhook. [More](docs/reference.md#a-child-that-fails).
|
|
206
|
+
- **A message that fails to wake its session is sent again** after 30 seconds; if the session still
|
|
207
|
+
does not take it, the footer and the tree panel show it as `stalled`, for you to prompt it.
|
|
208
|
+
[More](docs/reference.md#a-missed-wake).
|
|
209
|
+
- **Permission requests and questions reach you** through the session at the top of the tree, any
|
|
210
|
+
number of levels down. For a request, that session proposes an answer with `courier_answer`, and
|
|
211
|
+
OpenCode asks you in its own permission prompt there before anything is passed on: its model
|
|
212
|
+
cannot approve, only propose. For a question, it asks you and passes your answer back, or links
|
|
213
|
+
its own question to the child's so your pick goes straight to it.
|
|
214
|
+
[More](docs/reference.md#a-child-that-asks-for-permission).
|
|
226
215
|
- **Some forms only you can answer**, such as OpenCode's web search asking for a provider: the top
|
|
227
216
|
session is told, and you answer in the child's session. [More](docs/reference.md#a-child-that-shows-a-form).
|
|
228
217
|
- **You can see and stop a whole subtree** with `courier_tree` and `courier_stop`, or in the
|
|
@@ -249,49 +238,44 @@ options in the plugin's `options`, in the global config:
|
|
|
249
238
|
| `maxChildren` | `5` | How many sessions one session started may have live at once. |
|
|
250
239
|
| `maxTotal` | `20` | How many spawned sessions may be live at once in one tree. |
|
|
251
240
|
|
|
252
|
-
`courier_spawn` refuses a spawn past a limit, naming it and saying what to do instead
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
interrupted), or a session below it is live. One that has reported, with nothing live below it,
|
|
256
|
-
does not. Each spawned session's brief says when to split its task and when to do it itself
|
|
241
|
+
`courier_spawn` refuses a spawn past a limit, naming it and saying what to do instead. A session
|
|
242
|
+
counts while it is live: its turn runs, it owes its parent a report, or a session below it is live.
|
|
243
|
+
Each spawned session's brief says when to split its task and when to do it itself
|
|
257
244
|
([more](docs/reference.md#session-trees-and-their-limits)).
|
|
258
245
|
|
|
246
|
+
## Memory
|
|
247
|
+
|
|
248
|
+
The plugin keeps **one record per session tree** in the project, from facts it already has: when the
|
|
249
|
+
tree started and last moved, the top session's title and `split:` line, and for each session started
|
|
250
|
+
in the tree its title, depth, isolation, join group, status, duration, cost, context use and spawns
|
|
251
|
+
refused; never a report's body or artifacts. The one thing a model writes into it is **the tree's
|
|
252
|
+
lesson**, with `courier_note`: one line of at most 300 characters, from the session that verified the
|
|
253
|
+
tree, with the job it comes from and the reason. `courier_recall` reads the records back; the
|
|
254
|
+
`courier-orchestrate` skill asks for one call before the split decision and one `courier_note` before
|
|
255
|
+
the top replies. Each project keeps its newest 30 records and none older than 90 days. `"memory":
|
|
256
|
+
false` in the plugin's `options` turns it off ([more](docs/reference.md#memory)).
|
|
257
|
+
|
|
259
258
|
## In the terminal
|
|
260
259
|
|
|
261
|
-
The package ships a part for OpenCode's terminal UI,
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
reported:
|
|
260
|
+
The package ships a part for OpenCode's terminal UI, loaded from the server's plugin list; the server
|
|
261
|
+
half behaves the same with or without it. In a session that started sessions, the prompt's footer
|
|
262
|
+
shows what the tree under it is doing: how many wait on you (a permission request, a question or a
|
|
263
|
+
form), are [stalled](docs/reference.md#a-missed-wake), run or are idle, and each
|
|
264
|
+
[join group](docs/reference.md#join-groups)'s members reported:
|
|
267
265
|
|
|
268
266
|
```
|
|
269
267
|
courier 1 waiting on you · 2 running · group pair 1/2
|
|
270
268
|
```
|
|
271
269
|
|
|
272
|
-
`/courier tree` opens a panel beside the session with the tree under it, a row per session
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
rules, and `q` closes the panel; `/courier stop <session>` stops from the prompt. While open, the panel
|
|
282
|
-
follows you between sessions: into one of its tree it keeps that tree, the session on screen marked
|
|
283
|
-
`(on screen)`; elsewhere it shows that session's tree, or hides while you are in a session without one,
|
|
284
|
-
or on the home screen, and shows again once you are back in one; only `q` closes it for good.
|
|
285
|
-
A row whose report arrives flashes in your theme's success colour and fades back over 1.5 s, and the header's bar
|
|
286
|
-
grows to its new length; nothing in the footer moves. Next to the header's title digs Digby, the courier's mole, drawn
|
|
287
|
-
in your theme's colours, whose state follows the tree's: a breathing mound while all is idle, a trembling one while
|
|
288
|
-
children run, up with a blinking sign while one waits on you, slumped while one is stalled, popping up with a letter
|
|
289
|
-
for a report and raising mounds for a spawn. Where the panel has room the tree does not use, on an empty panel or 10
|
|
290
|
-
rows free below the last row, a larger Digby, 16 by 8, shows the same state there instead, paws, sign, letter and all
|
|
291
|
-
([more](docs/reference.md#in-the-terminal)).
|
|
292
|
-
To keep the panel still (`•` for a running row, no flash, the bar at once, Digby in each state's first frame), give the TUI part `"animations": false` in OpenCode's terminal config, `~/.config/opencode/cli.json`:
|
|
293
|
-
the TUI passes options to its plugins only from there, not from the server's `opencode.json`, and has no animations
|
|
294
|
-
setting a plugin can read ([more](docs/reference.md#in-the-terminal)).
|
|
270
|
+
`/courier tree` opens a panel beside the session with the tree under it, a row per session with a
|
|
271
|
+
mark for its state (`✓` reported, a spinner while it runs, `●` waiting on you, `!` stalled, `✗`
|
|
272
|
+
failed or interrupted), what it does, its report, its group's standing and, for a leaf, how long it
|
|
273
|
+
ran, its last tool call, its context use and its cost. `enter` or a click opens the session, `s`
|
|
274
|
+
stops it and everything under it, `x` forgets a `gone` session, `q` closes the panel;
|
|
275
|
+
`/courier stop <session>` stops from the prompt. Digby, the courier's mole, digs beside the header,
|
|
276
|
+
his state following the tree's. `"animations": false` in the TUI part's options keeps the panel
|
|
277
|
+
still; the TUI reads a plugin's options only from its own config, `~/.config/opencode/cli.json`
|
|
278
|
+
([more](docs/reference.md#in-the-terminal)):
|
|
295
279
|
|
|
296
280
|
```jsonc
|
|
297
281
|
{
|
|
@@ -299,40 +283,30 @@ setting a plugin can read ([more](docs/reference.md#in-the-terminal)).
|
|
|
299
283
|
}
|
|
300
284
|
```
|
|
301
285
|
|
|
302
|
-
`ctrl+x k` (the leader key, then `k`), `/asks` (or `/ask`), the palette's "Courier: what waits on
|
|
303
|
-
on "waiting on you" opens the asks inbox: every permission request, question and form
|
|
304
|
-
anywhere in the tree,
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
286
|
+
`ctrl+x k` (the leader key, then `k`), `/asks` (or `/ask`), the palette's "Courier: what waits on
|
|
287
|
+
you" or a click on "waiting on you" opens the asks inbox: every permission request, question and form
|
|
288
|
+
waiting anywhere in the tree, after the top session's own permission requests, with where each comes
|
|
289
|
+
from, kept current while open and answered in a dialog with no model in between.
|
|
290
|
+
The TUI plays OpenCode's sounds for a child's report, a group's release and a new ask. While a
|
|
291
|
+
permission prompt or form is shown in the session, OpenCode hides the footer, so open the inbox with
|
|
292
|
+
`ctrl+x k`.
|
|
309
293
|
|
|
310
|
-
|
|
311
|
-
|
|
294
|
+
`/courier memory` (or the palette's "Courier: memory") lists the project's [tree records](#memory),
|
|
295
|
+
newest first, grouped by the day each tree last moved, one row each with its lesson under it.
|
|
296
|
+
`enter` shows one in full; `x` forgets the record under the row, or in a record on its lesson's rows
|
|
297
|
+
only the lesson, after a confirm; `esc` goes back and closes. The search takes every letter but `x`
|
|
298
|
+
([more](docs/reference.md#the-memory-view)).
|
|
312
299
|
|
|
313
300
|
## Native child sessions
|
|
314
301
|
|
|
315
302
|
Off by default. With `"nativeChildren": true` in the plugin's `options`, a child started without
|
|
316
303
|
`isolate` is also created with OpenCode's own `parentID`, so OpenCode treats it as the parent's
|
|
317
|
-
subagent: the TUI's view of the top session
|
|
318
|
-
every session under it, and the
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
- **Deleting a session deletes its native children**, as OpenCode does with subagents.
|
|
324
|
-
- **A child does not take its parent's session rules**: courier passes it an empty ruleset, so it
|
|
325
|
-
may do what it could without the option.
|
|
326
|
-
- **Providers see a subagent**: the child's model requests carry its parent's id for cache routing
|
|
327
|
-
and two headers naming the parent; **on GitHub Copilot**, which meters such requests differently
|
|
328
|
-
from a person's, courier was not tested.
|
|
329
|
-
- **A child's asks show in the top session's view**, not in its own, and it leaves the TUI's session
|
|
330
|
-
lists. Its permission requests and questions are answered once, in OpenCode's own prompt there; the
|
|
331
|
-
top session's model is only told. `opencode run` and the TUI's auto-accept mode answer them as their
|
|
332
|
-
own subagents'.
|
|
333
|
-
- **Turn it on only with OpenCode's TUI or the paired app**, which show those prompts; with a bare
|
|
334
|
-
API client nobody prompts you, and only `courier_answer` is left, which prompts you in the top
|
|
335
|
-
session before it passes anything on.
|
|
304
|
+
subagent: the TUI's view of the top session, and the paired app's request dock, list and answer the
|
|
305
|
+
permission requests, questions and forms of every session under it, and the top session's model is
|
|
306
|
+
only told. Deleting a session then deletes its native children, the child gets an empty ruleset
|
|
307
|
+
rather than its parent's, and providers see a subagent (not tested on GitHub Copilot). Turn it on
|
|
308
|
+
only with OpenCode's TUI or the paired app, which show those prompts
|
|
309
|
+
([more](docs/reference.md#native-child-sessions)).
|
|
336
310
|
|
|
337
311
|
## Webhooks
|
|
338
312
|
|
|
@@ -363,7 +337,7 @@ every default):
|
|
|
363
337
|
arrives as a short summary marked as outside text, queued behind any running turn and waking the
|
|
364
338
|
session if idle.
|
|
365
339
|
|
|
366
|
-
The options, which events wake whom, the
|
|
340
|
+
The options, which events wake whom, the signing scheme and status codes: [the
|
|
367
341
|
reference](docs/reference.md#webhooks).
|
|
368
342
|
|
|
369
343
|
## Development
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { hub, openInProcess, permissions as domains, sharedState } from "./platf
|
|
|
8
8
|
import { builtVersions, versionNotice } from "./platform/version.js";
|
|
9
9
|
import { addSchedulingTools, cancelScheduled, dueJob, isScheduled } from "./scheduling/index.js";
|
|
10
10
|
import { addPersonTools, asksPermission, asksQuestion, formsShown, joinRelay, noticeCutOff, pendingOf, pendingQuestions, personEvents, portsAt, registerPersonRpc, relayQuestions, } from "./person/index.js";
|
|
11
|
+
import { addMemoryTools, memoryEvents, NOTE_TOOL, readMemory, RECALL_TOOL, registerMemoryRpc, sweepRecords } from "./memory/index.js";
|
|
11
12
|
import { addSkill } from "./skill.js";
|
|
12
13
|
import { addTreeTools, deliverReleased, wakeMissed, dirtyOf, headOf, inspectWorktree, pruneExpired, readLimits, readNativeChildren, registerTreeRpc, shapeContext, together, treeEvents, waitsOn, } from "./tree/index.js";
|
|
13
14
|
import { addWebhookTools, joinReceiver, readConfig, receiving, subscribedSince, } from "./webhook/index.js";
|
|
@@ -17,6 +18,8 @@ const gate = sharedState("spawning", () => ({ reserved: new Set(), turn: Promise
|
|
|
17
18
|
const inbox = sharedState("inbox", () => new Map());
|
|
18
19
|
/** The forms shown now, which the person events keep and the footer and tree panel count; outside the hub like the inbox. */
|
|
19
20
|
const shown = sharedState("formsShown", () => new Map());
|
|
21
|
+
/** What the memory shares: the models' windows, the locations with memory off, the tops that recalled and the turn ends recorded. */
|
|
22
|
+
const memoryState = sharedState("memory", () => ({ windows: new Map(), off: new Set(), tops: new Set(), seen: new Set() }));
|
|
20
23
|
/**
|
|
21
24
|
* What each tick of the scheduler does, in order, through the first loaded member, of whichever copy:
|
|
22
25
|
* due `courier_later` messages, released join groups, then missed wakes. Every hub-v2 member's `later` is its LaterPorts.
|
|
@@ -40,7 +43,7 @@ const personState = { ...watched, forms: hub.forms, shown };
|
|
|
40
43
|
*/
|
|
41
44
|
const outside = waitsOn(asksPermission, asksQuestion, together(isScheduled, subscribedSince));
|
|
42
45
|
/** What the hub's subscriptions do with each event of OpenCode's, module by module, in this order. */
|
|
43
|
-
const eventModules = [treeEvents(watchState, outside), personEvents(personState)];
|
|
46
|
+
const eventModules = [treeEvents(watchState, outside), personEvents(personState), memoryEvents(memoryState)];
|
|
44
47
|
/**
|
|
45
48
|
* The courier tools and webhook receiver, as a promise plugin joining the hub that runs the scheduler
|
|
46
49
|
* and the watcher; its member carries the location by which the question relay finds its ports.
|
|
@@ -70,7 +73,8 @@ export const courier = () => Plugin.define({
|
|
|
70
73
|
};
|
|
71
74
|
const { limits, problems } = readLimits(ctx.options);
|
|
72
75
|
const { nativeChildren, problems: nativeProblems } = readNativeChildren(ctx.options);
|
|
73
|
-
|
|
76
|
+
const { memory, problems: memoryProblems } = readMemory(ctx.options);
|
|
77
|
+
for (const problem of [...problems, ...nativeProblems, ...memoryProblems])
|
|
74
78
|
log(`courier: ${problem}`);
|
|
75
79
|
const roles = new Map();
|
|
76
80
|
const ports = {
|
|
@@ -117,21 +121,32 @@ export const courier = () => Plugin.define({
|
|
|
117
121
|
later.log(`courier webhook: not started: ${error instanceof Error ? error.message : String(error)}`);
|
|
118
122
|
}
|
|
119
123
|
const stopPorts = { courier: ports, cancelScheduled: (sessionID) => cancelScheduled(later, sessionID), cleanup: cleanupPorts };
|
|
120
|
-
|
|
124
|
+
const memoryPorts = { storage: ctx.storage, session: ctx.session, now: Date.now, log, projectID: ctx.location.project.id, memory, maxDepth: limits.maxDepth };
|
|
125
|
+
// The tools in a fixed order: the tree's, then the person's, the scheduled messages', the webhooks' and the memory's.
|
|
121
126
|
await ctx.tool.transform((tools) => {
|
|
122
127
|
addTreeTools(tools, { courier: ports, cleanup: cleanupPorts, stop: stopPorts });
|
|
123
128
|
addPersonTools(tools, { answer: answerPorts, questions: questionPorts, waiting: watched.waiting });
|
|
124
129
|
addSchedulingTools(tools, later);
|
|
125
130
|
addWebhookTools(tools, { hooks, receiving });
|
|
131
|
+
addMemoryTools(tools, { memory: memoryPorts, state: memoryState });
|
|
126
132
|
});
|
|
127
133
|
await ctx.skill.transform(addSkill);
|
|
128
134
|
// What the TUI part reads and its stop; a host that cannot register it still gets the rest, the tools included.
|
|
129
135
|
await Promise.resolve()
|
|
130
136
|
.then(() => registerTreeRpc(ctx.rpc, stopPorts))
|
|
131
137
|
.then(() => registerPersonRpc(ctx.rpc, { questions: questionPorts, permissions }))
|
|
138
|
+
.then(() => registerMemoryRpc(ctx.rpc, memoryPorts))
|
|
132
139
|
.catch((error) => log(`courier rpc: not registered, the TUI part shows nothing and cannot stop: ${String(error)}`));
|
|
133
|
-
await ctx.session.hook("context", (event) => shapeContext({ storage: ctx.storage, limits, roles, log }, event));
|
|
140
|
+
await ctx.session.hook("context", (event) => shapeContext({ storage: ctx.storage, limits, roles, hiddenFromLeaf: [RECALL_TOOL, NOTE_TOOL], log }, event));
|
|
134
141
|
const pruned = pruneExpired(ctx.storage, Date.now()).catch((error) => log(`courier roster prune: ${String(error)}`));
|
|
142
|
+
const swept = sweepRecords(ctx.storage, Date.now()).catch((error) => log(`courier memory sweep: ${String(error)}`));
|
|
143
|
+
// The windows the memory reads each part's context use by; a tree whose top runs in a location with memory off is not recorded.
|
|
144
|
+
const windowed = ports
|
|
145
|
+
.windows()
|
|
146
|
+
.then((windows) => windows.forEach((window) => memoryState.windows.set(`${window.providerID}/${window.id}`, window)))
|
|
147
|
+
.catch((error) => log(`courier memory: cannot list the models' context windows: ${String(error)}`));
|
|
148
|
+
if (!memory)
|
|
149
|
+
memoryState.off.add(ctx.location.directory);
|
|
135
150
|
const leaveRelay = joinRelay(questionPorts);
|
|
136
151
|
const noticed = noticeCutOff(questionPorts).catch((error) => questionPorts.log(`courier question: stored questions: ${String(error)}`));
|
|
137
152
|
const watchPorts = {
|
|
@@ -158,11 +173,15 @@ export const courier = () => Plugin.define({
|
|
|
158
173
|
// The leave waits for them, so they do not run on against a location that is closing.
|
|
159
174
|
track(member, pruned);
|
|
160
175
|
track(member, noticed);
|
|
176
|
+
track(member, swept);
|
|
177
|
+
track(member, windowed);
|
|
161
178
|
const leave = webhook ? joinReceiver(webhook, hooks) : undefined;
|
|
162
179
|
return async () => {
|
|
163
180
|
// Waits, for a moment at most, for the work this instance leaves behind, and on the last
|
|
164
181
|
// leave for the scheduler's owner key to be released, so another server takes over at once.
|
|
165
182
|
const leftHub = leaveHub();
|
|
183
|
+
if (!memory)
|
|
184
|
+
memoryState.off.delete(ctx.location.directory);
|
|
166
185
|
leaveRelay();
|
|
167
186
|
await leave?.();
|
|
168
187
|
await leftHub;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The memory's pure rules, on plain data: the option, a record's shape, each part's status, the split line, the
|
|
3
|
+
* retention and how a refresh keeps the parts the roster has forgotten. Imports nothing of the plugin's but `kernel/` and `platform/json.ts`.
|
|
4
|
+
*/
|
|
5
|
+
import { type Status } from "../kernel/protocol.js";
|
|
6
|
+
/** How many records a project keeps, the newest by `movedAt`. */
|
|
7
|
+
export declare const MAX_RECORDS = 30;
|
|
8
|
+
/** Records that last moved longer ago than this are dropped when the plugin loads. */
|
|
9
|
+
export declare const RETENTION_MS: number;
|
|
10
|
+
/** A record's `split` while its top has stated no split line. */
|
|
11
|
+
export declare const NOT_STATED = "not stated";
|
|
12
|
+
/** The most characters a lesson holds: courier_note refuses what is longer, and a record written by hand is clipped when read. */
|
|
13
|
+
export declare const LESSON_MAX = 300;
|
|
14
|
+
/**
|
|
15
|
+
* How a part stands: its last report's status, how its wait ended without one (`failed`, `interrupted`), `stopped` by
|
|
16
|
+
* courier_stop, `stalled` on a message it will not take, or `out`: it owes a report, running or not.
|
|
17
|
+
*/
|
|
18
|
+
export declare const PART_STATUSES: readonly ["done", "partial", "blocked", "failed", "interrupted", "stopped", "stalled", "out"];
|
|
19
|
+
export type PartStatus = (typeof PART_STATUSES)[number];
|
|
20
|
+
export declare const isPartStatus: (value: unknown) => value is PartStatus;
|
|
21
|
+
/** One session started in a tree, as its record keeps it; `duration` is in milliseconds, `context` a share of its model's window. */
|
|
22
|
+
export interface PartRecord {
|
|
23
|
+
readonly sessionID: string;
|
|
24
|
+
readonly title: string;
|
|
25
|
+
/** 1 for a session the top started. */
|
|
26
|
+
readonly depth: number;
|
|
27
|
+
readonly isolated: boolean;
|
|
28
|
+
readonly group?: string;
|
|
29
|
+
readonly status: PartStatus;
|
|
30
|
+
/** From its start to the end of its last turn, or to the refresh while it runs. */
|
|
31
|
+
readonly duration?: number;
|
|
32
|
+
readonly cost?: number;
|
|
33
|
+
/** How full its context was at the end, 0 to 1, when its model's window is known. */
|
|
34
|
+
readonly context?: number;
|
|
35
|
+
/** How many courier_spawn calls of its own failed, refused by a limit mostly. */
|
|
36
|
+
readonly refused?: number;
|
|
37
|
+
}
|
|
38
|
+
/** Whether and when the top called courier_recall (`at`, its first call), and how many calls the tree made in all. */
|
|
39
|
+
export interface Recalled {
|
|
40
|
+
readonly at?: number;
|
|
41
|
+
readonly calls: number;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The one line the tree's orchestrator wrote for the next one with courier_note: when, and from which depth when it was
|
|
45
|
+
* not the top's own session (1 for a session the top started).
|
|
46
|
+
*/
|
|
47
|
+
export interface Lesson {
|
|
48
|
+
readonly text: string;
|
|
49
|
+
readonly at: number;
|
|
50
|
+
readonly depth?: number;
|
|
51
|
+
}
|
|
52
|
+
/** What is remembered of one tree: its top session and each session started in it, never a report's body or artifacts. */
|
|
53
|
+
export interface TreeRecord {
|
|
54
|
+
/** When the top session was created, epoch milliseconds. */
|
|
55
|
+
readonly startedAt: number;
|
|
56
|
+
/** When a turn in the tree last ended, or the record was first written. */
|
|
57
|
+
readonly movedAt: number;
|
|
58
|
+
readonly title: string;
|
|
59
|
+
/** The top's split line, as `e2e/real-model-recursive.mjs` reads it, or `not stated`. */
|
|
60
|
+
readonly split: string;
|
|
61
|
+
/** Whether, at the last refresh, no session in the tree ran and every part had settled: none was out or stalled. */
|
|
62
|
+
readonly finished: boolean;
|
|
63
|
+
readonly recalled?: Recalled;
|
|
64
|
+
/** Absent until a session of the tree that is not a leaf wrote one; a second note replaces the first. */
|
|
65
|
+
readonly lesson?: Lesson;
|
|
66
|
+
readonly parts: ReadonlyArray<PartRecord>;
|
|
67
|
+
}
|
|
68
|
+
/** A record with where it is kept: its project and its top session. */
|
|
69
|
+
export interface StoredRecord extends TreeRecord {
|
|
70
|
+
readonly projectID: string;
|
|
71
|
+
readonly sessionID: string;
|
|
72
|
+
}
|
|
73
|
+
/** The `memory` option: whether records are kept and listed; on unless `false`. */
|
|
74
|
+
export declare function readMemory(options: unknown): {
|
|
75
|
+
memory: boolean;
|
|
76
|
+
problems: string[];
|
|
77
|
+
};
|
|
78
|
+
/** The split line a text states, as the real-model checker reads it: a line beginning with `split:` after any markdown, else one holding it. */
|
|
79
|
+
export declare function splitLineOf(text: string): string | undefined;
|
|
80
|
+
/** What a part's status is read from: whether courier_stop stopped it, whether it is stalled, and its report state. */
|
|
81
|
+
export interface Standing {
|
|
82
|
+
readonly stopped: boolean;
|
|
83
|
+
readonly stalled: boolean;
|
|
84
|
+
readonly report?: {
|
|
85
|
+
readonly known: boolean;
|
|
86
|
+
readonly owes: boolean;
|
|
87
|
+
readonly ended?: "failed" | "interrupted";
|
|
88
|
+
readonly status?: Status;
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/** A part's status by `Standing`; one whose report state is not known is still out. */
|
|
92
|
+
export declare function partStatus({ stopped, stalled, report }: Standing): PartStatus;
|
|
93
|
+
/**
|
|
94
|
+
* The parts of a refreshed record: those recorded before, in their order, each as read now when the roster still has it,
|
|
95
|
+
* else as last recorded, then those first seen now, in the order the tree is drawn.
|
|
96
|
+
*/
|
|
97
|
+
export declare function mergeParts(previous: ReadonlyArray<PartRecord>, current: ReadonlyArray<PartRecord>): PartRecord[];
|
|
98
|
+
/** `recalled` after one more call at `at`, the top's first call noted when `byTop`. */
|
|
99
|
+
export declare function noteRecall(recalled: Recalled | undefined, byTop: boolean, at: number): Recalled;
|
|
100
|
+
/** A text as the one line a lesson is: whitespace and line breaks collapsed to spaces, trimmed. */
|
|
101
|
+
export declare const oneLine: (text: string) => string;
|
|
102
|
+
/**
|
|
103
|
+
* A hand-written lesson's text as one line, clipped at `LESSON_MAX`; `clipped` is the length it had when it was clipped.
|
|
104
|
+
* courier_note does not clip: it refuses a line over `LESSON_MAX` (`note.ts`).
|
|
105
|
+
*/
|
|
106
|
+
export declare function lessonText(text: string): {
|
|
107
|
+
text: string;
|
|
108
|
+
clipped?: undefined;
|
|
109
|
+
} | {
|
|
110
|
+
text: string;
|
|
111
|
+
clipped: number;
|
|
112
|
+
};
|
|
113
|
+
/** The records a project keeps at `now`: the newest `MAX_RECORDS` by `movedAt`, none older than `RETENTION_MS`; the rest are dropped. */
|
|
114
|
+
export declare function keep<T extends Pick<StoredRecord, "movedAt">>(records: ReadonlyArray<T>, now: number): {
|
|
115
|
+
kept: T[];
|
|
116
|
+
dropped: T[];
|
|
117
|
+
};
|
|
118
|
+
/** Records newest first, by when each last moved. */
|
|
119
|
+
export declare const newestFirst: <T extends Pick<StoredRecord, "movedAt">>(records: ReadonlyArray<T>) => T[];
|