opencode-courier 0.13.7 → 0.14.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 +10 -11
- package/dist/index.d.ts +0 -8
- package/dist/index.js +6 -53
- package/dist/kernel/protocol.d.ts +0 -14
- package/dist/kernel/protocol.js +1 -15
- package/dist/kernel/tool.d.ts +0 -2
- package/dist/kernel/tool.js +0 -1
- package/dist/memory/domain.d.ts +6 -85
- package/dist/memory/domain.js +7 -63
- package/dist/memory/events.d.ts +0 -11
- package/dist/memory/events.js +1 -12
- package/dist/memory/forget.d.ts +0 -5
- package/dist/memory/forget.js +0 -4
- package/dist/memory/index.js +0 -2
- package/dist/memory/lessons.d.ts +0 -8
- package/dist/memory/lessons.js +0 -9
- package/dist/memory/note.d.ts +0 -9
- package/dist/memory/note.js +0 -5
- package/dist/memory/pin.d.ts +0 -5
- package/dist/memory/pin.js +0 -4
- package/dist/memory/ports.d.ts +2 -22
- package/dist/memory/recall.d.ts +0 -5
- package/dist/memory/recall.js +0 -12
- package/dist/memory/record.d.ts +0 -4
- package/dist/memory/record.js +8 -31
- package/dist/memory/rpc-handlers.d.ts +3 -6
- package/dist/memory/rpc-handlers.js +0 -6
- package/dist/memory/rpc.d.ts +6 -19
- package/dist/memory/rpc.js +1 -16
- package/dist/memory/store.d.ts +0 -5
- package/dist/memory/store.js +1 -16
- package/dist/memory/sweep.d.ts +0 -4
- package/dist/memory/sweep.js +0 -4
- package/dist/memory/text.d.ts +0 -25
- package/dist/memory/text.js +6 -36
- package/dist/memory/tools.d.ts +0 -2
- package/dist/memory/tools.js +0 -2
- package/dist/memory/tui.d.ts +1 -35
- package/dist/person/events.d.ts +0 -16
- package/dist/person/events.js +3 -30
- package/dist/person/form.d.ts +0 -9
- package/dist/person/form.js +0 -10
- package/dist/person/index.d.ts +0 -2
- package/dist/person/permission.d.ts +0 -34
- package/dist/person/permission.js +2 -28
- package/dist/person/ports.d.ts +0 -16
- package/dist/person/ports.js +0 -1
- package/dist/person/question/answer.d.ts +0 -28
- package/dist/person/question/answer.js +0 -49
- package/dist/person/question/index.js +0 -2
- package/dist/person/question/lifecycle.d.ts +0 -26
- package/dist/person/question/lifecycle.js +1 -32
- package/dist/person/question/pure.d.ts +0 -11
- package/dist/person/question/pure.js +0 -11
- package/dist/person/question/relay.d.ts +0 -1
- package/dist/person/question/relay.js +5 -67
- package/dist/person/question/runtime.d.ts +0 -14
- package/dist/person/question/runtime.js +1 -26
- package/dist/person/question/shared.d.ts +0 -17
- package/dist/person/question/shared.js +0 -20
- package/dist/person/question/types.d.ts +0 -44
- package/dist/person/rpc-handlers.d.ts +0 -7
- package/dist/person/rpc-handlers.js +0 -6
- package/dist/person/rpc.d.ts +0 -20
- package/dist/person/rpc.js +0 -16
- package/dist/person/text.d.ts +0 -45
- package/dist/person/text.js +2 -54
- package/dist/person/tools.d.ts +0 -3
- package/dist/person/tools.js +1 -3
- package/dist/person/tui.d.ts +0 -21
- package/dist/platform/bounded.d.ts +0 -8
- package/dist/platform/bounded.js +0 -8
- package/dist/platform/events.d.ts +0 -18
- package/dist/platform/events.js +3 -22
- package/dist/platform/hub.d.ts +0 -113
- package/dist/platform/hub.js +1 -76
- package/dist/platform/json.d.ts +0 -3
- package/dist/platform/json.js +1 -3
- package/dist/platform/storage.d.ts +0 -7
- package/dist/platform/storage.js +0 -6
- package/dist/platform/version.d.ts +0 -9
- package/dist/platform/version.js +0 -8
- package/dist/rpc.js +0 -1
- package/dist/scheduling/index.d.ts +0 -1
- package/dist/scheduling/index.js +0 -1
- package/dist/scheduling/later.d.ts +0 -11
- package/dist/scheduling/later.js +0 -14
- package/dist/scheduling/text.d.ts +0 -1
- package/dist/scheduling/text.js +0 -1
- package/dist/scheduling/tools.d.ts +0 -1
- package/dist/scheduling/tools.js +1 -5
- package/dist/skill.d.ts +1 -9
- package/dist/skill.js +97 -118
- package/dist/tree/cleanup.d.ts +0 -20
- package/dist/tree/cleanup.js +0 -26
- package/dist/tree/context.d.ts +0 -5
- package/dist/tree/context.js +0 -8
- package/dist/tree/domain.d.ts +0 -64
- package/dist/tree/domain.js +7 -45
- package/dist/tree/events.d.ts +0 -36
- package/dist/tree/events.js +4 -49
- package/dist/tree/forget.d.ts +0 -15
- package/dist/tree/forget.js +0 -18
- package/dist/tree/group.d.ts +0 -22
- package/dist/tree/group.js +1 -37
- package/dist/tree/index.d.ts +2 -2
- package/dist/tree/index.js +1 -3
- package/dist/tree/limits.d.ts +0 -4
- package/dist/tree/limits.js +1 -12
- package/dist/tree/mascot.d.ts +0 -25
- package/dist/tree/mascot.js +1 -32
- package/dist/tree/model.d.ts +0 -5
- package/dist/tree/model.js +2 -15
- package/dist/tree/nudge.d.ts +0 -11
- package/dist/tree/nudge.js +3 -16
- package/dist/tree/panel.d.ts +0 -8
- package/dist/tree/panel.js +3 -16
- package/dist/tree/ports.d.ts +2 -138
- package/dist/tree/report.d.ts +0 -10
- package/dist/tree/report.js +0 -11
- package/dist/tree/roster.d.ts +0 -21
- package/dist/tree/roster.js +0 -21
- package/dist/tree/rpc-handlers.d.ts +6 -1
- package/dist/tree/rpc-handlers.js +0 -5
- package/dist/tree/rpc.d.ts +9 -109
- package/dist/tree/rpc.js +2 -38
- package/dist/tree/running.d.ts +0 -6
- package/dist/tree/running.js +2 -10
- package/dist/tree/send.d.ts +0 -4
- package/dist/tree/send.js +4 -31
- package/dist/tree/spawn.d.ts +0 -7
- package/dist/tree/spawn.js +6 -47
- package/dist/tree/standing.d.ts +0 -8
- package/dist/tree/standing.js +1 -11
- package/dist/tree/status.d.ts +0 -8
- package/dist/tree/status.js +1 -9
- package/dist/tree/stop.d.ts +2 -9
- package/dist/tree/stop.js +58 -25
- package/dist/tree/store.d.ts +0 -8
- package/dist/tree/store.js +4 -38
- package/dist/tree/subtree.d.ts +8 -26
- package/dist/tree/subtree.js +37 -35
- package/dist/tree/summary.d.ts +0 -4
- package/dist/tree/summary.js +0 -8
- package/dist/tree/text.d.ts +1 -44
- package/dist/tree/text.js +6 -66
- package/dist/tree/tools.d.ts +0 -6
- package/dist/tree/tools.js +4 -8
- package/dist/tree/tui.d.ts +0 -81
- package/dist/tree/wake.d.ts +0 -30
- package/dist/tree/wake.js +5 -39
- package/dist/tui.js +22 -8
- package/package.json +8 -9
- package/dist/webhook/index.d.ts +0 -5
- package/dist/webhook/index.js +0 -3
- package/dist/webhook/receiver.d.ts +0 -80
- package/dist/webhook/receiver.js +0 -339
- package/dist/webhook/subscriptions.d.ts +0 -29
- package/dist/webhook/subscriptions.js +0 -52
- package/dist/webhook/text.d.ts +0 -23
- package/dist/webhook/text.js +0 -82
- package/dist/webhook/tools.d.ts +0 -19
- package/dist/webhook/tools.js +0 -46
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ A narrated tour of the terminal UI: the tree panel, the asks inbox, failing chil
|
|
|
23
23
|
## Quickstart
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
|
-
npm install -g @opencode/cli@2.2.
|
|
26
|
+
npm install -g @opencode/cli@2.2.6 # OpenCode V2, the version courier is tested on
|
|
27
27
|
opencode plugin add opencode-courier
|
|
28
28
|
opencode
|
|
29
29
|
```
|
|
@@ -40,12 +40,12 @@ prompt again.
|
|
|
40
40
|
|
|
41
41
|
## Why courier
|
|
42
42
|
|
|
43
|
-
OpenCode's own [`subagent` tool](https://opencode.ai/
|
|
43
|
+
OpenCode's own [`subagent` tool](https://opencode.ai/docs/agents) hands back a child's final
|
|
44
44
|
answer. Use it when one result is all you need. Use courier for more:
|
|
45
45
|
|
|
46
46
|
- **Messages mid-run.** A child reports progress or asks its parent at any time, and its parent
|
|
47
47
|
passes a finding that changes the other parts on to the children it affects.
|
|
48
|
-
- **Waking an idle session.** A message
|
|
48
|
+
- **Waking an idle session.** A message or a schedule starts a new turn.
|
|
49
49
|
- **Parallel edits.** `isolate: true` gives a child its own git worktree and branch to merge.
|
|
50
50
|
- **Asks reach you.** A child's permission requests and questions come to you through the top
|
|
51
51
|
session. Only you approve a request, in OpenCode's own prompt.
|
|
@@ -63,15 +63,16 @@ without reporting is reminded once, in the same turn, and its parent is told onl
|
|
|
63
63
|
not report. A message that does not wake its session is sent once more, then shown as `stalled`. On
|
|
64
64
|
OpenCode 2.2.0 a parent's system prompt carries one line per child with its state, kept current,
|
|
65
65
|
so it need not look. Children, held reports and scheduled messages survive a restart. The `courier-orchestrate` skill, listed in every session, tells a model when to
|
|
66
|
-
split a task and how to check and merge the parts
|
|
66
|
+
split a task and how to check and merge the parts; how to plan and review them is left to
|
|
67
|
+
your own skills. Every detail: [the reference](docs/reference.md).
|
|
67
68
|
|
|
68
69
|
## Install
|
|
69
70
|
|
|
70
71
|
Requires OpenCode V2: the `opencode` command from `@opencode/cli`. Each release is tested against
|
|
71
|
-
one OpenCode version; this one is tested on **OpenCode 2.2.
|
|
72
|
+
one OpenCode version; this one is tested on **OpenCode 2.2.6** (check with `opencode --version`).
|
|
72
73
|
|
|
73
74
|
```bash
|
|
74
|
-
npm install -g @opencode/cli@2.2.
|
|
75
|
+
npm install -g @opencode/cli@2.2.6
|
|
75
76
|
opencode plugin add opencode-courier
|
|
76
77
|
```
|
|
77
78
|
|
|
@@ -84,13 +85,13 @@ name, without a version: OpenCode offers updates only for an entry without an ex
|
|
|
84
85
|
<!-- compat-table:start rows=3: generated by script/compat-table.ts from the release tags -->
|
|
85
86
|
| opencode-courier | OpenCode V2 (`opencode` and `@opencode/plugin`) |
|
|
86
87
|
|---|---|
|
|
87
|
-
| 0.
|
|
88
|
+
| 0.14.0 | 2.2.6 |
|
|
89
|
+
| 0.13.7, 0.13.8 | 2.2.5 |
|
|
88
90
|
| 0.13.6 | 2.2.4 |
|
|
89
|
-
| 0.13.5 | 2.2.3 |
|
|
90
91
|
<!-- compat-table:end -->
|
|
91
92
|
|
|
92
93
|
Every release: [the full table](docs/reference.md#the-opencode-version). The server part loads on
|
|
93
|
-
any V2 host from 2.0.4 on. The terminal part is tested on OpenCode 2.2.
|
|
94
|
+
any V2 host from 2.0.4 on. The terminal part is tested on OpenCode 2.2.6 only.
|
|
94
95
|
|
|
95
96
|
## Options
|
|
96
97
|
|
|
@@ -110,7 +111,6 @@ Set the server options on the plugin's entry in the **global** config,
|
|
|
110
111
|
| `maxTotal` | `30` | Live spawned sessions in one tree. |
|
|
111
112
|
| `memory` | `true` | Keep a record of each session tree for later orchestrators ([memory](docs/reference.md#memory)). |
|
|
112
113
|
| `nativeChildren` | `false` | The default for a spawn that does not set `native`: `true` creates non-isolated children as OpenCode subagents ([native children](docs/reference.md#native-child-sessions)). |
|
|
113
|
-
| `webhook` | off | Start the webhook receiver ([webhooks](docs/reference.md#webhooks)). |
|
|
114
114
|
|
|
115
115
|
The terminal part reads its options only from `~/.config/opencode/cli.json`, in the same entry
|
|
116
116
|
form. Its one option, `"animations": false`, keeps the tree panel still.
|
|
@@ -126,7 +126,6 @@ form. Its one option, `"animations": false`, keeps the tree panel still.
|
|
|
126
126
|
| `courier_cleanup` | Removes an isolated child's worktree and forgets the child. |
|
|
127
127
|
| `courier_answer` | Proposes an answer to a child's permission request, or passes on your answer to its question. |
|
|
128
128
|
| `courier_later` / `courier_cancel` | Schedules a message for a session, or drops one. |
|
|
129
|
-
| `courier_subscribe` / `courier_unsubscribe` | Subscribes a session to webhook deliveries for a topic, or drops it. |
|
|
130
129
|
| `courier_recall` / `courier_note` | Reads what earlier trees in this project did, or writes this tree's one-line lesson. |
|
|
131
130
|
|
|
132
131
|
Parameters and behaviour: [the reference](docs/reference.md#the-tools).
|
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
import { Plugin } from "@opencode/plugin";
|
|
2
2
|
import { Plugin as EffectPlugin } from "@opencode/plugin/effect";
|
|
3
|
-
/**
|
|
4
|
-
* The courier tools and webhook receiver, as a promise plugin joining the hub that runs the scheduler
|
|
5
|
-
* and the watcher; its member carries the location by which the question relay finds its ports.
|
|
6
|
-
*/
|
|
7
3
|
export declare const courier: () => Plugin.Plugin;
|
|
8
|
-
/**
|
|
9
|
-
* The plugin OpenCode loads: an Effect plugin, since the question relay wraps OpenCode's question tool
|
|
10
|
-
* and only the Effect API makes its call interruptible; the rest is the promise plugin, adapted.
|
|
11
|
-
*/
|
|
12
4
|
declare const _default: EffectPlugin.Plugin<import("effect/Scope").Scope>;
|
|
13
5
|
export default _default;
|
package/dist/index.js
CHANGED
|
@@ -10,50 +10,27 @@ import { addSchedulingTools, cancelScheduled, dueJob, isScheduled } from "./sche
|
|
|
10
10
|
import { addPersonTools, asksPermission, asksQuestion, formsShown, joinRelay, noticeCutOff, pendingOf, pendingQuestions, personEvents, portsAt, registerPersonRpc, relayQuestions, } from "./person/index.js";
|
|
11
11
|
import { addMemoryTools, lessonsOf, memoryEvents, NOTE_TOOL, readMemory, RECALL_TOOL, registerMemoryRpc, sweepRecords } from "./memory/index.js";
|
|
12
12
|
import { addSkill } from "./skill.js";
|
|
13
|
-
import { addTreeTools, deliverReleased, wakeMissed, dirtyOf, headOf, inspectWorktree, onIdle, pruneExpired, readLimits, readNativeChildren, registerTreeRpc, shapeContext,
|
|
14
|
-
import { addWebhookTools, joinReceiver, readConfig, receiving, subscribedSince, } from "./webhook/index.js";
|
|
15
|
-
/** The spawns under way in the process, outside the hub, so spawns in other locations, copies and hub versions count them. */
|
|
13
|
+
import { addTreeTools, deliverReleased, wakeMissed, dirtyOf, headOf, inspectWorktree, onIdle, pruneExpired, readLimits, readNativeChildren, registerTreeRpc, shapeContext, treeEvents, waitsOn, } from "./tree/index.js";
|
|
16
14
|
const gate = sharedState("spawning", () => ({ reserved: new Set(), turn: Promise.resolve() }));
|
|
17
|
-
/** The type of each item enqueued for a spawned session, outside the hub like the gate. */
|
|
18
15
|
const inbox = sharedState("inbox", () => new Map());
|
|
19
|
-
/** The forms shown now, which the person events keep and the footer and tree panel count; outside the hub like the inbox. */
|
|
20
16
|
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
17
|
const memoryState = sharedState("memory", () => ({ windows: new Map(), off: new Set(), tops: new Set(), seen: new Set() }));
|
|
23
|
-
/**
|
|
24
|
-
* What each tick of the scheduler does, in order, through the first loaded member, of whichever copy:
|
|
25
|
-
* due `courier_later` messages, released join groups, then missed wakes. Every hub-v2 member's `later` is its LaterPorts.
|
|
26
|
-
*/
|
|
27
18
|
const jobs = [
|
|
28
19
|
dueJob,
|
|
29
20
|
(owner, claimed) => deliverReleased(owner.later, claimed),
|
|
30
21
|
(owner, claimed) => wakeMissed(owner.later, claimed),
|
|
31
22
|
];
|
|
32
23
|
const { join, track, nudge } = openInProcess(jobs);
|
|
33
|
-
/** The permission domains of the loaded instances: every member's is its location's, whichever copy joined it. */
|
|
34
24
|
const permissions = domains;
|
|
35
|
-
// One set of handled events and of told permission requests for every instance in the process, since
|
|
36
|
-
// several subscriptions and copies see the same events. The scheduler's claim set is the hub's own.
|
|
37
25
|
const { watched } = hub;
|
|
38
26
|
const watchState = { seen: watched.seen, inbox };
|
|
39
27
|
const personState = { ...watched, forms: hub.forms, shown };
|
|
40
|
-
|
|
41
|
-
* What a session waits on outside the tree, for the silent-turn rule, cheapest first: a request of its
|
|
42
|
-
* own to the person, then a scheduled message or a webhook it subscribed to since its last prompt.
|
|
43
|
-
*/
|
|
44
|
-
const outside = waitsOn(asksPermission, asksQuestion, together(isScheduled, subscribedSince));
|
|
45
|
-
/** What the hub's subscriptions do with each event of OpenCode's, module by module, in this order. */
|
|
28
|
+
const outside = waitsOn(asksPermission, asksQuestion, isScheduled);
|
|
46
29
|
const eventModules = [treeEvents(watchState, outside), personEvents(personState), memoryEvents(memoryState)];
|
|
47
|
-
/**
|
|
48
|
-
* The courier tools and webhook receiver, as a promise plugin joining the hub that runs the scheduler
|
|
49
|
-
* and the watcher; its member carries the location by which the question relay finds its ports.
|
|
50
|
-
*/
|
|
51
30
|
export const courier = () => Plugin.define({
|
|
52
31
|
id: "courier",
|
|
53
32
|
setup: async (ctx) => {
|
|
54
33
|
const log = (message) => console.error(message);
|
|
55
|
-
// One line per load when this OpenCode is not the version the plugin was built against; the
|
|
56
|
-
// tools are registered all the same, and no tool result says so.
|
|
57
34
|
try {
|
|
58
35
|
const notice = versionNotice(builtVersions(), ctx.app);
|
|
59
36
|
if (notice)
|
|
@@ -85,7 +62,6 @@ export const courier = () => Plugin.define({
|
|
|
85
62
|
roles,
|
|
86
63
|
nudge,
|
|
87
64
|
log,
|
|
88
|
-
// What a spawned session gets besides its role: the memory's lessons entry, handed to the tree here as `outside` hands it the `Waits` suppliers.
|
|
89
65
|
entries: () => lessonsOf(memoryPorts),
|
|
90
66
|
session: ctx.session,
|
|
91
67
|
agent: ctx.agent,
|
|
@@ -124,36 +100,24 @@ export const courier = () => Plugin.define({
|
|
|
124
100
|
newID: () => `later_${randomUUID()}`,
|
|
125
101
|
log,
|
|
126
102
|
};
|
|
127
|
-
const hooks = { storage: ctx.storage, session: ctx.session, now: Date.now, log: later.log };
|
|
128
|
-
let webhook;
|
|
129
|
-
try {
|
|
130
|
-
webhook = readConfig(ctx.options);
|
|
131
|
-
}
|
|
132
|
-
catch (error) {
|
|
133
|
-
later.log(`courier webhook: not started: ${error instanceof Error ? error.message : String(error)}`);
|
|
134
|
-
}
|
|
135
103
|
const stopPorts = { courier: ports, cancelScheduled: (sessionID) => cancelScheduled(later, sessionID), cleanup: cleanupPorts };
|
|
136
|
-
// The tools in a fixed order: the tree's, then the person's, the scheduled messages', the webhooks' and the memory's.
|
|
137
104
|
await ctx.tool.transform((tools) => {
|
|
138
105
|
addTreeTools(tools, { courier: ports, cleanup: cleanupPorts, stop: stopPorts });
|
|
139
106
|
addPersonTools(tools, { answer: answerPorts, questions: questionPorts, waiting: watched.waiting });
|
|
140
107
|
addSchedulingTools(tools, later);
|
|
141
|
-
addWebhookTools(tools, { hooks, receiving });
|
|
142
108
|
addMemoryTools(tools, { memory: memoryPorts, state: memoryState });
|
|
143
109
|
});
|
|
144
110
|
await ctx.skill.transform(addSkill);
|
|
145
|
-
// What the TUI part reads and its stop; a host that cannot register it still gets the rest, the tools included.
|
|
146
111
|
await Promise.resolve()
|
|
147
112
|
.then(() => registerTreeRpc(ctx.rpc, stopPorts))
|
|
148
113
|
.then(() => registerPersonRpc(ctx.rpc, { questions: questionPorts, permissions }))
|
|
149
114
|
.then(() => registerMemoryRpc(ctx.rpc, memoryPorts))
|
|
150
115
|
.catch((error) => log(`courier rpc: not registered, the TUI part shows nothing and cannot stop: ${String(error)}`));
|
|
151
|
-
//
|
|
116
|
+
// a host before 2.2.0 has no instructions entries (api-notes)
|
|
152
117
|
const instructions = ctx.session.instructions;
|
|
153
118
|
await ctx.session.hook("context", (event) => shapeContext({ storage: ctx.storage, limits, roles, instructions, hiddenFromLeaf: [RECALL_TOOL, NOTE_TOOL], log }, event));
|
|
154
119
|
const pruned = pruneExpired(ctx.storage, Date.now()).catch((error) => log(`courier roster prune: ${String(error)}`));
|
|
155
120
|
const swept = sweepRecords(ctx.storage, Date.now()).catch((error) => log(`courier memory sweep: ${String(error)}`));
|
|
156
|
-
// 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.
|
|
157
121
|
const windowed = ports
|
|
158
122
|
.models()
|
|
159
123
|
.then((models) => models.forEach(({ providerID, id, context }) => memoryState.windows.set(`${providerID}/${id}`, { providerID, id, context })))
|
|
@@ -171,12 +135,10 @@ export const courier = () => Plugin.define({
|
|
|
171
135
|
log: later.log,
|
|
172
136
|
nudge,
|
|
173
137
|
};
|
|
174
|
-
// A spawned session about to go idle owing a report is reminded once, in the same execution. The hook is
|
|
175
|
-
// experimental, and a host before 2.2.4 lacks it: the rest loads all the same, and its parents are told as before.
|
|
176
138
|
await Promise.resolve()
|
|
139
|
+
// a host before 2.2.4 takes this hook and never runs it (api-notes)
|
|
177
140
|
.then(() => ctx.session.hook("experimental.idle", (event) => onIdle(watchPorts, watchState.seen, outside, event).then(() => undefined)))
|
|
178
141
|
.catch((error) => log(`courier idle: not registered, a child that ends without a report is not reminded: ${String(error)}`));
|
|
179
|
-
// The hub's subscriptions to OpenCode's events, one and a standby, start with this copy's modules.
|
|
180
142
|
watchFromHub(hub, eventModules);
|
|
181
143
|
const member = {
|
|
182
144
|
directory: ctx.location.directory,
|
|
@@ -188,34 +150,25 @@ export const courier = () => Plugin.define({
|
|
|
188
150
|
log,
|
|
189
151
|
};
|
|
190
152
|
const leaveHub = join(member);
|
|
191
|
-
// The leave waits for them, so they do not run on against a location that is closing.
|
|
192
153
|
track(member, pruned);
|
|
193
154
|
track(member, noticed);
|
|
194
155
|
track(member, swept);
|
|
195
156
|
track(member, windowed);
|
|
196
|
-
const leave = webhook ? joinReceiver(webhook, hooks) : undefined;
|
|
197
157
|
return async () => {
|
|
198
|
-
// Waits, for a moment at most, for the work this instance leaves behind, and on the last
|
|
199
|
-
// leave for the scheduler's owner key to be released, so another server takes over at once.
|
|
200
158
|
const leftHub = leaveHub();
|
|
201
159
|
if (!memory)
|
|
202
160
|
memoryState.off.delete(ctx.location.directory);
|
|
203
161
|
leaveRelay();
|
|
204
|
-
await leave?.();
|
|
205
162
|
await leftHub;
|
|
206
163
|
};
|
|
207
164
|
},
|
|
208
165
|
});
|
|
209
|
-
|
|
210
|
-
* The plugin OpenCode loads: an Effect plugin, since the question relay wraps OpenCode's question tool
|
|
211
|
-
* and only the Effect API makes its call interruptible; the rest is the promise plugin, adapted.
|
|
212
|
-
*/
|
|
166
|
+
// an Effect plugin, so the question call can be interrupted (api-notes)
|
|
213
167
|
export default EffectPlugin.define({
|
|
214
168
|
id: "courier",
|
|
215
169
|
effect: (host) => Effect.gen(function* () {
|
|
216
170
|
yield* fromPromise(courier()).effect(host);
|
|
217
|
-
//
|
|
218
|
-
// member, bound now, while it stays loaded.
|
|
171
|
+
// the adapter hands the promise plugin this same location (api-notes)
|
|
219
172
|
yield* relayQuestions(host, portsAt(host.location));
|
|
220
173
|
}),
|
|
221
174
|
});
|
|
@@ -1,28 +1,14 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* What every module of the plugin shares of the protocol with the models: the envelope a message arrives in,
|
|
3
|
-
* the phrase that ends a turn, the statuses of a report, the split line and the skill's id, and how outside
|
|
4
|
-
* text is clipped and defused.
|
|
5
|
-
*/
|
|
6
|
-
/** Closes a tool result after which the caller most likely has nothing left to do. */
|
|
7
1
|
export declare const END_TURN = "If nothing else is left to do now, end your turn by replying without calling more tools.";
|
|
8
|
-
/** The statuses a report carries: the task is done, partly done, waits on the parent, or failed. */
|
|
9
2
|
export declare const STATUSES: readonly ["done", "partial", "blocked", "failed"];
|
|
10
3
|
export type Status = (typeof STATUSES)[number];
|
|
11
4
|
export declare const isStatus: (value: unknown) => value is Status;
|
|
12
|
-
/** The one line a session states its split decision in, so a transcript tells a decision from one never made. */
|
|
13
5
|
export declare const SPLIT_LINE = "split: no, because \u2026; considered: <the parts you weighed> or split: <n> parts: \u2026 (one phrase per part)";
|
|
14
|
-
/** The third form, for the root alone: it hands a heavy task whole to one session, to keep its own context. */
|
|
15
6
|
export declare const ONE_PART_LINE = "split: 1 part: <the task>, to keep my context";
|
|
16
|
-
/** `SPLIT_LINE` with the root's third form: the root's pointer offers it, a child's brief and role part never do. */
|
|
17
7
|
export declare const ROOT_SPLIT_LINE = "split: no, because \u2026; considered: <the parts you weighed> or split: <n> parts: \u2026 (one phrase per part) or split: 1 part: <the task>, to keep my context";
|
|
18
8
|
export declare const SKILL_ID = "courier-orchestrate";
|
|
19
9
|
export declare function envelope(from: string, message: string, attributes?: Record<string, string>): string;
|
|
20
10
|
export declare function clipText(text: string, max?: number): string;
|
|
21
|
-
/** `clipText` cut at the last space within `max` instead, so no word is cut in two, unless that keeps less than half of `max`. */
|
|
22
11
|
export declare function clipWords(text: string, max: number): string;
|
|
23
|
-
/** Defuses `<courier` and `</courier>` in outside text, so it cannot close the envelope or forge another. */
|
|
24
12
|
export declare function defuse(text: string): string;
|
|
25
|
-
/** Defuses `<context` and `</context>` in text put in a session instruction entry, so it cannot close the entry's block or open another. */
|
|
26
13
|
export declare function defuseBlock(text: string): string;
|
|
27
|
-
/** A readable message for a failed courier call; OpenCode's own errors can carry an empty message. */
|
|
28
14
|
export declare function describeFailure(tool: string, error: unknown): Error;
|
package/dist/kernel/protocol.js
CHANGED
|
@@ -1,18 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* What every module of the plugin shares of the protocol with the models: the envelope a message arrives in,
|
|
3
|
-
* the phrase that ends a turn, the statuses of a report, the split line and the skill's id, and how outside
|
|
4
|
-
* text is clipped and defused.
|
|
5
|
-
*/
|
|
6
|
-
/** Closes a tool result after which the caller most likely has nothing left to do. */
|
|
7
1
|
export const END_TURN = "If nothing else is left to do now, end your turn by replying without calling more tools.";
|
|
8
|
-
/** The statuses a report carries: the task is done, partly done, waits on the parent, or failed. */
|
|
9
2
|
export const STATUSES = ["done", "partial", "blocked", "failed"];
|
|
10
3
|
export const isStatus = (value) => typeof value === "string" && STATUSES.includes(value);
|
|
11
|
-
/** The one line a session states its split decision in, so a transcript tells a decision from one never made. */
|
|
12
4
|
export const SPLIT_LINE = "split: no, because …; considered: <the parts you weighed> or split: <n> parts: … (one phrase per part)";
|
|
13
|
-
/** The third form, for the root alone: it hands a heavy task whole to one session, to keep its own context. */
|
|
14
5
|
export const ONE_PART_LINE = "split: 1 part: <the task>, to keep my context";
|
|
15
|
-
/** `SPLIT_LINE` with the root's third form: the root's pointer offers it, a child's brief and role part never do. */
|
|
16
6
|
export const ROOT_SPLIT_LINE = `${SPLIT_LINE} or ${ONE_PART_LINE}`;
|
|
17
7
|
export const SKILL_ID = "courier-orchestrate";
|
|
18
8
|
export function envelope(from, message, attributes = {}) {
|
|
@@ -21,29 +11,25 @@ export function envelope(from, message, attributes = {}) {
|
|
|
21
11
|
.join("");
|
|
22
12
|
return `<courier from="${from}"${extra}>\n${message}\n</courier>`;
|
|
23
13
|
}
|
|
24
|
-
/** Longest piece of free text (a review body, a generic payload) copied into a delivered message. */
|
|
25
14
|
const MAX_TEXT = 1500;
|
|
26
15
|
const clipped = (text, kept) => `${kept}… [${text.length - kept.length} more characters]`;
|
|
27
16
|
export function clipText(text, max = MAX_TEXT) {
|
|
28
17
|
return text.length > max ? clipped(text, text.slice(0, max)) : text;
|
|
29
18
|
}
|
|
30
|
-
/** `clipText` cut at the last space within `max` instead, so no word is cut in two, unless that keeps less than half of `max`. */
|
|
31
19
|
export function clipWords(text, max) {
|
|
32
20
|
if (text.length <= max)
|
|
33
21
|
return text;
|
|
34
22
|
const space = text.lastIndexOf(" ", max);
|
|
35
23
|
return clipped(text, (space >= max / 2 ? text.slice(0, space) : text.slice(0, max)).trimEnd());
|
|
36
24
|
}
|
|
37
|
-
/** Defuses `<courier` and `</courier>` in outside text, so it cannot close the envelope or forge another. */
|
|
38
25
|
export function defuse(text) {
|
|
39
26
|
return text.replace(/<(\/?)(courier)/gi, "<$1$2");
|
|
40
27
|
}
|
|
41
|
-
/** Defuses `<context` and `</context>` in text put in a session instruction entry, so it cannot close the entry's block or open another. */
|
|
42
28
|
export function defuseBlock(text) {
|
|
43
29
|
return text.replace(/<(\/?)(context)/gi, "<$1$2");
|
|
44
30
|
}
|
|
45
|
-
/** A readable message for a failed courier call; OpenCode's own errors can carry an empty message. */
|
|
46
31
|
export function describeFailure(tool, error) {
|
|
32
|
+
// OpenCode errors can have an empty message (api-notes)
|
|
47
33
|
const tagged = error;
|
|
48
34
|
const message = (typeof tagged?.message === "string" && tagged.message) ||
|
|
49
35
|
[tagged?._tag, tagged?.sessionID].filter((item) => typeof item === "string").join(" ") ||
|
package/dist/kernel/tool.d.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
1
|
import type { Plugin } from "@opencode/plugin";
|
|
2
|
-
/** The editor OpenCode hands a tool transform. */
|
|
3
2
|
export type ToolEditor = Parameters<Parameters<Plugin.Context["tool"]["transform"]>[0]>[0];
|
|
4
|
-
/** A `.catch` handler that rethrows a failure of `tool` as the error the model reads. */
|
|
5
3
|
export declare const rethrow: (tool: string) => (error: unknown) => never;
|
package/dist/kernel/tool.js
CHANGED
package/dist/memory/domain.d.ts
CHANGED
|
@@ -1,143 +1,81 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The memory's pure rules, on plain data: the option, a record's shape, each part's status, the split line and its revisions,
|
|
3
|
-
* its reads and pin, the 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
1
|
import { type Status } from "../kernel/protocol.js";
|
|
6
|
-
/** How many unpinned records a project keeps: those with a lesson first, then the newest by `movedAt`. */
|
|
7
2
|
export declare const MAX_RECORDS = 30;
|
|
8
|
-
/** Unpinned records that last moved longer ago than this are dropped when the plugin loads. */
|
|
9
3
|
export declare const RETENTION_MS: number;
|
|
10
|
-
/** A record's `split` while its top has stated no split line. */
|
|
11
4
|
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
5
|
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, `gone` before it settled, `stalled` on a message it will not take, or `out`: it owes a report.
|
|
17
|
-
*/
|
|
18
6
|
export declare const PART_STATUSES: readonly ["done", "partial", "blocked", "failed", "interrupted", "stopped", "gone", "stalled", "out"];
|
|
19
7
|
export type PartStatus = (typeof PART_STATUSES)[number];
|
|
20
8
|
export declare const isPartStatus: (value: unknown) => value is PartStatus;
|
|
21
|
-
/** Whether a part has settled, as every part of a finished tree has: it neither owes a report nor is stalled. */
|
|
22
9
|
export declare const isSettled: (status: PartStatus) => status is "done" | "partial" | "blocked" | "failed" | "interrupted" | "stopped" | "gone";
|
|
23
|
-
/** One session started in a tree, as its record keeps it; `duration` is in milliseconds, `context` a share of its model's window. */
|
|
24
10
|
export interface PartRecord {
|
|
25
11
|
readonly sessionID: string;
|
|
26
12
|
readonly title: string;
|
|
27
|
-
/** 1 for a session the top started. */
|
|
28
13
|
readonly depth: number;
|
|
29
14
|
readonly isolated: boolean;
|
|
15
|
+
readonly subagent?: true;
|
|
30
16
|
readonly group?: string;
|
|
31
17
|
readonly status: PartStatus;
|
|
32
|
-
/** From its start to the end of its last turn, or to the refresh while it runs. */
|
|
33
18
|
readonly duration?: number;
|
|
34
19
|
readonly cost?: number;
|
|
35
|
-
/** How full its context was at the end, 0 to 1, when its model's window is known. */
|
|
36
20
|
readonly context?: number;
|
|
37
|
-
/** How many courier_spawn calls of its own failed, refused by a limit mostly. */
|
|
38
21
|
readonly refused?: number;
|
|
39
22
|
}
|
|
40
|
-
/** Whether and when the top called courier_recall (`at`, its first call), and how many calls the tree made in all. */
|
|
41
23
|
export interface Recalled {
|
|
42
24
|
readonly at?: number;
|
|
43
25
|
readonly calls: number;
|
|
44
26
|
}
|
|
45
|
-
/**
|
|
46
|
-
* The one line the tree's orchestrator wrote for the next one with courier_note: when, and from which depth when it was
|
|
47
|
-
* not the top's own session (1 for a session the top started).
|
|
48
|
-
*/
|
|
49
27
|
export interface Lesson {
|
|
50
28
|
readonly text: string;
|
|
51
29
|
readonly at: number;
|
|
52
30
|
readonly depth?: number;
|
|
53
31
|
}
|
|
54
|
-
/**
|
|
55
|
-
* How often other trees' courier_recall calls listed the record (`shown`) or gave it in full (`read`), and when one last
|
|
56
|
-
* did (`at`). Kept per reader tree and summed when read; a listing to the tree's own sessions counts nothing.
|
|
57
|
-
*/
|
|
58
32
|
export interface Reads {
|
|
59
33
|
readonly shown: number;
|
|
60
34
|
readonly read: number;
|
|
61
35
|
readonly at: number;
|
|
62
36
|
}
|
|
63
|
-
/** What a courier_recall call did with a record: listed it, or gave it in full. */
|
|
64
37
|
export type ReadKind = "shown" | "read";
|
|
65
|
-
/** The person's pin, set in the memory view: when. A pinned record is listed first and never swept. */
|
|
66
38
|
export interface Pinned {
|
|
67
39
|
readonly at: number;
|
|
68
40
|
}
|
|
69
|
-
/** How many revisions of the split line a record keeps, the newest. */
|
|
70
41
|
export declare const MAX_REVISIONS = 3;
|
|
71
|
-
/** A later split line the top stated, worded otherwise than the one before it: when, and how many parts the tree had then. */
|
|
72
42
|
export interface Revision {
|
|
73
43
|
readonly text: string;
|
|
74
44
|
readonly at: number;
|
|
75
45
|
readonly parts: number;
|
|
76
46
|
}
|
|
77
|
-
/** What is remembered of one tree: its top session and each session started in it, never a report's body or artifacts. */
|
|
78
47
|
export interface TreeRecord {
|
|
79
|
-
/** When the top session was created, epoch milliseconds. */
|
|
80
48
|
readonly startedAt: number;
|
|
81
|
-
/** When a turn in the tree last ended, or the record was first written. */
|
|
82
49
|
readonly movedAt: number;
|
|
83
50
|
readonly title: string;
|
|
84
|
-
/**
|
|
85
|
-
* The top's first split line, as `e2e/real-model-recursive.mjs` reads it, or `not stated`; a stated line takes precedence over a
|
|
86
|
-
* mention, which it replaces once, and is never replaced.
|
|
87
|
-
*/
|
|
88
51
|
readonly split: string;
|
|
89
|
-
/** Absent until the top states a split line worded otherwise than `split`; newest last, at most `MAX_REVISIONS`. */
|
|
90
52
|
readonly revisions?: ReadonlyArray<Revision>;
|
|
91
|
-
/**
|
|
92
|
-
* Whether, at the last refresh, no session in the tree ran or had a courier message waiting in its inbox for a turn, and
|
|
93
|
-
* every part had settled: none was out or stalled.
|
|
94
|
-
*/
|
|
95
53
|
readonly finished: boolean;
|
|
96
54
|
readonly recalled?: Recalled;
|
|
97
|
-
/** Absent until a session of the tree that is not a leaf wrote one; a second note replaces the first. */
|
|
98
55
|
readonly lesson?: Lesson;
|
|
99
|
-
/** Absent until another tree's courier_recall listed or read the record; kept beside the record, never written with it. */
|
|
100
56
|
readonly reads?: Reads;
|
|
101
|
-
/** Absent until the person pinned the record in the memory view; kept beside the record, never written with it. */
|
|
102
57
|
readonly pinned?: Pinned;
|
|
103
58
|
readonly parts: ReadonlyArray<PartRecord>;
|
|
104
59
|
}
|
|
105
|
-
/** A record with where it is kept: its project and its top session. */
|
|
106
60
|
export interface StoredRecord extends TreeRecord {
|
|
107
61
|
readonly projectID: string;
|
|
108
62
|
readonly sessionID: string;
|
|
109
63
|
}
|
|
110
|
-
/** The `memory` option: whether records are kept and listed; on unless `false`. */
|
|
111
64
|
export declare function readMemory(options: unknown): {
|
|
112
65
|
memory: boolean;
|
|
113
66
|
problems: string[];
|
|
114
67
|
};
|
|
115
|
-
/** 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. */
|
|
116
68
|
export declare function splitLineOf(text: string): string | undefined;
|
|
117
|
-
/** Whether a split line is stated, beginning with `split:` after any markdown, not a passing mention nor `not stated`. */
|
|
118
69
|
export declare const isStated: (line: string) => boolean;
|
|
119
|
-
/** The record's `split` once `text` is read: kept once stated; a mention gives way to the first stated line; `not stated` to either. */
|
|
120
70
|
export declare function splitOf(recorded: string, text: string): string;
|
|
121
|
-
/** Every line of a text that begins with `split:` after any markdown, trimmed: a passing mention of a split is none. */
|
|
122
71
|
export declare const splitLinesOf: (text: string) => string[];
|
|
123
|
-
/** A split line the top's replies state, with when its reply was created when the message says. */
|
|
124
72
|
export interface Stated {
|
|
125
73
|
readonly text: string;
|
|
126
74
|
readonly at?: number;
|
|
127
75
|
}
|
|
128
|
-
/**
|
|
129
|
-
* The revisions once the split lines `stated` are read at a refresh at `at` with `parts` parts: those `statedSince` the line recorded
|
|
130
|
-
* last, each appended when its words differ from the line before, so a re-read appends nothing.
|
|
131
|
-
*/
|
|
132
76
|
export declare function revise(previous: Pick<TreeRecord, "split" | "revisions">, stated: ReadonlyArray<Stated>, at: number, parts: number): Revision[] | undefined;
|
|
133
|
-
/**
|
|
134
|
-
* The split and revisions once the stated line `found` replaces a mention: when `found` is among the revisions (recorded beside the
|
|
135
|
-
* mention before #294), the first of them was stated first and is `split`, the rest stay; else `found` is, beside them all.
|
|
136
|
-
*/
|
|
137
77
|
export declare function replacingMention(found: string, revisions: ReadonlyArray<Revision> | undefined): Pick<TreeRecord, "split" | "revisions">;
|
|
138
|
-
/** A split line as the listing shows it: one line, without its block marks, the asterisks wrapping it or a span in it, nor an underscore or code span wrapping it. */
|
|
139
78
|
export declare function splitProse(split: string): string;
|
|
140
|
-
/** What a part's status is read from: whether courier_stop stopped it, whether it is stalled, and its report state. */
|
|
141
79
|
export interface Standing {
|
|
142
80
|
readonly stopped: boolean;
|
|
143
81
|
readonly stalled: boolean;
|
|
@@ -148,26 +86,16 @@ export interface Standing {
|
|
|
148
86
|
readonly status?: Status;
|
|
149
87
|
};
|
|
150
88
|
}
|
|
151
|
-
/** A part's status by `Standing`; one whose report state is not known is still out. */
|
|
152
89
|
export declare function partStatus({ stopped, stalled, report }: Standing): PartStatus;
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
90
|
+
export declare function subagentStatus({ running, stalled, outcome }: {
|
|
91
|
+
readonly running: boolean;
|
|
92
|
+
readonly stalled: boolean;
|
|
93
|
+
readonly outcome?: string;
|
|
94
|
+
}): PartStatus;
|
|
157
95
|
export declare function unreadPart(read: PartRecord, last: PartRecord | undefined, gone: boolean): PartRecord;
|
|
158
|
-
/**
|
|
159
|
-
* The parts of a refreshed record: those recorded before, in their order, each as `current` has it (read now, or found
|
|
160
|
-
* gone), else as last recorded, then those first seen now, in the order the tree is drawn.
|
|
161
|
-
*/
|
|
162
96
|
export declare function mergeParts(previous: ReadonlyArray<PartRecord>, current: ReadonlyArray<PartRecord>): PartRecord[];
|
|
163
|
-
/** `recalled` after one more call at `at`, the top's first call noted when `byTop`. */
|
|
164
97
|
export declare function noteRecall(recalled: Recalled | undefined, byTop: boolean, at: number): Recalled;
|
|
165
|
-
/** A text as the one line a lesson is: whitespace and line breaks collapsed to spaces, trimmed. */
|
|
166
98
|
export declare const oneLine: (text: string) => string;
|
|
167
|
-
/**
|
|
168
|
-
* A hand-written lesson's text as one line, clipped at `LESSON_MAX`; `clipped` is the length it had when it was clipped.
|
|
169
|
-
* courier_note does not clip: it refuses a line over `LESSON_MAX` (`note.ts`).
|
|
170
|
-
*/
|
|
171
99
|
export declare function lessonText(text: string): {
|
|
172
100
|
text: string;
|
|
173
101
|
clipped?: undefined;
|
|
@@ -175,17 +103,10 @@ export declare function lessonText(text: string): {
|
|
|
175
103
|
text: string;
|
|
176
104
|
clipped: number;
|
|
177
105
|
};
|
|
178
|
-
/** One reader tree's `reads` after courier_recall listed the record or gave it in full at `at`. */
|
|
179
106
|
export declare function noteRead(reads: Reads | undefined, kind: ReadKind, at: number): Reads;
|
|
180
|
-
/** The reads of every reader tree as one: the counts summed, `at` the latest; none without a reader. */
|
|
181
107
|
export declare function sumReads(reads: ReadonlyArray<Reads>): Reads | undefined;
|
|
182
|
-
/**
|
|
183
|
-
* The records a project keeps at `now`: every pinned one, whatever its age, and of the rest the first `MAX_RECORDS` by
|
|
184
|
-
* whether each has a lesson, then by `movedAt`, none older than `RETENTION_MS`; the rest are dropped.
|
|
185
|
-
*/
|
|
186
108
|
export declare function keep<T extends Pick<StoredRecord, "movedAt" | "lesson" | "pinned">>(records: ReadonlyArray<T>, now: number): {
|
|
187
109
|
kept: T[];
|
|
188
110
|
dropped: T[];
|
|
189
111
|
};
|
|
190
|
-
/** Records as courier_recall and the memory view list them: the pinned first, then by when each last moved, newest first. */
|
|
191
112
|
export declare const pinnedFirst: <T extends Pick<StoredRecord, "movedAt" | "pinned">>(records: ReadonlyArray<T>) => T[];
|