opencode-courier 0.12.0 → 0.13.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 +9 -8
- package/dist/index.js +7 -3
- package/dist/kernel/protocol.d.ts +2 -0
- package/dist/kernel/protocol.js +4 -0
- package/dist/memory/events.js +3 -1
- package/dist/memory/index.d.ts +2 -1
- package/dist/memory/index.js +2 -1
- package/dist/memory/lessons.d.ts +12 -0
- package/dist/memory/lessons.js +56 -0
- package/dist/memory/note.js +3 -0
- package/dist/memory/ports.d.ts +3 -1
- package/dist/memory/text.d.ts +11 -0
- package/dist/memory/text.js +22 -1
- package/dist/person/events.js +4 -1
- package/dist/person/permission.d.ts +1 -1
- package/dist/person/permission.js +1 -1
- package/dist/person/question/answer.d.ts +1 -1
- package/dist/person/question/answer.js +1 -1
- package/dist/person/question/lifecycle.d.ts +1 -1
- package/dist/person/question/lifecycle.js +1 -1
- package/dist/person/question/relay.js +1 -1
- package/dist/platform/events.d.ts +10 -2
- package/dist/platform/events.js +38 -8
- package/dist/skill.js +56 -59
- package/dist/tree/cleanup.js +2 -0
- package/dist/tree/context.d.ts +2 -2
- package/dist/tree/context.js +26 -10
- package/dist/tree/domain.d.ts +12 -0
- package/dist/tree/domain.js +10 -0
- package/dist/tree/events.js +21 -1
- package/dist/tree/group.js +3 -0
- package/dist/tree/index.d.ts +1 -1
- package/dist/tree/ports.d.ts +47 -7
- package/dist/tree/roster.d.ts +3 -11
- package/dist/tree/roster.js +4 -10
- package/dist/tree/send.js +46 -19
- package/dist/tree/spawn.js +57 -3
- package/dist/tree/standing.d.ts +17 -0
- package/dist/tree/standing.js +90 -0
- package/dist/tree/status.d.ts +3 -3
- package/dist/tree/status.js +4 -4
- package/dist/tree/stop.js +3 -0
- package/dist/tree/store.js +4 -2
- package/dist/tree/text.d.ts +37 -6
- package/dist/tree/text.js +74 -8
- package/dist/tree/tools.d.ts +0 -3
- package/dist/tree/tools.js +6 -19
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ A narrated tour of the terminal UI: the tree panel, the asks inbox, failing chil
|
|
|
18
18
|
## Quickstart
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
|
-
npm install -g @opencode/cli@2.
|
|
21
|
+
npm install -g @opencode/cli@2.2.0 # OpenCode V2, the version courier is tested on
|
|
22
22
|
opencode plugin add opencode-courier
|
|
23
23
|
opencode
|
|
24
24
|
```
|
|
@@ -54,17 +54,18 @@ The message lands in the parent's inbox and starts a new turn if the parent is i
|
|
|
54
54
|
|
|
55
55
|
Children started with the same `group` report together, in one message. A child that fails (a
|
|
56
56
|
rate or quota limit with when it lifts), or ends its turn without reporting, is reported to its
|
|
57
|
-
parent. A message that does not wake its session is sent once more, then shown as `stalled`.
|
|
58
|
-
|
|
57
|
+
parent. A message that does not wake its session is sent once more, then shown as `stalled`. On
|
|
58
|
+
OpenCode 2.2.0 a parent's system prompt carries one line per child with its state, kept current,
|
|
59
|
+
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
|
|
59
60
|
split a task and how to check and merge the parts. Every detail: [the reference](docs/reference.md).
|
|
60
61
|
|
|
61
62
|
## Install
|
|
62
63
|
|
|
63
64
|
Requires OpenCode V2: the `opencode` command from `@opencode/cli`. Each release is tested against
|
|
64
|
-
one OpenCode version; this one is tested on **OpenCode 2.
|
|
65
|
+
one OpenCode version; this one is tested on **OpenCode 2.2.0** (check with `opencode --version`).
|
|
65
66
|
|
|
66
67
|
```bash
|
|
67
|
-
npm install -g @opencode/cli@2.
|
|
68
|
+
npm install -g @opencode/cli@2.2.0
|
|
68
69
|
opencode plugin add opencode-courier
|
|
69
70
|
```
|
|
70
71
|
|
|
@@ -77,13 +78,13 @@ name, without a version: OpenCode offers updates only for an entry without an ex
|
|
|
77
78
|
<!-- compat-table:start rows=3: generated by script/compat-table.ts from the release tags -->
|
|
78
79
|
| opencode-courier | OpenCode V2 (`opencode` and `@opencode/plugin`) |
|
|
79
80
|
|---|---|
|
|
81
|
+
| 0.13.0 | 2.2.0 |
|
|
80
82
|
| 0.11.1, 0.12.0 | 2.1.4 |
|
|
81
83
|
| 0.11.0 | 2.1.3 |
|
|
82
|
-
| 0.10.2, 0.10.3 | 2.1.2 |
|
|
83
84
|
<!-- compat-table:end -->
|
|
84
85
|
|
|
85
86
|
Every release: [the full table](docs/reference.md#the-opencode-version). The server part loads on
|
|
86
|
-
any V2 host from 2.0.4 on. The terminal part is tested on OpenCode 2.
|
|
87
|
+
any V2 host from 2.0.4 on. The terminal part is tested on OpenCode 2.2.0 only.
|
|
87
88
|
|
|
88
89
|
## Options
|
|
89
90
|
|
|
@@ -114,7 +115,7 @@ form. Its one option, `"animations": false`, keeps the tree panel still.
|
|
|
114
115
|
|---|---|
|
|
115
116
|
| `courier_spawn` | Starts a child session with a task and returns at once. `isolate`, `group`, `agent`, `model`, `variant`, `native`, `title`. |
|
|
116
117
|
| `courier_send` | Messages a session, waking it if idle. With a `status`, it is a child's report. |
|
|
117
|
-
| `
|
|
118
|
+
| `courier_children` / `courier_tree` | One look at the sessions a session started, or at its whole tree. |
|
|
118
119
|
| `courier_stop` | Stops a session below you and everything under it. |
|
|
119
120
|
| `courier_cleanup` | Removes an isolated child's worktree and forgets the child. |
|
|
120
121
|
| `courier_answer` | Proposes an answer to a child's permission request, or passes on your answer to its question. |
|
package/dist/index.js
CHANGED
|
@@ -8,7 +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
|
+
import { addMemoryTools, lessonsOf, memoryEvents, NOTE_TOOL, readMemory, RECALL_TOOL, registerMemoryRpc, sweepRecords } from "./memory/index.js";
|
|
12
12
|
import { addSkill } from "./skill.js";
|
|
13
13
|
import { addTreeTools, deliverReleased, wakeMissed, dirtyOf, headOf, inspectWorktree, pruneExpired, readLimits, readNativeChildren, registerTreeRpc, shapeContext, together, treeEvents, waitsOn, } from "./tree/index.js";
|
|
14
14
|
import { addWebhookTools, joinReceiver, readConfig, receiving, subscribedSince, } from "./webhook/index.js";
|
|
@@ -77,6 +77,7 @@ export const courier = () => Plugin.define({
|
|
|
77
77
|
for (const problem of [...problems, ...nativeProblems, ...memoryProblems])
|
|
78
78
|
log(`courier: ${problem}`);
|
|
79
79
|
const roles = new Map();
|
|
80
|
+
const memoryPorts = { storage: ctx.storage, session: ctx.session, now: Date.now, log, projectID: ctx.location.project.id, memory, maxDepth: limits.maxDepth };
|
|
80
81
|
const ports = {
|
|
81
82
|
limits,
|
|
82
83
|
nativeChildren,
|
|
@@ -84,6 +85,8 @@ export const courier = () => Plugin.define({
|
|
|
84
85
|
roles,
|
|
85
86
|
nudge,
|
|
86
87
|
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
|
+
entries: () => lessonsOf(memoryPorts),
|
|
87
90
|
session: ctx.session,
|
|
88
91
|
agent: ctx.agent,
|
|
89
92
|
worktree: ctx.worktree,
|
|
@@ -130,7 +133,6 @@ export const courier = () => Plugin.define({
|
|
|
130
133
|
later.log(`courier webhook: not started: ${error instanceof Error ? error.message : String(error)}`);
|
|
131
134
|
}
|
|
132
135
|
const stopPorts = { courier: ports, cancelScheduled: (sessionID) => cancelScheduled(later, sessionID), cleanup: cleanupPorts };
|
|
133
|
-
const memoryPorts = { storage: ctx.storage, session: ctx.session, now: Date.now, log, projectID: ctx.location.project.id, memory, maxDepth: limits.maxDepth };
|
|
134
136
|
// The tools in a fixed order: the tree's, then the person's, the scheduled messages', the webhooks' and the memory's.
|
|
135
137
|
await ctx.tool.transform((tools) => {
|
|
136
138
|
addTreeTools(tools, { courier: ports, cleanup: cleanupPorts, stop: stopPorts });
|
|
@@ -146,7 +148,9 @@ export const courier = () => Plugin.define({
|
|
|
146
148
|
.then(() => registerPersonRpc(ctx.rpc, { questions: questionPorts, permissions }))
|
|
147
149
|
.then(() => registerMemoryRpc(ctx.rpc, memoryPorts))
|
|
148
150
|
.catch((error) => log(`courier rpc: not registered, the TUI part shows nothing and cannot stop: ${String(error)}`));
|
|
149
|
-
|
|
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.
|
|
152
|
+
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));
|
|
150
154
|
const pruned = pruneExpired(ctx.storage, Date.now()).catch((error) => log(`courier roster prune: ${String(error)}`));
|
|
151
155
|
const swept = sweepRecords(ctx.storage, Date.now()).catch((error) => log(`courier memory sweep: ${String(error)}`));
|
|
152
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.
|
|
@@ -22,5 +22,7 @@ export declare function clipText(text: string, max?: number): string;
|
|
|
22
22
|
export declare function clipWords(text: string, max: number): string;
|
|
23
23
|
/** Defuses `<courier` and `</courier>` in outside text, so it cannot close the envelope or forge another. */
|
|
24
24
|
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
|
+
export declare function defuseBlock(text: string): string;
|
|
25
27
|
/** A readable message for a failed courier call; OpenCode's own errors can carry an empty message. */
|
|
26
28
|
export declare function describeFailure(tool: string, error: unknown): Error;
|
package/dist/kernel/protocol.js
CHANGED
|
@@ -38,6 +38,10 @@ export function clipWords(text, max) {
|
|
|
38
38
|
export function defuse(text) {
|
|
39
39
|
return text.replace(/<(\/?)(courier)/gi, "<$1$2");
|
|
40
40
|
}
|
|
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
|
+
export function defuseBlock(text) {
|
|
43
|
+
return text.replace(/<(\/?)(context)/gi, "<$1$2");
|
|
44
|
+
}
|
|
41
45
|
/** A readable message for a failed courier call; OpenCode's own errors can carry an empty message. */
|
|
42
46
|
export function describeFailure(tool, error) {
|
|
43
47
|
const tagged = error;
|
package/dist/memory/events.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { claim } from "../platform/bounded.js";
|
|
2
2
|
import { childrenOf, endsIdle, indexedLineage } from "../tree/index.js";
|
|
3
3
|
import { refresh } from "./record.js";
|
|
4
|
-
const
|
|
4
|
+
const TURN_END_TYPES = ["session.execution.succeeded", "session.execution.failed", "session.execution.interrupted"];
|
|
5
|
+
const TURN_ENDS = new Set(TURN_END_TYPES);
|
|
5
6
|
/**
|
|
6
7
|
* The top of the tree a session is in: by the reverse index for a spawned one, else the session itself when it called
|
|
7
8
|
* courier_recall or has started sessions; undefined for a session in no tree.
|
|
@@ -41,6 +42,7 @@ export async function noteTurnEnd(ports, state, event) {
|
|
|
41
42
|
*/
|
|
42
43
|
export function memoryEvents(state) {
|
|
43
44
|
return {
|
|
45
|
+
types: TURN_END_TYPES,
|
|
44
46
|
after: (ports, event) => TURN_ENDS.has(event.type)
|
|
45
47
|
? noteTurnEnd(ports, state, event).then(() => undefined, (error) => ports.log(`courier memory: could not record the tree of ${event.data?.sessionID}: ${String(error)}`))
|
|
46
48
|
: undefined,
|
package/dist/memory/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { LESSON_MAX, MAX_RECORDS, NOT_STATED, readMemory, RETENTION_MS } from "./domain.js";
|
|
2
2
|
export { memoryEvents, type MemoryEventPorts } from "./events.js";
|
|
3
|
+
export { lessonsOf } from "./lessons.js";
|
|
3
4
|
export { note, type Noted, type NoteInput } from "./note.js";
|
|
4
5
|
export { pin, type PinOutcome } from "./pin.js";
|
|
5
6
|
export type { Lesson, MemoryPorts, MemoryState, PartRecord, PartStatus, Pinned, Reads, Recalled, RefreshPorts, Revision, StoredRecord, TreeRecord } from "./ports.js";
|
|
@@ -7,5 +8,5 @@ export { recall, type Recall, type RecallInput } from "./recall.js";
|
|
|
7
8
|
export { refresh } from "./record.js";
|
|
8
9
|
export { registerMemoryRpc, type MemoryRpcPorts } from "./rpc-handlers.js";
|
|
9
10
|
export { sweepRecords } from "./sweep.js";
|
|
10
|
-
export { NOTE_TOOL, RECALL_TOOL } from "./text.js";
|
|
11
|
+
export { LESSONS_KEY, NOTE_TOOL, RECALL_TOOL } from "./text.js";
|
|
11
12
|
export { addMemoryTools, type MemoryToolPorts } from "./tools.js";
|
package/dist/memory/index.js
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
// and the record's types.
|
|
3
3
|
export { LESSON_MAX, MAX_RECORDS, NOT_STATED, readMemory, RETENTION_MS } from "./domain.js";
|
|
4
4
|
export { memoryEvents } from "./events.js";
|
|
5
|
+
export { lessonsOf } from "./lessons.js";
|
|
5
6
|
export { note } from "./note.js";
|
|
6
7
|
export { pin } from "./pin.js";
|
|
7
8
|
export { recall } from "./recall.js";
|
|
8
9
|
export { refresh } from "./record.js";
|
|
9
10
|
export { registerMemoryRpc } from "./rpc-handlers.js";
|
|
10
11
|
export { sweepRecords } from "./sweep.js";
|
|
11
|
-
export { NOTE_TOOL, RECALL_TOOL } from "./text.js";
|
|
12
|
+
export { LESSONS_KEY, NOTE_TOOL, RECALL_TOOL } from "./text.js";
|
|
12
13
|
export { addMemoryTools } from "./tools.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type Entry } from "../tree/index.js";
|
|
2
|
+
import type { MemoryPorts } from "./ports.js";
|
|
3
|
+
/**
|
|
4
|
+
* The `lessons` entry of this project, as `courier_spawn` writes it to a spawned session before its prompt and to a root at
|
|
5
|
+
* its first spawn: none with the memory option off, or while no record of the project holds a lesson.
|
|
6
|
+
*/
|
|
7
|
+
export declare function lessonsOf(ports: MemoryPorts): Promise<ReadonlyArray<Entry>>;
|
|
8
|
+
/**
|
|
9
|
+
* Puts the project's lessons entry again, once a lesson was written, on the writer's tree's top, the top's current children
|
|
10
|
+
* below `maxDepth` and the writer, each only where it differs from the entry stored; a failure is logged. Returns the sessions put.
|
|
11
|
+
*/
|
|
12
|
+
export declare function relesson(ports: MemoryPorts, writerID: string, top: string): Promise<string[]>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { isNotFound } from "../platform/json.js";
|
|
2
|
+
import { childrenOf } from "../tree/index.js";
|
|
3
|
+
import { pinnedFirst } from "./domain.js";
|
|
4
|
+
import { memoryStore } from "./store.js";
|
|
5
|
+
import { LESSONS_KEY, lessonsEntry } from "./text.js";
|
|
6
|
+
/**
|
|
7
|
+
* The `lessons` entry of this project, as `courier_spawn` writes it to a spawned session before its prompt and to a root at
|
|
8
|
+
* its first spawn: none with the memory option off, or while no record of the project holds a lesson.
|
|
9
|
+
*/
|
|
10
|
+
export async function lessonsOf(ports) {
|
|
11
|
+
if (!ports.memory)
|
|
12
|
+
return [];
|
|
13
|
+
const value = lessonsEntry(pinnedFirst(await memoryStore(ports.storage).ofProject(ports.projectID)));
|
|
14
|
+
return value === undefined ? [] : [{ key: LESSONS_KEY, value }];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Puts the project's lessons entry again, once a lesson was written, on the writer's tree's top, the top's current children
|
|
18
|
+
* below `maxDepth` and the writer, each only where it differs from the entry stored; a failure is logged. Returns the sessions put.
|
|
19
|
+
*/
|
|
20
|
+
export async function relesson(ports, writerID, top) {
|
|
21
|
+
const entries = ports.session.instructions?.entry;
|
|
22
|
+
if (!entries)
|
|
23
|
+
return [];
|
|
24
|
+
let sessions = [];
|
|
25
|
+
let entry;
|
|
26
|
+
try {
|
|
27
|
+
;
|
|
28
|
+
[entry] = await lessonsOf(ports);
|
|
29
|
+
if (!entry)
|
|
30
|
+
return [];
|
|
31
|
+
const children = ports.maxDepth > 1 ? await childrenOf(ports.storage, top) : [];
|
|
32
|
+
sessions = [...new Set([top, ...children.map((child) => child.sessionID), writerID])];
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
ports.log(`courier_note: the lessons entry could not be read again: ${String(error)}`);
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
const value = entry;
|
|
39
|
+
const renew = async (sessionID) => {
|
|
40
|
+
try {
|
|
41
|
+
const stored = (await entries.list({ sessionID })).find((one) => one.key === value.key);
|
|
42
|
+
if (stored?.value === value.value)
|
|
43
|
+
return false;
|
|
44
|
+
await entries.put({ sessionID, key: value.key, value: value.value });
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
// A child on the roster that OpenCode no longer knows is a normal path, not one to log.
|
|
49
|
+
if (!isNotFound(error))
|
|
50
|
+
ports.log(`courier_note: the lessons entry of ${sessionID} could not be written: ${String(error)}`);
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const put = await Promise.all(sessions.map(renew));
|
|
55
|
+
return sessions.filter((_, index) => put[index]);
|
|
56
|
+
}
|
package/dist/memory/note.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { lineage } from "../tree/index.js";
|
|
2
2
|
import { LESSON_MAX, oneLine } from "./domain.js";
|
|
3
|
+
import { relesson } from "./lessons.js";
|
|
3
4
|
import { memoryStore } from "./store.js";
|
|
4
5
|
/**
|
|
5
6
|
* courier_note: writes `text`, as one line of at most `LESSON_MAX` characters (longer is refused, never clipped), as the lesson
|
|
@@ -25,5 +26,7 @@ export async function note(ports, callerID, input) {
|
|
|
25
26
|
return { kind: "unrecorded", top };
|
|
26
27
|
const lesson = { text, at: ports.now(), ...(depth ? { depth } : {}) };
|
|
27
28
|
await store.put(projectID, top, { ...record, lesson });
|
|
29
|
+
// The lessons entry of the top, its children and the writer moves with it; the next tree's sessions get it at their spawn.
|
|
30
|
+
await relesson(ports, callerID, top);
|
|
28
31
|
return { kind: "noted", projectID, top, lesson, ...(record.lesson ? { replaced: record.lesson } : {}) };
|
|
29
32
|
}
|
package/dist/memory/ports.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** The memory's ports: what refreshing a record and courier_recall need of the plugin context, and what the process shares. */
|
|
2
2
|
import type { Plugin } from "@opencode/plugin";
|
|
3
3
|
import type { Storage } from "../platform/storage.js";
|
|
4
|
-
import type { ModelWindow } from "../tree/index.js";
|
|
4
|
+
import type { Instructions, ModelWindow } from "../tree/index.js";
|
|
5
5
|
import type { Pinned, ReadKind, Reads, StoredRecord, TreeRecord } from "./domain.js";
|
|
6
6
|
export type { Lesson, PartRecord, PartStatus, Pinned, ReadKind, Reads, Recalled, Revision, StoredRecord, TreeRecord } from "./domain.js";
|
|
7
7
|
type Context = Plugin.Context;
|
|
@@ -14,6 +14,8 @@ export interface RefreshPorts {
|
|
|
14
14
|
}
|
|
15
15
|
/** What courier_recall and courier_note run on: one instance's ports. */
|
|
16
16
|
export interface MemoryPorts extends RefreshPorts {
|
|
17
|
+
/** With `instructions`, the keyed session entries, which courier_note puts the lessons entry through; missing on a host before 2.2.0. */
|
|
18
|
+
readonly session: RefreshPorts["session"] & Instructions;
|
|
17
19
|
/** The project of the plugin's location, whose records the tools read and write. */
|
|
18
20
|
readonly projectID: string;
|
|
19
21
|
/** The `memory` option: off, and the tools write and list nothing. */
|
package/dist/memory/text.d.ts
CHANGED
|
@@ -33,3 +33,14 @@ export declare const NOTE_EMPTY = "Not noted: text is empty. Write one line, wha
|
|
|
33
33
|
/** The refusal of a lesson longer than `LESSON_MAX` as one line: nothing is written, and the earlier lesson stays. */
|
|
34
34
|
export declare const noteLongRefusal: (length: number) => string;
|
|
35
35
|
export declare const noteUnrecordedRefusal: (top: string) => string;
|
|
36
|
+
/** The key of the session instruction entry carrying the project's lessons, `<context key="lessons">`. */
|
|
37
|
+
export declare const LESSONS_KEY = "lessons";
|
|
38
|
+
/** How every lessons entry begins. */
|
|
39
|
+
export declare const LESSONS_PREFIX = "opencode-courier lessons:";
|
|
40
|
+
/** How many lessons the entry carries: the records' order, pinned first, then newest first. */
|
|
41
|
+
export declare const LESSONS_MAX = 5;
|
|
42
|
+
/**
|
|
43
|
+
* The lessons entry: the lessons of the project's records as courier_recall lists them, pinned first and then newest first,
|
|
44
|
+
* at most `LESSONS_MAX`, each with its tree's title; undefined while no record holds one.
|
|
45
|
+
*/
|
|
46
|
+
export declare function lessonsEntry(records: ReadonlyArray<StoredRecord>): string | undefined;
|
package/dist/memory/text.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** The memory's model-facing texts: what courier_recall and courier_note say. */
|
|
2
|
-
import { clipText, clipWords } from "../kernel/protocol.js";
|
|
2
|
+
import { clipText, clipWords, defuseBlock } from "../kernel/protocol.js";
|
|
3
3
|
import { LESSON_MAX, oneLine, PART_STATUSES, splitProse } from "./domain.js";
|
|
4
4
|
/** The tool's name, which the skill and the role part name too. */
|
|
5
5
|
export const RECALL_TOOL = "courier_recall";
|
|
@@ -114,3 +114,24 @@ export const noteLongRefusal = (length) => `Not noted: the lesson is ${length} c
|
|
|
114
114
|
`if there was one, stays. Keep one line, ${ONE_LINE}, and leave out what the record's facts already say.`;
|
|
115
115
|
export const noteUnrecordedRefusal = (top) => `Not noted: the tree under ${top} has no record in this project to write a lesson on. A record is written by a ${RECALL_TOOL} call ` +
|
|
116
116
|
"and as each turn in the tree ends, so call it once first, or note once a session you started has reported.";
|
|
117
|
+
// The lessons entry: what a spawned session's system prompt says of the project's earlier trees.
|
|
118
|
+
/** The key of the session instruction entry carrying the project's lessons, `<context key="lessons">`. */
|
|
119
|
+
export const LESSONS_KEY = "lessons";
|
|
120
|
+
/** How every lessons entry begins. */
|
|
121
|
+
export const LESSONS_PREFIX = "opencode-courier lessons:";
|
|
122
|
+
/** How many lessons the entry carries: the records' order, pinned first, then newest first. */
|
|
123
|
+
export const LESSONS_MAX = 5;
|
|
124
|
+
/**
|
|
125
|
+
* The lessons entry: the lessons of the project's records as courier_recall lists them, pinned first and then newest first,
|
|
126
|
+
* at most `LESSONS_MAX`, each with its tree's title; undefined while no record holds one.
|
|
127
|
+
*/
|
|
128
|
+
export function lessonsEntry(records) {
|
|
129
|
+
const lessons = records.filter((record) => record.lesson).slice(0, LESSONS_MAX);
|
|
130
|
+
if (!lessons.length)
|
|
131
|
+
return undefined;
|
|
132
|
+
return [
|
|
133
|
+
`${LESSONS_PREFIX} what the orchestrators of earlier session trees in this project concluded, each with its tree's title; ` +
|
|
134
|
+
`${RECALL_TOOL} has the records with the facts behind them.`,
|
|
135
|
+
...lessons.map((record) => `- ${record.sessionID} "${clipText(defuseBlock(oneLine(record.title)), 60)}": ${defuseBlock(lessonLine(record.lesson))}`),
|
|
136
|
+
].join("\n");
|
|
137
|
+
}
|
package/dist/person/events.js
CHANGED
|
@@ -75,6 +75,8 @@ export async function relayPending(ports, state) {
|
|
|
75
75
|
};
|
|
76
76
|
await Promise.all([...sessions].map(relay));
|
|
77
77
|
}
|
|
78
|
+
/** What `routePerson` dispatches on. */
|
|
79
|
+
const PERSON_TYPES = ["permission.asked", "permission.replied", "form.created", "form.replied", "form.cancelled", "location.shutdown"];
|
|
78
80
|
/** The events of what a spawned session asks the person: permission requests, forms, and locations closing on them. */
|
|
79
81
|
function routePerson(ports, state, event) {
|
|
80
82
|
if (event.type === "permission.asked")
|
|
@@ -103,9 +105,10 @@ function routePerson(ports, state, event) {
|
|
|
103
105
|
*/
|
|
104
106
|
export function personEvents(state) {
|
|
105
107
|
return {
|
|
108
|
+
types: PERSON_TYPES,
|
|
106
109
|
handle: (ports, event) => routePerson(ports, state, event),
|
|
107
110
|
// Question forms shown while no instance followed the events were not seen: released now,
|
|
108
|
-
// and again
|
|
111
|
+
// and again once the stream is surely connected.
|
|
109
112
|
followed: () => (eventsFollowed() ? formsMayHaveBeenMissed : undefined),
|
|
110
113
|
// Alongside the new subscription; a request both relays see is relayed once.
|
|
111
114
|
subscribed: (ports) => relayPending(ports, state).catch((error) => ports.log(`courier watch: could not relay pending requests: ${String(error)}`)),
|
|
@@ -108,6 +108,6 @@ export declare function ownRequests(permissions: Iterable<Permissions>, sessionI
|
|
|
108
108
|
requestID: string;
|
|
109
109
|
} | undefined;
|
|
110
110
|
}[]>;
|
|
111
|
-
/** The requests a session waits on, in whichever location holds them, for
|
|
111
|
+
/** The requests a session waits on, in whichever location holds them, for courier_children. */
|
|
112
112
|
export declare function pendingOf(permissions: Iterable<Permissions>, sessionID: string): Promise<Pending[]>;
|
|
113
113
|
export {};
|
|
@@ -130,7 +130,7 @@ export async function ownRequests(permissions, sessionID) {
|
|
|
130
130
|
found.set(request.id, request);
|
|
131
131
|
return [...found.values()].map((request) => ({ request, passesOn: passedOn(request.metadata) }));
|
|
132
132
|
}
|
|
133
|
-
/** The requests a session waits on, in whichever location holds them, for
|
|
133
|
+
/** The requests a session waits on, in whichever location holds them, for courier_children. */
|
|
134
134
|
export async function pendingOf(permissions, sessionID) {
|
|
135
135
|
const found = new Map();
|
|
136
136
|
for (const { requests } of await listEverywhere(permissions, sessionID))
|
|
@@ -50,7 +50,7 @@ export declare function answerForPerson(ports: QuestionPorts, input: QuestionAns
|
|
|
50
50
|
export declare function asksQuestion(ports: {
|
|
51
51
|
readonly storage: RosterStorage;
|
|
52
52
|
}, sessionID: string): Promise<boolean>;
|
|
53
|
-
/** The questions a session waits on, for
|
|
53
|
+
/** The questions a session waits on, for courier_children; `stopped` when its call was cut off. */
|
|
54
54
|
export declare function pendingQuestions(storage: RosterStorage, sessionID: string): Promise<{
|
|
55
55
|
type: "question";
|
|
56
56
|
requestID: string;
|
|
@@ -190,7 +190,7 @@ export async function answerForPerson(ports, input) {
|
|
|
190
190
|
export async function asksQuestion(ports, sessionID) {
|
|
191
191
|
return (await pendingQuestions(ports.storage, sessionID)).length > 0;
|
|
192
192
|
}
|
|
193
|
-
/** The questions a session waits on, for
|
|
193
|
+
/** The questions a session waits on, for courier_children; `stopped` when its call was cut off. */
|
|
194
194
|
export async function pendingQuestions(storage, sessionID) {
|
|
195
195
|
const found = new Map();
|
|
196
196
|
const add = (asked, stopped) => found.set(asked.requestID, {
|
|
@@ -36,7 +36,7 @@ export declare function formShown(event: {
|
|
|
36
36
|
export declare function formsMayHaveBeenMissed(): void;
|
|
37
37
|
/**
|
|
38
38
|
* Called when an instance starts following OpenCode's events: a shown form is missed only while none
|
|
39
|
-
* follows, so waiting calls are released; true tells the caller to release again
|
|
39
|
+
* follows, so waiting calls are released; true tells the caller to release again once connected.
|
|
40
40
|
*/
|
|
41
41
|
export declare function eventsFollowed(): boolean;
|
|
42
42
|
/** Called when an instance's event stream ended or broke. */
|
|
@@ -75,7 +75,7 @@ export function formsMayHaveBeenMissed() {
|
|
|
75
75
|
}
|
|
76
76
|
/**
|
|
77
77
|
* Called when an instance starts following OpenCode's events: a shown form is missed only while none
|
|
78
|
-
* follows, so waiting calls are released; true tells the caller to release again
|
|
78
|
+
* follows, so waiting calls are released; true tells the caller to release again once connected.
|
|
79
79
|
*/
|
|
80
80
|
export function eventsFollowed() {
|
|
81
81
|
const missed = shared.following === 0 && shared.followed;
|
|
@@ -31,7 +31,7 @@ async function relay(ports, question) {
|
|
|
31
31
|
return true;
|
|
32
32
|
}
|
|
33
33
|
catch (error) {
|
|
34
|
-
// The question stays registered and the call waits on:
|
|
34
|
+
// The question stays registered and the call waits on: courier_children lists it, and an answer
|
|
35
35
|
// that comes anyway, through a link or courier_answer, still reaches the call.
|
|
36
36
|
ports.log(`courier question: could not tell ${question.top} about ${question.requestID}: ${String(error)}`);
|
|
37
37
|
return false;
|
|
@@ -10,13 +10,17 @@ export interface EventPorts {
|
|
|
10
10
|
readonly event: Pick<Plugin.Context["event"], "subscribe">;
|
|
11
11
|
readonly log: (message: string) => void;
|
|
12
12
|
}
|
|
13
|
+
/** An event type `event.subscribe` can filter on: any of OpenCode's but `server.connected`, which a plugin never receives. */
|
|
14
|
+
export type EventType = NonNullable<NonNullable<Parameters<EventPorts["event"]["subscribe"]>[0]>["types"]>[number];
|
|
13
15
|
/** What a module does with OpenCode's events, by phase; each phase runs the modules in list order. */
|
|
14
16
|
export interface EventModule<P extends EventPorts> {
|
|
17
|
+
/** The event types it handles; without them, every event, so the subscription is not filtered. */
|
|
18
|
+
readonly types?: readonly EventType[];
|
|
15
19
|
/** For each event; the first to throw ends the phase. */
|
|
16
20
|
readonly handle?: (ports: P, event: HostEvent) => Promise<unknown> | undefined;
|
|
17
21
|
/** For each event, once the `handle` phase has ended, however it did; the first to throw ends this one too. */
|
|
18
22
|
readonly after?: (ports: P, event: HostEvent) => Promise<void> | undefined;
|
|
19
|
-
/** As a subscription starts; returns what to run
|
|
23
|
+
/** As a subscription starts; returns what to run once the stream is surely connected, if anything. */
|
|
20
24
|
readonly followed?: () => (() => void) | undefined;
|
|
21
25
|
/** Alongside each (re)subscription; the watcher's end waits for it. */
|
|
22
26
|
readonly subscribed?: (ports: P) => Promise<void>;
|
|
@@ -25,8 +29,12 @@ export interface EventModule<P extends EventPorts> {
|
|
|
25
29
|
}
|
|
26
30
|
/** How long to wait before subscribing again after the event stream ended or broke. */
|
|
27
31
|
export declare const RESUBSCRIBE_MS = 5000;
|
|
32
|
+
/** How long after subscribing the stream is taken as connected, should no event come first: a plugin's reports no connection. */
|
|
33
|
+
export declare const CONNECTED_MS = 1000;
|
|
34
|
+
/** The types `modules` declare, sorted; undefined, every type, once one of them declares none. */
|
|
35
|
+
export declare function typesOf(modules: readonly EventModule<never>[]): EventType[] | undefined;
|
|
28
36
|
/** Follows OpenCode's events through `ports`, subscribing again after `retryMs` when a subscription ends, until `signal` aborts. */
|
|
29
|
-
export declare function follow<P extends EventPorts>(ports: P, modules: readonly EventModule<P>[], signal: AbortSignal, retryMs?: number): Promise<void>;
|
|
37
|
+
export declare function follow<P extends EventPorts>(ports: P, modules: readonly EventModule<P>[], signal: AbortSignal, retryMs?: number, connectedMs?: number): Promise<void>;
|
|
30
38
|
/**
|
|
31
39
|
* Makes the hub start its subscriptions to OpenCode's events with this copy's modules, the copy loaded
|
|
32
40
|
* last; `join` starts them. The second is a standby, connected before the first's member leaves.
|
package/dist/platform/events.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** How long to wait before subscribing again after the event stream ended or broke. */
|
|
2
2
|
export const RESUBSCRIBE_MS = 5_000;
|
|
3
|
+
/** How long after subscribing the stream is taken as connected, should no event come first: a plugin's reports no connection. */
|
|
4
|
+
export const CONNECTED_MS = 1_000;
|
|
3
5
|
const pause = (ms, signal) => new Promise((resolve) => {
|
|
4
6
|
const timer = setTimeout(done, ms);
|
|
5
7
|
function done() {
|
|
@@ -32,6 +34,17 @@ const until = (events, signal) => ({
|
|
|
32
34
|
}),
|
|
33
35
|
}),
|
|
34
36
|
});
|
|
37
|
+
/** The types `modules` declare, sorted; undefined, every type, once one of them declares none. */
|
|
38
|
+
export function typesOf(modules) {
|
|
39
|
+
const types = new Set();
|
|
40
|
+
for (const module of modules) {
|
|
41
|
+
if (!module.types)
|
|
42
|
+
return undefined;
|
|
43
|
+
for (const type of module.types)
|
|
44
|
+
types.add(type);
|
|
45
|
+
}
|
|
46
|
+
return [...types].sort((a, b) => Number(a > b) - Number(a < b));
|
|
47
|
+
}
|
|
35
48
|
/** One event through the `handle` phase, then, however that ended, the `after` phase. */
|
|
36
49
|
async function handle(ports, modules, event) {
|
|
37
50
|
try {
|
|
@@ -70,18 +83,31 @@ function subscribed(ports, modules, alongside) {
|
|
|
70
83
|
}
|
|
71
84
|
}
|
|
72
85
|
/** One subscription, its events through `modules`' phases, until its stream ends or breaks or `signal` aborts. */
|
|
73
|
-
async function subscription(ports, modules, signal, alongside) {
|
|
86
|
+
async function subscription(ports, modules, signal, alongside, connectedMs) {
|
|
74
87
|
let following = false;
|
|
75
88
|
let events;
|
|
89
|
+
let first = [];
|
|
90
|
+
let timer;
|
|
91
|
+
// The stream is surely connected at its first event, filtered or not, or `connectedMs` after subscribing.
|
|
92
|
+
const connected = () => {
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
for (const run of first.splice(0))
|
|
95
|
+
run();
|
|
96
|
+
};
|
|
97
|
+
const types = typesOf(modules);
|
|
98
|
+
const wanted = types && new Set(types);
|
|
76
99
|
try {
|
|
77
|
-
events = ports.event.subscribe({ signal })[Symbol.asyncIterator]();
|
|
78
|
-
|
|
100
|
+
events = ports.event.subscribe({ signal, ...(types && { types }) })[Symbol.asyncIterator]();
|
|
101
|
+
first = modules.flatMap((module) => module.followed?.() ?? []);
|
|
102
|
+
if (first.length > 0)
|
|
103
|
+
timer = setTimeout(connected, connectedMs);
|
|
79
104
|
following = true;
|
|
80
105
|
subscribed(ports, modules, alongside);
|
|
81
106
|
for await (const event of until(events, signal)) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
107
|
+
connected();
|
|
108
|
+
// A host that ignores `types` sends every event; none outside them reaches a module.
|
|
109
|
+
if (wanted && !wanted.has(event.type))
|
|
110
|
+
continue;
|
|
85
111
|
await handleLogged(ports, modules, event);
|
|
86
112
|
}
|
|
87
113
|
}
|
|
@@ -90,6 +116,8 @@ async function subscription(ports, modules, signal, alongside) {
|
|
|
90
116
|
ports.log(`courier watch: event stream broke: ${String(error)}`);
|
|
91
117
|
}
|
|
92
118
|
finally {
|
|
119
|
+
clearTimeout(timer);
|
|
120
|
+
first = [];
|
|
93
121
|
if (following)
|
|
94
122
|
for (const module of modules)
|
|
95
123
|
module.left?.();
|
|
@@ -99,10 +127,10 @@ async function subscription(ports, modules, signal, alongside) {
|
|
|
99
127
|
}
|
|
100
128
|
}
|
|
101
129
|
/** Follows OpenCode's events through `ports`, subscribing again after `retryMs` when a subscription ends, until `signal` aborts. */
|
|
102
|
-
export async function follow(ports, modules, signal, retryMs = RESUBSCRIBE_MS) {
|
|
130
|
+
export async function follow(ports, modules, signal, retryMs = RESUBSCRIBE_MS, connectedMs = CONNECTED_MS) {
|
|
103
131
|
const alongside = new Set();
|
|
104
132
|
while (!signal.aborted) {
|
|
105
|
-
await subscription(ports, modules, signal, alongside);
|
|
133
|
+
await subscription(ports, modules, signal, alongside, connectedMs);
|
|
106
134
|
if (!signal.aborted)
|
|
107
135
|
await pause(retryMs, signal);
|
|
108
136
|
}
|
|
@@ -113,8 +141,10 @@ export async function follow(ports, modules, signal, retryMs = RESUBSCRIBE_MS) {
|
|
|
113
141
|
* last; `join` starts them. The second is a standby, connected before the first's member leaves.
|
|
114
142
|
*/
|
|
115
143
|
export function watchFromHub(hub, modules, retryMs = RESUBSCRIBE_MS) {
|
|
144
|
+
const types = typesOf(modules);
|
|
116
145
|
hub.subscribe = (member) => {
|
|
117
146
|
const stop = new AbortController();
|
|
147
|
+
member.log(`courier watch: following ${types ? types.join(", ") : "every event"} through ${member.directory}`);
|
|
118
148
|
// A member of any copy runs them: every hub-v2 member's `watch` ports are the same shape.
|
|
119
149
|
return { member, stop, done: follow(member.watch, modules, stop.signal, retryMs) };
|
|
120
150
|
};
|