@loopbacktools/protocol 0.0.0-stage → 0.4.1
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/LICENSE +21 -0
- package/README.md +14 -2
- package/dist/openapi/openapi.1.json +1903 -0
- package/dist/src/briefing.d.ts +80 -0
- package/dist/src/briefing.js +219 -0
- package/dist/src/generated/document.gen.d.ts +3256 -0
- package/dist/src/generated/document.gen.js +25 -0
- package/dist/src/generated/requests.gen.d.ts +480 -0
- package/dist/src/generated/requests.gen.js +1 -0
- package/dist/src/generated/schemas.gen.d.ts +5 -0
- package/dist/src/generated/schemas.gen.js +6 -0
- package/dist/src/generated/schemas.r1.gen.d.ts +5433 -0
- package/dist/src/generated/schemas.r1.gen.js +58 -0
- package/dist/src/generated/vocabularies.gen.d.ts +11 -0
- package/dist/src/generated/vocabularies.gen.js +13 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +4 -0
- package/dist/src/schema.d.ts +9 -0
- package/dist/src/schema.js +8 -0
- package/dist/src/unknown.d.ts +94 -0
- package/dist/src/unknown.js +59 -0
- package/dist/src/versions.d.ts +11 -0
- package/dist/src/versions.js +11 -0
- package/openapi/openapi.1.json +1903 -0
- package/package.json +61 -3
- package/spec/main.tsp +660 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { AgentEntry, BriefingInput, BriefingPrompt, PointAnswer } from "./generated/requests.gen.ts";
|
|
2
|
+
import type { Actor as ReadActor, Block as ReadBlock, BriefingPrompt as ReadBriefingPrompt, Entry as ReadEntry, Responded } from "./generated/schemas.gen.ts";
|
|
3
|
+
/** The most top-level points a briefing may hold. */
|
|
4
|
+
export declare const MAX_TOP_POINTS = 50;
|
|
5
|
+
/** How deep sub-points may go: `3.1.2` is as deep as it gets. */
|
|
6
|
+
export declare const MAX_POINT_DEPTH = 3;
|
|
7
|
+
/**
|
|
8
|
+
* Where a point stands. It needs the person while they have not touched it or the agent answered it
|
|
9
|
+
* last; it is with the agent while the agent owes an answer on it; it is open when touched with
|
|
10
|
+
* nothing pending, as a resolved point is once something beneath it is written on again.
|
|
11
|
+
*/
|
|
12
|
+
export type PointState = "needs_you" | "with_agent" | "open" | "resolved";
|
|
13
|
+
/** One turn on one point: the person's words or resolve, or the agent's answer. */
|
|
14
|
+
export type PointTurn = {
|
|
15
|
+
readonly author: "human";
|
|
16
|
+
readonly text?: string;
|
|
17
|
+
readonly resolved: boolean;
|
|
18
|
+
readonly by?: ReadActor;
|
|
19
|
+
readonly at: string;
|
|
20
|
+
} | {
|
|
21
|
+
readonly author: "agent";
|
|
22
|
+
readonly answer: string;
|
|
23
|
+
readonly content?: readonly ReadBlock[];
|
|
24
|
+
readonly at: string;
|
|
25
|
+
};
|
|
26
|
+
/** A point as it stands, with its own turns and its sub-points. */
|
|
27
|
+
export interface PointNode {
|
|
28
|
+
readonly id: string;
|
|
29
|
+
readonly title: string;
|
|
30
|
+
readonly detail?: string;
|
|
31
|
+
readonly content?: readonly ReadBlock[];
|
|
32
|
+
/** 1 for a top-level point, 2 beneath it, and so on. */
|
|
33
|
+
readonly depth: number;
|
|
34
|
+
readonly state: PointState;
|
|
35
|
+
readonly turns: readonly PointTurn[];
|
|
36
|
+
readonly points: readonly PointNode[];
|
|
37
|
+
/** How many points, this one and those beneath it, need the person. */
|
|
38
|
+
readonly needsYou: number;
|
|
39
|
+
/**
|
|
40
|
+
* The thread entry, counting from 1, that last resolved this point, itself or from above; 0 if
|
|
41
|
+
* none ever did. Anything the person drafted before it was written before the point was settled.
|
|
42
|
+
*/
|
|
43
|
+
readonly resolvedAtEntry: number;
|
|
44
|
+
}
|
|
45
|
+
/** A briefing as it stands. */
|
|
46
|
+
export interface BriefingTree {
|
|
47
|
+
readonly sections: readonly {
|
|
48
|
+
readonly title?: string;
|
|
49
|
+
readonly points: readonly PointNode[];
|
|
50
|
+
}[];
|
|
51
|
+
/** The points the agent owes an answer on, in the order they are read. */
|
|
52
|
+
readonly owed: readonly string[];
|
|
53
|
+
readonly needsYou: number;
|
|
54
|
+
/** Whether every point, at every depth, is resolved: then the briefing is over. */
|
|
55
|
+
readonly resolved: boolean;
|
|
56
|
+
}
|
|
57
|
+
/** Whether a reply says anything: text that is more than whitespace. */
|
|
58
|
+
export declare const hasWords: (text: string | undefined) => boolean;
|
|
59
|
+
/** A briefing as it opens: its top-level points numbered straight through every section. */
|
|
60
|
+
export declare const numberBriefing: (input: Pick<BriefingInput, "title" | "summary" | "sections">) => BriefingPrompt;
|
|
61
|
+
/** The briefing as its thread leaves it. Entries naming a point it does not have change nothing. */
|
|
62
|
+
export declare const foldBriefing: (prompt: ReadBriefingPrompt, thread: readonly ReadEntry[]) => BriefingTree;
|
|
63
|
+
/** A point of the briefing by its id, at any depth. */
|
|
64
|
+
export declare const findPoint: (tree: BriefingTree, id: string) => PointNode | undefined;
|
|
65
|
+
/**
|
|
66
|
+
* Why the person's reply does not fit the briefing, or null when it does. Each item names a point
|
|
67
|
+
* the briefing has, once, with words or resolved; and the reply either resolves every point or
|
|
68
|
+
* leaves the agent something to answer.
|
|
69
|
+
*/
|
|
70
|
+
export declare const respondedProblem: (prompt: ReadBriefingPrompt, thread: readonly ReadEntry[], reply: Responded) => string | null;
|
|
71
|
+
/**
|
|
72
|
+
* Why the agent's move does not fit the briefing, or null when it does. Each answer names an open
|
|
73
|
+
* point, once; sub-points go no deeper than `MAX_POINT_DEPTH`; and every owed point is answered.
|
|
74
|
+
*/
|
|
75
|
+
export declare const answersProblem: (tree: BriefingTree, answers: readonly PointAnswer[]) => string | null;
|
|
76
|
+
/**
|
|
77
|
+
* The thread entries an agent's move becomes: one per answer, with any sub-points numbered on from
|
|
78
|
+
* those the point already has. The move must already fit (see `answersProblem`).
|
|
79
|
+
*/
|
|
80
|
+
export declare const answerEntries: (tree: BriefingTree, answers: readonly PointAnswer[], at: string) => AgentEntry[];
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// A briefing's points, and where each stands, are never stored: they are folded from its prompt and
|
|
2
|
+
// its thread. The server validates with this fold and every frontend renders with it, so the two
|
|
3
|
+
// cannot disagree about what a reply means. Folding takes messages as a reader decodes them, so a
|
|
4
|
+
// reply or a block from a newer revision (an `unknown` one) is folded past rather than refused; what
|
|
5
|
+
// is built here for the server to write (a numbered prompt, the agent's entries) is the exact shape.
|
|
6
|
+
/** The most top-level points a briefing may hold. */
|
|
7
|
+
export const MAX_TOP_POINTS = 50;
|
|
8
|
+
/** How deep sub-points may go: `3.1.2` is as deep as it gets. */
|
|
9
|
+
export const MAX_POINT_DEPTH = 3;
|
|
10
|
+
/** Whether a reply says anything: text that is more than whitespace. */
|
|
11
|
+
export const hasWords = (text) => text !== undefined && text.trim() !== "";
|
|
12
|
+
const pointOf = (id, point) => ({
|
|
13
|
+
id,
|
|
14
|
+
title: point.title,
|
|
15
|
+
...(point.detail === undefined ? {} : { detail: point.detail }),
|
|
16
|
+
...(point.content === undefined ? {} : { content: point.content }),
|
|
17
|
+
});
|
|
18
|
+
/** A briefing as it opens: its top-level points numbered straight through every section. */
|
|
19
|
+
export const numberBriefing = (input) => {
|
|
20
|
+
let next = 0;
|
|
21
|
+
return {
|
|
22
|
+
title: input.title,
|
|
23
|
+
...(input.summary === undefined ? {} : { summary: input.summary }),
|
|
24
|
+
sections: input.sections.map((section) => ({
|
|
25
|
+
...(section.title === undefined ? {} : { title: section.title }),
|
|
26
|
+
points: section.points.map((point) => pointOf(String((next += 1)), point)),
|
|
27
|
+
})),
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
const resolveAll = (node, entry) => {
|
|
31
|
+
node.resolved = true;
|
|
32
|
+
node.resolvedAtEntry = entry;
|
|
33
|
+
for (const child of node.children)
|
|
34
|
+
resolveAll(child, entry);
|
|
35
|
+
};
|
|
36
|
+
/** Whether a point is, or sits beneath, one of these. */
|
|
37
|
+
const within = (node, among) => {
|
|
38
|
+
for (let at = node; at !== undefined; at = at.parent) {
|
|
39
|
+
if (among.has(at))
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
};
|
|
44
|
+
const reopen = (node) => {
|
|
45
|
+
for (let at = node; at !== undefined; at = at.parent)
|
|
46
|
+
at.resolved = false;
|
|
47
|
+
};
|
|
48
|
+
const stateOf = (node) => {
|
|
49
|
+
if (node.resolved)
|
|
50
|
+
return "resolved";
|
|
51
|
+
const last = node.turns.at(-1);
|
|
52
|
+
if (last === undefined || last.author === "agent")
|
|
53
|
+
return "needs_you";
|
|
54
|
+
// Words sent with a resolve are a closing note: even once the point is reopened from below,
|
|
55
|
+
// nothing is owed on them.
|
|
56
|
+
return hasWords(last.text) && !last.resolved ? "with_agent" : "open";
|
|
57
|
+
};
|
|
58
|
+
const finish = (node) => {
|
|
59
|
+
const points = node.children.map(finish);
|
|
60
|
+
const state = stateOf(node);
|
|
61
|
+
return {
|
|
62
|
+
...node.point,
|
|
63
|
+
depth: node.depth,
|
|
64
|
+
state,
|
|
65
|
+
turns: node.turns,
|
|
66
|
+
points,
|
|
67
|
+
needsYou: (state === "needs_you" ? 1 : 0) + points.reduce((sum, p) => sum + p.needsYou, 0),
|
|
68
|
+
resolvedAtEntry: node.resolvedAtEntry,
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
/** Every point, in the order it is read: each followed by everything beneath it. */
|
|
72
|
+
const inOrder = (points) => points.flatMap((point) => [point, ...inOrder(point.points)]);
|
|
73
|
+
/** The briefing as its thread leaves it. Entries naming a point it does not have change nothing. */
|
|
74
|
+
export const foldBriefing = (prompt, thread) => {
|
|
75
|
+
const byId = new Map();
|
|
76
|
+
const add = (point, parent) => {
|
|
77
|
+
const node = {
|
|
78
|
+
point,
|
|
79
|
+
depth: parent === undefined ? 1 : parent.depth + 1,
|
|
80
|
+
parent,
|
|
81
|
+
turns: [],
|
|
82
|
+
children: [],
|
|
83
|
+
resolved: false,
|
|
84
|
+
resolvedAtEntry: 0,
|
|
85
|
+
};
|
|
86
|
+
byId.set(point.id, node);
|
|
87
|
+
parent?.children.push(node);
|
|
88
|
+
return node;
|
|
89
|
+
};
|
|
90
|
+
const sections = prompt.sections.map((section) => ({
|
|
91
|
+
title: section.title,
|
|
92
|
+
points: section.points.map((point) => add(point, undefined)),
|
|
93
|
+
}));
|
|
94
|
+
for (const [index, entry] of thread.entries()) {
|
|
95
|
+
if (entry.author === "human") {
|
|
96
|
+
if (entry.reply.outcome !== "responded")
|
|
97
|
+
continue;
|
|
98
|
+
// Resolving wins, whatever order a reply lists its points in: words on or beneath a point
|
|
99
|
+
// resolved in the same reply are closing notes, and reopen nothing.
|
|
100
|
+
const resolving = new Set(entry.reply.points.flatMap((item) => {
|
|
101
|
+
const node = item.resolved === true ? byId.get(item.point) : undefined;
|
|
102
|
+
return node === undefined ? [] : [node];
|
|
103
|
+
}));
|
|
104
|
+
for (const item of entry.reply.points) {
|
|
105
|
+
const node = byId.get(item.point);
|
|
106
|
+
if (node === undefined)
|
|
107
|
+
continue;
|
|
108
|
+
const settled = within(node, resolving);
|
|
109
|
+
node.turns.push({
|
|
110
|
+
author: "human",
|
|
111
|
+
...(item.text === undefined ? {} : { text: item.text }),
|
|
112
|
+
resolved: settled,
|
|
113
|
+
...(entry.by === undefined ? {} : { by: entry.by }),
|
|
114
|
+
at: entry.at,
|
|
115
|
+
});
|
|
116
|
+
if (!settled && hasWords(item.text))
|
|
117
|
+
reopen(node);
|
|
118
|
+
}
|
|
119
|
+
for (const node of resolving)
|
|
120
|
+
resolveAll(node, index + 1);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
const node = entry.point === undefined ? undefined : byId.get(entry.point);
|
|
124
|
+
if (node === undefined)
|
|
125
|
+
continue;
|
|
126
|
+
node.turns.push({
|
|
127
|
+
author: "agent",
|
|
128
|
+
answer: entry.answer,
|
|
129
|
+
...(entry.content === undefined ? {} : { content: entry.content }),
|
|
130
|
+
at: entry.at,
|
|
131
|
+
});
|
|
132
|
+
for (const point of entry.points ?? [])
|
|
133
|
+
add(point, node);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const finished = sections.map((section) => ({
|
|
137
|
+
...(section.title === undefined ? {} : { title: section.title }),
|
|
138
|
+
points: section.points.map(finish),
|
|
139
|
+
}));
|
|
140
|
+
const all = inOrder(finished.flatMap((section) => section.points));
|
|
141
|
+
return {
|
|
142
|
+
sections: finished,
|
|
143
|
+
owed: all.filter((point) => point.state === "with_agent").map((point) => point.id),
|
|
144
|
+
needsYou: all.reduce((sum, point) => sum + (point.state === "needs_you" ? 1 : 0), 0),
|
|
145
|
+
resolved: all.every((point) => point.state === "resolved"),
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
/** A point of the briefing by its id, at any depth. */
|
|
149
|
+
export const findPoint = (tree, id) => inOrder(tree.sections.flatMap((section) => section.points)).find((point) => point.id === id);
|
|
150
|
+
/** "2", "2 and 3.1", "2, 3 and 3.1". */
|
|
151
|
+
const listed = (ids) => ids.length <= 1 ? (ids[0] ?? "") : `${ids.slice(0, -1).join(", ")} and ${ids.at(-1)}`;
|
|
152
|
+
/**
|
|
153
|
+
* Why the person's reply does not fit the briefing, or null when it does. Each item names a point
|
|
154
|
+
* the briefing has, once, with words or resolved; and the reply either resolves every point or
|
|
155
|
+
* leaves the agent something to answer.
|
|
156
|
+
*/
|
|
157
|
+
export const respondedProblem = (prompt, thread, reply) => {
|
|
158
|
+
const tree = foldBriefing(prompt, thread);
|
|
159
|
+
const seen = new Set();
|
|
160
|
+
for (const item of reply.points) {
|
|
161
|
+
if (findPoint(tree, item.point) === undefined)
|
|
162
|
+
return `No point "${item.point}".`;
|
|
163
|
+
if (seen.has(item.point))
|
|
164
|
+
return `Point ${item.point} appears twice.`;
|
|
165
|
+
seen.add(item.point);
|
|
166
|
+
if (!hasWords(item.text) && item.resolved !== true) {
|
|
167
|
+
return `Say something on point ${item.point}, or mark it resolved.`;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const after = foldBriefing(prompt, [...thread, { author: "human", reply, at: "" }]);
|
|
171
|
+
if (after.resolved || after.owed.length > 0)
|
|
172
|
+
return null;
|
|
173
|
+
const open = inOrder(after.sections.flatMap((section) => section.points))
|
|
174
|
+
.filter((point) => point.state !== "resolved")
|
|
175
|
+
.map((point) => point.id);
|
|
176
|
+
const shown = open.length > 6 ? [...open.slice(0, 5), `${open.length - 5} more`] : open;
|
|
177
|
+
return `Nothing here for the agent to answer: write on a point, or resolve every point to finish (${listed(shown)} ${open.length === 1 ? "is" : "are"} still open).`;
|
|
178
|
+
};
|
|
179
|
+
/**
|
|
180
|
+
* Why the agent's move does not fit the briefing, or null when it does. Each answer names an open
|
|
181
|
+
* point, once; sub-points go no deeper than `MAX_POINT_DEPTH`; and every owed point is answered.
|
|
182
|
+
*/
|
|
183
|
+
export const answersProblem = (tree, answers) => {
|
|
184
|
+
const seen = new Set();
|
|
185
|
+
for (const answer of answers) {
|
|
186
|
+
const point = findPoint(tree, answer.point);
|
|
187
|
+
if (point === undefined)
|
|
188
|
+
return `No point "${answer.point}".`;
|
|
189
|
+
if (seen.has(answer.point))
|
|
190
|
+
return `Point ${answer.point} appears twice.`;
|
|
191
|
+
seen.add(answer.point);
|
|
192
|
+
if (point.state === "resolved") {
|
|
193
|
+
return `Point ${answer.point} is resolved: the person is done with it.`;
|
|
194
|
+
}
|
|
195
|
+
if ((answer.points?.length ?? 0) > 0 && point.depth >= MAX_POINT_DEPTH) {
|
|
196
|
+
return `Point ${answer.point} is as deep as points go: answer it without sub-points.`;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const missing = tree.owed.filter((id) => !seen.has(id));
|
|
200
|
+
return missing.length === 0
|
|
201
|
+
? null
|
|
202
|
+
: `Answer every point the person wrote on: ${listed(missing)} ${missing.length === 1 ? "is" : "are"} missing.`;
|
|
203
|
+
};
|
|
204
|
+
/**
|
|
205
|
+
* The thread entries an agent's move becomes: one per answer, with any sub-points numbered on from
|
|
206
|
+
* those the point already has. The move must already fit (see `answersProblem`).
|
|
207
|
+
*/
|
|
208
|
+
export const answerEntries = (tree, answers, at) => answers.map((answer) => {
|
|
209
|
+
const had = findPoint(tree, answer.point)?.points.length ?? 0;
|
|
210
|
+
const points = (answer.points ?? []).map((point, index) => pointOf(`${answer.point}.${had + index + 1}`, point));
|
|
211
|
+
return {
|
|
212
|
+
author: "agent",
|
|
213
|
+
answer: answer.answer,
|
|
214
|
+
...(answer.content === undefined ? {} : { content: answer.content }),
|
|
215
|
+
point: answer.point,
|
|
216
|
+
...(points.length === 0 ? {} : { points }),
|
|
217
|
+
at,
|
|
218
|
+
};
|
|
219
|
+
});
|