opencode-courier 0.7.3 → 0.8.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 +109 -135
- package/dist/index.js +23 -4
- package/dist/memory/domain.d.ts +119 -0
- package/dist/memory/domain.js +84 -0
- package/dist/memory/events.d.ts +26 -0
- package/dist/memory/events.js +46 -0
- package/dist/memory/forget.d.ts +10 -0
- package/dist/memory/forget.js +20 -0
- package/dist/memory/index.d.ts +10 -0
- package/dist/memory/index.js +11 -0
- package/dist/memory/note.d.ts +37 -0
- package/dist/memory/note.js +29 -0
- package/dist/memory/ports.d.ts +44 -0
- package/dist/memory/ports.js +1 -0
- package/dist/memory/recall.d.ts +27 -0
- package/dist/memory/recall.js +48 -0
- package/dist/memory/record.d.ts +7 -0
- package/dist/memory/record.js +92 -0
- package/dist/memory/rpc-handlers.d.ts +134 -0
- package/dist/memory/rpc-handlers.js +23 -0
- package/dist/memory/rpc.d.ts +227 -0
- package/dist/memory/rpc.js +60 -0
- package/dist/memory/store.d.ts +10 -0
- package/dist/memory/store.js +90 -0
- package/dist/memory/sweep.d.ts +4 -0
- package/dist/memory/sweep.js +12 -0
- package/dist/memory/text.d.ts +27 -0
- package/dist/memory/text.js +95 -0
- package/dist/memory/tools.d.ts +16 -0
- package/dist/memory/tools.js +78 -0
- package/dist/memory/tui.d.ts +73 -0
- package/dist/person/rpc.d.ts +4 -4
- package/dist/platform/json.d.ts +1 -0
- package/dist/platform/json.js +1 -0
- package/dist/rpc.d.ts +1 -0
- package/dist/rpc.js +1 -0
- package/dist/skill.js +10 -3
- package/dist/tree/context.d.ts +2 -2
- package/dist/tree/context.js +4 -3
- package/dist/tree/index.d.ts +6 -5
- package/dist/tree/index.js +5 -4
- package/dist/tree/ports.d.ts +4 -2
- package/dist/tree/roster.d.ts +17 -0
- package/dist/tree/roster.js +24 -0
- package/dist/tree/rpc-handlers.d.ts +18 -18
- package/dist/tree/rpc.d.ts +18 -18
- package/dist/tree/status.d.ts +5 -5
- package/dist/tree/subtree.d.ts +2 -2
- package/dist/tree/text.js +2 -1
- package/dist/tree/tui.d.ts +11 -3
- package/dist/tui.js +544 -121
- package/package.json +1 -1
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { newestFirst } from "./domain.js";
|
|
2
|
+
import { forget } from "./forget.js";
|
|
3
|
+
import { memoryRpc } from "./rpc.js";
|
|
4
|
+
import { memoryStore } from "./store.js";
|
|
5
|
+
/** A stored record as the RPC gives it: without its project, which the listing names once. */
|
|
6
|
+
const toRecord = ({ projectID: _project, ...record }) => record;
|
|
7
|
+
/**
|
|
8
|
+
* The records of the ports' project, newest first, as the memory view lists them; nothing, and `memory` false, with the
|
|
9
|
+
* option off. Read from the store alone: unlike courier_recall, a look from the terminal is noted on no record.
|
|
10
|
+
*/
|
|
11
|
+
export async function records(ports) {
|
|
12
|
+
const { projectID } = ports;
|
|
13
|
+
if (!ports.memory)
|
|
14
|
+
return { memory: false, projectID, records: [] };
|
|
15
|
+
return { memory: true, projectID, records: newestFirst(await memoryStore(ports.storage).ofProject(projectID)).map(toRecord) };
|
|
16
|
+
}
|
|
17
|
+
/** Serves the memory's RPC at the plugin's location, after the tree's and the person's; disposed with the plugin. */
|
|
18
|
+
export function registerMemoryRpc(rpc, ports) {
|
|
19
|
+
return rpc.register(memoryRpc, {
|
|
20
|
+
records: () => records(ports),
|
|
21
|
+
forget: async ({ sessionID, lesson }) => ({ outcome: await forget(ports, sessionID, { lesson: lesson === true }) }),
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
/** A part's statuses, as `memory/domain.ts`'s `PART_STATUSES` has them; a unit test keeps the two equal. */
|
|
3
|
+
export declare const PART_STATUSES: readonly ["done", "partial", "blocked", "failed", "interrupted", "stopped", "stalled", "out"];
|
|
4
|
+
/** One session started in a tree, as its record keeps it: durations in milliseconds, context use a share of the model's window. */
|
|
5
|
+
declare const Part: Schema.Struct<{
|
|
6
|
+
readonly sessionID: Schema.String;
|
|
7
|
+
readonly title: Schema.String;
|
|
8
|
+
/** 1 for a session the top started. */
|
|
9
|
+
readonly depth: Schema.Int;
|
|
10
|
+
readonly isolated: Schema.Boolean;
|
|
11
|
+
readonly group: Schema.optionalKey<Schema.String>;
|
|
12
|
+
readonly status: Schema.Literals<readonly ["done", "partial", "blocked", "failed", "interrupted", "stopped", "stalled", "out"]>;
|
|
13
|
+
readonly duration: Schema.optionalKey<Schema.Number>;
|
|
14
|
+
readonly cost: Schema.optionalKey<Schema.Number>;
|
|
15
|
+
readonly context: Schema.optionalKey<Schema.Number>;
|
|
16
|
+
readonly refused: Schema.optionalKey<Schema.Int>;
|
|
17
|
+
}>;
|
|
18
|
+
/** The one line the tree's orchestrator wrote with courier_note: when, and from which depth when not the top's own. */
|
|
19
|
+
declare const Lesson: Schema.Struct<{
|
|
20
|
+
readonly text: Schema.String;
|
|
21
|
+
readonly at: Schema.Number;
|
|
22
|
+
readonly depth: Schema.optionalKey<Schema.Int>;
|
|
23
|
+
}>;
|
|
24
|
+
/** A tree's record as `courier_recall` reads it, with the top session it is kept under; times in epoch milliseconds. */
|
|
25
|
+
declare const Record: Schema.Struct<{
|
|
26
|
+
readonly sessionID: Schema.String;
|
|
27
|
+
readonly startedAt: Schema.Number;
|
|
28
|
+
readonly movedAt: Schema.Number;
|
|
29
|
+
readonly title: Schema.String;
|
|
30
|
+
readonly split: Schema.String;
|
|
31
|
+
readonly finished: Schema.Boolean;
|
|
32
|
+
readonly recalled: Schema.optionalKey<Schema.Struct<{
|
|
33
|
+
readonly at: Schema.optionalKey<Schema.Number>;
|
|
34
|
+
readonly calls: Schema.Int;
|
|
35
|
+
}>>;
|
|
36
|
+
readonly lesson: Schema.optionalKey<Schema.Struct<{
|
|
37
|
+
readonly text: Schema.String;
|
|
38
|
+
readonly at: Schema.Number;
|
|
39
|
+
readonly depth: Schema.optionalKey<Schema.Int>;
|
|
40
|
+
}>>;
|
|
41
|
+
readonly parts: Schema.$Array<Schema.Struct<{
|
|
42
|
+
readonly sessionID: Schema.String;
|
|
43
|
+
readonly title: Schema.String;
|
|
44
|
+
/** 1 for a session the top started. */
|
|
45
|
+
readonly depth: Schema.Int;
|
|
46
|
+
readonly isolated: Schema.Boolean;
|
|
47
|
+
readonly group: Schema.optionalKey<Schema.String>;
|
|
48
|
+
readonly status: Schema.Literals<readonly ["done", "partial", "blocked", "failed", "interrupted", "stopped", "stalled", "out"]>;
|
|
49
|
+
readonly duration: Schema.optionalKey<Schema.Number>;
|
|
50
|
+
readonly cost: Schema.optionalKey<Schema.Number>;
|
|
51
|
+
readonly context: Schema.optionalKey<Schema.Number>;
|
|
52
|
+
readonly refused: Schema.optionalKey<Schema.Int>;
|
|
53
|
+
}>>;
|
|
54
|
+
}>;
|
|
55
|
+
/** The records of the project of the location asked, newest first; none, and `memory` false, when the option is off there. */
|
|
56
|
+
declare const Records: Schema.Struct<{
|
|
57
|
+
readonly memory: Schema.Boolean;
|
|
58
|
+
readonly projectID: Schema.String;
|
|
59
|
+
readonly records: Schema.$Array<Schema.Struct<{
|
|
60
|
+
readonly sessionID: Schema.String;
|
|
61
|
+
readonly startedAt: Schema.Number;
|
|
62
|
+
readonly movedAt: Schema.Number;
|
|
63
|
+
readonly title: Schema.String;
|
|
64
|
+
readonly split: Schema.String;
|
|
65
|
+
readonly finished: Schema.Boolean;
|
|
66
|
+
readonly recalled: Schema.optionalKey<Schema.Struct<{
|
|
67
|
+
readonly at: Schema.optionalKey<Schema.Number>;
|
|
68
|
+
readonly calls: Schema.Int;
|
|
69
|
+
}>>;
|
|
70
|
+
readonly lesson: Schema.optionalKey<Schema.Struct<{
|
|
71
|
+
readonly text: Schema.String;
|
|
72
|
+
readonly at: Schema.Number;
|
|
73
|
+
readonly depth: Schema.optionalKey<Schema.Int>;
|
|
74
|
+
}>>;
|
|
75
|
+
readonly parts: Schema.$Array<Schema.Struct<{
|
|
76
|
+
readonly sessionID: Schema.String;
|
|
77
|
+
readonly title: Schema.String;
|
|
78
|
+
/** 1 for a session the top started. */
|
|
79
|
+
readonly depth: Schema.Int;
|
|
80
|
+
readonly isolated: Schema.Boolean;
|
|
81
|
+
readonly group: Schema.optionalKey<Schema.String>;
|
|
82
|
+
readonly status: Schema.Literals<readonly ["done", "partial", "blocked", "failed", "interrupted", "stopped", "stalled", "out"]>;
|
|
83
|
+
readonly duration: Schema.optionalKey<Schema.Number>;
|
|
84
|
+
readonly cost: Schema.optionalKey<Schema.Number>;
|
|
85
|
+
readonly context: Schema.optionalKey<Schema.Number>;
|
|
86
|
+
readonly refused: Schema.optionalKey<Schema.Int>;
|
|
87
|
+
}>>;
|
|
88
|
+
}>>;
|
|
89
|
+
}>;
|
|
90
|
+
/** What a forget did: removed the record, dropped only its lesson, or found nothing to forget. */
|
|
91
|
+
declare const Forgotten: Schema.Struct<{
|
|
92
|
+
readonly outcome: Schema.Literals<readonly ["record", "lesson", "none"]>;
|
|
93
|
+
}>;
|
|
94
|
+
export type MemoryRecord = typeof Record.Type;
|
|
95
|
+
export type MemoryPart = typeof Part.Type;
|
|
96
|
+
export type MemoryLesson = typeof Lesson.Type;
|
|
97
|
+
export type Records = typeof Records.Type;
|
|
98
|
+
export type Forgotten = typeof Forgotten.Type;
|
|
99
|
+
export declare const memoryRpc: {
|
|
100
|
+
readonly id: "courier-memory";
|
|
101
|
+
readonly methods: {
|
|
102
|
+
readonly records: {
|
|
103
|
+
readonly input: import("effect/StandardSchema").StandardSchemaV1<{}, {}> & Schema.Struct<{}>;
|
|
104
|
+
readonly output: import("effect/StandardSchema").StandardSchemaV1<{
|
|
105
|
+
readonly memory: boolean;
|
|
106
|
+
readonly projectID: string;
|
|
107
|
+
readonly records: readonly {
|
|
108
|
+
readonly sessionID: string;
|
|
109
|
+
readonly title: string;
|
|
110
|
+
readonly parts: readonly {
|
|
111
|
+
readonly status: "done" | "partial" | "blocked" | "failed" | "interrupted" | "stopped" | "stalled" | "out";
|
|
112
|
+
readonly sessionID: string;
|
|
113
|
+
readonly title: string;
|
|
114
|
+
readonly depth: number;
|
|
115
|
+
readonly isolated: boolean;
|
|
116
|
+
readonly group?: string | undefined;
|
|
117
|
+
readonly duration?: number | undefined;
|
|
118
|
+
readonly cost?: number | undefined;
|
|
119
|
+
readonly context?: number | undefined;
|
|
120
|
+
readonly refused?: number | undefined;
|
|
121
|
+
}[];
|
|
122
|
+
readonly startedAt: number;
|
|
123
|
+
readonly movedAt: number;
|
|
124
|
+
readonly split: string;
|
|
125
|
+
readonly finished: boolean;
|
|
126
|
+
readonly recalled?: {
|
|
127
|
+
readonly calls: number;
|
|
128
|
+
readonly at?: number | undefined;
|
|
129
|
+
} | undefined;
|
|
130
|
+
readonly lesson?: {
|
|
131
|
+
readonly text: string;
|
|
132
|
+
readonly at: number;
|
|
133
|
+
readonly depth?: number | undefined;
|
|
134
|
+
} | undefined;
|
|
135
|
+
}[];
|
|
136
|
+
}, {
|
|
137
|
+
readonly memory: boolean;
|
|
138
|
+
readonly projectID: string;
|
|
139
|
+
readonly records: readonly {
|
|
140
|
+
readonly sessionID: string;
|
|
141
|
+
readonly title: string;
|
|
142
|
+
readonly parts: readonly {
|
|
143
|
+
readonly status: "done" | "partial" | "blocked" | "failed" | "interrupted" | "stopped" | "stalled" | "out";
|
|
144
|
+
readonly sessionID: string;
|
|
145
|
+
readonly title: string;
|
|
146
|
+
readonly depth: number;
|
|
147
|
+
readonly isolated: boolean;
|
|
148
|
+
readonly group?: string | undefined;
|
|
149
|
+
readonly duration?: number | undefined;
|
|
150
|
+
readonly cost?: number | undefined;
|
|
151
|
+
readonly context?: number | undefined;
|
|
152
|
+
readonly refused?: number | undefined;
|
|
153
|
+
}[];
|
|
154
|
+
readonly startedAt: number;
|
|
155
|
+
readonly movedAt: number;
|
|
156
|
+
readonly split: string;
|
|
157
|
+
readonly finished: boolean;
|
|
158
|
+
readonly recalled?: {
|
|
159
|
+
readonly calls: number;
|
|
160
|
+
readonly at?: number | undefined;
|
|
161
|
+
} | undefined;
|
|
162
|
+
readonly lesson?: {
|
|
163
|
+
readonly text: string;
|
|
164
|
+
readonly at: number;
|
|
165
|
+
readonly depth?: number | undefined;
|
|
166
|
+
} | undefined;
|
|
167
|
+
}[];
|
|
168
|
+
}> & Schema.Struct<{
|
|
169
|
+
readonly memory: Schema.Boolean;
|
|
170
|
+
readonly projectID: Schema.String;
|
|
171
|
+
readonly records: Schema.$Array<Schema.Struct<{
|
|
172
|
+
readonly sessionID: Schema.String;
|
|
173
|
+
readonly startedAt: Schema.Number;
|
|
174
|
+
readonly movedAt: Schema.Number;
|
|
175
|
+
readonly title: Schema.String;
|
|
176
|
+
readonly split: Schema.String;
|
|
177
|
+
readonly finished: Schema.Boolean;
|
|
178
|
+
readonly recalled: Schema.optionalKey<Schema.Struct<{
|
|
179
|
+
readonly at: Schema.optionalKey<Schema.Number>;
|
|
180
|
+
readonly calls: Schema.Int;
|
|
181
|
+
}>>;
|
|
182
|
+
readonly lesson: Schema.optionalKey<Schema.Struct<{
|
|
183
|
+
readonly text: Schema.String;
|
|
184
|
+
readonly at: Schema.Number;
|
|
185
|
+
readonly depth: Schema.optionalKey<Schema.Int>;
|
|
186
|
+
}>>;
|
|
187
|
+
readonly parts: Schema.$Array<Schema.Struct<{
|
|
188
|
+
readonly sessionID: Schema.String;
|
|
189
|
+
readonly title: Schema.String;
|
|
190
|
+
/** 1 for a session the top started. */
|
|
191
|
+
readonly depth: Schema.Int;
|
|
192
|
+
readonly isolated: Schema.Boolean;
|
|
193
|
+
readonly group: Schema.optionalKey<Schema.String>;
|
|
194
|
+
readonly status: Schema.Literals<readonly ["done", "partial", "blocked", "failed", "interrupted", "stopped", "stalled", "out"]>;
|
|
195
|
+
readonly duration: Schema.optionalKey<Schema.Number>;
|
|
196
|
+
readonly cost: Schema.optionalKey<Schema.Number>;
|
|
197
|
+
readonly context: Schema.optionalKey<Schema.Number>;
|
|
198
|
+
readonly refused: Schema.optionalKey<Schema.Int>;
|
|
199
|
+
}>>;
|
|
200
|
+
}>>;
|
|
201
|
+
}>;
|
|
202
|
+
};
|
|
203
|
+
/** Forgets the record of the tree under `sessionID` in the project asked, or with `lesson` only its lesson, keeping the facts. */
|
|
204
|
+
readonly forget: {
|
|
205
|
+
readonly input: import("effect/StandardSchema").StandardSchemaV1<{
|
|
206
|
+
readonly sessionID: string;
|
|
207
|
+
readonly lesson?: boolean | undefined;
|
|
208
|
+
}, {
|
|
209
|
+
readonly sessionID: string;
|
|
210
|
+
readonly lesson?: boolean | undefined;
|
|
211
|
+
}> & Schema.Struct<{
|
|
212
|
+
readonly sessionID: Schema.String;
|
|
213
|
+
readonly lesson: Schema.optionalKey<Schema.Boolean>;
|
|
214
|
+
}>;
|
|
215
|
+
readonly output: import("effect/StandardSchema").StandardSchemaV1<{
|
|
216
|
+
readonly outcome: "lesson" | "record" | "none";
|
|
217
|
+
}, {
|
|
218
|
+
readonly outcome: "lesson" | "record" | "none";
|
|
219
|
+
}> & Schema.Struct<{
|
|
220
|
+
readonly outcome: Schema.Literals<readonly ["record", "lesson", "none"]>;
|
|
221
|
+
}>;
|
|
222
|
+
};
|
|
223
|
+
};
|
|
224
|
+
readonly events: {};
|
|
225
|
+
};
|
|
226
|
+
export type MemoryRpc = typeof memoryRpc;
|
|
227
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The memory's RPC, the contract its TUI part, the memory view, and its server half share. Standard Schema values, as
|
|
3
|
+
* the tree's and the person's; imports only `@opencode/plugin/rpc` and `effect`.
|
|
4
|
+
*/
|
|
5
|
+
import { Rpc } from "@opencode/plugin/rpc";
|
|
6
|
+
import { Schema } from "effect";
|
|
7
|
+
const Count = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
|
|
8
|
+
/** A part's statuses, as `memory/domain.ts`'s `PART_STATUSES` has them; a unit test keeps the two equal. */
|
|
9
|
+
export const PART_STATUSES = ["done", "partial", "blocked", "failed", "interrupted", "stopped", "stalled", "out"];
|
|
10
|
+
/** One session started in a tree, as its record keeps it: durations in milliseconds, context use a share of the model's window. */
|
|
11
|
+
const Part = Schema.Struct({
|
|
12
|
+
sessionID: Schema.String,
|
|
13
|
+
title: Schema.String,
|
|
14
|
+
/** 1 for a session the top started. */
|
|
15
|
+
depth: Count,
|
|
16
|
+
isolated: Schema.Boolean,
|
|
17
|
+
group: Schema.optionalKey(Schema.String),
|
|
18
|
+
status: Schema.Literals(PART_STATUSES),
|
|
19
|
+
duration: Schema.optionalKey(Schema.Number),
|
|
20
|
+
cost: Schema.optionalKey(Schema.Number),
|
|
21
|
+
context: Schema.optionalKey(Schema.Number),
|
|
22
|
+
refused: Schema.optionalKey(Count),
|
|
23
|
+
});
|
|
24
|
+
/** The one line the tree's orchestrator wrote with courier_note: when, and from which depth when not the top's own. */
|
|
25
|
+
const Lesson = Schema.Struct({ text: Schema.String, at: Schema.Number, depth: Schema.optionalKey(Count) });
|
|
26
|
+
/** A tree's record as `courier_recall` reads it, with the top session it is kept under; times in epoch milliseconds. */
|
|
27
|
+
const Record = Schema.Struct({
|
|
28
|
+
sessionID: Schema.String,
|
|
29
|
+
startedAt: Schema.Number,
|
|
30
|
+
movedAt: Schema.Number,
|
|
31
|
+
title: Schema.String,
|
|
32
|
+
split: Schema.String,
|
|
33
|
+
finished: Schema.Boolean,
|
|
34
|
+
recalled: Schema.optionalKey(Schema.Struct({ at: Schema.optionalKey(Schema.Number), calls: Count })),
|
|
35
|
+
lesson: Schema.optionalKey(Lesson),
|
|
36
|
+
parts: Schema.Array(Part),
|
|
37
|
+
});
|
|
38
|
+
/** The records of the project of the location asked, newest first; none, and `memory` false, when the option is off there. */
|
|
39
|
+
const Records = Schema.Struct({
|
|
40
|
+
memory: Schema.Boolean,
|
|
41
|
+
projectID: Schema.String,
|
|
42
|
+
records: Schema.Array(Record),
|
|
43
|
+
});
|
|
44
|
+
/** What a forget did: removed the record, dropped only its lesson, or found nothing to forget. */
|
|
45
|
+
const Forgotten = Schema.Struct({ outcome: Schema.Literals(["record", "lesson", "none"]) });
|
|
46
|
+
export const memoryRpc = Rpc.define({
|
|
47
|
+
id: "courier-memory",
|
|
48
|
+
methods: {
|
|
49
|
+
records: {
|
|
50
|
+
input: Schema.toStandardSchemaV1(Schema.Struct({})),
|
|
51
|
+
output: Schema.toStandardSchemaV1(Records),
|
|
52
|
+
},
|
|
53
|
+
/** Forgets the record of the tree under `sessionID` in the project asked, or with `lesson` only its lesson, keeping the facts. */
|
|
54
|
+
forget: {
|
|
55
|
+
input: Schema.toStandardSchemaV1(Schema.Struct({ sessionID: Schema.String, lesson: Schema.optionalKey(Schema.Boolean) })),
|
|
56
|
+
output: Schema.toStandardSchemaV1(Forgotten),
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
events: {},
|
|
60
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type Storage } from "../platform/storage.js";
|
|
2
|
+
import { type TreeRecord } from "./domain.js";
|
|
3
|
+
import type { MemoryStore } from "./ports.js";
|
|
4
|
+
export declare const recordKey: (projectID: string, sessionID: string) => string;
|
|
5
|
+
/** The key prefix of a project's records. */
|
|
6
|
+
export declare const projectPrefix: (projectID: string) => string;
|
|
7
|
+
/** A record as stored, or undefined for a value that is not one. */
|
|
8
|
+
export declare function readRecord(value: unknown): TreeRecord | undefined;
|
|
9
|
+
/** The `MemoryStore` over the plugin's storage. */
|
|
10
|
+
export declare function memoryStore(storage: Storage): MemoryStore;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { finite, num, obj, str } from "../platform/json.js";
|
|
2
|
+
import { scanEntries } from "../platform/storage.js";
|
|
3
|
+
import { isPartStatus, lessonText, NOT_STATED } from "./domain.js";
|
|
4
|
+
// The memory's storage: the only file that knows its key and layout. A record is read leniently, as one written by
|
|
5
|
+
// hand with `e2e/kv.ts set` is: what is missing takes a default, a part without a session id is left out.
|
|
6
|
+
const PREFIX = "memory/";
|
|
7
|
+
export const recordKey = (projectID, sessionID) => `${PREFIX}${projectID}/${sessionID}`;
|
|
8
|
+
/** The key prefix of a project's records. */
|
|
9
|
+
export const projectPrefix = (projectID) => `${PREFIX}${projectID}/`;
|
|
10
|
+
function readPart(value) {
|
|
11
|
+
const given = obj(value);
|
|
12
|
+
const sessionID = str(given.sessionID);
|
|
13
|
+
if (sessionID === undefined)
|
|
14
|
+
return undefined;
|
|
15
|
+
const optional = (field) => {
|
|
16
|
+
const number = finite(given[field]);
|
|
17
|
+
return number === undefined ? {} : { [field]: number };
|
|
18
|
+
};
|
|
19
|
+
const group = str(given.group);
|
|
20
|
+
return {
|
|
21
|
+
sessionID,
|
|
22
|
+
title: str(given.title) ?? "",
|
|
23
|
+
depth: num(given.depth) ?? 1,
|
|
24
|
+
isolated: given.isolated === true,
|
|
25
|
+
...(group ? { group } : {}),
|
|
26
|
+
status: isPartStatus(given.status) ? given.status : "out",
|
|
27
|
+
...optional("duration"),
|
|
28
|
+
...optional("cost"),
|
|
29
|
+
...optional("context"),
|
|
30
|
+
...optional("refused"),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function readRecalled(value) {
|
|
34
|
+
const given = obj(value);
|
|
35
|
+
const calls = num(given.calls);
|
|
36
|
+
if (calls === undefined)
|
|
37
|
+
return undefined;
|
|
38
|
+
const at = num(given.at);
|
|
39
|
+
return at === undefined ? { calls } : { at, calls };
|
|
40
|
+
}
|
|
41
|
+
/** A lesson as `{ text, at, depth }`, or as a bare string written by hand, dated `movedAt`; none for an empty text. */
|
|
42
|
+
function readLesson(value, movedAt) {
|
|
43
|
+
const given = typeof value === "string" ? { text: value } : obj(value);
|
|
44
|
+
const { text } = lessonText(str(given.text) ?? "");
|
|
45
|
+
if (!text)
|
|
46
|
+
return undefined;
|
|
47
|
+
const depth = num(given.depth);
|
|
48
|
+
return { text, at: num(given.at) ?? movedAt, ...(depth !== undefined && Number.isInteger(depth) && depth > 0 ? { depth } : {}) };
|
|
49
|
+
}
|
|
50
|
+
/** A record as stored, or undefined for a value that is not one. */
|
|
51
|
+
export function readRecord(value) {
|
|
52
|
+
const given = obj(value);
|
|
53
|
+
if (!Array.isArray(given.parts))
|
|
54
|
+
return undefined;
|
|
55
|
+
const recalled = readRecalled(given.recalled);
|
|
56
|
+
const movedAt = num(given.movedAt) ?? num(given.startedAt) ?? 0;
|
|
57
|
+
const lesson = readLesson(given.lesson, movedAt);
|
|
58
|
+
return {
|
|
59
|
+
startedAt: num(given.startedAt) ?? 0,
|
|
60
|
+
movedAt,
|
|
61
|
+
title: str(given.title) ?? "",
|
|
62
|
+
split: str(given.split) ?? NOT_STATED,
|
|
63
|
+
finished: given.finished === true,
|
|
64
|
+
...(recalled ? { recalled } : {}),
|
|
65
|
+
...(lesson ? { lesson } : {}),
|
|
66
|
+
parts: given.parts.map(readPart).filter((part) => part !== undefined),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** The `MemoryStore` over the plugin's storage. */
|
|
70
|
+
export function memoryStore(storage) {
|
|
71
|
+
const under = async (prefix) => {
|
|
72
|
+
const entries = await scanEntries(storage, prefix);
|
|
73
|
+
return entries.flatMap(({ key, value }) => {
|
|
74
|
+
const parts = `${prefix.slice(PREFIX.length)}${key}`.split("/");
|
|
75
|
+
const record = parts.length === 2 ? readRecord(value) : undefined;
|
|
76
|
+
return record ? [{ ...record, projectID: parts[0], sessionID: parts[1] }] : [];
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
return {
|
|
80
|
+
get: async (projectID, sessionID) => readRecord(await storage.get(recordKey(projectID, sessionID))),
|
|
81
|
+
async put(projectID, sessionID, record) {
|
|
82
|
+
await storage.set(recordKey(projectID, sessionID), { ...record, parts: record.parts.map((part) => ({ ...part })) });
|
|
83
|
+
},
|
|
84
|
+
async remove(projectID, sessionID) {
|
|
85
|
+
await storage.remove(recordKey(projectID, sessionID));
|
|
86
|
+
},
|
|
87
|
+
ofProject: (projectID) => under(projectPrefix(projectID)),
|
|
88
|
+
all: () => under(PREFIX),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Storage } from "../platform/storage.js";
|
|
2
|
+
import { type StoredRecord } from "./domain.js";
|
|
3
|
+
/** On loading: drops, per project, the records past `MAX_RECORDS` or `RETENTION_MS` at `now`. Returns those dropped. */
|
|
4
|
+
export declare function sweepRecords(storage: Storage, now: number): Promise<StoredRecord[]>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { keep } from "./domain.js";
|
|
2
|
+
import { memoryStore } from "./store.js";
|
|
3
|
+
/** On loading: drops, per project, the records past `MAX_RECORDS` or `RETENTION_MS` at `now`. Returns those dropped. */
|
|
4
|
+
export async function sweepRecords(storage, now) {
|
|
5
|
+
const store = memoryStore(storage);
|
|
6
|
+
const byProject = new Map();
|
|
7
|
+
for (const record of await store.all())
|
|
8
|
+
byProject.set(record.projectID, [...(byProject.get(record.projectID) ?? []), record]);
|
|
9
|
+
const dropped = [...byProject.values()].flatMap((records) => keep(records, now).dropped);
|
|
10
|
+
await Promise.all(dropped.map((record) => store.remove(record.projectID, record.sessionID)));
|
|
11
|
+
return dropped;
|
|
12
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type Lesson, type StoredRecord } from "./domain.js";
|
|
2
|
+
/** The tool's name, which the skill and the role part name too. */
|
|
3
|
+
export declare const RECALL_TOOL = "courier_recall";
|
|
4
|
+
/** The lesson tool's name, which the skill's last step names. */
|
|
5
|
+
export declare const NOTE_TOOL = "courier_note";
|
|
6
|
+
/** A duration in milliseconds as a model reads it: seconds under half a minute, else whole minutes, else hours and minutes. */
|
|
7
|
+
export declare function duration(ms: number): string;
|
|
8
|
+
/** A lesson as the listing and the note's result show it: dated, and from which depth when a session below the top wrote it. */
|
|
9
|
+
export declare function lessonLine(lesson: Lesson): string;
|
|
10
|
+
/** One record as the listing shows it: its top, when it started, its title, its split line, its parts, how long it ran and what it cost. */
|
|
11
|
+
export declare function recordLine(record: StoredRecord, top: string): string;
|
|
12
|
+
/** What courier_recall says when the memory option is off. */
|
|
13
|
+
export declare const RECALL_OFF = "Memory is off: the plugin's memory option is false, so no record of a session tree is kept or listed in this project. Decide from the task alone.";
|
|
14
|
+
/** The listing: every record of the project, newest first, the caller's own tree marked. */
|
|
15
|
+
export declare function recallListText(records: ReadonlyArray<StoredRecord>, top: string): string;
|
|
16
|
+
/** One record in full, as JSON: durations in milliseconds, times in epoch milliseconds, context use as a share of the model's window. */
|
|
17
|
+
export declare function recallRecordText(record: StoredRecord): string;
|
|
18
|
+
export declare const recallMissingText: (sessionID: string) => string;
|
|
19
|
+
/** What courier_note says once the lesson is written: where, and what it replaced. */
|
|
20
|
+
export declare function noteText(top: string, lesson: Lesson, replaced: Lesson | undefined): string;
|
|
21
|
+
/** What courier_note says when the memory option is off. */
|
|
22
|
+
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.";
|
|
23
|
+
export declare const noteLeafRefusal: (depth: number, maxDepth: number) => string;
|
|
24
|
+
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.";
|
|
25
|
+
/** The refusal of a lesson longer than `LESSON_MAX` as one line: nothing is written, and the earlier lesson stays. */
|
|
26
|
+
export declare const noteLongRefusal: (length: number) => string;
|
|
27
|
+
export declare const noteUnrecordedRefusal: (top: string) => string;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/** The memory's model-facing texts: what courier_recall and courier_note say. */
|
|
2
|
+
import { clipText } from "../kernel/protocol.js";
|
|
3
|
+
import { LESSON_MAX, oneLine, PART_STATUSES } from "./domain.js";
|
|
4
|
+
/** The tool's name, which the skill and the role part name too. */
|
|
5
|
+
export const RECALL_TOOL = "courier_recall";
|
|
6
|
+
/** The lesson tool's name, which the skill's last step names. */
|
|
7
|
+
export const NOTE_TOOL = "courier_note";
|
|
8
|
+
const count = (n, noun) => `${n} ${noun}${n === 1 ? "" : "s"}`;
|
|
9
|
+
/** A duration in milliseconds as a model reads it: seconds under half a minute, else whole minutes, else hours and minutes. */
|
|
10
|
+
export function duration(ms) {
|
|
11
|
+
const minutes = Math.round(ms / 60_000);
|
|
12
|
+
if (minutes < 1)
|
|
13
|
+
return `${Math.max(0, Math.round(ms / 1000))} s`;
|
|
14
|
+
if (minutes < 60)
|
|
15
|
+
return `${minutes} min`;
|
|
16
|
+
return `${Math.floor(minutes / 60)} h ${minutes % 60} min`;
|
|
17
|
+
}
|
|
18
|
+
/** How a listing counts the parts of a record: by status, in the order of `PART_STATUSES`, plus how many were isolated and how many spawns were refused. */
|
|
19
|
+
function partsLine(parts) {
|
|
20
|
+
if (!parts.length)
|
|
21
|
+
return "no parts";
|
|
22
|
+
const statuses = PART_STATUSES.flatMap((status) => {
|
|
23
|
+
const n = parts.filter((part) => part.status === status).length;
|
|
24
|
+
return n ? [`${n} ${status}`] : [];
|
|
25
|
+
}).join(", ");
|
|
26
|
+
const isolated = parts.filter((part) => part.isolated).length;
|
|
27
|
+
const refused = parts.reduce((n, part) => n + (part.refused ?? 0), 0);
|
|
28
|
+
const deepest = Math.max(...parts.map((part) => part.depth));
|
|
29
|
+
return [
|
|
30
|
+
`${count(parts.length, "part")}: ${statuses}`,
|
|
31
|
+
...(deepest > 1 ? [`${deepest} levels deep`] : []),
|
|
32
|
+
...(isolated ? [`${isolated} isolated`] : []),
|
|
33
|
+
...(refused ? [`${count(refused, "spawn")} refused`] : []),
|
|
34
|
+
].join(", ");
|
|
35
|
+
}
|
|
36
|
+
const day = (at) => new Date(at).toISOString().slice(0, 10);
|
|
37
|
+
/** A lesson as the listing and the note's result show it: dated, and from which depth when a session below the top wrote it. */
|
|
38
|
+
export function lessonLine(lesson) {
|
|
39
|
+
const from = lesson.depth === undefined ? "" : `, from depth ${lesson.depth}`;
|
|
40
|
+
return `lesson (${day(lesson.at)}${from}): "${clipText(oneLine(lesson.text), LESSON_MAX)}"`;
|
|
41
|
+
}
|
|
42
|
+
/** One record as the listing shows it: its top, when it started, its title, its split line, its parts, how long it ran and what it cost. */
|
|
43
|
+
export function recordLine(record, top) {
|
|
44
|
+
const cost = record.parts.reduce((sum, part) => sum + (part.cost ?? 0), 0);
|
|
45
|
+
const costed = record.parts.some((part) => part.cost !== undefined);
|
|
46
|
+
return [
|
|
47
|
+
`- ${record.sessionID}${record.sessionID === top ? " (this tree)" : ""} ${day(record.startedAt)}: "${clipText(oneLine(record.title), 60)}"`,
|
|
48
|
+
clipText(oneLine(record.split), 160),
|
|
49
|
+
partsLine(record.parts),
|
|
50
|
+
duration(record.movedAt - record.startedAt),
|
|
51
|
+
...(costed ? [`$${cost.toFixed(2)}`] : []),
|
|
52
|
+
record.finished ? "finished" : "not finished",
|
|
53
|
+
...(record.recalled ? [`recalled${record.recalled.at === undefined ? " by a part" : ""}`] : []),
|
|
54
|
+
...(record.lesson ? [lessonLine(record.lesson)] : []),
|
|
55
|
+
].join(" · ");
|
|
56
|
+
}
|
|
57
|
+
/** What courier_recall says when the memory option is off. */
|
|
58
|
+
export const RECALL_OFF = "Memory is off: the plugin's memory option is false, so no record of a session tree is kept or listed in this project. Decide from the task alone.";
|
|
59
|
+
/** The listing: every record of the project, newest first, the caller's own tree marked. */
|
|
60
|
+
export function recallListText(records, top) {
|
|
61
|
+
const others = records.filter((record) => record.sessionID !== top);
|
|
62
|
+
const intro = others.length
|
|
63
|
+
? `${count(others.length, "earlier tree")} recorded in this project, newest first; weigh how they were split and how their parts went before you decide. ` +
|
|
64
|
+
"A record holds what the plugin saw, not the work, and a lesson what its orchestrator concluded, next to the facts that bear it out: " +
|
|
65
|
+
"courier_recall with a sessionID gives one in full, with each part's status, duration, cost and context use."
|
|
66
|
+
: "No earlier tree is recorded in this project: a tree's record is written by a courier_recall call and as each turn in it ends, and kept 90 days. Decide from the task alone.";
|
|
67
|
+
return [intro, ...records.map((record) => recordLine(record, top))].join("\n");
|
|
68
|
+
}
|
|
69
|
+
/** One record in full, as JSON: durations in milliseconds, times in epoch milliseconds, context use as a share of the model's window. */
|
|
70
|
+
export function recallRecordText(record) {
|
|
71
|
+
return `Record of the tree under ${record.sessionID}, in project ${record.projectID} (durations in milliseconds, context use as a share of the model's window):\n${JSON.stringify(record, null, 2)}`;
|
|
72
|
+
}
|
|
73
|
+
export const recallMissingText = (sessionID) => `No record of a tree whose top session is ${sessionID} in this project. courier_recall without sessionID lists the records there are.`;
|
|
74
|
+
/** What a lesson is for, which the tool's description and the note's result both say. */
|
|
75
|
+
const NOT_A_RULE = "A lesson that holds every time belongs in AGENTS.md or the skill, which only the person puts there: tell them instead of noting it.";
|
|
76
|
+
/** What courier_note says once the lesson is written: where, and what it replaced. */
|
|
77
|
+
export function noteText(top, lesson, replaced) {
|
|
78
|
+
return [
|
|
79
|
+
`Noted on the record of the tree under ${top} as its ${lessonLine(lesson)}.`,
|
|
80
|
+
...(replaced ? [`It replaces the ${lessonLine(replaced)}.`] : []),
|
|
81
|
+
`${RECALL_TOOL} lists it after the tree's facts; a second ${NOTE_TOOL} replaces it. ${NOT_A_RULE}`,
|
|
82
|
+
].join(" ");
|
|
83
|
+
}
|
|
84
|
+
/** What courier_note says when the memory option is off. */
|
|
85
|
+
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.";
|
|
86
|
+
export const noteLeafRefusal = (depth, maxDepth) => `Not noted: this session is a leaf, at depth ${depth} of a session tree that goes at most ${maxDepth} deep (maxDepth). A lesson is ` +
|
|
87
|
+
"written by the session that verified the tree, the top or one that started sessions; put what you learned in your report instead.";
|
|
88
|
+
/** What a lesson holds, which the empty and the long refusal both ask for. */
|
|
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
|
+
export const NOTE_EMPTY = `Not noted: text is empty. Write one line, ${ONE_LINE}, or nothing.`;
|
|
91
|
+
/** The refusal of a lesson longer than `LESSON_MAX` as one line: nothing is written, and the earlier lesson stays. */
|
|
92
|
+
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, ` +
|
|
93
|
+
`if there was one, stays. Keep one line, ${ONE_LINE}, and leave out what the record's facts already say.`;
|
|
94
|
+
export const noteUnrecordedRefusal = (top) => `Not noted: the tree under ${top} has no record in this project to write a lesson on. A record is written by a ${RECALL_TOOL} call ` +
|
|
95
|
+
"and as each turn in the tree ends, so call it once first, or note once a session you started has reported.";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { type ToolEditor } from "../kernel/tool.js";
|
|
3
|
+
import type { MemoryPorts, MemoryState } from "./ports.js";
|
|
4
|
+
export declare const RecallInput: Schema.Struct<{
|
|
5
|
+
readonly sessionID: Schema.optional<Schema.String>;
|
|
6
|
+
}>;
|
|
7
|
+
export declare const NoteInput: Schema.Struct<{
|
|
8
|
+
readonly text: Schema.String;
|
|
9
|
+
}>;
|
|
10
|
+
/** What the memory's tools run on: one instance's ports and the process's shared state. */
|
|
11
|
+
export interface MemoryToolPorts {
|
|
12
|
+
readonly memory: MemoryPorts;
|
|
13
|
+
readonly state: Pick<MemoryState, "tops">;
|
|
14
|
+
}
|
|
15
|
+
/** Adds `courier_recall` and `courier_note`, direct tools rather than code-mode ones. */
|
|
16
|
+
export declare function addMemoryTools(tools: ToolEditor, ports: MemoryToolPorts): void;
|