opencode-courier 0.5.0 → 0.6.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 +20 -13
- package/dist/index.js +6 -3
- package/dist/person/form.d.ts +4 -1
- package/dist/person/form.js +5 -0
- package/dist/person/index.d.ts +2 -1
- package/dist/person/index.js +1 -0
- package/dist/person/permission.d.ts +14 -0
- package/dist/person/permission.js +15 -0
- package/dist/person/ports.d.ts +8 -2
- package/dist/person/rpc-handlers.d.ts +37 -30
- package/dist/person/rpc-handlers.js +26 -16
- package/dist/person/rpc.d.ts +43 -24
- package/dist/person/rpc.js +4 -3
- package/dist/person/tui.d.ts +23 -12
- package/dist/tree/domain.d.ts +23 -0
- package/dist/tree/domain.js +26 -0
- package/dist/tree/events.d.ts +2 -2
- package/dist/tree/events.js +5 -3
- package/dist/tree/index.d.ts +1 -1
- package/dist/tree/panel.d.ts +7 -2
- package/dist/tree/panel.js +19 -16
- package/dist/tree/ports.d.ts +7 -0
- package/dist/tree/roster.d.ts +5 -0
- package/dist/tree/roster.js +5 -3
- package/dist/tree/rpc-handlers.d.ts +45 -5
- package/dist/tree/rpc-handlers.js +30 -1
- package/dist/tree/rpc.d.ts +79 -19
- package/dist/tree/rpc.js +20 -8
- package/dist/tree/subtree.d.ts +40 -9
- package/dist/tree/subtree.js +40 -19
- package/dist/tree/summary.d.ts +2 -2
- package/dist/tree/summary.js +8 -16
- package/dist/tree/text.js +4 -1
- package/dist/tree/tools.js +2 -1
- package/dist/tree/tui.d.ts +11 -4
- package/dist/tui.js +567 -174
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -45,8 +45,8 @@ sometimes stall on a request; if nothing moves for a few minutes, send the promp
|
|
|
45
45
|
Courier is built on OpenCode V2's plugin API (`@opencode/plugin`), not the V1 one
|
|
46
46
|
(`@opencode-ai/plugin`), and needs OpenCode V2. Each release is pinned to one V2 version and tested
|
|
47
47
|
against it end to end; the current one is tested on **OpenCode 2.0.29**. CI also runs the live
|
|
48
|
-
suite on the newest OpenCode release, so a host release that breaks the
|
|
49
|
-
first. Older releases and their versions: [Supported OpenCode version](#supported-opencode-version).
|
|
48
|
+
suite on the newest OpenCode release, on `main` and nightly, so a host release that breaks the
|
|
49
|
+
plugin shows up there first. Older releases and their versions: [Supported OpenCode version](#supported-opencode-version).
|
|
50
50
|
|
|
51
51
|
## Courier or the built-in subagent tool?
|
|
52
52
|
|
|
@@ -117,7 +117,7 @@ line to the server log naming both ([more](docs/reference.md#the-opencode-versio
|
|
|
117
117
|
|
|
118
118
|
| opencode-courier | OpenCode V2 (`opencode` and `@opencode/plugin`) |
|
|
119
119
|
|---|---|
|
|
120
|
-
| 0.5.0 | 2.0.29 |
|
|
120
|
+
| 0.5.0, 0.6.0 | 2.0.29 |
|
|
121
121
|
| 0.4.0, 0.4.1 | 2.0.28 |
|
|
122
122
|
| 0.2.3, 0.3.0 | 2.0.26 |
|
|
123
123
|
| 0.2.2 | 2.0.24 |
|
|
@@ -226,7 +226,8 @@ Each tool's parameters: [the reference](docs/reference.md#the-tools).
|
|
|
226
226
|
[terminal](#in-the-terminal). [More](docs/reference.md#seeing-and-stopping-a-subtree).
|
|
227
227
|
|
|
228
228
|
Children, held reports, scheduled messages and open questions survive a compaction or a restart;
|
|
229
|
-
entries are dropped after 14 days
|
|
229
|
+
entries are dropped after 14 days, or at once when OpenCode deletes the child, unless it is isolated
|
|
230
|
+
([roster](docs/reference.md#roster)).
|
|
230
231
|
|
|
231
232
|
## Session trees
|
|
232
233
|
|
|
@@ -256,28 +257,34 @@ does not. Each spawned session's brief says when to split its task and when to d
|
|
|
256
257
|
|
|
257
258
|
The package ships a part for OpenCode's terminal UI, which the TUI loads from the server's plugin
|
|
258
259
|
list; the server half behaves the same whether or not a TUI is attached. In a session that started
|
|
259
|
-
sessions, the prompt's footer shows what the tree under it is doing: how many run, wait on you
|
|
260
|
-
are idle, and for each [join group](docs/reference.md#join-groups) how many of its members have
|
|
260
|
+
sessions, the prompt's footer shows what the tree under it is doing: how many run, wait on you (a
|
|
261
|
+
permission request, a question or a form they show) or are idle, and for each [join group](docs/reference.md#join-groups) how many of its members have
|
|
261
262
|
reported:
|
|
262
263
|
|
|
263
264
|
```
|
|
264
|
-
courier
|
|
265
|
+
courier 1 waiting on you · 2 running · group pair 1/2
|
|
265
266
|
```
|
|
266
267
|
|
|
267
268
|
`/courier tree` opens a panel beside the session with the tree under it, a row per session indented
|
|
268
|
-
by depth, with what it does, its report and its group's standing; a session
|
|
269
|
-
|
|
269
|
+
by depth, with what it does, its report and its group's standing; a session waiting on you gets a line per
|
|
270
|
+
request or form (`shows a form: Web Search`), a session that started others gets
|
|
271
|
+
a bar of the parts reported, a leaf how long it ran, its last tool call and its cost. A tree that has
|
|
272
|
+
started more than `maxTotal` sessions over time is listed live ones first, then newest, and the header
|
|
273
|
+
says how many older ones are left out; the footer counts them all. `↑`/`↓` select,
|
|
270
274
|
`enter` opens the session, `s` stops it and everything under it after you confirm, as `courier_stop`
|
|
271
|
-
would,
|
|
275
|
+
would, `x` forgets a `gone` session after you confirm, removing its worktree by `courier_cleanup`'s
|
|
276
|
+
rules, and `q` closes the panel; `/courier stop <session>` stops from the prompt. While open, the panel
|
|
272
277
|
follows you between sessions: into one of its tree it keeps that tree, the session on screen marked
|
|
273
278
|
`(on screen)`; elsewhere it shows that session's tree, or hides while you are in a session without one,
|
|
274
279
|
or on the home screen, and shows again once you are back in one; only `q` closes it for good.
|
|
275
280
|
|
|
276
281
|
`ctrl+x k` (the leader key, then `k`), `/asks` (or `/ask`), the palette's "Courier: what waits on you" or a click
|
|
277
282
|
on "waiting on you" opens the asks inbox: every permission request, question and form waiting
|
|
278
|
-
anywhere in the tree,
|
|
279
|
-
|
|
280
|
-
|
|
283
|
+
anywhere in the tree, however many sessions it has, oldest first, after the top session's own permission
|
|
284
|
+
requests, such as the prompt `courier_answer` has OpenCode show there, with where it comes from, kept current
|
|
285
|
+
while it is open as asks arrive or are settled elsewhere, answered in a dialog with no model in between; the session that started the tree hears once that you settled it. The TUI plays OpenCode's
|
|
286
|
+
sounds for a child's report and a group's release, and OpenCode's own for a new ask, which the courier
|
|
287
|
+
adds a notification to for a native child ([more](docs/reference.md#in-the-terminal)).
|
|
281
288
|
|
|
282
289
|
While a permission prompt or form is shown in the session, OpenCode hides the footer, so open the
|
|
283
290
|
inbox with `ctrl+x k` (or `/asks` once the prompt is back).
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import { watchFromHub } from "./platform/events.js";
|
|
|
7
7
|
import { hub, openInProcess, permissions as domains, sharedState } from "./platform/hub.js";
|
|
8
8
|
import { builtVersions, versionNotice } from "./platform/version.js";
|
|
9
9
|
import { addSchedulingTools, cancelScheduled, dueJob, isScheduled } from "./scheduling/index.js";
|
|
10
|
-
import { addPersonTools, asksPermission, asksQuestion, joinRelay, noticeCutOff, pendingOf, pendingQuestions, personEvents, portsAt, registerPersonRpc, relayQuestions, } from "./person/index.js";
|
|
10
|
+
import { addPersonTools, asksPermission, asksQuestion, formsShown, joinRelay, noticeCutOff, pendingOf, pendingQuestions, personEvents, portsAt, registerPersonRpc, relayQuestions, } from "./person/index.js";
|
|
11
11
|
import { addSkill } from "./skill.js";
|
|
12
12
|
import { addTreeTools, deliverReleased, dirtyOf, headOf, inspectWorktree, pruneExpired, readLimits, readNativeChildren, registerTreeRpc, shapeContext, together, treeEvents, waitsOn, } from "./tree/index.js";
|
|
13
13
|
import { addWebhookTools, joinReceiver, readConfig, receiving, subscribedSince, } from "./webhook/index.js";
|
|
@@ -15,6 +15,8 @@ import { addWebhookTools, joinReceiver, readConfig, receiving, subscribedSince,
|
|
|
15
15
|
const gate = sharedState("spawning", () => ({ reserved: new Set(), turn: Promise.resolve() }));
|
|
16
16
|
/** The type of each item enqueued for a spawned session, outside the hub like the gate. */
|
|
17
17
|
const inbox = sharedState("inbox", () => new Map());
|
|
18
|
+
/** The forms shown now, which the person events keep and the footer and tree panel count; outside the hub like the inbox. */
|
|
19
|
+
const shown = sharedState("formsShown", () => new Map());
|
|
18
20
|
/**
|
|
19
21
|
* What each tick of the scheduler does, in order, through the first loaded member, of whichever copy:
|
|
20
22
|
* due `courier_later` messages, then released join groups. Every hub-v2 member's `later` is its LaterPorts.
|
|
@@ -30,7 +32,7 @@ const permissions = domains;
|
|
|
30
32
|
// several subscriptions and copies see the same events. The scheduler's claim set is the hub's own.
|
|
31
33
|
const { watched } = hub;
|
|
32
34
|
const watchState = { seen: watched.seen, inbox };
|
|
33
|
-
const personState = { ...watched, forms: hub.forms };
|
|
35
|
+
const personState = { ...watched, forms: hub.forms, shown };
|
|
34
36
|
/**
|
|
35
37
|
* What a session waits on outside the tree, for the silent-turn rule, cheapest first: a request of its
|
|
36
38
|
* own to the person, then a scheduled message or a webhook it subscribed to since its last prompt.
|
|
@@ -93,6 +95,7 @@ export const courier = () => Plugin.define({
|
|
|
93
95
|
]);
|
|
94
96
|
return [...requests, ...questions];
|
|
95
97
|
},
|
|
98
|
+
forms: (sessionID) => Promise.resolve(formsShown(shown, sessionID)),
|
|
96
99
|
};
|
|
97
100
|
const cleanupPorts = { ...ports, inspect: inspectWorktree };
|
|
98
101
|
const answerPorts = { storage: ctx.storage, permissions, assert: (input) => ctx.permission.assert(input) };
|
|
@@ -123,7 +126,7 @@ export const courier = () => Plugin.define({
|
|
|
123
126
|
// What the TUI part reads and its stop; a host that cannot register it still gets the rest, the tools included.
|
|
124
127
|
await Promise.resolve()
|
|
125
128
|
.then(() => registerTreeRpc(ctx.rpc, stopPorts))
|
|
126
|
-
.then(() => registerPersonRpc(ctx.rpc, { questions: questionPorts, permissions
|
|
129
|
+
.then(() => registerPersonRpc(ctx.rpc, { questions: questionPorts, permissions }))
|
|
127
130
|
.catch((error) => log(`courier rpc: not registered, the TUI part shows nothing and cannot stop: ${String(error)}`));
|
|
128
131
|
await ctx.session.hook("context", (event) => shapeContext({ storage: ctx.storage, limits, roles, log }, event));
|
|
129
132
|
const pruned = pruneExpired(ctx.storage, Date.now()).catch((error) => log(`courier roster prune: ${String(error)}`));
|
package/dist/person/form.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type ShownForm } from "../tree/index.js";
|
|
2
|
+
import { type Form, type FormsShown, type PersonPorts, type PersonState } from "./ports.js";
|
|
2
3
|
/** The `metadata.kind` of the forms OpenCode's question tool asks with; the question relay passes those on. */
|
|
3
4
|
export declare const QUESTION_FORM = "question";
|
|
4
5
|
/** The part of OpenCode's `form.created` event the notice is made from. */
|
|
@@ -24,3 +25,5 @@ export interface FormSettled {
|
|
|
24
25
|
export declare function reportForm(ports: PersonPorts, state: PersonState, event: FormCreated): Promise<string[]>;
|
|
25
26
|
/** Tells the session told about a form that it was answered or withdrawn, so it does not send the person to a form that is gone. */
|
|
26
27
|
export declare function reportFormSettled(ports: PersonPorts, state: PersonState, event: FormSettled): Promise<string[]>;
|
|
28
|
+
/** The forms a session shows now, oldest first, as the tree's `forms` port lists them. */
|
|
29
|
+
export declare const formsShown: (shown: FormsShown, sessionID: string) => ShownForm[];
|
package/dist/person/form.js
CHANGED
|
@@ -15,6 +15,8 @@ export async function reportForm(ports, state, event) {
|
|
|
15
15
|
const form = event.data.form;
|
|
16
16
|
if (kindOf(form) === QUESTION_FORM)
|
|
17
17
|
return [];
|
|
18
|
+
if (!state.forms.settled.has(form.id))
|
|
19
|
+
setBounded(state.shown, form.id, { sessionID: form.sessionID, title: form.title }, SEEN_MAX);
|
|
18
20
|
const chain = await lineage(ports.storage, form.sessionID);
|
|
19
21
|
// As with a permission request, claimed after the lookup, so a form settled meanwhile is not told.
|
|
20
22
|
if (!chain.length || state.forms.settled.has(form.id) || state.forms.told.has(form.id))
|
|
@@ -41,6 +43,7 @@ export async function reportFormSettled(ports, state, event) {
|
|
|
41
43
|
if (!claim(state.seen, event.id))
|
|
42
44
|
return [];
|
|
43
45
|
const { id, sessionID } = event.data;
|
|
46
|
+
state.shown.delete(id);
|
|
44
47
|
const telling = state.forms.told.get(id);
|
|
45
48
|
if (!telling) {
|
|
46
49
|
claim(state.forms.settled, id);
|
|
@@ -62,3 +65,5 @@ export async function reportFormSettled(ports, state, event) {
|
|
|
62
65
|
});
|
|
63
66
|
return [topOf(chain)];
|
|
64
67
|
}
|
|
68
|
+
/** The forms a session shows now, oldest first, as the tree's `forms` port lists them. */
|
|
69
|
+
export const formsShown = (shown, sessionID) => [...shown].flatMap(([id, form]) => (form.sessionID === sessionID ? [{ id, title: form.title }] : []));
|
package/dist/person/index.d.ts
CHANGED
|
@@ -2,9 +2,10 @@ import type { PendingPrompt, WatchPorts } from "../tree/index.js";
|
|
|
2
2
|
import type { PersonPorts } from "./ports.js";
|
|
3
3
|
import type { Prompt } from "./question/index.js";
|
|
4
4
|
export { personEvents } from "./events.js";
|
|
5
|
+
export { formsShown } from "./form.js";
|
|
5
6
|
export { asksPermission, pendingOf } from "./permission.js";
|
|
6
7
|
export type { AnswerPorts, Permissions, Waiting } from "./permission.js";
|
|
7
|
-
export type { PersonPorts, PersonState } from "./ports.js";
|
|
8
|
+
export type { FormsShown, PersonPorts, PersonState } from "./ports.js";
|
|
8
9
|
export { registerPersonRpc } from "./rpc-handlers.js";
|
|
9
10
|
export { asksQuestion, joinRelay, noticeCutOff, pendingQuestions, portsAt, relayQuestions } from "./question/index.js";
|
|
10
11
|
export type { QuestionPorts } from "./question/index.js";
|
package/dist/person/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { personEvents } from "./events.js";
|
|
2
|
+
export { formsShown } from "./form.js";
|
|
2
3
|
export { asksPermission, pendingOf } from "./permission.js";
|
|
3
4
|
export { registerPersonRpc } from "./rpc-handlers.js";
|
|
4
5
|
export { asksQuestion, joinRelay, noticeCutOff, pendingQuestions, portsAt, relayQuestions } from "./question/index.js";
|
|
@@ -17,6 +17,7 @@ export interface PermissionRequest {
|
|
|
17
17
|
readonly resources: ReadonlyArray<string>;
|
|
18
18
|
readonly save?: ReadonlyArray<string>;
|
|
19
19
|
readonly message?: string;
|
|
20
|
+
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
20
21
|
}
|
|
21
22
|
/**
|
|
22
23
|
* What answering a permission request did; `answered` is false when no location loaded here held the
|
|
@@ -94,6 +95,19 @@ export declare const isDeclined: (error: unknown) => boolean;
|
|
|
94
95
|
* session, a proposed `once` only once OpenCode allowed it there; `answered` is false when no location loaded here holds it.
|
|
95
96
|
*/
|
|
96
97
|
export declare function answer(ports: AnswerPorts, waiting: Waiting, callerID: string, input: AnswerInput): Promise<PermissionAnswered>;
|
|
98
|
+
/** The child's request a prompt of courier_answer's asks about, read from the metadata `askCaller` gives it; undefined for any other request. */
|
|
99
|
+
export declare function passedOn(metadata: Readonly<Record<string, unknown>> | undefined): {
|
|
100
|
+
sessionID: string;
|
|
101
|
+
requestID: string;
|
|
102
|
+
} | undefined;
|
|
103
|
+
/** The requests a session waits on of its own, in whichever location holds them, each with the child's request it passes on when courier_answer asks it. */
|
|
104
|
+
export declare function ownRequests(permissions: Iterable<Permissions>, sessionID: string): Promise<{
|
|
105
|
+
request: PermissionRequest;
|
|
106
|
+
passesOn: {
|
|
107
|
+
sessionID: string;
|
|
108
|
+
requestID: string;
|
|
109
|
+
} | undefined;
|
|
110
|
+
}[]>;
|
|
97
111
|
/** The requests a session waits on, in whichever location holds them, for courier_status. */
|
|
98
112
|
export declare function pendingOf(permissions: Iterable<Permissions>, sessionID: string): Promise<Pending[]>;
|
|
99
113
|
export {};
|
|
@@ -115,6 +115,21 @@ export async function answer(ports, waiting, callerID, input) {
|
|
|
115
115
|
throw new Error(refusedText(sessionID, requestID, refused));
|
|
116
116
|
return { sessionID, requestID, reply, answered: true };
|
|
117
117
|
}
|
|
118
|
+
/** The child's request a prompt of courier_answer's asks about, read from the metadata `askCaller` gives it; undefined for any other request. */
|
|
119
|
+
export function passedOn(metadata) {
|
|
120
|
+
const { source, asks, from, requestID } = metadata ?? {};
|
|
121
|
+
if (source !== "courier" || asks !== "permission" || typeof from !== "string" || typeof requestID !== "string")
|
|
122
|
+
return undefined;
|
|
123
|
+
return { sessionID: from, requestID };
|
|
124
|
+
}
|
|
125
|
+
/** The requests a session waits on of its own, in whichever location holds them, each with the child's request it passes on when courier_answer asks it. */
|
|
126
|
+
export async function ownRequests(permissions, sessionID) {
|
|
127
|
+
const found = new Map();
|
|
128
|
+
for (const { requests } of await listEverywhere(permissions, sessionID))
|
|
129
|
+
for (const request of requests)
|
|
130
|
+
found.set(request.id, request);
|
|
131
|
+
return [...found.values()].map((request) => ({ request, passesOn: passedOn(request.metadata) }));
|
|
132
|
+
}
|
|
118
133
|
/** The requests a session waits on, in whichever location holds them, for courier_status. */
|
|
119
134
|
export async function pendingOf(permissions, sessionID) {
|
|
120
135
|
const found = new Map();
|
package/dist/person/ports.d.ts
CHANGED
|
@@ -19,15 +19,21 @@ export interface PersonPorts {
|
|
|
19
19
|
readonly now: () => number;
|
|
20
20
|
readonly log: (message: string) => void;
|
|
21
21
|
}
|
|
22
|
+
/** The forms spawned sessions and others show, which only the person can answer, by form id; not the question tool's. */
|
|
23
|
+
export type FormsShown = Map<string, {
|
|
24
|
+
readonly sessionID: string;
|
|
25
|
+
readonly title: string;
|
|
26
|
+
}>;
|
|
22
27
|
/**
|
|
23
|
-
* What every plugin instance in the process shares of the person events: the handled event ids,
|
|
24
|
-
*
|
|
28
|
+
* What every plugin instance in the process shares of the person events: the handled event ids (the tree's too),
|
|
29
|
+
* the requests and forms told about or answered first, and the forms shown now.
|
|
25
30
|
*/
|
|
26
31
|
export interface PersonState {
|
|
27
32
|
readonly seen: Set<string>;
|
|
28
33
|
readonly waiting: Waiting;
|
|
29
34
|
readonly answered: Set<string>;
|
|
30
35
|
readonly forms: FormsTold;
|
|
36
|
+
readonly shown: FormsShown;
|
|
31
37
|
}
|
|
32
38
|
/** The session at the top of a lineage: where its permission requests go, since that is where the person is. */
|
|
33
39
|
export declare const topOf: (chain: RosterEntry[]) => string;
|
|
@@ -6,12 +6,10 @@ import { type Answered, type Asks } from "./rpc.js";
|
|
|
6
6
|
export interface PersonRpcPorts {
|
|
7
7
|
readonly questions: QuestionPorts;
|
|
8
8
|
readonly permissions: () => Iterable<Permissions>;
|
|
9
|
-
/** At most this many sessions of a tree are read, as courier_tree reads them. */
|
|
10
|
-
readonly maxTotal: number;
|
|
11
9
|
}
|
|
12
10
|
/**
|
|
13
|
-
* The permission requests and questions waiting in the courier tree a session is in,
|
|
14
|
-
* top of its lineage down, so each session of the tree sees the same inbox.
|
|
11
|
+
* The permission requests and questions waiting in the courier tree a session is in, the top's own requests first, read from the
|
|
12
|
+
* roster from the top of its lineage down, so each session of the tree sees the same inbox.
|
|
15
13
|
*/
|
|
16
14
|
export declare function asks(ports: PersonRpcPorts, sessionID: string): Promise<Asks>;
|
|
17
15
|
/** Passes the person's answers to a question on, as courier_answer does; a refusal, such as answers that do not fit, comes back as `problem`. */
|
|
@@ -33,6 +31,25 @@ export declare function registerPersonRpc(rpc: Pick<Plugin.Context["rpc"], "regi
|
|
|
33
31
|
readonly sessionID: import("effect/Schema").String;
|
|
34
32
|
}>;
|
|
35
33
|
readonly output: import("effect/StandardSchema").StandardSchemaV1<{
|
|
34
|
+
readonly top: string;
|
|
35
|
+
readonly sessions: readonly {
|
|
36
|
+
readonly sessionID: string;
|
|
37
|
+
readonly title: string;
|
|
38
|
+
readonly parentID: string;
|
|
39
|
+
readonly depth: number;
|
|
40
|
+
readonly group?: string | undefined;
|
|
41
|
+
}[];
|
|
42
|
+
readonly permissions: readonly {
|
|
43
|
+
readonly sessionID: string;
|
|
44
|
+
readonly resources: readonly string[];
|
|
45
|
+
readonly action: string;
|
|
46
|
+
readonly requestID: string;
|
|
47
|
+
readonly save?: readonly string[] | undefined;
|
|
48
|
+
readonly passesOn?: {
|
|
49
|
+
readonly sessionID: string;
|
|
50
|
+
readonly requestID: string;
|
|
51
|
+
} | undefined;
|
|
52
|
+
}[];
|
|
36
53
|
readonly questions: readonly {
|
|
37
54
|
readonly questions: readonly {
|
|
38
55
|
readonly options: readonly {
|
|
@@ -47,13 +64,7 @@ export declare function registerPersonRpc(rpc: Pick<Plugin.Context["rpc"], "regi
|
|
|
47
64
|
readonly requestID: string;
|
|
48
65
|
readonly stopped?: boolean | undefined;
|
|
49
66
|
}[];
|
|
50
|
-
|
|
51
|
-
readonly sessionID: string;
|
|
52
|
-
readonly resources: readonly string[];
|
|
53
|
-
readonly action: string;
|
|
54
|
-
readonly requestID: string;
|
|
55
|
-
readonly save?: readonly string[] | undefined;
|
|
56
|
-
}[];
|
|
67
|
+
}, {
|
|
57
68
|
readonly top: string;
|
|
58
69
|
readonly sessions: readonly {
|
|
59
70
|
readonly sessionID: string;
|
|
@@ -62,8 +73,17 @@ export declare function registerPersonRpc(rpc: Pick<Plugin.Context["rpc"], "regi
|
|
|
62
73
|
readonly depth: number;
|
|
63
74
|
readonly group?: string | undefined;
|
|
64
75
|
}[];
|
|
65
|
-
readonly
|
|
66
|
-
|
|
76
|
+
readonly permissions: readonly {
|
|
77
|
+
readonly sessionID: string;
|
|
78
|
+
readonly resources: readonly string[];
|
|
79
|
+
readonly action: string;
|
|
80
|
+
readonly requestID: string;
|
|
81
|
+
readonly save?: readonly string[] | undefined;
|
|
82
|
+
readonly passesOn?: {
|
|
83
|
+
readonly sessionID: string;
|
|
84
|
+
readonly requestID: string;
|
|
85
|
+
} | undefined;
|
|
86
|
+
}[];
|
|
67
87
|
readonly questions: readonly {
|
|
68
88
|
readonly questions: readonly {
|
|
69
89
|
readonly options: readonly {
|
|
@@ -78,22 +98,6 @@ export declare function registerPersonRpc(rpc: Pick<Plugin.Context["rpc"], "regi
|
|
|
78
98
|
readonly requestID: string;
|
|
79
99
|
readonly stopped?: boolean | undefined;
|
|
80
100
|
}[];
|
|
81
|
-
readonly permissions: readonly {
|
|
82
|
-
readonly sessionID: string;
|
|
83
|
-
readonly resources: readonly string[];
|
|
84
|
-
readonly action: string;
|
|
85
|
-
readonly requestID: string;
|
|
86
|
-
readonly save?: readonly string[] | undefined;
|
|
87
|
-
}[];
|
|
88
|
-
readonly top: string;
|
|
89
|
-
readonly sessions: readonly {
|
|
90
|
-
readonly sessionID: string;
|
|
91
|
-
readonly title: string;
|
|
92
|
-
readonly parentID: string;
|
|
93
|
-
readonly depth: number;
|
|
94
|
-
readonly group?: string | undefined;
|
|
95
|
-
}[];
|
|
96
|
-
readonly truncated?: number | undefined;
|
|
97
101
|
}> & import("effect/Schema").Struct<{
|
|
98
102
|
readonly top: import("effect/Schema").String;
|
|
99
103
|
readonly sessions: import("effect/Schema").$Array<import("effect/Schema").Struct<{
|
|
@@ -109,6 +113,10 @@ export declare function registerPersonRpc(rpc: Pick<Plugin.Context["rpc"], "regi
|
|
|
109
113
|
readonly action: import("effect/Schema").String;
|
|
110
114
|
readonly resources: import("effect/Schema").$Array<import("effect/Schema").String>;
|
|
111
115
|
readonly save: import("effect/Schema").optionalKey<import("effect/Schema").$Array<import("effect/Schema").String>>;
|
|
116
|
+
readonly passesOn: import("effect/Schema").optionalKey<import("effect/Schema").Struct<{
|
|
117
|
+
readonly sessionID: import("effect/Schema").String;
|
|
118
|
+
readonly requestID: import("effect/Schema").String;
|
|
119
|
+
}>>;
|
|
112
120
|
}>>;
|
|
113
121
|
readonly questions: import("effect/Schema").$Array<import("effect/Schema").Struct<{
|
|
114
122
|
readonly sessionID: import("effect/Schema").String;
|
|
@@ -124,7 +132,6 @@ export declare function registerPersonRpc(rpc: Pick<Plugin.Context["rpc"], "regi
|
|
|
124
132
|
}>>;
|
|
125
133
|
readonly stopped: import("effect/Schema").optionalKey<import("effect/Schema").Boolean>;
|
|
126
134
|
}>>;
|
|
127
|
-
readonly truncated: import("effect/Schema").optionalKey<import("effect/Schema").Int>;
|
|
128
135
|
}>;
|
|
129
136
|
};
|
|
130
137
|
readonly answer: {
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { lineage, subtree } from "../tree/index.js";
|
|
2
|
-
import { pendingOf } from "./permission.js";
|
|
2
|
+
import { ownRequests, pendingOf } from "./permission.js";
|
|
3
3
|
import { answerForPerson, pendingQuestions } from "./question/index.js";
|
|
4
4
|
import { personRpc } from "./rpc.js";
|
|
5
5
|
/**
|
|
6
|
-
* The permission requests and questions waiting in the courier tree a session is in,
|
|
7
|
-
* top of its lineage down, so each session of the tree sees the same inbox.
|
|
6
|
+
* The permission requests and questions waiting in the courier tree a session is in, the top's own requests first, read from the
|
|
7
|
+
* roster from the top of its lineage down, so each session of the tree sees the same inbox.
|
|
8
8
|
*/
|
|
9
9
|
export async function asks(ports, sessionID) {
|
|
10
10
|
const { storage } = ports.questions;
|
|
11
11
|
const top = (await lineage(storage, sessionID)).at(-1)?.parentID ?? sessionID;
|
|
12
|
-
const { nodes
|
|
12
|
+
const { nodes } = await subtree(storage, top, { now: ports.questions.now() });
|
|
13
|
+
const own = nodes.length ? await ownRequests(ports.permissions(), top).catch(() => []) : [];
|
|
13
14
|
const waiting = await Promise.all(nodes.map(({ entry }) => Promise.all([
|
|
14
15
|
pendingOf(ports.permissions(), entry.sessionID).catch(() => []),
|
|
15
16
|
pendingQuestions(storage, entry.sessionID).catch(() => []),
|
|
@@ -23,24 +24,33 @@ export async function asks(ports, sessionID) {
|
|
|
23
24
|
depth,
|
|
24
25
|
...(entry.group ? { group: entry.group } : {}),
|
|
25
26
|
})),
|
|
26
|
-
permissions:
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
]
|
|
36
|
-
|
|
27
|
+
permissions: [
|
|
28
|
+
...own.map(({ request, passesOn }) => ({
|
|
29
|
+
sessionID: top,
|
|
30
|
+
requestID: request.id,
|
|
31
|
+
action: request.action,
|
|
32
|
+
resources: [...request.resources],
|
|
33
|
+
...(request.save?.length ? { save: [...request.save] } : {}),
|
|
34
|
+
...(passesOn ? { passesOn } : {}),
|
|
35
|
+
})),
|
|
36
|
+
...nodes.flatMap(({ entry }, index) => waiting[index][0].flatMap((pending) => pending.type === "permission"
|
|
37
|
+
? [
|
|
38
|
+
{
|
|
39
|
+
sessionID: entry.sessionID,
|
|
40
|
+
requestID: pending.requestID,
|
|
41
|
+
action: pending.action,
|
|
42
|
+
resources: pending.resources,
|
|
43
|
+
...(pending.save ? { save: pending.save } : {}),
|
|
44
|
+
},
|
|
45
|
+
]
|
|
46
|
+
: [])),
|
|
47
|
+
],
|
|
37
48
|
questions: nodes.flatMap(({ entry }, index) => waiting[index][1].map(({ requestID, questions, stopped }) => ({
|
|
38
49
|
sessionID: entry.sessionID,
|
|
39
50
|
requestID,
|
|
40
51
|
questions,
|
|
41
52
|
...(stopped ? { stopped } : {}),
|
|
42
53
|
}))),
|
|
43
|
-
...(truncated ? { truncated: ports.maxTotal } : {}),
|
|
44
54
|
};
|
|
45
55
|
}
|
|
46
56
|
/** Passes the person's answers to a question on, as courier_answer does; a refusal, such as answers that do not fit, comes back as `problem`. */
|
package/dist/person/rpc.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ declare const Session: Schema.Struct<{
|
|
|
8
8
|
readonly depth: Schema.Int;
|
|
9
9
|
readonly group: Schema.optionalKey<Schema.String>;
|
|
10
10
|
}>;
|
|
11
|
-
/** A permission request a session of the tree waits on. */
|
|
11
|
+
/** A permission request a session of the tree, the top among them, waits on. */
|
|
12
12
|
declare const Permission: Schema.Struct<{
|
|
13
13
|
readonly sessionID: Schema.String;
|
|
14
14
|
readonly requestID: Schema.String;
|
|
@@ -16,6 +16,11 @@ declare const Permission: Schema.Struct<{
|
|
|
16
16
|
readonly resources: Schema.$Array<Schema.String>;
|
|
17
17
|
/** What "always" would allow from now on; absent when it is not offered. */
|
|
18
18
|
readonly save: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
19
|
+
/** The request below that allowing this one passes on: the top's courier_answer prompt. */
|
|
20
|
+
readonly passesOn: Schema.optionalKey<Schema.Struct<{
|
|
21
|
+
readonly sessionID: Schema.String;
|
|
22
|
+
readonly requestID: Schema.String;
|
|
23
|
+
}>>;
|
|
19
24
|
}>;
|
|
20
25
|
/** A question a session of the tree waits on; `stopped` when its call was cut off and the answer goes as a message. */
|
|
21
26
|
declare const Question: Schema.Struct<{
|
|
@@ -45,6 +50,7 @@ declare const Asks: Schema.Struct<{
|
|
|
45
50
|
readonly depth: Schema.Int;
|
|
46
51
|
readonly group: Schema.optionalKey<Schema.String>;
|
|
47
52
|
}>>;
|
|
53
|
+
/** The top session's own requests first, then those of the sessions below. */
|
|
48
54
|
readonly permissions: Schema.$Array<Schema.Struct<{
|
|
49
55
|
readonly sessionID: Schema.String;
|
|
50
56
|
readonly requestID: Schema.String;
|
|
@@ -52,6 +58,11 @@ declare const Asks: Schema.Struct<{
|
|
|
52
58
|
readonly resources: Schema.$Array<Schema.String>;
|
|
53
59
|
/** What "always" would allow from now on; absent when it is not offered. */
|
|
54
60
|
readonly save: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
61
|
+
/** The request below that allowing this one passes on: the top's courier_answer prompt. */
|
|
62
|
+
readonly passesOn: Schema.optionalKey<Schema.Struct<{
|
|
63
|
+
readonly sessionID: Schema.String;
|
|
64
|
+
readonly requestID: Schema.String;
|
|
65
|
+
}>>;
|
|
55
66
|
}>>;
|
|
56
67
|
readonly questions: Schema.$Array<Schema.Struct<{
|
|
57
68
|
readonly sessionID: Schema.String;
|
|
@@ -67,8 +78,6 @@ declare const Asks: Schema.Struct<{
|
|
|
67
78
|
}>>;
|
|
68
79
|
readonly stopped: Schema.optionalKey<Schema.Boolean>;
|
|
69
80
|
}>>;
|
|
70
|
-
/** Set when the tree had more sessions than `maxTotal`, which were not read. */
|
|
71
|
-
readonly truncated: Schema.optionalKey<Schema.Int>;
|
|
72
81
|
}>;
|
|
73
82
|
export type Asks = typeof Asks.Type;
|
|
74
83
|
export type AskedQuestion = typeof Question.Type;
|
|
@@ -93,20 +102,7 @@ export declare const personRpc: {
|
|
|
93
102
|
readonly sessionID: Schema.String;
|
|
94
103
|
}>;
|
|
95
104
|
readonly output: import("effect/StandardSchema").StandardSchemaV1<{
|
|
96
|
-
readonly
|
|
97
|
-
readonly sessionID: string;
|
|
98
|
-
readonly requestID: string;
|
|
99
|
-
readonly questions: readonly {
|
|
100
|
-
readonly options: readonly {
|
|
101
|
-
readonly label: string;
|
|
102
|
-
readonly description: string;
|
|
103
|
-
}[];
|
|
104
|
-
readonly question: string;
|
|
105
|
-
readonly header: string;
|
|
106
|
-
readonly multiple?: boolean | undefined;
|
|
107
|
-
}[];
|
|
108
|
-
readonly stopped?: boolean | undefined;
|
|
109
|
-
}[];
|
|
105
|
+
readonly top: string;
|
|
110
106
|
readonly sessions: readonly {
|
|
111
107
|
readonly sessionID: string;
|
|
112
108
|
readonly title: string;
|
|
@@ -120,10 +116,11 @@ export declare const personRpc: {
|
|
|
120
116
|
readonly requestID: string;
|
|
121
117
|
readonly action: string;
|
|
122
118
|
readonly save?: readonly string[] | undefined;
|
|
119
|
+
readonly passesOn?: {
|
|
120
|
+
readonly sessionID: string;
|
|
121
|
+
readonly requestID: string;
|
|
122
|
+
} | undefined;
|
|
123
123
|
}[];
|
|
124
|
-
readonly top: string;
|
|
125
|
-
readonly truncated?: number | undefined;
|
|
126
|
-
}, {
|
|
127
124
|
readonly questions: readonly {
|
|
128
125
|
readonly sessionID: string;
|
|
129
126
|
readonly requestID: string;
|
|
@@ -138,6 +135,8 @@ export declare const personRpc: {
|
|
|
138
135
|
}[];
|
|
139
136
|
readonly stopped?: boolean | undefined;
|
|
140
137
|
}[];
|
|
138
|
+
}, {
|
|
139
|
+
readonly top: string;
|
|
141
140
|
readonly sessions: readonly {
|
|
142
141
|
readonly sessionID: string;
|
|
143
142
|
readonly title: string;
|
|
@@ -151,9 +150,25 @@ export declare const personRpc: {
|
|
|
151
150
|
readonly requestID: string;
|
|
152
151
|
readonly action: string;
|
|
153
152
|
readonly save?: readonly string[] | undefined;
|
|
153
|
+
readonly passesOn?: {
|
|
154
|
+
readonly sessionID: string;
|
|
155
|
+
readonly requestID: string;
|
|
156
|
+
} | undefined;
|
|
157
|
+
}[];
|
|
158
|
+
readonly questions: readonly {
|
|
159
|
+
readonly sessionID: string;
|
|
160
|
+
readonly requestID: string;
|
|
161
|
+
readonly questions: readonly {
|
|
162
|
+
readonly options: readonly {
|
|
163
|
+
readonly label: string;
|
|
164
|
+
readonly description: string;
|
|
165
|
+
}[];
|
|
166
|
+
readonly question: string;
|
|
167
|
+
readonly header: string;
|
|
168
|
+
readonly multiple?: boolean | undefined;
|
|
169
|
+
}[];
|
|
170
|
+
readonly stopped?: boolean | undefined;
|
|
154
171
|
}[];
|
|
155
|
-
readonly top: string;
|
|
156
|
-
readonly truncated?: number | undefined;
|
|
157
172
|
}> & Schema.Struct<{
|
|
158
173
|
/** The session at the top of the tree, which the sessions below were started from. */
|
|
159
174
|
readonly top: Schema.String;
|
|
@@ -166,6 +181,7 @@ export declare const personRpc: {
|
|
|
166
181
|
readonly depth: Schema.Int;
|
|
167
182
|
readonly group: Schema.optionalKey<Schema.String>;
|
|
168
183
|
}>>;
|
|
184
|
+
/** The top session's own requests first, then those of the sessions below. */
|
|
169
185
|
readonly permissions: Schema.$Array<Schema.Struct<{
|
|
170
186
|
readonly sessionID: Schema.String;
|
|
171
187
|
readonly requestID: Schema.String;
|
|
@@ -173,6 +189,11 @@ export declare const personRpc: {
|
|
|
173
189
|
readonly resources: Schema.$Array<Schema.String>;
|
|
174
190
|
/** What "always" would allow from now on; absent when it is not offered. */
|
|
175
191
|
readonly save: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
192
|
+
/** The request below that allowing this one passes on: the top's courier_answer prompt. */
|
|
193
|
+
readonly passesOn: Schema.optionalKey<Schema.Struct<{
|
|
194
|
+
readonly sessionID: Schema.String;
|
|
195
|
+
readonly requestID: Schema.String;
|
|
196
|
+
}>>;
|
|
176
197
|
}>>;
|
|
177
198
|
readonly questions: Schema.$Array<Schema.Struct<{
|
|
178
199
|
readonly sessionID: Schema.String;
|
|
@@ -188,8 +209,6 @@ export declare const personRpc: {
|
|
|
188
209
|
}>>;
|
|
189
210
|
readonly stopped: Schema.optionalKey<Schema.Boolean>;
|
|
190
211
|
}>>;
|
|
191
|
-
/** Set when the tree had more sessions than `maxTotal`, which were not read. */
|
|
192
|
-
readonly truncated: Schema.optionalKey<Schema.Int>;
|
|
193
212
|
}>;
|
|
194
213
|
};
|
|
195
214
|
readonly answer: {
|
package/dist/person/rpc.js
CHANGED
|
@@ -14,7 +14,7 @@ const Session = Schema.Struct({
|
|
|
14
14
|
depth: Count,
|
|
15
15
|
group: Schema.optionalKey(Schema.String),
|
|
16
16
|
});
|
|
17
|
-
/** A permission request a session of the tree waits on. */
|
|
17
|
+
/** A permission request a session of the tree, the top among them, waits on. */
|
|
18
18
|
const Permission = Schema.Struct({
|
|
19
19
|
sessionID: Schema.String,
|
|
20
20
|
requestID: Schema.String,
|
|
@@ -22,6 +22,8 @@ const Permission = Schema.Struct({
|
|
|
22
22
|
resources: Schema.Array(Schema.String),
|
|
23
23
|
/** What "always" would allow from now on; absent when it is not offered. */
|
|
24
24
|
save: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
25
|
+
/** The request below that allowing this one passes on: the top's courier_answer prompt. */
|
|
26
|
+
passesOn: Schema.optionalKey(Schema.Struct({ sessionID: Schema.String, requestID: Schema.String })),
|
|
25
27
|
});
|
|
26
28
|
/** One question of OpenCode's question tool. */
|
|
27
29
|
const Prompt = Schema.Struct({
|
|
@@ -43,10 +45,9 @@ const Asks = Schema.Struct({
|
|
|
43
45
|
top: Schema.String,
|
|
44
46
|
/** The sessions below the top, in the order a tree is drawn. */
|
|
45
47
|
sessions: Schema.Array(Session),
|
|
48
|
+
/** The top session's own requests first, then those of the sessions below. */
|
|
46
49
|
permissions: Schema.Array(Permission),
|
|
47
50
|
questions: Schema.Array(Question),
|
|
48
|
-
/** Set when the tree had more sessions than `maxTotal`, which were not read. */
|
|
49
|
-
truncated: Schema.optionalKey(Count),
|
|
50
51
|
});
|
|
51
52
|
/** What answering a question did; `problem` says why nothing was passed on, when it was refused. */
|
|
52
53
|
const Answered = Schema.Struct({
|