opencode-courier 0.13.8 → 0.15.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.
Files changed (163) hide show
  1. package/README.md +7 -9
  2. package/dist/index.d.ts +0 -8
  3. package/dist/index.js +7 -54
  4. package/dist/kernel/protocol.d.ts +3 -17
  5. package/dist/kernel/protocol.js +3 -17
  6. package/dist/kernel/tool.d.ts +0 -2
  7. package/dist/kernel/tool.js +0 -1
  8. package/dist/memory/domain.d.ts +6 -85
  9. package/dist/memory/domain.js +7 -63
  10. package/dist/memory/events.d.ts +0 -11
  11. package/dist/memory/events.js +1 -12
  12. package/dist/memory/forget.d.ts +0 -5
  13. package/dist/memory/forget.js +0 -4
  14. package/dist/memory/index.js +0 -2
  15. package/dist/memory/lessons.d.ts +1 -9
  16. package/dist/memory/lessons.js +2 -11
  17. package/dist/memory/note.d.ts +2 -11
  18. package/dist/memory/note.js +5 -11
  19. package/dist/memory/pin.d.ts +0 -5
  20. package/dist/memory/pin.js +0 -4
  21. package/dist/memory/ports.d.ts +2 -22
  22. package/dist/memory/recall.d.ts +0 -5
  23. package/dist/memory/recall.js +0 -12
  24. package/dist/memory/record.d.ts +0 -4
  25. package/dist/memory/record.js +8 -31
  26. package/dist/memory/rpc-handlers.d.ts +3 -6
  27. package/dist/memory/rpc-handlers.js +0 -6
  28. package/dist/memory/rpc.d.ts +6 -19
  29. package/dist/memory/rpc.js +1 -16
  30. package/dist/memory/store.d.ts +0 -5
  31. package/dist/memory/store.js +1 -16
  32. package/dist/memory/sweep.d.ts +0 -4
  33. package/dist/memory/sweep.js +0 -4
  34. package/dist/memory/text.d.ts +1 -26
  35. package/dist/memory/text.js +8 -38
  36. package/dist/memory/tools.d.ts +0 -2
  37. package/dist/memory/tools.js +4 -6
  38. package/dist/memory/tui.d.ts +1 -35
  39. package/dist/person/events.d.ts +0 -16
  40. package/dist/person/events.js +3 -30
  41. package/dist/person/form.d.ts +0 -9
  42. package/dist/person/form.js +0 -10
  43. package/dist/person/index.d.ts +0 -2
  44. package/dist/person/permission.d.ts +0 -34
  45. package/dist/person/permission.js +2 -28
  46. package/dist/person/ports.d.ts +0 -16
  47. package/dist/person/ports.js +0 -1
  48. package/dist/person/question/answer.d.ts +0 -28
  49. package/dist/person/question/answer.js +0 -49
  50. package/dist/person/question/index.js +0 -2
  51. package/dist/person/question/lifecycle.d.ts +0 -26
  52. package/dist/person/question/lifecycle.js +1 -32
  53. package/dist/person/question/pure.d.ts +0 -11
  54. package/dist/person/question/pure.js +0 -11
  55. package/dist/person/question/relay.d.ts +0 -1
  56. package/dist/person/question/relay.js +5 -67
  57. package/dist/person/question/runtime.d.ts +0 -14
  58. package/dist/person/question/runtime.js +1 -26
  59. package/dist/person/question/shared.d.ts +0 -17
  60. package/dist/person/question/shared.js +0 -20
  61. package/dist/person/question/types.d.ts +0 -44
  62. package/dist/person/rpc-handlers.d.ts +0 -7
  63. package/dist/person/rpc-handlers.js +0 -6
  64. package/dist/person/rpc.d.ts +0 -20
  65. package/dist/person/rpc.js +0 -16
  66. package/dist/person/text.d.ts +0 -45
  67. package/dist/person/text.js +2 -54
  68. package/dist/person/tools.d.ts +0 -3
  69. package/dist/person/tools.js +3 -4
  70. package/dist/person/tui.d.ts +0 -21
  71. package/dist/platform/bounded.d.ts +0 -8
  72. package/dist/platform/bounded.js +0 -8
  73. package/dist/platform/events.d.ts +0 -18
  74. package/dist/platform/events.js +3 -22
  75. package/dist/platform/hub.d.ts +0 -113
  76. package/dist/platform/hub.js +1 -76
  77. package/dist/platform/json.d.ts +0 -3
  78. package/dist/platform/json.js +1 -3
  79. package/dist/platform/storage.d.ts +0 -7
  80. package/dist/platform/storage.js +0 -6
  81. package/dist/platform/version.d.ts +0 -9
  82. package/dist/platform/version.js +0 -8
  83. package/dist/rpc.js +0 -1
  84. package/dist/scheduling/index.d.ts +0 -1
  85. package/dist/scheduling/index.js +0 -1
  86. package/dist/scheduling/later.d.ts +0 -11
  87. package/dist/scheduling/later.js +0 -14
  88. package/dist/scheduling/text.d.ts +0 -1
  89. package/dist/scheduling/text.js +0 -1
  90. package/dist/scheduling/tools.d.ts +0 -1
  91. package/dist/scheduling/tools.js +6 -8
  92. package/dist/skill.d.ts +1 -9
  93. package/dist/skill.js +40 -97
  94. package/dist/tree/cleanup.d.ts +0 -20
  95. package/dist/tree/cleanup.js +0 -26
  96. package/dist/tree/context.d.ts +0 -5
  97. package/dist/tree/context.js +17 -13
  98. package/dist/tree/domain.d.ts +0 -64
  99. package/dist/tree/domain.js +7 -45
  100. package/dist/tree/events.d.ts +5 -36
  101. package/dist/tree/events.js +23 -55
  102. package/dist/tree/forget.d.ts +0 -15
  103. package/dist/tree/forget.js +0 -18
  104. package/dist/tree/group.d.ts +0 -22
  105. package/dist/tree/group.js +6 -39
  106. package/dist/tree/index.d.ts +2 -2
  107. package/dist/tree/index.js +1 -3
  108. package/dist/tree/limits.d.ts +1 -4
  109. package/dist/tree/limits.js +9 -13
  110. package/dist/tree/mascot.d.ts +0 -25
  111. package/dist/tree/mascot.js +1 -32
  112. package/dist/tree/model.d.ts +0 -5
  113. package/dist/tree/model.js +2 -15
  114. package/dist/tree/nudge.d.ts +0 -11
  115. package/dist/tree/nudge.js +3 -16
  116. package/dist/tree/panel.d.ts +0 -8
  117. package/dist/tree/panel.js +3 -16
  118. package/dist/tree/ports.d.ts +5 -139
  119. package/dist/tree/report.d.ts +1 -10
  120. package/dist/tree/report.js +4 -11
  121. package/dist/tree/roster.d.ts +0 -21
  122. package/dist/tree/roster.js +0 -21
  123. package/dist/tree/rpc-handlers.d.ts +6 -1
  124. package/dist/tree/rpc-handlers.js +0 -5
  125. package/dist/tree/rpc.d.ts +9 -109
  126. package/dist/tree/rpc.js +2 -38
  127. package/dist/tree/running.d.ts +0 -6
  128. package/dist/tree/running.js +2 -10
  129. package/dist/tree/send.d.ts +0 -4
  130. package/dist/tree/send.js +4 -31
  131. package/dist/tree/spawn.d.ts +1 -7
  132. package/dist/tree/spawn.js +15 -56
  133. package/dist/tree/standing.d.ts +0 -8
  134. package/dist/tree/standing.js +1 -11
  135. package/dist/tree/status.d.ts +0 -8
  136. package/dist/tree/status.js +1 -9
  137. package/dist/tree/stop.d.ts +2 -9
  138. package/dist/tree/stop.js +58 -25
  139. package/dist/tree/store.d.ts +0 -8
  140. package/dist/tree/store.js +4 -38
  141. package/dist/tree/subtree.d.ts +8 -26
  142. package/dist/tree/subtree.js +37 -35
  143. package/dist/tree/summary.d.ts +0 -4
  144. package/dist/tree/summary.js +0 -8
  145. package/dist/tree/text.d.ts +4 -46
  146. package/dist/tree/text.js +44 -83
  147. package/dist/tree/tools.d.ts +0 -6
  148. package/dist/tree/tools.js +15 -15
  149. package/dist/tree/tui.d.ts +0 -81
  150. package/dist/tree/wake.d.ts +0 -30
  151. package/dist/tree/wake.js +5 -39
  152. package/dist/tui.js +24 -10
  153. package/package.json +8 -9
  154. package/dist/webhook/index.d.ts +0 -5
  155. package/dist/webhook/index.js +0 -3
  156. package/dist/webhook/receiver.d.ts +0 -80
  157. package/dist/webhook/receiver.js +0 -339
  158. package/dist/webhook/subscriptions.d.ts +0 -29
  159. package/dist/webhook/subscriptions.js +0 -52
  160. package/dist/webhook/text.d.ts +0 -23
  161. package/dist/webhook/text.js +0 -82
  162. package/dist/webhook/tools.d.ts +0 -19
  163. 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.5 # OpenCode V2, the version courier is tested on
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/v2/docs/agents) hands back a child's final
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, a schedule or a GitHub webhook starts a new turn.
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.
@@ -69,10 +69,10 @@ your own skills. Every detail: [the reference](docs/reference.md).
69
69
  ## Install
70
70
 
71
71
  Requires OpenCode V2: the `opencode` command from `@opencode/cli`. Each release is tested against
72
- one OpenCode version; this one is tested on **OpenCode 2.2.5** (check with `opencode --version`).
72
+ one OpenCode version; this one is tested on **OpenCode 2.2.6** (check with `opencode --version`).
73
73
 
74
74
  ```bash
75
- npm install -g @opencode/cli@2.2.5
75
+ npm install -g @opencode/cli@2.2.6
76
76
  opencode plugin add opencode-courier
77
77
  ```
78
78
 
@@ -85,13 +85,13 @@ name, without a version: OpenCode offers updates only for an entry without an ex
85
85
  <!-- compat-table:start rows=3: generated by script/compat-table.ts from the release tags -->
86
86
  | opencode-courier | OpenCode V2 (`opencode` and `@opencode/plugin`) |
87
87
  |---|---|
88
+ | 0.14.0, 0.15.0 | 2.2.6 |
88
89
  | 0.13.7, 0.13.8 | 2.2.5 |
89
90
  | 0.13.6 | 2.2.4 |
90
- | 0.13.5 | 2.2.3 |
91
91
  <!-- compat-table:end -->
92
92
 
93
93
  Every release: [the full table](docs/reference.md#the-opencode-version). The server part loads on
94
- any V2 host from 2.0.4 on. The terminal part is tested on OpenCode 2.2.5 only.
94
+ any V2 host from 2.0.4 on. The terminal part is tested on OpenCode 2.2.6 only.
95
95
 
96
96
  ## Options
97
97
 
@@ -111,7 +111,6 @@ Set the server options on the plugin's entry in the **global** config,
111
111
  | `maxTotal` | `30` | Live spawned sessions in one tree. |
112
112
  | `memory` | `true` | Keep a record of each session tree for later orchestrators ([memory](docs/reference.md#memory)). |
113
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)). |
114
- | `webhook` | off | Start the webhook receiver ([webhooks](docs/reference.md#webhooks)). |
115
114
 
116
115
  The terminal part reads its options only from `~/.config/opencode/cli.json`, in the same entry
117
116
  form. Its one option, `"animations": false`, keeps the tree panel still.
@@ -127,7 +126,6 @@ form. Its one option, `"animations": false`, keeps the tree panel still.
127
126
  | `courier_cleanup` | Removes an isolated child's worktree and forgets the child. |
128
127
  | `courier_answer` | Proposes an answer to a child's permission request, or passes on your answer to its question. |
129
128
  | `courier_later` / `courier_cancel` | Schedules a message for a session, or drops one. |
130
- | `courier_subscribe` / `courier_unsubscribe` | Subscribes a session to webhook deliveries for a topic, or drops it. |
131
129
  | `courier_recall` / `courier_note` | Reads what earlier trees in this project did, or writes this tree's one-line lesson. |
132
130
 
133
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, together, treeEvents, waitsOn, } from "./tree/index.js";
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
- // `instructions` is read off the context as is: a host before 2.2.0 has none, and the hook then adds the role part.
116
+ // a host before 2.2.0 has no instructions entries (api-notes)
152
117
  const instructions = ctx.session.instructions;
153
- await ctx.session.hook("context", (event) => shapeContext({ storage: ctx.storage, limits, roles, instructions, hiddenFromLeaf: [RECALL_TOOL, NOTE_TOOL], log }, event));
118
+ await ctx.session.hook("context", (event) => shapeContext({ storage: ctx.storage, session: ctx.session, 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
- // The promise plugin's instance is handed this same location and has just joined with it: its
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
- 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
- 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
- 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";
5
+ export declare const SPLIT_LINE = "`split: no, because \u2026; considered: <the parts you weighed>` or `split: <n> parts: \u2026` (one phrase per part)";
6
+ export declare const ONE_PART_LINE = "`split: 1 part: <the task>, to keep my context`";
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;
@@ -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
- 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
- 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. */
4
+ export const SPLIT_LINE = "`split: no, because …; considered: <the parts you weighed>` or `split: <n> parts: …` (one phrase per part)";
5
+ export const ONE_PART_LINE = "`split: 1 part: <the task>, to keep my context`";
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, "&lt;$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, "&lt;$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(" ") ||
@@ -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;
@@ -1,5 +1,4 @@
1
1
  import { describeFailure } from "./protocol.js";
2
- /** A `.catch` handler that rethrows a failure of `tool` as the error the model reads. */
3
2
  export const rethrow = (tool) => (error) => {
4
3
  throw describeFailure(tool, error);
5
4
  };
@@ -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
- * A part whose session could not be read, from what was read of it and its last record: its title, duration and cost as
155
- * last recorded; once OpenCode no longer knows it (`gone`), settled as read, else as last recorded, else `gone`.
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[];