opencode-courier 0.14.0 → 0.16.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 +1 -1
- package/dist/index.js +1 -1
- package/dist/kernel/protocol.d.ts +3 -3
- package/dist/kernel/protocol.js +2 -2
- package/dist/memory/lessons.d.ts +1 -1
- package/dist/memory/lessons.js +2 -2
- package/dist/memory/note.d.ts +2 -2
- package/dist/memory/note.js +5 -6
- package/dist/memory/text.d.ts +1 -1
- package/dist/memory/text.js +2 -2
- package/dist/memory/tools.js +4 -4
- package/dist/person/text.d.ts +1 -1
- package/dist/person/text.js +1 -1
- package/dist/person/tools.js +5 -3
- package/dist/scheduling/later.js +1 -1
- package/dist/scheduling/text.js +2 -2
- package/dist/scheduling/tools.js +5 -3
- package/dist/skill.js +39 -87
- package/dist/tree/context.js +17 -5
- package/dist/tree/events.d.ts +5 -0
- package/dist/tree/events.js +39 -7
- package/dist/tree/group.js +6 -3
- package/dist/tree/limits.d.ts +1 -0
- package/dist/tree/limits.js +20 -6
- package/dist/tree/nudge.d.ts +2 -0
- package/dist/tree/ports.d.ts +3 -1
- package/dist/tree/report.d.ts +3 -0
- package/dist/tree/report.js +7 -0
- package/dist/tree/spawn.d.ts +1 -0
- package/dist/tree/spawn.js +9 -9
- package/dist/tree/stop.js +18 -6
- package/dist/tree/text.d.ts +5 -2
- package/dist/tree/text.js +67 -25
- package/dist/tree/tools.js +15 -10
- package/dist/tui.js +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -85,7 +85,7 @@ name, without a version: OpenCode offers updates only for an entry without an ex
|
|
|
85
85
|
<!-- compat-table:start rows=3: generated by script/compat-table.ts from the release tags -->
|
|
86
86
|
| opencode-courier | OpenCode V2 (`opencode` and `@opencode/plugin`) |
|
|
87
87
|
|---|---|
|
|
88
|
-
| 0.14.0 | 2.2.6 |
|
|
88
|
+
| 0.14.0, 0.15.0, 0.16.0 | 2.2.6 |
|
|
89
89
|
| 0.13.7, 0.13.8 | 2.2.5 |
|
|
90
90
|
| 0.13.6 | 2.2.4 |
|
|
91
91
|
<!-- compat-table:end -->
|
package/dist/index.js
CHANGED
|
@@ -115,7 +115,7 @@ export const courier = () => Plugin.define({
|
|
|
115
115
|
.catch((error) => log(`courier rpc: not registered, the TUI part shows nothing and cannot stop: ${String(error)}`));
|
|
116
116
|
// a host before 2.2.0 has no instructions entries (api-notes)
|
|
117
117
|
const instructions = ctx.session.instructions;
|
|
118
|
-
await ctx.session.hook("context", (event) => shapeContext({ storage: ctx.storage, limits, roles, instructions, hiddenFromLeaf: [RECALL_TOOL, NOTE_TOOL], log }, event));
|
|
118
|
+
await ctx.session.hook("context", (event) => shapeContext({ storage: ctx.storage, session: ctx.session, limits, roles, instructions, hiddenFromLeaf: [RECALL_TOOL, NOTE_TOOL], log }, event));
|
|
119
119
|
const pruned = pruneExpired(ctx.storage, Date.now()).catch((error) => log(`courier roster prune: ${String(error)}`));
|
|
120
120
|
const swept = sweepRecords(ctx.storage, Date.now()).catch((error) => log(`courier memory sweep: ${String(error)}`));
|
|
121
121
|
const windowed = ports
|
|
@@ -2,9 +2,9 @@ export declare const END_TURN = "If nothing else is left to do now, end your tur
|
|
|
2
2
|
export declare const STATUSES: readonly ["done", "partial", "blocked", "failed"];
|
|
3
3
|
export type Status = (typeof STATUSES)[number];
|
|
4
4
|
export declare const isStatus: (value: unknown) => value is Status;
|
|
5
|
-
export declare const SPLIT_LINE = "split: no, because \u2026; considered: <the parts you weighed
|
|
6
|
-
export declare const ONE_PART_LINE = "split: 1 part: <the task>, to keep my context";
|
|
7
|
-
export declare const ROOT_SPLIT_LINE = "split: no, because \u2026; considered: <the parts you weighed
|
|
5
|
+
export declare const SPLIT_LINE = "`split: no, because \u2026; considered: <the parts you weighed>` or `split: <n> parts: \u2026` (one phrase per part)";
|
|
6
|
+
export declare const ONE_PART_LINE = "`split: 1 part: <the task>, to keep my context`";
|
|
7
|
+
export declare const ROOT_SPLIT_LINE = "`split: no, because \u2026; considered: <the parts you weighed>` or `split: <n> parts: \u2026` (one phrase per part) or `split: 1 part: <the task>, to keep my context`";
|
|
8
8
|
export declare const SKILL_ID = "courier-orchestrate";
|
|
9
9
|
export declare function envelope(from: string, message: string, attributes?: Record<string, string>): string;
|
|
10
10
|
export declare function clipText(text: string, max?: number): string;
|
package/dist/kernel/protocol.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export const END_TURN = "If nothing else is left to do now, end your turn by replying without calling more tools.";
|
|
2
2
|
export const STATUSES = ["done", "partial", "blocked", "failed"];
|
|
3
3
|
export const isStatus = (value) => typeof value === "string" && STATUSES.includes(value);
|
|
4
|
-
export const SPLIT_LINE = "split: no, because …; considered: <the parts you weighed
|
|
5
|
-
export const ONE_PART_LINE = "split: 1 part: <the task>, to keep my context";
|
|
4
|
+
export const SPLIT_LINE = "`split: no, because …; considered: <the parts you weighed>` or `split: <n> parts: …` (one phrase per part)";
|
|
5
|
+
export const ONE_PART_LINE = "`split: 1 part: <the task>, to keep my context`";
|
|
6
6
|
export const ROOT_SPLIT_LINE = `${SPLIT_LINE} or ${ONE_PART_LINE}`;
|
|
7
7
|
export const SKILL_ID = "courier-orchestrate";
|
|
8
8
|
export function envelope(from, message, attributes = {}) {
|
package/dist/memory/lessons.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { type Entry } from "../tree/index.js";
|
|
2
2
|
import type { MemoryPorts } from "./ports.js";
|
|
3
3
|
export declare function lessonsOf(ports: MemoryPorts): Promise<ReadonlyArray<Entry>>;
|
|
4
|
-
export declare function relesson(ports: MemoryPorts,
|
|
4
|
+
export declare function relesson(ports: MemoryPorts, top: string): Promise<string[]>;
|
package/dist/memory/lessons.js
CHANGED
|
@@ -9,7 +9,7 @@ export async function lessonsOf(ports) {
|
|
|
9
9
|
const value = lessonsEntry(pinnedFirst(await memoryStore(ports.storage).ofProject(ports.projectID)));
|
|
10
10
|
return value === undefined ? [] : [{ key: LESSONS_KEY, value }];
|
|
11
11
|
}
|
|
12
|
-
export async function relesson(ports,
|
|
12
|
+
export async function relesson(ports, top) {
|
|
13
13
|
const entries = ports.session.instructions?.entry;
|
|
14
14
|
if (!entries)
|
|
15
15
|
return [];
|
|
@@ -21,7 +21,7 @@ export async function relesson(ports, writerID, top) {
|
|
|
21
21
|
if (!entry)
|
|
22
22
|
return [];
|
|
23
23
|
const children = ports.maxDepth > 1 ? await childrenOf(ports.storage, top) : [];
|
|
24
|
-
sessions = [
|
|
24
|
+
sessions = [top, ...children.map((child) => child.sessionID)];
|
|
25
25
|
}
|
|
26
26
|
catch (error) {
|
|
27
27
|
ports.log(`courier_note: the lessons entry could not be read again: ${String(error)}`);
|
package/dist/memory/note.d.ts
CHANGED
|
@@ -6,9 +6,9 @@ export interface NoteInput {
|
|
|
6
6
|
export type Refusal = {
|
|
7
7
|
readonly kind: "off";
|
|
8
8
|
} | {
|
|
9
|
-
readonly kind: "
|
|
9
|
+
readonly kind: "below";
|
|
10
10
|
readonly depth: number;
|
|
11
|
-
readonly
|
|
11
|
+
readonly top: string;
|
|
12
12
|
} | {
|
|
13
13
|
readonly kind: "empty";
|
|
14
14
|
} | {
|
package/dist/memory/note.js
CHANGED
|
@@ -6,22 +6,21 @@ export async function note(ports, callerID, input) {
|
|
|
6
6
|
if (!ports.memory)
|
|
7
7
|
return { kind: "off" };
|
|
8
8
|
const chain = await lineage(ports.storage, callerID);
|
|
9
|
-
const
|
|
10
|
-
if (
|
|
11
|
-
return { kind: "
|
|
9
|
+
const top = chain.at(-1)?.parentID ?? callerID;
|
|
10
|
+
if (chain.length)
|
|
11
|
+
return { kind: "below", depth: chain.length, top };
|
|
12
12
|
const text = oneLine(typeof input.text === "string" ? input.text : "");
|
|
13
13
|
if (!text)
|
|
14
14
|
return { kind: "empty" };
|
|
15
15
|
if (text.length > LESSON_MAX)
|
|
16
16
|
return { kind: "long", length: text.length };
|
|
17
|
-
const top = chain.at(-1)?.parentID ?? callerID;
|
|
18
17
|
const store = memoryStore(ports.storage);
|
|
19
18
|
const { projectID } = ports;
|
|
20
19
|
const record = await store.get(projectID, top);
|
|
21
20
|
if (!record)
|
|
22
21
|
return { kind: "unrecorded", top };
|
|
23
|
-
const lesson = { text, at: ports.now()
|
|
22
|
+
const lesson = { text, at: ports.now() };
|
|
24
23
|
await store.put(projectID, top, { ...record, lesson });
|
|
25
|
-
await relesson(ports,
|
|
24
|
+
await relesson(ports, top);
|
|
26
25
|
return { kind: "noted", projectID, top, lesson, ...(record.lesson ? { replaced: record.lesson } : {}) };
|
|
27
26
|
}
|
package/dist/memory/text.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export declare function recallRecordText(record: StoredRecord): string;
|
|
|
11
11
|
export declare const recallMissingText: (sessionID: string) => string;
|
|
12
12
|
export declare function noteText(top: string, lesson: Lesson, replaced: Lesson | undefined): string;
|
|
13
13
|
export declare const NOTE_OFF = "Not noted: the plugin's memory option is false, so no record of a session tree is kept in this project to write a lesson on.";
|
|
14
|
-
export declare const
|
|
14
|
+
export declare const noteBelowRefusal: (depth: number, top: string) => string;
|
|
15
15
|
export declare const NOTE_EMPTY = "Not noted: text is empty. Write one line, what the next orchestrator in this project should know about running a tree like this one, with the job it comes from and the reason, or nothing.";
|
|
16
16
|
export declare const noteLongRefusal: (length: number) => string;
|
|
17
17
|
export declare const noteUnrecordedRefusal: (top: string) => string;
|
package/dist/memory/text.js
CHANGED
|
@@ -84,8 +84,8 @@ export function noteText(top, lesson, replaced) {
|
|
|
84
84
|
].join(" ");
|
|
85
85
|
}
|
|
86
86
|
export const NOTE_OFF = "Not noted: the plugin's memory option is false, so no record of a session tree is kept in this project to write a lesson on.";
|
|
87
|
-
export const
|
|
88
|
-
"
|
|
87
|
+
export const noteBelowRefusal = (depth, top) => `Not noted: this session is at depth ${depth} of the session tree under ${top}, and only that top session writes the tree's ` +
|
|
88
|
+
"lesson, once the whole tree is verified. Put what you learned in your report instead: the top reads it and writes the tree's lesson.";
|
|
89
89
|
const ONE_LINE = "what the next orchestrator in this project should know about running a tree like this one, with the job it comes from and the reason";
|
|
90
90
|
export const NOTE_EMPTY = `Not noted: text is empty. Write one line, ${ONE_LINE}, or nothing.`;
|
|
91
91
|
export const noteLongRefusal = (length) => `Not noted: the lesson is ${length} characters, at most ${LESSON_MAX}; write it shorter. Nothing was written, and the earlier lesson, ` +
|
package/dist/memory/tools.js
CHANGED
|
@@ -4,7 +4,7 @@ import { rethrow } from "../kernel/tool.js";
|
|
|
4
4
|
import { LESSON_MAX } from "./domain.js";
|
|
5
5
|
import { note } from "./note.js";
|
|
6
6
|
import { recall } from "./recall.js";
|
|
7
|
-
import { NOTE_EMPTY, NOTE_OFF, NOTE_TOOL,
|
|
7
|
+
import { NOTE_EMPTY, NOTE_OFF, NOTE_TOOL, noteBelowRefusal, noteLongRefusal, noteText, noteUnrecordedRefusal, RECALL_OFF, RECALL_TOOL, recallListText, recallMissingText, recallRecordText } from "./text.js";
|
|
8
8
|
export const RecallInput = Schema.Struct({
|
|
9
9
|
sessionID: Schema.optional(Schema.String.annotate({ description: "The top session of a listed record, for that record in full; without it, one line per record of this project." })),
|
|
10
10
|
});
|
|
@@ -50,7 +50,7 @@ export function addMemoryTools(tools, ports) {
|
|
|
50
50
|
"so that a lesson for one kind of job can be told from one that holds everywhere. One line, at most " +
|
|
51
51
|
`${LESSON_MAX} characters; a longer one is refused, not clipped, so write it again shorter. Kept on your own tree's ` +
|
|
52
52
|
"record and listed by courier_recall after its facts, dated; a second call replaces the first. From the top session " +
|
|
53
|
-
"
|
|
53
|
+
"only: a session below it puts what it learned in its report. No code, no secrets, and not a rule that holds every time: that belongs " +
|
|
54
54
|
"in AGENTS.md or the skill, for the person to put there, so tell them instead.",
|
|
55
55
|
input: NoteInput,
|
|
56
56
|
execute: async (input, context) => {
|
|
@@ -63,8 +63,8 @@ export function addMemoryTools(tools, ports) {
|
|
|
63
63
|
}
|
|
64
64
|
case "off":
|
|
65
65
|
throw refuse(NOTE_OFF);
|
|
66
|
-
case "
|
|
67
|
-
throw refuse(
|
|
66
|
+
case "below":
|
|
67
|
+
throw refuse(noteBelowRefusal(result.depth, result.top));
|
|
68
68
|
case "empty":
|
|
69
69
|
throw refuse(NOTE_EMPTY);
|
|
70
70
|
case "long":
|
package/dist/person/text.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Asked, Elsewhere, Form, Outcome, PermissionAnswered, PermissionRequest, Prompt, QuestionAnswered, Reply, Withdrawal } from "./ports.js";
|
|
2
2
|
export type AnswerResult = PermissionAnswered | QuestionAnswered;
|
|
3
3
|
export declare function answerText(result: AnswerResult): string;
|
|
4
|
-
export declare const REJECTED = "Refused by the session
|
|
4
|
+
export declare const REJECTED = "Refused by the person or the top session of the tree. Do without it, or report back why you cannot.";
|
|
5
5
|
export declare function refusedText(sessionID: string, requestID: string, reason: string): string;
|
|
6
6
|
export declare const origin: (startedBy?: string) => string;
|
|
7
7
|
export declare const STAYS_QUIET = "and then it does not report back on its own: if it stays quiet, message it with courier_send to have it carry on.";
|
package/dist/person/text.js
CHANGED
|
@@ -18,7 +18,7 @@ export function answerText(result) {
|
|
|
18
18
|
return `Passed the answers to question ${result.requestID} on to ${result.sessionID}${how}; it carries on and reports back with courier_send. ${END_TURN}`;
|
|
19
19
|
}
|
|
20
20
|
// a bare rejection ends the child's turn (api-notes)
|
|
21
|
-
export const REJECTED = "Refused by the session
|
|
21
|
+
export const REJECTED = "Refused by the person or the top session of the tree. Do without it, or report back why you cannot.";
|
|
22
22
|
export function refusedText(sessionID, requestID, reason) {
|
|
23
23
|
return `not allowed in your session (${reason}). The refusal was passed on with that reason to request ${requestID} of ${sessionID}, so its call fails and it carries on; it reports back with courier_send. Tell the person, and do not propose it again unless they ask.`;
|
|
24
24
|
}
|
package/dist/person/tools.js
CHANGED
|
@@ -42,13 +42,15 @@ export function addPersonTools(tools, ports) {
|
|
|
42
42
|
name: "courier_answer",
|
|
43
43
|
options: { codemode: false },
|
|
44
44
|
description: "Pass on the answer to a permission request or a question that a session you started with courier_spawn (or " +
|
|
45
|
-
"one started from it) waits on, after a notice from it named the request.
|
|
45
|
+
"one started from it) waits on, after a notice from it named the request. Only the top session of a tree gets " +
|
|
46
|
+
"those notices and can answer. For a permission request, reply is " +
|
|
46
47
|
"the answer you propose, once or reject: proposing is not deciding. With once, OpenCode asks the person you are " +
|
|
47
48
|
"working with in your session, as its own permission prompt for what that session asked, under your permission " +
|
|
48
49
|
"rules, and the call returns once they have answered; what they allow is passed on, and a refusal with a reason " +
|
|
49
50
|
"fails this call and is passed on with the reason. Do not ask them in your reply or with your question tool first. " +
|
|
50
|
-
"For a question,
|
|
51
|
-
"
|
|
51
|
+
"For a question, the question tool call the notice asks for passes the person's answer on by itself; use answers " +
|
|
52
|
+
"only when they answered some other way, or the notice or that call's result tells you to, and never answer for " +
|
|
53
|
+
"them. The session carries on once it has the answer.",
|
|
52
54
|
input: AnswerInput,
|
|
53
55
|
execute: async (input, context) => {
|
|
54
56
|
const call = { messageID: context.messageID, callID: context.id };
|
package/dist/scheduling/later.js
CHANGED
|
@@ -76,7 +76,7 @@ export async function deliverDue(ports, claimed) {
|
|
|
76
76
|
sessionID: due.sessionID,
|
|
77
77
|
text: envelope(due.from, due.message, { scheduled: new Date(due.createdAt).toISOString() }),
|
|
78
78
|
description: `Scheduled message from ${due.from}`,
|
|
79
|
-
metadata: { source: "courier", from: due.from, scheduled: due.id },
|
|
79
|
+
metadata: { source: "courier", from: due.from, scheduled: due.id, scheduledAt: due.createdAt },
|
|
80
80
|
delivery: "queue",
|
|
81
81
|
})
|
|
82
82
|
.catch((error) => ports.log(`courier_later ${due.id} to ${due.sessionID} not delivered: ${String(error)}`));
|
package/dist/scheduling/text.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export function laterText(entry, fireAt, own) {
|
|
2
2
|
const next = own
|
|
3
3
|
? "It arrives when due, after your current turn if one is running, so do not wait for it: once nothing " +
|
|
4
|
-
"else is left to do now, end your turn by replying without calling more tools.
|
|
5
|
-
"
|
|
4
|
+
"else is left to do now, end your turn by replying without calling more tools. Once the last report " +
|
|
5
|
+
"it covers arrives, cancel it with courier_cancel."
|
|
6
6
|
: "Cancel it with courier_cancel if it is no longer needed.";
|
|
7
7
|
return `Scheduled ${entry.id} for ${fireAt}, to ${entry.sessionID}. ${next}`;
|
|
8
8
|
}
|
package/dist/scheduling/tools.js
CHANGED
|
@@ -18,8 +18,9 @@ export function addSchedulingTools(tools, later) {
|
|
|
18
18
|
name: "courier_later",
|
|
19
19
|
options: { codemode: false },
|
|
20
20
|
description: "Schedule a message for a session (this one by default), delivered when due and waking it if idle. " +
|
|
21
|
-
"
|
|
22
|
-
"
|
|
21
|
+
"For a long task or batch of sessions you start, use it as a safety net: schedule one check-in per group, and " +
|
|
22
|
+
"one for the ungrouped sessions, end your turn by replying without calling more tools, and cancel each with " +
|
|
23
|
+
"courier_cancel when its last report arrives, as a report does not cancel it. Survives server restarts; " +
|
|
23
24
|
"may arrive up to ~15 seconds late.",
|
|
24
25
|
input: LaterInput,
|
|
25
26
|
execute: async (input, context) => {
|
|
@@ -34,7 +35,8 @@ export function addSchedulingTools(tools, later) {
|
|
|
34
35
|
tools.add({
|
|
35
36
|
name: "courier_cancel",
|
|
36
37
|
options: { codemode: false },
|
|
37
|
-
description: "Cancel a message scheduled with courier_later, e.g.
|
|
38
|
+
description: "Cancel a message scheduled with courier_later, e.g. a check-in whose reports arrived. Nothing else cancels " +
|
|
39
|
+
"it but courier_stop of the session it is for.",
|
|
38
40
|
input: CancelInput,
|
|
39
41
|
execute: async (input) => {
|
|
40
42
|
const cancelled = await cancel(later, input.id).catch(rethrow("courier_cancel"));
|
package/dist/skill.js
CHANGED
|
@@ -1,110 +1,62 @@
|
|
|
1
1
|
import { ONE_PART_LINE, SKILL_ID, SPLIT_LINE } from "./kernel/protocol.js";
|
|
2
|
-
export const SKILL_DESCRIPTION = "How opencode-courier's session tools work. Load
|
|
3
|
-
"independent, substantial parts touching separate files or areas. Starting the
|
|
4
|
-
"
|
|
2
|
+
export const SKILL_DESCRIPTION = "How opencode-courier's session tools work. Load it when you decide whether to split a task across sessions with " +
|
|
3
|
+
"courier_spawn: one with 2 or more independent, substantial parts touching separate files or areas. Starting the " +
|
|
4
|
+
"parts, waiting without polling, permission requests and questions from below, verifying and merging.";
|
|
5
5
|
export const SKILL_CONTENT = [
|
|
6
6
|
"# How opencode-courier's session tools work",
|
|
7
7
|
"",
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"brief says if a session started you.",
|
|
8
|
+
"If courier_spawn is not among your tools, you cannot split: do the task yourself. Planning the parts, staging a " +
|
|
9
|
+
"tightly coupled core and reviewing the merge are not in this skill: if a skill for them is listed to you, load it too.",
|
|
11
10
|
"",
|
|
12
|
-
"
|
|
13
|
-
"is a playbook's: if a skill for that is listed to you, load it too.",
|
|
11
|
+
"## 1. Split or do",
|
|
14
12
|
"",
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"the root
|
|
20
|
-
|
|
21
|
-
"when it is small, sequential or tightly coupled. Several whole jobs in one prompt, such as 4 tickets, are that many " +
|
|
22
|
-
"parts: each session runs this skill for its job, so the tree below you repeats its shape once per job.",
|
|
23
|
-
"- Only the root, the session nobody started, may start exactly one session for the whole task, and only when the " +
|
|
24
|
-
"task is heavy (long reading, many edits, long runs) and its own context is worth keeping; it still verifies and " +
|
|
25
|
-
"integrates that session's report itself. Every other session never starts exactly one session for the whole task: " +
|
|
26
|
-
"it only adds a hop, and a chain of single hops uses up the tree's depth. One session for one stage is a part, not " +
|
|
27
|
-
"the whole, as your brief says: a core you stage before the rest, when 2 or more parts follow it, or a review of " +
|
|
28
|
-
"the parts once you have merged them.",
|
|
29
|
-
`- Say which in one line of your reply, before you start: ${SPLIT_LINE}; the root has a third form, ${ONE_PART_LINE}.`,
|
|
13
|
+
"Split or do by the rule in your brief, the text above your task. At the root, the top session of the tree, which has no brief: split a task with 2 " +
|
|
14
|
+
"or more independent, substantial parts touching separate files or areas, do it yourself when it is small, " +
|
|
15
|
+
"sequential or tightly coupled, or hand a heavy one (long reading, many edits, long runs) whole to one session to " +
|
|
16
|
+
`keep your context. Say which in one line of your reply: ${SPLIT_LINE}, or, at the root only, ${ONE_PART_LINE}. Several whole ` +
|
|
17
|
+
"jobs in one prompt, such as 4 tickets, are that many parts. Below the root, never start one session for the whole " +
|
|
18
|
+
"task: it only adds a hop, and a chain of single hops uses up the tree's depth.",
|
|
30
19
|
"",
|
|
31
20
|
"## 2. Start the parts",
|
|
32
21
|
"",
|
|
33
|
-
"- One courier_spawn per part that
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
"group of their own once the first group has reported.",
|
|
22
|
+
"- One courier_spawn per part, the parts that run at once in one group. When they do not all fit the tree's limits " +
|
|
23
|
+
"at once, start waves, each its own group, or leave them ungrouped and start the next as each report frees a slot.",
|
|
24
|
+
"- No two parts edit the same files; isolate: true for each part that edits files in parallel with another.",
|
|
37
25
|
"- Write each task so the session can do it without you, since it sees none of your conversation: what to produce " +
|
|
38
|
-
"and where, the context it needs, how to check it
|
|
39
|
-
"
|
|
40
|
-
|
|
41
|
-
"
|
|
42
|
-
"its branch is for you to merge.",
|
|
43
|
-
"- Leave model and variant unset, so a part runs on your own model, unless the part plainly calls for another: a " +
|
|
44
|
-
"cheaper one or a lower variant for a small, well-specified part, a stronger one or a higher variant for a design " +
|
|
45
|
-
"question or a hard bug. native: true for a part that only reads, checks or looks something up and reports once; " +
|
|
46
|
-
"leave it off for a part the person may want to open, or that should outlive you.",
|
|
47
|
-
"- A part that depends on another starts once that one is done: merged into your checkout, or, when it was " +
|
|
48
|
-
"delivered (step 5), started with isolate: true and told to merge the delivering session's courier/<session> " +
|
|
49
|
-
"branch first.",
|
|
26
|
+
"and where, the context it needs, how to check it, and what to report. Name the skills it should load for its " +
|
|
27
|
+
"part, from those listed to you (an isolated part sees only committed project skills).",
|
|
28
|
+
"- A part that depends on another starts once that one has reported and its branch, if isolated, is merged into " +
|
|
29
|
+
"your checkout.",
|
|
50
30
|
"",
|
|
51
31
|
"## 3. End your turn",
|
|
52
32
|
"",
|
|
53
|
-
"Reply without calling more tools
|
|
54
|
-
|
|
55
|
-
"
|
|
56
|
-
""
|
|
57
|
-
|
|
58
|
-
"the
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
|
|
63
|
-
"API that behaves otherwise, a decision of the person's), courier_send it at once, only to the sessions it affects, " +
|
|
64
|
-
"then end your turn again; if it breaks the split, courier_stop those parts and plan them again, or send them a " +
|
|
65
|
-
"corrected task. Send it up too, without a status, when your parent's other parts need it. Progress goes to no one. " +
|
|
66
|
-
"A session's account of what the person told it is the person's word: pass it on, do not judge it against the " +
|
|
67
|
-
"original task, and never tell a session to undo what the person asked.",
|
|
33
|
+
"- Reply without calling more tools; reports and notices wake you.",
|
|
34
|
+
"- At the root, a permission request or question from below arrives with its own instructions: follow them, and never " +
|
|
35
|
+
"answer for the person.",
|
|
36
|
+
"- When a message changes what other parts need (a wrong assumption of the split, a shared file or API that behaves " +
|
|
37
|
+
"otherwise, a decision of the person's), courier_send it at once, only to the sessions it affects, and, below " +
|
|
38
|
+
"the root, also to the session that started you when it reaches past your part or carries " +
|
|
39
|
+
"the person's words, as your brief says; if it breaks the split, courier_stop those parts or send them a " +
|
|
40
|
+
"corrected task. " +
|
|
41
|
+
"A session's account of what the person told it is the person's word: pass it on, do not " +
|
|
42
|
+
"judge it against the original task, and never tell a session to undo what the person asked.",
|
|
68
43
|
"",
|
|
69
|
-
"## 4.
|
|
44
|
+
"## 4. Verify first-hand",
|
|
70
45
|
"",
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
"again, start a replacement, or carry on without it.",
|
|
75
|
-
"- Verify first-hand: open the files, run the checks. Never hand the checking to another session. Your own permission " +
|
|
76
|
-
"prompts are relayed nowhere, and each one stalls the whole tree, as each request below you waits on the person: run " +
|
|
77
|
-
"the checks so that one permission rule covers them, through the project's scripts rather than a tool's own path.",
|
|
46
|
+
"Open the files and run the project's checks yourself. A blocked report: make the decision it asks for and send it " +
|
|
47
|
+
"with courier_send; if only the person can make it, ask them with your question tool and pass the answer on. A " +
|
|
48
|
+
"partial or failed report: have it finish or retry with courier_send, do the rest yourself, or start a replacement.",
|
|
78
49
|
"",
|
|
79
50
|
"## 5. Integrate",
|
|
80
51
|
"",
|
|
81
|
-
"Merge each reported branch into your own checkout (git merge courier/<session>), resolve conflicts
|
|
82
|
-
"checks once more
|
|
83
|
-
"deliverable of its own, its session delivers it and reports where, still naming its courier/<session> branch when " +
|
|
84
|
-
"it is isolated; you verify each delivery and merge nothing of it into your checkout.",
|
|
85
|
-
"",
|
|
86
|
-
"## 6. Report upward",
|
|
87
|
-
"",
|
|
88
|
-
"- If a session started you: one courier_send to it, with one status for the whole (done only when every part is " +
|
|
89
|
-
"verified and integrated, or verified and delivered; partial, blocked or failed otherwise) and the artifacts.",
|
|
90
|
-
"- At the top, before you reply to the person: if the next tree in this project should know something about running " +
|
|
91
|
-
"one like this (how it was split, what a part got wrong and what caught it, what a brief should have said, what the " +
|
|
92
|
-
"person decided), one courier_note with that one line; none when there is nothing. Name the job it comes from and " +
|
|
93
|
-
"the reason, so a lesson for one kind of job is not taken for every job; the tool refuses a line over its limit " +
|
|
94
|
-
"rather than clipping it, so write it shorter. A rule that holds every time goes to AGENTS.md or a skill, through " +
|
|
95
|
-
"the person, not into a note.",
|
|
52
|
+
"Merge each isolated part's reported branch into your own checkout (git merge courier/<session>), resolve conflicts " +
|
|
53
|
+
"yourself and run the checks once more. Then courier_cleanup each session you are done with, isolated or not.",
|
|
96
54
|
"",
|
|
97
|
-
"##
|
|
55
|
+
"## 6. Done",
|
|
98
56
|
"",
|
|
99
|
-
|
|
100
|
-
"
|
|
101
|
-
"
|
|
102
|
-
"- A refusal from courier_spawn naming maxDepth, maxChildren or maxTotal: do that part yourself, or end your turn and " +
|
|
103
|
-
"start it once a report has arrived. Do not retry it in a loop.",
|
|
104
|
-
"- Lost track, after a compaction or a restart: one courier_tree shows every session under you with its depth, its " +
|
|
105
|
-
"parent and whether it still owes a report; one look, not a loop.",
|
|
106
|
-
"- A subtree that went wrong or is no longer needed: courier_stop its top session; everything under it is stopped " +
|
|
107
|
-
"too, and nothing in it reports back. cleanup: true removes their worktrees.",
|
|
57
|
+
"The task is done only when every part is verified and integrated. At the root, before you reply to the person: one " +
|
|
58
|
+
"courier_note if the next tree in this project should know something about running one like this; none when there " +
|
|
59
|
+
"is nothing.",
|
|
108
60
|
].join("\n");
|
|
109
61
|
// A skill:// path: the read tool serves it from the registry (api-notes)
|
|
110
62
|
const SKILL_PATH = `skill://${SKILL_ID}/SKILL.md`;
|
package/dist/tree/context.js
CHANGED
|
@@ -5,12 +5,13 @@ const ROLES_MAX = 1000;
|
|
|
5
5
|
const ROLE_LINE = new RegExp(`^${ROLE_PREFIX}`, "m");
|
|
6
6
|
export async function shapeContext(ports, event) {
|
|
7
7
|
try {
|
|
8
|
-
const { depth, entry } = await roleOf(ports, event.sessionID);
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
if (depth !== null && depth >= ports.limits.maxDepth)
|
|
8
|
+
const { depth, entry, subagent } = await roleOf(ports, event.sessionID);
|
|
9
|
+
const leaf = subagent || (depth !== null && depth >= ports.limits.maxDepth);
|
|
10
|
+
if (leaf)
|
|
12
11
|
for (const tool of ["courier_spawn", ...(ports.hiddenFromLeaf ?? [])])
|
|
13
12
|
delete event.tools[tool];
|
|
13
|
+
if (subagent || (depth === null && !event.tools.courier_spawn))
|
|
14
|
+
return;
|
|
14
15
|
if (entry || event.system.some((part) => ROLE_LINE.test(part.text)))
|
|
15
16
|
return;
|
|
16
17
|
event.system.push({ type: "text", text: rolePart(depth, ports.limits) });
|
|
@@ -19,13 +20,24 @@ export async function shapeContext(ports, event) {
|
|
|
19
20
|
ports.log(`courier: cannot tell the role of session ${event.sessionID}: ${String(error)}`);
|
|
20
21
|
}
|
|
21
22
|
}
|
|
22
|
-
async function roleOf(ports, sessionID) {
|
|
23
|
+
async function roleOf(ports, sessionID, below = new Set()) {
|
|
23
24
|
const known = ports.roles.get(sessionID);
|
|
24
25
|
if (known !== undefined)
|
|
25
26
|
return known;
|
|
26
27
|
const children = childStore(ports.storage);
|
|
27
28
|
const chain = await children.indexedLineage(sessionID);
|
|
28
29
|
const depth = chain.length || ((await children.children(sessionID)).length ? 0 : null);
|
|
30
|
+
if (depth === null) {
|
|
31
|
+
const { parentID } = await ports.session.get({ sessionID });
|
|
32
|
+
if (parentID && !below.has(parentID)) {
|
|
33
|
+
const above = await roleOf(ports, parentID, new Set([...below, sessionID]));
|
|
34
|
+
if (above.depth === null && !above.subagent)
|
|
35
|
+
return { depth: null, entry: false };
|
|
36
|
+
const role = { depth: null, entry: false, subagent: true };
|
|
37
|
+
setBounded(ports.roles, sessionID, role, ROLES_MAX);
|
|
38
|
+
return role;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
29
41
|
const role = { depth, entry: depth !== null && (await hasEntry(ports, sessionID)) };
|
|
30
42
|
setBounded(ports.roles, sessionID, role, ROLES_MAX);
|
|
31
43
|
return role;
|
package/dist/tree/events.d.ts
CHANGED
package/dist/tree/events.js
CHANGED
|
@@ -3,8 +3,9 @@ import { claim, SEEN_MAX, setBounded } from "../platform/bounded.js";
|
|
|
3
3
|
import { endsIdle, retryDelay } from "./domain.js";
|
|
4
4
|
import { forgetChild, forgetDeleted } from "./forget.js";
|
|
5
5
|
import { hasCompleteGroup, leave, outIn } from "./group.js";
|
|
6
|
+
import { liveBelow } from "./limits.js";
|
|
6
7
|
import { owedUnwaited } from "./nudge.js";
|
|
7
|
-
import { stoppedSincePrompt } from "./report.js";
|
|
8
|
+
import { reportedSincePrompt, stoppedSincePrompt } from "./report.js";
|
|
8
9
|
import { syncStanding } from "./standing.js";
|
|
9
10
|
import { childStore, groupStore } from "./store.js";
|
|
10
11
|
import { lastReply } from "./status.js";
|
|
@@ -26,9 +27,13 @@ export async function reportFailure(ports, seen, event) {
|
|
|
26
27
|
try {
|
|
27
28
|
if (quiet)
|
|
28
29
|
return [];
|
|
30
|
+
const below = await liveBelow(ports, sessionID).then((live) => live.map((entry) => entry.title), (error) => {
|
|
31
|
+
ports.log(`courier watch: could not read the sessions under ${sessionID}: ${String(error)}`);
|
|
32
|
+
return [];
|
|
33
|
+
});
|
|
29
34
|
await Promise.all(entries.map((entry, index) => ports.session.synthetic({
|
|
30
35
|
sessionID: entry.parentID,
|
|
31
|
-
text: envelope(sessionID, failureNotice(entry.title, error, out[index]?.group, at), { failed: error.type }),
|
|
36
|
+
text: envelope(sessionID, failureNotice(entry.title, error, out[index]?.group, at, below), { failed: error.type }),
|
|
32
37
|
description: `Session ${sessionID} failed`,
|
|
33
38
|
metadata: { source: "courier", from: sessionID, failed: true },
|
|
34
39
|
delivery: "steer",
|
|
@@ -43,21 +48,48 @@ export async function reportFailure(ports, seen, event) {
|
|
|
43
48
|
}
|
|
44
49
|
// only inbox.enqueued carries the item's type (api-notes)
|
|
45
50
|
const UNOWED = new Set(["user", "compaction", "move"]);
|
|
51
|
+
const OWN = "own";
|
|
52
|
+
const OWN_SCHEDULED = "own-scheduled:";
|
|
53
|
+
const MESSAGE_FROM = "from:";
|
|
54
|
+
function kindOf(sessionID, item) {
|
|
55
|
+
const metadata = item.payload?.metadata;
|
|
56
|
+
if (item.type !== "synthetic" || metadata?.source !== "courier")
|
|
57
|
+
return item.type;
|
|
58
|
+
if (typeof metadata.group === "string")
|
|
59
|
+
return OWN;
|
|
60
|
+
const ownCheckIn = typeof metadata.scheduled === "string" && metadata.from === sessionID && typeof metadata.scheduledAt === "number";
|
|
61
|
+
if (ownCheckIn)
|
|
62
|
+
return `${OWN_SCHEDULED}${metadata.scheduledAt}`;
|
|
63
|
+
const statusless = typeof metadata.from === "string" && Object.keys(metadata).length === 2;
|
|
64
|
+
return statusless ? `${MESSAGE_FROM}${metadata.from}` : item.type;
|
|
65
|
+
}
|
|
66
|
+
async function ownAfterReport(children, sessionID, kind) {
|
|
67
|
+
if (kind === OWN)
|
|
68
|
+
return reportedSincePrompt(children, sessionID);
|
|
69
|
+
if (kind?.startsWith(MESSAGE_FROM))
|
|
70
|
+
return (await children.indexedParent(kind.slice(MESSAGE_FROM.length))) === sessionID && reportedSincePrompt(children, sessionID);
|
|
71
|
+
if (!kind?.startsWith(OWN_SCHEDULED))
|
|
72
|
+
return false;
|
|
73
|
+
return reportedSincePrompt(children, sessionID, Number(kind.slice(OWN_SCHEDULED.length)));
|
|
74
|
+
}
|
|
46
75
|
export async function noteEnqueued(ports, inbox, event) {
|
|
47
76
|
const { sessionID, inboxID, item } = event.data;
|
|
48
77
|
if ((await childStore(ports.storage).indexedParent(sessionID)) === undefined)
|
|
49
78
|
return false;
|
|
50
|
-
setBounded(inbox, inboxID, item
|
|
79
|
+
setBounded(inbox, inboxID, kindOf(sessionID, item), SEEN_MAX);
|
|
51
80
|
return true;
|
|
52
81
|
}
|
|
53
82
|
export async function notePrompt(ports, event, inbox) {
|
|
54
83
|
const { sessionID, inboxID } = event.data;
|
|
55
|
-
|
|
84
|
+
const kind = inboxID === undefined ? undefined : inbox?.get(inboxID);
|
|
85
|
+
if (UNOWED.has(kind ?? ""))
|
|
56
86
|
return false;
|
|
57
87
|
const children = childStore(ports.storage);
|
|
58
88
|
const parentID = await children.indexedParent(sessionID);
|
|
59
89
|
if (parentID === undefined)
|
|
60
90
|
return false;
|
|
91
|
+
if (await ownAfterReport(children, sessionID, kind))
|
|
92
|
+
return false;
|
|
61
93
|
// the idle hook's reminder still publishes inbox.delivered (api-notes)
|
|
62
94
|
if (inboxID !== undefined && (await children.nudgeOf(sessionID)).inboxID === inboxID)
|
|
63
95
|
return false;
|
|
@@ -150,10 +182,10 @@ const TREE_TYPES = [
|
|
|
150
182
|
"session.deleted",
|
|
151
183
|
];
|
|
152
184
|
async function sealGroups(ports, event) {
|
|
153
|
-
// a shutdown stamps no time.idle (api-notes)
|
|
154
|
-
if (event.data.reason === "shutdown")
|
|
155
|
-
return;
|
|
156
185
|
try {
|
|
186
|
+
// a shutdown ends a turn only on a plain decline's aborted step (api-notes)
|
|
187
|
+
if (event.data.reason === "shutdown" && !endsIdle(await ports.session.context({ sessionID: event.data.sessionID }).catch(() => [])))
|
|
188
|
+
return;
|
|
157
189
|
if (await hasCompleteGroup(groupStore(ports.storage), event.data.sessionID))
|
|
158
190
|
ports.nudge();
|
|
159
191
|
}
|
package/dist/tree/group.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isNotFound, millis } from "../platform/json.js";
|
|
2
2
|
import { envelope } from "../kernel/protocol.js";
|
|
3
|
-
import { complete, standingOf } from "./domain.js";
|
|
3
|
+
import { complete, endedAt, standingOf, turnEnd } from "./domain.js";
|
|
4
4
|
import { syncStanding } from "./standing.js";
|
|
5
5
|
import { groupStore } from "./store.js";
|
|
6
6
|
import { groupNotice } from "./text.js";
|
|
@@ -109,8 +109,11 @@ async function sealed(ports, parentID, members) {
|
|
|
109
109
|
const joined = Math.max(...members.map((member) => member.joinedAt));
|
|
110
110
|
try {
|
|
111
111
|
const { time } = await ports.session.get({ sessionID: parentID });
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
if (millis(time.idle) >= joined)
|
|
113
|
+
return true;
|
|
114
|
+
// a plain decline ends the turn on an aborted step, no idle (api-notes)
|
|
115
|
+
const end = turnEnd(await ports.session.context({ sessionID: parentID }).catch(() => []));
|
|
116
|
+
return end !== undefined && (endedAt(end, time) ?? Number.NEGATIVE_INFINITY) >= joined;
|
|
114
117
|
}
|
|
115
118
|
catch (error) {
|
|
116
119
|
if (isNotFound(error))
|