opencode-courier 0.11.1 → 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 +19 -14
- package/dist/index.js +19 -6
- 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/memory/tools.js +2 -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.d.ts +1 -1
- package/dist/skill.js +97 -45
- 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 +32 -2
- package/dist/tree/domain.js +28 -2
- 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/model.d.ts +18 -0
- package/dist/tree/model.js +65 -0
- package/dist/tree/panel.js +3 -2
- package/dist/tree/ports.d.ts +59 -13
- package/dist/tree/roster.d.ts +3 -11
- package/dist/tree/roster.js +4 -10
- package/dist/tree/rpc-handlers.d.ts +6 -0
- package/dist/tree/rpc.d.ts +16 -0
- package/dist/tree/rpc.js +4 -0
- package/dist/tree/send.js +46 -19
- package/dist/tree/spawn.d.ts +8 -0
- package/dist/tree/spawn.js +74 -21
- package/dist/tree/standing.d.ts +17 -0
- package/dist/tree/standing.js +90 -0
- package/dist/tree/status.d.ts +35 -19
- package/dist/tree/status.js +6 -6
- package/dist/tree/stop.js +3 -0
- package/dist/tree/store.js +4 -2
- package/dist/tree/subtree.d.ts +16 -8
- package/dist/tree/text.d.ts +48 -6
- package/dist/tree/text.js +113 -8
- package/dist/tree/tools.d.ts +3 -3
- package/dist/tree/tools.js +24 -20
- package/dist/tui.js +8 -0
- package/package.json +6 -6
package/dist/tree/ports.d.ts
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
import type { Plugin } from "@opencode/plugin";
|
|
6
6
|
import type { Status } from "../kernel/protocol.js";
|
|
7
7
|
import type { Storage } from "../platform/storage.js";
|
|
8
|
-
import type { Limits, Member, RosterEntry } from "./domain.js";
|
|
9
|
-
export type { Artifacts, HeldReport, LeftMember, Limits, Member, RosterEntry, Standing } from "./domain.js";
|
|
8
|
+
import type { Limits, Member, ModelRef, RosterEntry } from "./domain.js";
|
|
9
|
+
export type { Artifacts, HeldReport, LeftMember, Limits, Member, ModelRef, RosterEntry, Standing } from "./domain.js";
|
|
10
10
|
type Context = Plugin.Context;
|
|
11
11
|
export type RosterStorage = Storage;
|
|
12
12
|
/** How a session's wait for a report ended: it reported to its parent, or its turn failed or was interrupted. */
|
|
@@ -43,6 +43,8 @@ export interface ChildStore {
|
|
|
43
43
|
entriesOf(sessionID: string): Promise<RosterEntry[]>;
|
|
44
44
|
/** The session that started a session, by the reverse index alone, without scanning the roster. */
|
|
45
45
|
indexedParent(sessionID: string): Promise<string | undefined>;
|
|
46
|
+
/** The sessions above a session, its parent first, by the reverse index alone: empty when it does not name the session. */
|
|
47
|
+
indexedAncestors(sessionID: string): Promise<ReadonlyArray<string>>;
|
|
46
48
|
/** A session's roster entry, by the reverse index alone, without scanning the roster. */
|
|
47
49
|
indexedEntry(sessionID: string): Promise<RosterEntry | undefined>;
|
|
48
50
|
/**
|
|
@@ -149,10 +151,42 @@ export interface Sent {
|
|
|
149
151
|
readonly status?: Status;
|
|
150
152
|
readonly report?: boolean;
|
|
151
153
|
readonly held?: Held;
|
|
154
|
+
/** The status it was sent with to a session below the sender, where a status means nothing. */
|
|
155
|
+
readonly dropped?: Status;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* What the `context` hook remembers of a session: its depth, null for one nobody spawned that has started none, and
|
|
159
|
+
* whether it carries the role entry (`ROLE_KEY`), which then names its role in place of the hook's system part.
|
|
160
|
+
*/
|
|
161
|
+
export interface Role {
|
|
162
|
+
readonly depth: number | null;
|
|
163
|
+
readonly entry: boolean;
|
|
164
|
+
}
|
|
165
|
+
/** The keyed session entries, missing on a host before 2.2.0 (every copy's ports carry the host's `session`): an entry is then skipped. */
|
|
166
|
+
export type Instructions = Partial<Pick<Context["session"], "instructions">>;
|
|
167
|
+
/** A keyed session instruction entry: its key, `^[a-z0-9][a-z0-9._-]*$`, and its text. */
|
|
168
|
+
export interface Entry {
|
|
169
|
+
readonly key: string;
|
|
170
|
+
readonly value: string;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Supplies the entries other modules write to a spawned session below `maxDepth` before its prompt, and to a root at its
|
|
174
|
+
* first spawn beside its role; `src/index.ts` composes it from the modules' suppliers, as it does the `Waits` port.
|
|
175
|
+
*/
|
|
176
|
+
export type Entries = () => Promise<ReadonlyArray<Entry>>;
|
|
177
|
+
/** What writing a parent's `standing` entry needs: the tools', the watcher's and the scheduler's ports all have these. */
|
|
178
|
+
export interface StandingPorts {
|
|
179
|
+
readonly storage: RosterStorage;
|
|
180
|
+
readonly now: () => number;
|
|
181
|
+
readonly session?: Instructions;
|
|
182
|
+
readonly log?: (message: string) => void;
|
|
152
183
|
}
|
|
153
184
|
/** The slice of the plugin context the courier tools use; tests pass a fake. */
|
|
154
185
|
export interface CourierPorts {
|
|
155
|
-
|
|
186
|
+
/** `instructions`, the keyed session entries, is missing on a host before 2.2.0: the role then goes in a system part. */
|
|
187
|
+
readonly session: Pick<Context["session"], "create" | "prompt" | "synthetic" | "get" | "context" | "interrupt"> & Instructions;
|
|
188
|
+
/** The entries of other modules a spawned session gets before its prompt, the memory's lessons; none when not given. */
|
|
189
|
+
readonly entries?: Entries;
|
|
156
190
|
readonly agent: Pick<Context["agent"], "get">;
|
|
157
191
|
readonly worktree: Pick<Context["worktree"], "create" | "remove">;
|
|
158
192
|
/** The project of the plugin's location; an isolated child's worktree is made in it, and removed from it. */
|
|
@@ -168,24 +202,30 @@ export interface CourierPorts {
|
|
|
168
202
|
readonly pending: (sessionID: string) => Promise<ReadonlyArray<Pending>>;
|
|
169
203
|
/** The forms a session shows, which only the person can answer: web search's, a plugin's; not the question tool's. */
|
|
170
204
|
readonly forms: (sessionID: string) => Promise<ReadonlyArray<ShownForm>>;
|
|
171
|
-
/** The
|
|
172
|
-
readonly
|
|
205
|
+
/** The models available, each with its context window and variants, as courier_spawn checks a choice and the panel reads a session's context use. */
|
|
206
|
+
readonly models: () => Promise<ReadonlyArray<KnownModel>>;
|
|
207
|
+
/** OpenCode's default model here, which a session with no model of its own runs on; undefined when there is none. */
|
|
208
|
+
readonly defaultModel: () => Promise<ModelRef | undefined>;
|
|
173
209
|
readonly limits: Limits;
|
|
174
|
-
/** The `nativeChildren` option: a child started without isolate is created with OpenCode's own `parentID`. */
|
|
210
|
+
/** The `nativeChildren` option: a child started without isolate and without `native` is created with OpenCode's own `parentID`. */
|
|
175
211
|
readonly nativeChildren?: boolean;
|
|
176
212
|
readonly gate: SpawnGate;
|
|
177
|
-
/** The
|
|
178
|
-
readonly roles: Map<string,
|
|
213
|
+
/** The roles the `context` hook remembers; a parent's is dropped when it starts a child, a root's kept once its entry is written. */
|
|
214
|
+
readonly roles: Map<string, Role>;
|
|
179
215
|
/** Has the scheduler deliver what is due now rather than at its next tick: a group a report has just completed. */
|
|
180
216
|
readonly nudge: () => void;
|
|
181
217
|
readonly log: (message: string) => void;
|
|
182
218
|
}
|
|
183
|
-
/** A model's context window in tokens, as
|
|
219
|
+
/** A model's context window in tokens, as the panel and the memory read a session's context use by. */
|
|
184
220
|
export interface ModelWindow {
|
|
185
221
|
readonly providerID: string;
|
|
186
222
|
readonly id: string;
|
|
187
223
|
readonly context: number;
|
|
188
224
|
}
|
|
225
|
+
/** A model OpenCode lists, as `CourierPorts.models` gives it: its window, and the ids of its variants. */
|
|
226
|
+
export interface KnownModel extends ModelWindow {
|
|
227
|
+
readonly variants: ReadonlyArray<string>;
|
|
228
|
+
}
|
|
189
229
|
/** A form a session shows, as `CourierPorts.forms` lists it; `person/` keeps them. */
|
|
190
230
|
export interface ShownForm {
|
|
191
231
|
readonly id: string;
|
|
@@ -270,6 +310,9 @@ export interface CleanupPorts {
|
|
|
270
310
|
readonly now: () => number;
|
|
271
311
|
/** Has the scheduler deliver what is due now: a group the forgotten child may have been the last member out of. */
|
|
272
312
|
readonly nudge: () => void;
|
|
313
|
+
/** For the parent's `standing` entry once the child is forgotten; without them it is left as it was. */
|
|
314
|
+
readonly session?: Instructions;
|
|
315
|
+
readonly log?: (message: string) => void;
|
|
273
316
|
}
|
|
274
317
|
export interface StopPorts {
|
|
275
318
|
readonly courier: CourierPorts;
|
|
@@ -279,7 +322,8 @@ export interface StopPorts {
|
|
|
279
322
|
}
|
|
280
323
|
export interface GroupPorts {
|
|
281
324
|
readonly storage: Storage;
|
|
282
|
-
readonly session: Pick<Context["session"], "synthetic" | "get"
|
|
325
|
+
readonly session: Pick<Context["session"], "synthetic" | "get"> & Instructions;
|
|
326
|
+
readonly now: () => number;
|
|
283
327
|
readonly log: (message: string) => void;
|
|
284
328
|
}
|
|
285
329
|
/** The error of OpenCode's `session.execution.failed` event. */
|
|
@@ -293,7 +337,7 @@ export interface ExecutionError {
|
|
|
293
337
|
export interface WatchPorts {
|
|
294
338
|
readonly storage: RosterStorage;
|
|
295
339
|
/** `context` reads the last reply of a session that ended its turn without reporting; `get` is what every member joins with too. */
|
|
296
|
-
readonly session: Pick<Context["session"], "synthetic" | "context" | "get"
|
|
340
|
+
readonly session: Pick<Context["session"], "synthetic" | "context" | "get"> & Instructions;
|
|
297
341
|
readonly event: Pick<Context["event"], "subscribe">;
|
|
298
342
|
/** The permission domains of every loaded location, which the `Waits` suppliers ask; an isolated child's are in its worktree's. */
|
|
299
343
|
readonly permissions: () => Iterable<Pick<Context["permission"], "list">>;
|
|
@@ -320,8 +364,10 @@ export type Waits = (ports: Pick<WatchPorts, "storage" | "permissions">, session
|
|
|
320
364
|
export interface ContextPorts {
|
|
321
365
|
readonly storage: RosterStorage;
|
|
322
366
|
readonly limits: Limits;
|
|
323
|
-
/** Each session's
|
|
324
|
-
readonly roles: Map<string,
|
|
367
|
+
/** Each session's role once looked up; a session nobody spawned reads as depth null until it starts one. */
|
|
368
|
+
readonly roles: Map<string, Role>;
|
|
369
|
+
/** The keyed session entries, listed when a session's role is looked up; missing on a host before 2.2.0, which gets the system part. */
|
|
370
|
+
readonly instructions?: Context["session"]["instructions"];
|
|
325
371
|
/** Tools of other modules hidden from a leaf alongside courier_spawn; `src/index.ts` names courier_recall and courier_note. */
|
|
326
372
|
readonly hiddenFromLeaf?: ReadonlyArray<string>;
|
|
327
373
|
readonly log: (message: string) => void;
|
package/dist/tree/roster.d.ts
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import { type Status } from "../kernel/protocol.js";
|
|
2
|
+
import { type Exists } from "./domain.js";
|
|
2
3
|
import type { RosterEntry, RosterStorage } from "./ports.js";
|
|
3
|
-
|
|
4
|
-
* Entries older than this are dropped when their parent's roster is read, and on plugin setup,
|
|
5
|
-
* except isolated children whose worktree is still there: those stay until courier_cleanup.
|
|
6
|
-
*/
|
|
7
|
-
export declare const RETENTION_MS: number;
|
|
4
|
+
export { RETENTION_MS, type Exists } from "./domain.js";
|
|
8
5
|
/** Every parent's children. */
|
|
9
6
|
export declare function allEntries(storage: RosterStorage): Promise<RosterEntry[]>;
|
|
10
7
|
/**
|
|
@@ -33,12 +30,7 @@ export declare function reportState(storage: RosterStorage, sessionID: string):
|
|
|
33
30
|
* person is there. Throws unless the session was spawned and `callerID` is that top; `what` names it.
|
|
34
31
|
*/
|
|
35
32
|
export declare function answeringTop(storage: RosterStorage, sessionID: string, callerID: string, what: string): Promise<string>;
|
|
36
|
-
/**
|
|
37
|
-
export type Exists = (directory: string) => boolean;
|
|
38
|
-
/**
|
|
39
|
-
* Whether an entry is past retention at `now`: recorded more than `RETENTION_MS` before, unless it is an isolated
|
|
40
|
-
* child whose worktree exists, which is kept so it can still be cleaned up.
|
|
41
|
-
*/
|
|
33
|
+
/** `domain.ts`'s `isExpired`, asking the file system whether an isolated child's worktree is still there unless a test passes a fake. */
|
|
42
34
|
export declare const isExpired: (entry: RosterEntry, now: number, exists?: Exists) => boolean;
|
|
43
35
|
/** A parent's children, oldest first, after dropping the expired ones. */
|
|
44
36
|
export declare function current(storage: RosterStorage, parentID: string, now: number, exists?: Exists): Promise<RosterEntry[]>;
|
package/dist/tree/roster.js
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { isStatus } from "../kernel/protocol.js";
|
|
3
|
+
import { isExpired as expiredBy } from "./domain.js";
|
|
3
4
|
import { forgetChild, forgetReports } from "./forget.js";
|
|
4
5
|
import { stoppedSincePrompt } from "./report.js";
|
|
5
6
|
import { childStore } from "./store.js";
|
|
6
|
-
|
|
7
|
-
* Entries older than this are dropped when their parent's roster is read, and on plugin setup,
|
|
8
|
-
* except isolated children whose worktree is still there: those stay until courier_cleanup.
|
|
9
|
-
*/
|
|
10
|
-
export const RETENTION_MS = 14 * 24 * 60 * 60_000;
|
|
7
|
+
export { RETENTION_MS } from "./domain.js";
|
|
11
8
|
/** Every parent's children. */
|
|
12
9
|
export async function allEntries(storage) {
|
|
13
10
|
return childStore(storage).all();
|
|
@@ -54,11 +51,8 @@ export async function answeringTop(storage, sessionID, callerID, what) {
|
|
|
54
51
|
throw new Error(`${sessionID}'s ${what} go to ${top}, the session at the top of the sessions started from it with courier_spawn; ${callerID} cannot answer them.`);
|
|
55
52
|
return top;
|
|
56
53
|
}
|
|
57
|
-
/**
|
|
58
|
-
|
|
59
|
-
* child whose worktree exists, which is kept so it can still be cleaned up.
|
|
60
|
-
*/
|
|
61
|
-
export const isExpired = (entry, now, exists = existsSync) => now - entry.createdAt > RETENTION_MS && !(entry.isolated && exists(entry.directory));
|
|
54
|
+
/** `domain.ts`'s `isExpired`, asking the file system whether an isolated child's worktree is still there unless a test passes a fake. */
|
|
55
|
+
export const isExpired = (entry, now, exists = existsSync) => expiredBy(entry, now, exists);
|
|
62
56
|
/** Forgets the given entries that are expired at `now`, and returns the rest. */
|
|
63
57
|
async function dropExpired(storage, entries, now, exists) {
|
|
64
58
|
const expired = new Set(entries.filter((entry) => isExpired(entry, now, exists)));
|
|
@@ -87,6 +87,8 @@ export declare function registerTreeRpc(rpc: Pick<Plugin.Context["rpc"], "regist
|
|
|
87
87
|
readonly status?: string | undefined;
|
|
88
88
|
} | undefined;
|
|
89
89
|
readonly idle?: number | undefined;
|
|
90
|
+
readonly native?: boolean | undefined;
|
|
91
|
+
readonly model?: string | undefined;
|
|
90
92
|
readonly tool?: {
|
|
91
93
|
readonly name: string;
|
|
92
94
|
readonly at: number;
|
|
@@ -130,6 +132,8 @@ export declare function registerTreeRpc(rpc: Pick<Plugin.Context["rpc"], "regist
|
|
|
130
132
|
readonly status?: string | undefined;
|
|
131
133
|
} | undefined;
|
|
132
134
|
readonly idle?: number | undefined;
|
|
135
|
+
readonly native?: boolean | undefined;
|
|
136
|
+
readonly model?: string | undefined;
|
|
133
137
|
readonly tool?: {
|
|
134
138
|
readonly name: string;
|
|
135
139
|
readonly at: number;
|
|
@@ -159,6 +163,8 @@ export declare function registerTreeRpc(rpc: Pick<Plugin.Context["rpc"], "regist
|
|
|
159
163
|
readonly standing: import("effect/Schema").String;
|
|
160
164
|
}>>;
|
|
161
165
|
readonly isolated: import("effect/Schema").Boolean;
|
|
166
|
+
readonly native: import("effect/Schema").optionalKey<import("effect/Schema").Boolean>;
|
|
167
|
+
readonly model: import("effect/Schema").optionalKey<import("effect/Schema").String>;
|
|
162
168
|
readonly created: import("effect/Schema").Number;
|
|
163
169
|
readonly idle: import("effect/Schema").optionalKey<import("effect/Schema").Number>;
|
|
164
170
|
readonly cost: import("effect/Schema").optionalKey<import("effect/Schema").Number>;
|
package/dist/tree/rpc.d.ts
CHANGED
|
@@ -60,6 +60,10 @@ declare const Row: Schema.Struct<{
|
|
|
60
60
|
readonly standing: Schema.String;
|
|
61
61
|
}>>;
|
|
62
62
|
readonly isolated: Schema.Boolean;
|
|
63
|
+
/** Set when it is OpenCode's subagent of its parent (a native child), which OpenCode's session lists leave out. */
|
|
64
|
+
readonly native: Schema.optionalKey<Schema.Boolean>;
|
|
65
|
+
/** The model it was started on, `provider/model` with `#variant` when one is set, when that is not its parent's. */
|
|
66
|
+
readonly model: Schema.optionalKey<Schema.String>;
|
|
63
67
|
readonly created: Schema.Number;
|
|
64
68
|
/** When its last turn ended, if it is not running. */
|
|
65
69
|
readonly idle: Schema.optionalKey<Schema.Number>;
|
|
@@ -117,6 +121,10 @@ declare const Tree: Schema.Struct<{
|
|
|
117
121
|
readonly standing: Schema.String;
|
|
118
122
|
}>>;
|
|
119
123
|
readonly isolated: Schema.Boolean;
|
|
124
|
+
/** Set when it is OpenCode's subagent of its parent (a native child), which OpenCode's session lists leave out. */
|
|
125
|
+
readonly native: Schema.optionalKey<Schema.Boolean>;
|
|
126
|
+
/** The model it was started on, `provider/model` with `#variant` when one is set, when that is not its parent's. */
|
|
127
|
+
readonly model: Schema.optionalKey<Schema.String>;
|
|
120
128
|
readonly created: Schema.Number;
|
|
121
129
|
/** When its last turn ended, if it is not running. */
|
|
122
130
|
readonly idle: Schema.optionalKey<Schema.Number>;
|
|
@@ -258,6 +266,8 @@ export declare const treeRpc: {
|
|
|
258
266
|
readonly status?: string | undefined;
|
|
259
267
|
} | undefined;
|
|
260
268
|
readonly idle?: number | undefined;
|
|
269
|
+
readonly native?: boolean | undefined;
|
|
270
|
+
readonly model?: string | undefined;
|
|
261
271
|
readonly tool?: {
|
|
262
272
|
readonly name: string;
|
|
263
273
|
readonly at: number;
|
|
@@ -301,6 +311,8 @@ export declare const treeRpc: {
|
|
|
301
311
|
readonly status?: string | undefined;
|
|
302
312
|
} | undefined;
|
|
303
313
|
readonly idle?: number | undefined;
|
|
314
|
+
readonly native?: boolean | undefined;
|
|
315
|
+
readonly model?: string | undefined;
|
|
304
316
|
readonly tool?: {
|
|
305
317
|
readonly name: string;
|
|
306
318
|
readonly at: number;
|
|
@@ -341,6 +353,10 @@ export declare const treeRpc: {
|
|
|
341
353
|
readonly standing: Schema.String;
|
|
342
354
|
}>>;
|
|
343
355
|
readonly isolated: Schema.Boolean;
|
|
356
|
+
/** Set when it is OpenCode's subagent of its parent (a native child), which OpenCode's session lists leave out. */
|
|
357
|
+
readonly native: Schema.optionalKey<Schema.Boolean>;
|
|
358
|
+
/** The model it was started on, `provider/model` with `#variant` when one is set, when that is not its parent's. */
|
|
359
|
+
readonly model: Schema.optionalKey<Schema.String>;
|
|
344
360
|
readonly created: Schema.Number;
|
|
345
361
|
/** When its last turn ended, if it is not running. */
|
|
346
362
|
readonly idle: Schema.optionalKey<Schema.Number>;
|
package/dist/tree/rpc.js
CHANGED
|
@@ -50,6 +50,10 @@ const Row = Schema.Struct({
|
|
|
50
50
|
/** Its join group, and its standing there: `held`, `out`, how it left, or `released`. */
|
|
51
51
|
group: Schema.optionalKey(Schema.Struct({ name: Schema.String, standing: Schema.String })),
|
|
52
52
|
isolated: Schema.Boolean,
|
|
53
|
+
/** Set when it is OpenCode's subagent of its parent (a native child), which OpenCode's session lists leave out. */
|
|
54
|
+
native: Schema.optionalKey(Schema.Boolean),
|
|
55
|
+
/** The model it was started on, `provider/model` with `#variant` when one is set, when that is not its parent's. */
|
|
56
|
+
model: Schema.optionalKey(Schema.String),
|
|
53
57
|
created: Schema.Number,
|
|
54
58
|
/** When its last turn ended, if it is not running. */
|
|
55
59
|
idle: Schema.optionalKey(Schema.Number),
|
package/dist/tree/send.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { envelope, isStatus, STATUSES } from "../kernel/protocol.js";
|
|
2
2
|
import { groupStanding, holdReport, membershipFor, unholdForBlocked } from "./group.js";
|
|
3
|
+
import { syncStanding } from "./standing.js";
|
|
3
4
|
import { childStore, groupStore } from "./store.js";
|
|
4
5
|
import { reportBody } from "./text.js";
|
|
5
6
|
import { noteSent } from "./wake.js";
|
|
@@ -8,37 +9,63 @@ import { noteSent } from "./wake.js";
|
|
|
8
9
|
* to the one that started it, it is its report or its progress (`noteReport`); a group member's final report is held (`hold`).
|
|
9
10
|
*/
|
|
10
11
|
export async function send(ports, from, input) {
|
|
11
|
-
|
|
12
|
-
const status = input.status ?? undefined;
|
|
13
|
-
if (status !== undefined && !isStatus(status))
|
|
14
|
-
throw new Error(`status must be one of ${STATUSES.join(", ")}, not ${JSON.stringify(status)}.`);
|
|
12
|
+
const { status, dropped } = await statusFor(ports, from, input);
|
|
15
13
|
// Noted first: the delivery may wake the parent, whose turn may end before a later note.
|
|
16
14
|
const noted = await noteReport(ports, from, input.sessionID, status).catch(() => undefined);
|
|
17
15
|
try {
|
|
18
16
|
const held = noted?.member && status && status !== "blocked" ? await hold(ports, noted.member, { at: noted.at, status, message: input.message, ...(input.artifacts ? { artifacts: input.artifacts } : {}) }) : undefined;
|
|
17
|
+
// The parent's standing before the delivery, which may wake it: its first step then shows the report or progress noted.
|
|
18
|
+
if (noted)
|
|
19
|
+
await syncStanding(ports, input.sessionID);
|
|
19
20
|
if (held)
|
|
20
21
|
return { status, report: true, held };
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
const delivered = await ports.session.synthetic({
|
|
24
|
-
sessionID: input.sessionID,
|
|
25
|
-
text,
|
|
26
|
-
description,
|
|
27
|
-
metadata: { source: "courier", from, ...(status ? { status } : {}) },
|
|
28
|
-
delivery: input.queue ? "queue" : "steer",
|
|
29
|
-
});
|
|
30
|
-
// Watched now, not when OpenCode's enqueued event is handled: a spawn in the same step must count the child it wakes.
|
|
31
|
-
await noteSent(ports, { sessionID: input.sessionID, inboxID: delivered.id, text, description }).catch(() => undefined);
|
|
32
|
-
// A blocked report, once delivered, is the member's latest word: an earlier report held for it would be stale.
|
|
33
|
-
if (noted?.member && status === "blocked")
|
|
34
|
-
await unholdForBlocked(groupStore(ports.storage), noted.member).catch(() => undefined);
|
|
35
|
-
return { messageID: delivered.id, ...(status ? { status } : {}), ...(noted ? { report: noted.report } : {}) };
|
|
22
|
+
const delivered = await deliver(ports, from, input, status, noted);
|
|
23
|
+
return { ...delivered, ...(noted ? { report: noted.report } : {}), ...(dropped ? { dropped } : {}) };
|
|
36
24
|
}
|
|
37
25
|
catch (error) {
|
|
38
26
|
await noted?.undo().catch(() => undefined);
|
|
27
|
+
// The standing as before the note, once the note is taken back.
|
|
28
|
+
if (noted)
|
|
29
|
+
await syncStanding(ports, input.sessionID);
|
|
39
30
|
throw error;
|
|
40
31
|
}
|
|
41
32
|
}
|
|
33
|
+
/** Delivers the message, watches it, and takes a held report back after a blocked one; the message id and status delivered. */
|
|
34
|
+
async function deliver(ports, from, input, status, noted) {
|
|
35
|
+
const text = envelope(from, reportBody(input.message, input.artifacts), status ? { status } : {});
|
|
36
|
+
const description = `Message from ${from}`;
|
|
37
|
+
const delivered = await ports.session.synthetic({
|
|
38
|
+
sessionID: input.sessionID,
|
|
39
|
+
text,
|
|
40
|
+
description,
|
|
41
|
+
metadata: { source: "courier", from, ...(status ? { status } : {}) },
|
|
42
|
+
delivery: input.queue ? "queue" : "steer",
|
|
43
|
+
});
|
|
44
|
+
// Watched now, not when OpenCode's enqueued event is handled: a spawn in the same step must count the child it wakes.
|
|
45
|
+
await noteSent(ports, { sessionID: input.sessionID, inboxID: delivered.id, text, description }).catch(() => undefined);
|
|
46
|
+
// A blocked report, once delivered, is the member's latest word: an earlier report held for it would be stale.
|
|
47
|
+
if (noted?.member && status === "blocked")
|
|
48
|
+
await unholdForBlocked(groupStore(ports.storage), noted.member).catch(() => undefined);
|
|
49
|
+
return { messageID: delivered.id, ...(status ? { status } : {}) };
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The message's status, checked, as OpenCode does not check the schema. A status is a report's: on a message to a
|
|
53
|
+
* session below the sender it means nothing, so it is `dropped`, and the result says so.
|
|
54
|
+
*/
|
|
55
|
+
async function statusFor(ports, from, input) {
|
|
56
|
+
// Some models send null for an optional field they leave out.
|
|
57
|
+
const given = input.status ?? undefined;
|
|
58
|
+
if (given === undefined)
|
|
59
|
+
return {};
|
|
60
|
+
if (!isStatus(given))
|
|
61
|
+
throw new Error(`status must be one of ${STATUSES.join(", ")}, not ${JSON.stringify(given)}.`);
|
|
62
|
+
return (await isBelow(ports, from, input.sessionID)) ? { dropped: given } : { status: given };
|
|
63
|
+
}
|
|
64
|
+
/** Whether `from` started `to`, directly or through others, by the reverse index; false when the index does not name `to` or cannot be read. */
|
|
65
|
+
async function isBelow(ports, from, to) {
|
|
66
|
+
const above = await childStore(ports.storage).indexedAncestors(to).catch(() => []);
|
|
67
|
+
return above.includes(from);
|
|
68
|
+
}
|
|
42
69
|
/**
|
|
43
70
|
* Notes `from`'s message to `to` when `to` started it: as its report when it carries a status, or when an earlier
|
|
44
71
|
* release briefed `from`, else as progress. Returns which, when, how to take the note back, and `from`'s group membership.
|
package/dist/tree/spawn.d.ts
CHANGED
|
@@ -5,12 +5,20 @@ export interface SpawnInput {
|
|
|
5
5
|
readonly agent?: string;
|
|
6
6
|
readonly isolate?: boolean;
|
|
7
7
|
readonly group?: string;
|
|
8
|
+
/** `provider/model` to run on; left out, the agent's own model, else the caller's. */
|
|
9
|
+
readonly model?: string;
|
|
10
|
+
/** The variant of that model, its reasoning effort; left out, the chosen model's own. */
|
|
11
|
+
readonly variant?: string;
|
|
12
|
+
/** Whether the child is OpenCode's subagent of the caller; left out, the `nativeChildren` option decides. */
|
|
13
|
+
readonly native?: boolean;
|
|
8
14
|
}
|
|
9
15
|
/**
|
|
10
16
|
* Creates a child session, hands it the task and returns at once; the child reports back with
|
|
11
17
|
* courier_send. Refused past a limit, whether or not the `context` hook hid the tool.
|
|
12
18
|
*/
|
|
13
19
|
export declare function spawn(ports: CourierPorts, parentID: string, input: SpawnInput): Promise<{
|
|
20
|
+
native?: boolean | undefined;
|
|
21
|
+
model?: string | undefined;
|
|
14
22
|
uncommitted?: boolean | undefined;
|
|
15
23
|
fromParent?: boolean | undefined;
|
|
16
24
|
groupError?: string | undefined;
|
package/dist/tree/spawn.js
CHANGED
|
@@ -1,24 +1,15 @@
|
|
|
1
1
|
import { describeFailure } from "../kernel/protocol.js";
|
|
2
|
-
import { isGroupName } from "./domain.js";
|
|
2
|
+
import { isGroupName, modelName } from "./domain.js";
|
|
3
3
|
import { joinGroup, leaveGroup } from "./group.js";
|
|
4
4
|
import { admit } from "./limits.js";
|
|
5
|
+
import { chooseModel } from "./model.js";
|
|
6
|
+
import { putEntries, syncStanding } from "./standing.js";
|
|
5
7
|
import { childStore, groupStore } from "./store.js";
|
|
6
|
-
import { childBrief, childBranch } from "./text.js";
|
|
8
|
+
import { childBrief, childBranch, nativeIsolateRefusal, ROLE_KEY, rolePart } from "./text.js";
|
|
7
9
|
function titleOf(task) {
|
|
8
10
|
const line = task.trim().split("\n")[0] ?? "";
|
|
9
11
|
return line.length > 60 ? `${line.slice(0, 57)}...` : line;
|
|
10
12
|
}
|
|
11
|
-
/**
|
|
12
|
-
* The model a child runs on, always passed on so no OpenCode version falls back to its default:
|
|
13
|
-
* the named agent's own, else its parent's.
|
|
14
|
-
*/
|
|
15
|
-
async function inheritedModel(ports, parentID, agent) {
|
|
16
|
-
const parent = await ports.session.get({ sessionID: parentID });
|
|
17
|
-
if (!agent)
|
|
18
|
-
return parent.model;
|
|
19
|
-
const named = await ports.agent.get({ agentID: agent, location: { directory: ports.directory } });
|
|
20
|
-
return named.data.model ?? parent.model;
|
|
21
|
-
}
|
|
22
13
|
/** Removes a worktree made for a child that was never created; one that cannot be removed stays. */
|
|
23
14
|
async function dropWorktree(ports, directory) {
|
|
24
15
|
try {
|
|
@@ -37,16 +28,19 @@ export async function spawn(ports, parentID, input) {
|
|
|
37
28
|
const group = typeof input.group === "string" ? input.group.trim() || undefined : (input.group ?? undefined);
|
|
38
29
|
if (group !== undefined && !isGroupName(group))
|
|
39
30
|
throw new Error(`group must be a name of 1 to 60 letters, digits, dots, dashes or underscores, not ${JSON.stringify(group)}.`);
|
|
31
|
+
const native = typeof input.native === "boolean" ? input.native : undefined;
|
|
32
|
+
if (native && input.isolate)
|
|
33
|
+
throw new Error(nativeIsolateRefusal());
|
|
40
34
|
const admitted = await admit(ports, parentID);
|
|
41
35
|
try {
|
|
42
|
-
return await start(ports, parentID, input, admitted, group);
|
|
36
|
+
return await start(ports, parentID, { ...input, native }, admitted, group);
|
|
43
37
|
}
|
|
44
38
|
finally {
|
|
45
39
|
admitted.release();
|
|
46
40
|
}
|
|
47
41
|
}
|
|
48
42
|
async function start(ports, parentID, input, admitted, group) {
|
|
49
|
-
const { worktree, handedDown, ...child } = await create(ports, parentID, input);
|
|
43
|
+
const { worktree, handedDown, model, ...child } = await create(ports, parentID, input);
|
|
50
44
|
admitted.reservation.sessionID = child.sessionID;
|
|
51
45
|
const entry = {
|
|
52
46
|
...child,
|
|
@@ -56,10 +50,17 @@ async function start(ports, parentID, input, admitted, group) {
|
|
|
56
50
|
reports: "status",
|
|
57
51
|
...(worktree ? { source: ports.directory, project: ports.projectID } : {}),
|
|
58
52
|
...(group ? { group } : {}),
|
|
53
|
+
...(model ? { model } : {}),
|
|
59
54
|
};
|
|
60
55
|
const { rosterError, groupError } = await enrol(ports, entry, group);
|
|
61
|
-
ports.
|
|
62
|
-
|
|
56
|
+
await nameBoth(ports, parentID, entry.sessionID, admitted.depth);
|
|
57
|
+
try {
|
|
58
|
+
await handOver(ports, entry, admitted.depth, input.task, !rosterError, !rosterError && !groupError);
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
// The parent's standing with the child on it, prompted or failed, announced at its next step.
|
|
62
|
+
await syncStanding(ports, parentID);
|
|
63
|
+
}
|
|
63
64
|
return {
|
|
64
65
|
sessionID: entry.sessionID,
|
|
65
66
|
directory: entry.directory,
|
|
@@ -68,12 +69,16 @@ async function start(ports, parentID, input, admitted, group) {
|
|
|
68
69
|
...(groupError ? { groupError } : {}),
|
|
69
70
|
...(handedDown ? { fromParent: true } : {}),
|
|
70
71
|
...(handedDown?.uncommitted ? { uncommitted: true } : {}),
|
|
72
|
+
...(model ? { model } : {}),
|
|
73
|
+
...(entry.native ? { native: true } : {}),
|
|
71
74
|
};
|
|
72
75
|
}
|
|
73
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* Creates the child's session, in a worktree of its own if asked, which goes again if the session cannot be created.
|
|
78
|
+
* `model` names the child's model when it is not its parent's, for the roster and the result.
|
|
79
|
+
*/
|
|
74
80
|
async function create(ports, parentID, input) {
|
|
75
|
-
|
|
76
|
-
const model = await inheritedModel(ports, parentID, input.agent).catch(() => undefined);
|
|
81
|
+
const { model, parent } = await chooseModel(ports, parentID, input, input.agent);
|
|
77
82
|
const handedDown = input.isolate ? await parentWork(ports, parentID) : undefined;
|
|
78
83
|
const worktree = input.isolate
|
|
79
84
|
? (await ports.worktree.create({ projectID: ports.projectID, ...(handedDown ? { from: handedDown.directory, branch: handedDown.head } : {}) })).directory
|
|
@@ -81,7 +86,7 @@ async function create(ports, parentID, input) {
|
|
|
81
86
|
const base = worktree ? await ports.head(worktree) : undefined;
|
|
82
87
|
const title = input.title ?? titleOf(input.task);
|
|
83
88
|
// OpenCode takes a location or a parentID, never both, so an isolated child cannot be a native one.
|
|
84
|
-
const native = ports.nativeChildren === true && !worktree;
|
|
89
|
+
const native = (input.native ?? ports.nativeChildren === true) && !worktree;
|
|
85
90
|
const child = await ports.session
|
|
86
91
|
.create({
|
|
87
92
|
title,
|
|
@@ -106,6 +111,7 @@ async function create(ports, parentID, input) {
|
|
|
106
111
|
handedDown,
|
|
107
112
|
...(base ? { base } : {}),
|
|
108
113
|
...(native ? { native: true } : {}),
|
|
114
|
+
...(model && (!parent || modelName(model) !== modelName(parent)) ? { model: modelName(model) } : {}),
|
|
109
115
|
};
|
|
110
116
|
}
|
|
111
117
|
/**
|
|
@@ -171,3 +177,50 @@ async function join(ports, entry, group) {
|
|
|
171
177
|
return describeFailure("group", error).message;
|
|
172
178
|
}
|
|
173
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* The child's role and the other modules' entries before its prompt, so its first request carries them, and a root's own at
|
|
182
|
+
* its first spawn, remembered for the `context` hook; a parent that is no root is forgotten there, as its depth may have changed.
|
|
183
|
+
*/
|
|
184
|
+
async function nameBoth(ports, parentID, childID, depth) {
|
|
185
|
+
const extra = await extraEntries(ports);
|
|
186
|
+
await nameRole(ports, childID, depth);
|
|
187
|
+
if (depth < ports.limits.maxDepth)
|
|
188
|
+
await putEntries(ports, childID, extra);
|
|
189
|
+
const rootNamed = ports.roles.get(parentID)?.entry === true;
|
|
190
|
+
const named = depth === 1 && (rootNamed || (await nameRole(ports, parentID, 0)));
|
|
191
|
+
if (named && !rootNamed)
|
|
192
|
+
await putEntries(ports, parentID, extra);
|
|
193
|
+
if (named)
|
|
194
|
+
ports.roles.set(parentID, { depth: 0, entry: true });
|
|
195
|
+
else
|
|
196
|
+
ports.roles.delete(parentID);
|
|
197
|
+
}
|
|
198
|
+
/** The entries the other modules give a spawned session, the memory's lessons; none when they cannot be read, logged. */
|
|
199
|
+
async function extraEntries(ports) {
|
|
200
|
+
if (!ports.entries || !ports.session.instructions)
|
|
201
|
+
return [];
|
|
202
|
+
try {
|
|
203
|
+
return await ports.entries();
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
ports.log(`courier_spawn: the entries of the other modules could not be read, so the session starts without them: ${String(error)}`);
|
|
207
|
+
return [];
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Writes a session's role part to its `role` entry: a child's system prompt carries it from its first request, a root's
|
|
212
|
+
* transcript as an update. False on a host without entries, or when the put failed, logged: the `context` hook adds the part.
|
|
213
|
+
*/
|
|
214
|
+
async function nameRole(ports, sessionID, depth) {
|
|
215
|
+
const entries = ports.session.instructions?.entry;
|
|
216
|
+
if (!entries)
|
|
217
|
+
return false;
|
|
218
|
+
try {
|
|
219
|
+
await entries.put({ sessionID, key: ROLE_KEY, value: rolePart(depth, ports.limits) });
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
ports.log(`courier_spawn: the role entry of ${sessionID} could not be written, so its role goes in a system part: ${String(error)}`);
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Entry, ReportNote, RosterStorage, StandingPorts } from "./ports.js";
|
|
2
|
+
import { type ChildState, type StandingRow } from "./text.js";
|
|
3
|
+
/** A child's state from its report notes, as `reportOf` and the silent-turn rule read them. */
|
|
4
|
+
export declare function stateOf(notes: {
|
|
5
|
+
readonly [K in "prompt" | "settled" | "progress" | "told" | "stopped"]: ReportNote;
|
|
6
|
+
}): ChildState;
|
|
7
|
+
/** The rows of a parent's standing entry: its unexpired children, oldest first, each with its state and group standing. */
|
|
8
|
+
export declare function standingRows(storage: RosterStorage, parentID: string, now: number): Promise<StandingRow[]>;
|
|
9
|
+
/** What `syncStanding` did to the parent's entry. */
|
|
10
|
+
export type Synced = "put" | "unchanged" | "removed" | "absent" | "skipped";
|
|
11
|
+
/**
|
|
12
|
+
* Writes a parent's `standing` entry once something of it changed: put when its text differs from the entry's, removed when
|
|
13
|
+
* the parent has no children left; skipped without the keyed entries. A failure is logged, never thrown.
|
|
14
|
+
*/
|
|
15
|
+
export declare function syncStanding(ports: StandingPorts, parentID: string): Promise<Synced>;
|
|
16
|
+
/** Writes `given` entries to a session, each put logged on failure and never thrown: the session gets its task all the same. */
|
|
17
|
+
export declare function putEntries(ports: StandingPorts, sessionID: string, given: ReadonlyArray<Entry>): Promise<void>;
|