@khorsheed/dsh-library 0.0.0-stage → 0.1.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/CHANGELOG.md +19 -0
- package/README.en.md +49 -0
- package/README.i18n.yaml +6 -0
- package/README.md +48 -2
- package/cordis.patch.yml +17 -0
- package/lib/client.js +66334 -0
- package/lib/index.js +3318 -0
- package/lib/profiles-CFrPQdfO.js +1692 -0
- package/lib/tool.js +1597 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +3811 -0
- package/lib/typert.remote-client.d.ts +137 -0
- package/lib/typert.remote-client.js +3077 -0
- package/lib/types/cards.d.ts +41 -0
- package/lib/types/cards.js +111 -0
- package/lib/types/client/LibraryPane.d.ts +26 -0
- package/lib/types/client/LibraryPane.js +129 -0
- package/lib/types/client/bind/BindChip.d.ts +33 -0
- package/lib/types/client/bind/BindChip.js +102 -0
- package/lib/types/client/contract.d.ts +167 -0
- package/lib/types/client/contract.js +1 -0
- package/lib/types/client/covers.d.ts +36 -0
- package/lib/types/client/covers.js +66 -0
- package/lib/types/client/definition.d.ts +27 -0
- package/lib/types/client/definition.js +32 -0
- package/lib/types/client/icons-local.d.ts +41 -0
- package/lib/types/client/icons-local.js +25 -0
- package/lib/types/client/icons.d.ts +27 -0
- package/lib/types/client/icons.js +11 -0
- package/lib/types/client/index.d.ts +35 -0
- package/lib/types/client/index.js +231 -0
- package/lib/types/client/judge.d.ts +70 -0
- package/lib/types/client/judge.js +262 -0
- package/lib/types/client/locales.d.ts +553 -0
- package/lib/types/client/locales.js +1076 -0
- package/lib/types/client/look.d.ts +63 -0
- package/lib/types/client/look.js +64 -0
- package/lib/types/client/math.d.ts +24 -0
- package/lib/types/client/math.js +47 -0
- package/lib/types/client/mistakes.d.ts +21 -0
- package/lib/types/client/mistakes.js +28 -0
- package/lib/types/client/preset-visibility.d.ts +109 -0
- package/lib/types/client/preset-visibility.js +123 -0
- package/lib/types/client/quiz-drafts.d.ts +29 -0
- package/lib/types/client/quiz-drafts.js +50 -0
- package/lib/types/client/reveal.d.ts +21 -0
- package/lib/types/client/reveal.js +31 -0
- package/lib/types/client/store.d.ts +111 -0
- package/lib/types/client/store.js +48 -0
- package/lib/types/client/titles.d.ts +17 -0
- package/lib/types/client/titles.js +23 -0
- package/lib/types/client/ui.d.ts +96 -0
- package/lib/types/client/ui.js +124 -0
- package/lib/types/client/views/book.d.ts +57 -0
- package/lib/types/client/views/book.js +164 -0
- package/lib/types/client/views/cards.d.ts +17 -0
- package/lib/types/client/views/cards.js +53 -0
- package/lib/types/client/views/home.d.ts +26 -0
- package/lib/types/client/views/home.js +263 -0
- package/lib/types/client/views/lesson.d.ts +42 -0
- package/lib/types/client/views/lesson.js +146 -0
- package/lib/types/client/views/path.d.ts +32 -0
- package/lib/types/client/views/path.js +64 -0
- package/lib/types/client/views/quiz.d.ts +42 -0
- package/lib/types/client/views/quiz.js +366 -0
- package/lib/types/client/views/station-quiz.d.ts +43 -0
- package/lib/types/client/views/station-quiz.js +203 -0
- package/lib/types/client/views/station.d.ts +18 -0
- package/lib/types/client/views/station.js +142 -0
- package/lib/types/image-refs.d.ts +35 -0
- package/lib/types/image-refs.js +71 -0
- package/lib/types/index.d.ts +51 -0
- package/lib/types/index.js +31 -0
- package/lib/types/mastery.d.ts +81 -0
- package/lib/types/mastery.js +110 -0
- package/lib/types/profiles.d.ts +31 -0
- package/lib/types/profiles.js +25 -0
- package/lib/types/remote.d.ts +355 -0
- package/lib/types/remote.js +332 -0
- package/lib/types/service/assess.d.ts +144 -0
- package/lib/types/service/assess.js +773 -0
- package/lib/types/service/bindings.d.ts +92 -0
- package/lib/types/service/bindings.js +198 -0
- package/lib/types/service/cards.d.ts +85 -0
- package/lib/types/service/cards.js +320 -0
- package/lib/types/service/core.d.ts +124 -0
- package/lib/types/service/core.js +169 -0
- package/lib/types/service/pages.d.ts +60 -0
- package/lib/types/service/pages.js +125 -0
- package/lib/types/service/roadmap.d.ts +99 -0
- package/lib/types/service/roadmap.js +284 -0
- package/lib/types/service/rules.d.ts +63 -0
- package/lib/types/service/rules.js +245 -0
- package/lib/types/service/shelf.d.ts +136 -0
- package/lib/types/service/shelf.js +422 -0
- package/lib/types/service.d.ts +42 -0
- package/lib/types/service.js +41 -0
- package/lib/types/store.d.ts +257 -0
- package/lib/types/store.js +1376 -0
- package/lib/types/tool-definition.d.ts +49 -0
- package/lib/types/tool-definition.js +49 -0
- package/lib/types/tool.d.ts +71 -0
- package/lib/types/tool.js +174 -0
- package/lib/types/tools/assess.d.ts +11 -0
- package/lib/types/tools/assess.js +82 -0
- package/lib/types/tools/binding.d.ts +11 -0
- package/lib/types/tools/binding.js +46 -0
- package/lib/types/tools/cards.d.ts +11 -0
- package/lib/types/tools/cards.js +100 -0
- package/lib/types/tools/kit.d.ts +215 -0
- package/lib/types/tools/kit.js +281 -0
- package/lib/types/tools/notes.d.ts +11 -0
- package/lib/types/tools/notes.js +40 -0
- package/lib/types/tools/page.d.ts +11 -0
- package/lib/types/tools/page.js +59 -0
- package/lib/types/tools/project.d.ts +11 -0
- package/lib/types/tools/project.js +67 -0
- package/lib/types/tools/quiz.d.ts +11 -0
- package/lib/types/tools/quiz.js +74 -0
- package/lib/types/tools/read.d.ts +11 -0
- package/lib/types/tools/read.js +127 -0
- package/lib/types/tools/resource.d.ts +11 -0
- package/lib/types/tools/resource.js +147 -0
- package/lib/types/tools/roadmap.d.ts +11 -0
- package/lib/types/tools/roadmap.js +102 -0
- package/lib/types/types.d.ts +752 -0
- package/lib/types/types.js +159 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +135 -3
package/lib/tool.js
ADDED
|
@@ -0,0 +1,1597 @@
|
|
|
1
|
+
import { U as isThickResource, V as formatLocator, j as LIBRARY_RESOURCE_KINDS, r as masteryReason, t as profileOf, x as normalizeQuizOptions } from "./profiles-CFrPQdfO.js";
|
|
2
|
+
import z from "@deepseek-ai/schemastery";
|
|
3
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
4
|
+
//#region lib/types/image-refs.js
|
|
5
|
+
/** How many sessions are remembered. */
|
|
6
|
+
const SESSIONS = 200;
|
|
7
|
+
/** The id without its digest prefix, for matching what the model wrote. */
|
|
8
|
+
function bareId(id) {
|
|
9
|
+
return id.replace(/^sha256:/, "");
|
|
10
|
+
}
|
|
11
|
+
/** A whole reference, when a value carries every field. */
|
|
12
|
+
function asAttachmentRef(value) {
|
|
13
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
14
|
+
const ref = value;
|
|
15
|
+
if (typeof ref.attachmentId !== "string" || typeof ref.mediaType !== "string") return void 0;
|
|
16
|
+
if (typeof ref.bytes !== "number" || ref.bytes <= 0) return void 0;
|
|
17
|
+
if (typeof ref.width !== "number" || typeof ref.height !== "number") return void 0;
|
|
18
|
+
return {
|
|
19
|
+
attachmentId: ref.attachmentId,
|
|
20
|
+
mediaType: ref.mediaType,
|
|
21
|
+
bytes: ref.bytes,
|
|
22
|
+
width: ref.width,
|
|
23
|
+
height: ref.height
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** The image references each session's user messages carried, newest last. */
|
|
27
|
+
var ImageRefCache = class {
|
|
28
|
+
bySession = /* @__PURE__ */ new Map();
|
|
29
|
+
/** Remember the images one committed event carries (non-image events are ignored). */
|
|
30
|
+
observe(sessionId, event) {
|
|
31
|
+
if (event.type !== "user/message") return;
|
|
32
|
+
const content = event.data?.content;
|
|
33
|
+
if (!Array.isArray(content)) return;
|
|
34
|
+
for (const block of content) {
|
|
35
|
+
if (block?.type !== "image") continue;
|
|
36
|
+
const ref = asAttachmentRef(block.attachment);
|
|
37
|
+
if (ref !== void 0) this.remember(sessionId, ref);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
remember(sessionId, ref) {
|
|
41
|
+
const kept = (this.bySession.get(sessionId) ?? []).filter((entry) => entry.attachmentId !== ref.attachmentId);
|
|
42
|
+
kept.push(ref);
|
|
43
|
+
this.bySession.delete(sessionId);
|
|
44
|
+
this.bySession.set(sessionId, kept.slice(-20));
|
|
45
|
+
while (this.bySession.size > SESSIONS) this.bySession.delete(this.bySession.keys().next().value);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The whole reference for what the model wrote: a complete reference
|
|
49
|
+
* passes through; an id (with or without its prefix) is looked up; nothing
|
|
50
|
+
* at all means the session's latest photo.
|
|
51
|
+
*/
|
|
52
|
+
resolve(sessionId, written) {
|
|
53
|
+
const whole = asAttachmentRef(written);
|
|
54
|
+
const known = sessionId === void 0 ? [] : this.bySession.get(sessionId) ?? [];
|
|
55
|
+
const id = typeof written === "string" ? written : typeof written?.attachmentId === "string" ? written.attachmentId : void 0;
|
|
56
|
+
if (id !== void 0) {
|
|
57
|
+
const match = known.find((ref) => bareId(ref.attachmentId) === bareId(id));
|
|
58
|
+
if (match !== void 0) return match;
|
|
59
|
+
}
|
|
60
|
+
if (whole !== void 0) return whole;
|
|
61
|
+
return id === void 0 ? known.at(-1) : void 0;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region lib/types/tools/kit.js
|
|
66
|
+
/** JSON pass-through output — what the render emits IS what the model reads, so it goes compact (indentation doubled every result). */
|
|
67
|
+
function jsonOutput() {
|
|
68
|
+
return {
|
|
69
|
+
schema: { type: "json" },
|
|
70
|
+
render: (_args, value) => [{
|
|
71
|
+
type: "text",
|
|
72
|
+
text: JSON.stringify(value)
|
|
73
|
+
}]
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/** Unwrap a service outcome: the domain value, or a thrown Error whose message IS the lesson. */
|
|
77
|
+
function unwrap(outcome) {
|
|
78
|
+
if (outcome.ok) return outcome;
|
|
79
|
+
throw new Error(outcome.message);
|
|
80
|
+
}
|
|
81
|
+
const READ_WHATS = [
|
|
82
|
+
"projects",
|
|
83
|
+
"resources",
|
|
84
|
+
"toc",
|
|
85
|
+
"roadmap",
|
|
86
|
+
"pages",
|
|
87
|
+
"cards",
|
|
88
|
+
"quiz"
|
|
89
|
+
];
|
|
90
|
+
/** The calling session's id, when the call came from a session. */
|
|
91
|
+
function sessionIdOf(exec) {
|
|
92
|
+
const id = exec?.agent?.session?.id;
|
|
93
|
+
return id === void 0 ? void 0 : String(id);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Payloads arrive as objects — or, from models that stringify deep parameters
|
|
97
|
+
* (common with big nested ones), as their JSON text; both parse. A refusal
|
|
98
|
+
* names what actually arrived, so the model fixes the transport instead of
|
|
99
|
+
* guessing at the shape.
|
|
100
|
+
*/
|
|
101
|
+
function payload(value) {
|
|
102
|
+
if (typeof value === "string") try {
|
|
103
|
+
value = JSON.parse(value);
|
|
104
|
+
} catch {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
108
|
+
}
|
|
109
|
+
function payloadList(value) {
|
|
110
|
+
if (typeof value === "string") try {
|
|
111
|
+
value = JSON.parse(value);
|
|
112
|
+
} catch {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (Array.isArray(value)) return value;
|
|
116
|
+
const items = payload(value)?.items;
|
|
117
|
+
return Array.isArray(items) ? items : void 0;
|
|
118
|
+
}
|
|
119
|
+
function arrival(field, value) {
|
|
120
|
+
if (value === void 0) return `no ${field} field arrived — set the ${field} field itself; its value may be the object or its JSON text`;
|
|
121
|
+
if (typeof value === "string") try {
|
|
122
|
+
JSON.parse(value);
|
|
123
|
+
return `${field} arrived as JSON text that is not the expected shape`;
|
|
124
|
+
} catch {
|
|
125
|
+
return `${field} arrived as text that is not valid JSON — send the object itself (or its JSON text)`;
|
|
126
|
+
}
|
|
127
|
+
return `${field} arrived as ${Array.isArray(value) ? "an array" : typeof value} with an unexpected shape`;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* A contents sent as one text: one row per line. Models also join the rows
|
|
131
|
+
* with ;/; on a single line, or send literal "\\n" escapes — a single line
|
|
132
|
+
* holding several separators splits on them (a 30-chapter book once landed
|
|
133
|
+
* as ONE row, and every station's chapters then fell out of range).
|
|
134
|
+
*/
|
|
135
|
+
function splitTocText(text) {
|
|
136
|
+
const lines = text.replace(/\\n/g, "\n").split("\n").map((line) => line.trim()).filter((line) => line !== "");
|
|
137
|
+
if (lines.length !== 1) return lines;
|
|
138
|
+
const parts = lines[0].split(/\s*[;;]\s*/).map((part) => part.trim()).filter((part) => part !== "");
|
|
139
|
+
return parts.length >= 3 ? parts : lines;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* A station's chapters as a locator: row numbers and ranges, whatever the
|
|
143
|
+
* dash or separator ("15–16", "3-4、7", "第3-4章"); words around them (an
|
|
144
|
+
* appendix, a part name) come back as `rest` for a topic side reference.
|
|
145
|
+
*/
|
|
146
|
+
function chaptersLocator(chapters) {
|
|
147
|
+
const normalized = chapters.replace(/[–—~~至到]/g, "-").replace(/[,、;;/]/g, ",");
|
|
148
|
+
const ranges = [...normalized.matchAll(/(\d+)\s*(?:-\s*(\d+))?/g)].map((match) => match[2] === void 0 ? match[1] : `${match[1]}-${match[2]}`);
|
|
149
|
+
const rest = normalized.replace(/(\d+)\s*(?:-\s*(\d+))?/g, "").replace(/[第章节,+\s-]+/g, " ").trim();
|
|
150
|
+
return {
|
|
151
|
+
locator: ranges.join(","),
|
|
152
|
+
...rest === "" ? {} : { rest }
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/** The first non-blank string of the candidates (the resource/book alias pairs). */
|
|
156
|
+
function pickString(...values) {
|
|
157
|
+
return values.find((value) => typeof value === "string" && value.trim() !== "")?.trim();
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* One cover argument → the stored `{ ref, locator?, … }`. Accepts the string
|
|
161
|
+
* shorthand ("r_…#1-5" | "https://…" | "topic: …"), the v5 object, and every
|
|
162
|
+
* legacy union member ({ kind: "book", bookId|book, ranges } |
|
|
163
|
+
* { kind: "resource", resourceId|resource } | { kind: "url", url, title? } |
|
|
164
|
+
* { kind: "topic", text }). Undefined when nothing usable arrived.
|
|
165
|
+
*/
|
|
166
|
+
function normalizeCoverArg(value) {
|
|
167
|
+
if (typeof value === "string") {
|
|
168
|
+
const shorthand = value.trim();
|
|
169
|
+
if (shorthand === "") return void 0;
|
|
170
|
+
if (shorthand.startsWith("topic:") || /^https?:\/\//.test(shorthand)) return { ref: shorthand };
|
|
171
|
+
const match = /^([A-Za-z0-9][A-Za-z0-9._-]*)\s*(?:#\s*(.+))?$/.exec(shorthand);
|
|
172
|
+
return {
|
|
173
|
+
ref: match?.[1] ?? shorthand,
|
|
174
|
+
...match?.[2] === void 0 ? {} : { locator: match[2].trim() }
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
const record = payload(value);
|
|
178
|
+
if (record === void 0) return void 0;
|
|
179
|
+
const provenance = {
|
|
180
|
+
...typeof record.by === "string" ? { by: record.by } : {},
|
|
181
|
+
...typeof record.verifiedAt === "string" ? { verifiedAt: record.verifiedAt } : {}
|
|
182
|
+
};
|
|
183
|
+
if (typeof record.ref === "string" && record.ref.trim() !== "") return {
|
|
184
|
+
ref: record.ref.trim(),
|
|
185
|
+
...typeof record.locator === "string" && record.locator.trim() !== "" ? { locator: record.locator.trim() } : {},
|
|
186
|
+
...typeof record.title === "string" && record.title.trim() !== "" ? { title: record.title.trim() } : {},
|
|
187
|
+
...provenance
|
|
188
|
+
};
|
|
189
|
+
const bookId = pickString(record.bookId, record.book);
|
|
190
|
+
if ((record.kind === "book" || record.kind === void 0) && bookId !== void 0) {
|
|
191
|
+
const ranges = Array.isArray(record.ranges) ? record.ranges.flatMap((range) => {
|
|
192
|
+
const row = payload(range);
|
|
193
|
+
const from = typeof row?.from === "number" ? row.from : void 0;
|
|
194
|
+
const to = typeof row?.to === "number" ? row.to : void 0;
|
|
195
|
+
return from === void 0 || to === void 0 ? [] : [{
|
|
196
|
+
from,
|
|
197
|
+
to
|
|
198
|
+
}];
|
|
199
|
+
}) : [];
|
|
200
|
+
return {
|
|
201
|
+
ref: bookId,
|
|
202
|
+
...ranges.length === 0 ? {} : { locator: formatLocator(ranges) },
|
|
203
|
+
...provenance
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
const resourceId = pickString(record.resourceId, record.resource);
|
|
207
|
+
if (record.kind === "resource" && resourceId !== void 0) return {
|
|
208
|
+
ref: resourceId,
|
|
209
|
+
...provenance
|
|
210
|
+
};
|
|
211
|
+
if (record.kind === "url" && typeof record.url === "string") return {
|
|
212
|
+
ref: record.url,
|
|
213
|
+
...typeof record.title === "string" ? { title: record.title } : {},
|
|
214
|
+
...provenance
|
|
215
|
+
};
|
|
216
|
+
if (record.kind === "topic" && typeof record.text === "string") return {
|
|
217
|
+
ref: `topic:${record.text}`,
|
|
218
|
+
...provenance
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* A question's two axes are optional on the wire: target defaults to the
|
|
223
|
+
* station's topic, evidence to domain knowledge. Flat shorthands fill them in —
|
|
224
|
+
* cardIds imply a marked target; pageId / url imply photo / web evidence; a
|
|
225
|
+
* bare string sets just the kind. The service's two-axis gate is untouched: it
|
|
226
|
+
* judges the normalized shape.
|
|
227
|
+
*/
|
|
228
|
+
function normalizeQuestionAxes(question) {
|
|
229
|
+
const { cardIds, quote, url, pageId, ...rest } = question;
|
|
230
|
+
const flatIds = Array.isArray(cardIds) ? cardIds.filter((id) => typeof id === "string" && id !== "") : void 0;
|
|
231
|
+
const rawTarget = typeof rest.target === "string" ? { kind: rest.target } : rest.target;
|
|
232
|
+
let target = (rawTarget !== null && typeof rawTarget === "object" && !Array.isArray(rawTarget) ? rawTarget : void 0) ?? (flatIds !== void 0 && flatIds.length > 0 ? { kind: "marked" } : { kind: "topic" });
|
|
233
|
+
if (flatIds !== void 0 && flatIds.length > 0 && target.cardIds === void 0) target = {
|
|
234
|
+
...target,
|
|
235
|
+
cardIds: flatIds
|
|
236
|
+
};
|
|
237
|
+
const rawEvidence = typeof rest.evidence === "string" ? { kind: rest.evidence } : rest.evidence;
|
|
238
|
+
let evidence = (rawEvidence !== null && typeof rawEvidence === "object" && !Array.isArray(rawEvidence) ? rawEvidence : void 0) ?? { kind: typeof pageId === "string" && pageId !== "" ? "photo" : typeof url === "string" && url !== "" ? "web" : "knowledge" };
|
|
239
|
+
for (const [key, value] of [
|
|
240
|
+
["quote", quote],
|
|
241
|
+
["url", url],
|
|
242
|
+
["pageId", pageId]
|
|
243
|
+
]) if (typeof value === "string" && value !== "" && evidence[key] === void 0) evidence = {
|
|
244
|
+
...evidence,
|
|
245
|
+
[key]: value
|
|
246
|
+
};
|
|
247
|
+
return {
|
|
248
|
+
...rest,
|
|
249
|
+
target,
|
|
250
|
+
evidence
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* The roadmap nodes schema. Only the canonical shape is advertised; execute
|
|
255
|
+
* still takes the older spellings (also, dependsOn, read stations, acceptance
|
|
256
|
+
* refs, chapterRanges, covers) — the schema items allow extra keys, and the
|
|
257
|
+
* JSON-text branch lets a stringified list through to execute's parser.
|
|
258
|
+
*/
|
|
259
|
+
const NODES_SCHEMA = { oneOf: [{
|
|
260
|
+
type: "array",
|
|
261
|
+
items: {
|
|
262
|
+
type: "object",
|
|
263
|
+
additionalProperties: true,
|
|
264
|
+
properties: {
|
|
265
|
+
id: {
|
|
266
|
+
type: "string",
|
|
267
|
+
description: "Unique: s1, s2…"
|
|
268
|
+
},
|
|
269
|
+
title: { type: "string" },
|
|
270
|
+
chapters: {
|
|
271
|
+
oneOf: [{ type: "string" }, { type: "integer" }],
|
|
272
|
+
description: "Contents rows it covers: \"4\" | \"3-4\"."
|
|
273
|
+
},
|
|
274
|
+
points: {
|
|
275
|
+
type: "array",
|
|
276
|
+
description: "Required: its 3–5 knowledge points — the chapter quiz asks them.",
|
|
277
|
+
items: {
|
|
278
|
+
type: "object",
|
|
279
|
+
additionalProperties: true,
|
|
280
|
+
properties: {
|
|
281
|
+
id: {
|
|
282
|
+
type: "string",
|
|
283
|
+
description: "A stable slug."
|
|
284
|
+
},
|
|
285
|
+
title: { type: "string" }
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
goals: {
|
|
290
|
+
type: "json",
|
|
291
|
+
description: "What the learner can do after it: [\"…\"]."
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}, {
|
|
296
|
+
type: "string",
|
|
297
|
+
description: "The same list as JSON text."
|
|
298
|
+
}] };
|
|
299
|
+
/**
|
|
300
|
+
* The quiz questions schema. ids are numbered when missing; target and
|
|
301
|
+
* evidence follow from cardIds / quote / pageId / url and the textbook (the
|
|
302
|
+
* full two-axis objects still parse).
|
|
303
|
+
*/
|
|
304
|
+
const QUESTIONS_SCHEMA = { oneOf: [{
|
|
305
|
+
type: "array",
|
|
306
|
+
items: {
|
|
307
|
+
type: "object",
|
|
308
|
+
additionalProperties: true,
|
|
309
|
+
properties: {
|
|
310
|
+
point: {
|
|
311
|
+
type: "string",
|
|
312
|
+
description: "The station point it probes."
|
|
313
|
+
},
|
|
314
|
+
kind: {
|
|
315
|
+
type: "string",
|
|
316
|
+
enum: [
|
|
317
|
+
"choice",
|
|
318
|
+
"fill",
|
|
319
|
+
"free"
|
|
320
|
+
]
|
|
321
|
+
},
|
|
322
|
+
prompt: { type: "string" },
|
|
323
|
+
options: {
|
|
324
|
+
type: "json",
|
|
325
|
+
description: "choice only: [\"…\", \"…\"]."
|
|
326
|
+
},
|
|
327
|
+
answer: {
|
|
328
|
+
type: "string",
|
|
329
|
+
description: "choice: the right option, verbatim."
|
|
330
|
+
},
|
|
331
|
+
explanation: { type: "string" },
|
|
332
|
+
quote: {
|
|
333
|
+
type: "string",
|
|
334
|
+
description: "The page text it rests on (required for a material textbook)."
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}, {
|
|
339
|
+
type: "string",
|
|
340
|
+
description: "The same list as JSON text."
|
|
341
|
+
}] };
|
|
342
|
+
/** The per-call helpers every tool closes over, built once per tool set. */
|
|
343
|
+
function createToolKit(service, images) {
|
|
344
|
+
/** The session's binding, when it has one. */
|
|
345
|
+
const bindingOf = (exec) => {
|
|
346
|
+
const sessionId = sessionIdOf(exec);
|
|
347
|
+
return sessionId === void 0 ? void 0 : service.getBinding(sessionId);
|
|
348
|
+
};
|
|
349
|
+
/** The project: the argument, else the binding, else the resource's owner, else a lesson. */
|
|
350
|
+
const projectOf = (value, exec, resource) => {
|
|
351
|
+
if (typeof value === "string" && value.trim() !== "") return value.trim();
|
|
352
|
+
const bound = bindingOf(exec)?.projectId;
|
|
353
|
+
if (bound !== void 0) return bound;
|
|
354
|
+
if (resource !== void 0) {
|
|
355
|
+
const owner = service.findResourceOwner(resource);
|
|
356
|
+
if (owner !== void 0) return owner;
|
|
357
|
+
}
|
|
358
|
+
throw new Error("no project: this session is not bound to one and no resource resolves one — library_read · projects lists them; ask the learner which one (ask_user_question), then library_binding { project, resource? }");
|
|
359
|
+
};
|
|
360
|
+
/** A successful write knows its work context: an unbound session binds to it, and a project-bound session picks up the resource it just registered — create the project, register the book, draft the path, and the chain has strung itself. Never steals another project's session, never yanks a resource off one. */
|
|
361
|
+
const autoBind = async (exec, binding) => {
|
|
362
|
+
const sessionId = sessionIdOf(exec);
|
|
363
|
+
if (sessionId === void 0) return;
|
|
364
|
+
service.noteSessionWrite(sessionId);
|
|
365
|
+
const current = bindingOf(exec);
|
|
366
|
+
if (current !== void 0 && (current.projectId !== binding.projectId || current.resourceId !== void 0)) return;
|
|
367
|
+
if (current !== void 0 && binding.resourceId === void 0) return;
|
|
368
|
+
await service.setBinding(sessionId, binding);
|
|
369
|
+
};
|
|
370
|
+
/** The textbooks of the bound project, as one teaching line for a refusal. */
|
|
371
|
+
const textbookList = (exec) => {
|
|
372
|
+
const textbooks = (sessionIdOf(exec) === void 0 ? void 0 : service.bindingSummary(sessionIdOf(exec), 0))?.textbooks ?? [];
|
|
373
|
+
return textbooks.length === 0 ? "" : ` — this project's textbooks: ${textbooks.map((textbook) => `${textbook.seq ?? "-"}. ${textbook.id} 《${textbook.title}》`).join("; ")}`;
|
|
374
|
+
};
|
|
375
|
+
/** The resource: the argument (either spelling), else the binding's resource, else a lesson. */
|
|
376
|
+
const resourceOf = (args, exec) => {
|
|
377
|
+
const named = pickString(args.resource, args.book);
|
|
378
|
+
if (named !== void 0) return named;
|
|
379
|
+
const bound = bindingOf(exec)?.resourceId;
|
|
380
|
+
if (bound !== void 0) return bound;
|
|
381
|
+
throw new Error(`no textbook named: pass resource${textbookList(exec) || " (library_read · resources lists them)"}, or bind this session to one (library_binding)`);
|
|
382
|
+
};
|
|
383
|
+
/** The shared scope parameters (write tools). */
|
|
384
|
+
const scopeParams = { project: {
|
|
385
|
+
type: "string",
|
|
386
|
+
description: "Defaults to the binding."
|
|
387
|
+
} };
|
|
388
|
+
return {
|
|
389
|
+
service,
|
|
390
|
+
images,
|
|
391
|
+
bindingOf,
|
|
392
|
+
projectOf,
|
|
393
|
+
autoBind,
|
|
394
|
+
textbookList,
|
|
395
|
+
resourceOf,
|
|
396
|
+
scopeParams,
|
|
397
|
+
textbookParams: {
|
|
398
|
+
...scopeParams,
|
|
399
|
+
resource: {
|
|
400
|
+
type: "string",
|
|
401
|
+
description: "Defaults to the binding."
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
//#endregion
|
|
407
|
+
//#region lib/types/tools/read.js
|
|
408
|
+
/**
|
|
409
|
+
* The `library_read` tool (moved verbatim from tool-definition.ts; the shared
|
|
410
|
+
* helpers and schemas are in `tools/kit.ts`).
|
|
411
|
+
*
|
|
412
|
+
* @module @khorsheed/dsh-library/tools/read
|
|
413
|
+
*/
|
|
414
|
+
/**
|
|
415
|
+
* A path as the coach needs it: each station's id, title, state, rows and
|
|
416
|
+
* points, and whether its 讲义 exist — never the 讲义 text itself (a read of
|
|
417
|
+
* an 11-station path once returned 7,300 characters).
|
|
418
|
+
*/
|
|
419
|
+
function compactRoadmap(read) {
|
|
420
|
+
const stations = (roadmap) => roadmap.nodes.map((node) => ({
|
|
421
|
+
id: node.id,
|
|
422
|
+
title: node.title,
|
|
423
|
+
state: node.state,
|
|
424
|
+
...node.gate === "quiz" ? {} : { gate: node.gate },
|
|
425
|
+
rows: node.covers.flatMap((cover) => cover.locator === void 0 ? [] : [cover.locator]).join(",") || void 0,
|
|
426
|
+
points: (node.points ?? []).map((point) => `${point.id}: ${point.title}`),
|
|
427
|
+
...node.goals.length === 0 ? {} : { goals: node.goals },
|
|
428
|
+
...node.acceptance === void 0 || node.acceptance.length === 0 ? {} : { acceptance: node.acceptance },
|
|
429
|
+
...node.notes === void 0 ? {} : { notes: `${node.notes.text.length} chars, revision ${node.notes.revisions}` },
|
|
430
|
+
...node.quizStats === void 0 ? {} : { best: node.quizStats.bestRatio }
|
|
431
|
+
}));
|
|
432
|
+
return {
|
|
433
|
+
...read.roadmap === void 0 ? { adopted: null } : { adopted: { stations: stations(read.roadmap) } },
|
|
434
|
+
...read.draft === void 0 ? {} : { draft: {
|
|
435
|
+
stations: stations(read.draft),
|
|
436
|
+
note: read.draft.note
|
|
437
|
+
} }
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
/** Build the `library_read` tool over the shared kit. */
|
|
441
|
+
function libraryReadTool(kit) {
|
|
442
|
+
const { service, bindingOf, projectOf, resourceOf } = kit;
|
|
443
|
+
return defineTool({
|
|
444
|
+
name: "library_read",
|
|
445
|
+
description: "Read the library: projects (all, with this session's binding) | resources (one project's textbooks in study order) | toc (a textbook's contents rows) | roadmap (a textbook's path and pending draft) | pages (photographed pages) | cards (the learner's cards, weakest first) | quiz (one quiz, or the list). project and resource default to the binding.",
|
|
446
|
+
parameters: {
|
|
447
|
+
what: {
|
|
448
|
+
type: "string",
|
|
449
|
+
description: "One of the reads above."
|
|
450
|
+
},
|
|
451
|
+
project: { type: "string" },
|
|
452
|
+
resource: { type: "string" },
|
|
453
|
+
node: {
|
|
454
|
+
type: "string",
|
|
455
|
+
description: "A station (pages / cards)."
|
|
456
|
+
},
|
|
457
|
+
quiz: {
|
|
458
|
+
type: "string",
|
|
459
|
+
description: "A quiz id."
|
|
460
|
+
},
|
|
461
|
+
query: {
|
|
462
|
+
type: "string",
|
|
463
|
+
description: "cards: search term, gloss, sentences."
|
|
464
|
+
},
|
|
465
|
+
due: {
|
|
466
|
+
type: "boolean",
|
|
467
|
+
description: "cards: only those due for review."
|
|
468
|
+
}
|
|
469
|
+
},
|
|
470
|
+
output: jsonOutput(),
|
|
471
|
+
isConcurrencySafe: () => true,
|
|
472
|
+
async execute(args, exec) {
|
|
473
|
+
const argsAny = args;
|
|
474
|
+
switch (typeof args.what === "string" && args.what !== "" ? args.what : args.quiz !== void 0 ? "quiz" : args.resource !== void 0 || args.node !== void 0 ? "roadmap" : "projects") {
|
|
475
|
+
case "projects": return {
|
|
476
|
+
...await service.listProjects(),
|
|
477
|
+
binding: bindingOf(exec) ?? null
|
|
478
|
+
};
|
|
479
|
+
case "resources":
|
|
480
|
+
case "books": {
|
|
481
|
+
const detail = unwrap(await service.getProject({ id: projectOf(args.project, exec) }));
|
|
482
|
+
return {
|
|
483
|
+
project: detail.project,
|
|
484
|
+
resources: detail.resources
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
case "toc": {
|
|
488
|
+
const resource = resourceOf(args, exec);
|
|
489
|
+
return unwrap(await service.getResource({
|
|
490
|
+
projectId: projectOf(args.project, exec, resource),
|
|
491
|
+
resourceId: resource
|
|
492
|
+
})).resource;
|
|
493
|
+
}
|
|
494
|
+
case "roadmap": {
|
|
495
|
+
const resource = resourceOf(args, exec);
|
|
496
|
+
return compactRoadmap(await service.getRoadmap({
|
|
497
|
+
projectId: projectOf(args.project, exec, resource),
|
|
498
|
+
resourceId: resource
|
|
499
|
+
}));
|
|
500
|
+
}
|
|
501
|
+
case "pages": {
|
|
502
|
+
const resource = resourceOf(args, exec);
|
|
503
|
+
return await service.listPages({
|
|
504
|
+
projectId: projectOf(args.project, exec, resource),
|
|
505
|
+
resourceId: resource,
|
|
506
|
+
...args.node === void 0 ? {} : { nodeId: args.node }
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
case "cards": {
|
|
510
|
+
const resource = pickString(args.resource, args.book);
|
|
511
|
+
const result = await service.listCards({
|
|
512
|
+
projectId: projectOf(args.project, exec, resource),
|
|
513
|
+
...resource === void 0 ? {} : { resourceId: resource },
|
|
514
|
+
...args.node === void 0 ? {} : { nodeId: args.node },
|
|
515
|
+
...args.query === void 0 ? {} : { query: args.query },
|
|
516
|
+
...argsAny.kind === "word" || argsAny.kind === "concept" ? { kind: argsAny.kind } : {},
|
|
517
|
+
...[
|
|
518
|
+
"photo",
|
|
519
|
+
"chat",
|
|
520
|
+
"quiz",
|
|
521
|
+
"manual"
|
|
522
|
+
].includes(String(argsAny.source ?? "")) ? { source: argsAny.source } : {},
|
|
523
|
+
...typeof argsAny.weakMin === "number" ? { weakMin: argsAny.weakMin } : {},
|
|
524
|
+
...args.due === true ? { due: true } : {},
|
|
525
|
+
limit: typeof argsAny.limit === "number" ? argsAny.limit : 30
|
|
526
|
+
});
|
|
527
|
+
return {
|
|
528
|
+
total: result.total,
|
|
529
|
+
cards: result.cards.map((card) => ({
|
|
530
|
+
id: card.id,
|
|
531
|
+
kind: card.kind,
|
|
532
|
+
term: card.term,
|
|
533
|
+
...card.aliases.length === 0 ? {} : { aliases: card.aliases },
|
|
534
|
+
gloss: card.gloss,
|
|
535
|
+
...card.userGloss === void 0 ? {} : { userGloss: card.userGloss },
|
|
536
|
+
...card.resourceId === void 0 ? {} : { resource: card.resourceId },
|
|
537
|
+
contexts: card.contexts.slice(0, 2).map((context) => context.sentence),
|
|
538
|
+
weakness: card.weakness,
|
|
539
|
+
box: card.mastery.box,
|
|
540
|
+
dueAt: card.mastery.dueAt,
|
|
541
|
+
sources: card.sources.map((source) => source.note === void 0 ? source.type : `${source.type}: ${source.note}`).slice(-4)
|
|
542
|
+
}))
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
case "quiz": {
|
|
546
|
+
const resource = pickString(args.resource, args.book) ?? bindingOf(exec)?.resourceId;
|
|
547
|
+
const project = projectOf(args.project, exec, resource);
|
|
548
|
+
if (args.quiz === void 0 || args.quiz.trim() === "") return await service.listAssessments({
|
|
549
|
+
projectId: project,
|
|
550
|
+
type: "quiz",
|
|
551
|
+
...resource === void 0 ? {} : { resourceId: resource }
|
|
552
|
+
});
|
|
553
|
+
return unwrap(await service.getAssessment({
|
|
554
|
+
projectId: project,
|
|
555
|
+
assessmentId: args.quiz.trim()
|
|
556
|
+
})).assessment;
|
|
557
|
+
}
|
|
558
|
+
case "stats": return await service.stats();
|
|
559
|
+
default: throw new Error(`what is one of ${READ_WHATS.join(" | ")} — got ${JSON.stringify(args.what)}`);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
//#endregion
|
|
565
|
+
//#region lib/types/tools/project.js
|
|
566
|
+
/**
|
|
567
|
+
* The `library_project` tool (moved verbatim from tool-definition.ts; the shared
|
|
568
|
+
* helpers and schemas are in `tools/kit.ts`).
|
|
569
|
+
*
|
|
570
|
+
* @module @khorsheed/dsh-library/tools/project
|
|
571
|
+
*/
|
|
572
|
+
/** Build the `library_project` tool over the shared kit. */
|
|
573
|
+
function libraryProjectTool(kit) {
|
|
574
|
+
const { service, autoBind } = kit;
|
|
575
|
+
return defineTool({
|
|
576
|
+
name: "library_project",
|
|
577
|
+
description: "Create a learning project (one goal), or edit one (projectId + what changes; goal \"\" clears). template: subject | language (marks are words) | custom. Next: shelve its textbooks (library_textbook).",
|
|
578
|
+
parameters: {
|
|
579
|
+
projectId: {
|
|
580
|
+
type: "string",
|
|
581
|
+
description: "Edit this project; omit to create (ids are minted)."
|
|
582
|
+
},
|
|
583
|
+
name: { type: "string" },
|
|
584
|
+
template: {
|
|
585
|
+
type: "string",
|
|
586
|
+
enum: [
|
|
587
|
+
"subject",
|
|
588
|
+
"language",
|
|
589
|
+
"custom"
|
|
590
|
+
]
|
|
591
|
+
},
|
|
592
|
+
goal: {
|
|
593
|
+
type: "string",
|
|
594
|
+
description: "One line."
|
|
595
|
+
}
|
|
596
|
+
},
|
|
597
|
+
output: jsonOutput(),
|
|
598
|
+
isConcurrencySafe: () => false,
|
|
599
|
+
async execute(args, exec) {
|
|
600
|
+
const legacyId = args.id;
|
|
601
|
+
const editId = pickString(args.projectId, legacyId);
|
|
602
|
+
if (editId !== void 0) {
|
|
603
|
+
const patch = {};
|
|
604
|
+
if (typeof args.name === "string") patch.name = args.name;
|
|
605
|
+
if (typeof args.template === "string") patch.template = args.template;
|
|
606
|
+
if (typeof args.goal === "string") patch.goal = args.goal;
|
|
607
|
+
if (Object.keys(patch).length === 0) throw new Error("editing a project takes projectId plus the fields to change (name? / goal? / template?) — omit projectId to CREATE one (name + template)");
|
|
608
|
+
const edited = await service.updateProject({
|
|
609
|
+
id: editId,
|
|
610
|
+
patch
|
|
611
|
+
});
|
|
612
|
+
if (edited.ok) {
|
|
613
|
+
await autoBind(exec, { projectId: edited.project.id });
|
|
614
|
+
return edited;
|
|
615
|
+
}
|
|
616
|
+
if (typeof args.name !== "string" || args.name.trim() === "") throw new Error(edited.message);
|
|
617
|
+
const created = unwrap(await service.createProject({
|
|
618
|
+
name: args.name,
|
|
619
|
+
...typeof args.template === "string" ? { template: args.template } : {},
|
|
620
|
+
...typeof args.goal === "string" ? { goal: args.goal } : {}
|
|
621
|
+
}));
|
|
622
|
+
await autoBind(exec, { projectId: created.project.id });
|
|
623
|
+
return {
|
|
624
|
+
...created,
|
|
625
|
+
note: `no project ${JSON.stringify(editId)} existed — created a new one instead (ids are minted (p_…), never chosen by the caller; omit projectId to create)`
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
if (typeof args.name !== "string" || args.name.trim() === "") throw new Error("creating a project takes name (+ template: \"language\" | \"subject\" | \"custom\", goal?) — e.g. { name: \"数学\", template: \"subject\" }");
|
|
629
|
+
const created = unwrap(await service.createProject({
|
|
630
|
+
name: args.name,
|
|
631
|
+
...typeof args.template === "string" ? { template: args.template } : {},
|
|
632
|
+
...typeof args.goal === "string" ? { goal: args.goal } : {}
|
|
633
|
+
}));
|
|
634
|
+
await autoBind(exec, { projectId: created.project.id });
|
|
635
|
+
return created;
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
//#endregion
|
|
640
|
+
//#region lib/types/tools/resource.js
|
|
641
|
+
/**
|
|
642
|
+
* The `library_textbook` tool (moved verbatim from tool-definition.ts; the shared
|
|
643
|
+
* helpers and schemas are in `tools/kit.ts`).
|
|
644
|
+
*
|
|
645
|
+
* @module @khorsheed/dsh-library/tools/resource
|
|
646
|
+
*/
|
|
647
|
+
/** Build the `library_textbook` tool over the shared kit (the file keeps its v5 name). */
|
|
648
|
+
function libraryResourceTool(kit) {
|
|
649
|
+
const { service, projectOf, autoBind, scopeParams } = kit;
|
|
650
|
+
return defineTool({
|
|
651
|
+
name: "library_textbook",
|
|
652
|
+
description: "Shelve, correct, reorder or remove a textbook (anything with a contents) — in study order. origin: learner (their own) | picked (a book or course you chose; its path takes only the chapters the goal needs) | authored (you write it: one chapter per lesson, your 讲义 are its text). A real book: verify the edition and contents online and cite the page (tocSource { by: \"web\", url }) — search the title with its author or ISBN, narrowed with site: to catalog or publisher pages (site:book.douban.com, site:worldcat.org, the publisher's domain), one query per edition to tell apart, never a \" in a query; never ask for an upload. A correction (resourceId) sends only what changes. A web page a station cites is no textbook — the path names its url.",
|
|
653
|
+
parameters: {
|
|
654
|
+
...scopeParams,
|
|
655
|
+
op: {
|
|
656
|
+
type: "string",
|
|
657
|
+
required: true,
|
|
658
|
+
description: "add | update | move | remove."
|
|
659
|
+
},
|
|
660
|
+
resourceId: {
|
|
661
|
+
type: "string",
|
|
662
|
+
description: "The textbook to update / move / remove."
|
|
663
|
+
},
|
|
664
|
+
title: {
|
|
665
|
+
type: "string",
|
|
666
|
+
description: "The exact edition's title."
|
|
667
|
+
},
|
|
668
|
+
origin: {
|
|
669
|
+
type: "string",
|
|
670
|
+
enum: [
|
|
671
|
+
"learner",
|
|
672
|
+
"picked",
|
|
673
|
+
"authored"
|
|
674
|
+
]
|
|
675
|
+
},
|
|
676
|
+
toc: {
|
|
677
|
+
type: "json",
|
|
678
|
+
description: "The contents: one chapter per line in one string, or [\"…\", \"…\"]."
|
|
679
|
+
},
|
|
680
|
+
tocSource: {
|
|
681
|
+
type: "json",
|
|
682
|
+
description: "{ by: \"web\" | \"model\" | \"photo\", url? } — web needs the url."
|
|
683
|
+
},
|
|
684
|
+
seq: {
|
|
685
|
+
type: "integer",
|
|
686
|
+
description: "Its place in the study order, from 1."
|
|
687
|
+
},
|
|
688
|
+
bookKind: {
|
|
689
|
+
type: "string",
|
|
690
|
+
enum: ["knowledge", "material"],
|
|
691
|
+
description: "material only when the exact text is what is tested (exam papers, literature)."
|
|
692
|
+
},
|
|
693
|
+
edition: { type: "string" },
|
|
694
|
+
author: { type: "string" },
|
|
695
|
+
url: {
|
|
696
|
+
type: "string",
|
|
697
|
+
description: "A digital textbook's home page."
|
|
698
|
+
},
|
|
699
|
+
note: {
|
|
700
|
+
type: "string",
|
|
701
|
+
description: "One line on why it is here."
|
|
702
|
+
}
|
|
703
|
+
},
|
|
704
|
+
output: jsonOutput(),
|
|
705
|
+
isConcurrencySafe: () => false,
|
|
706
|
+
async execute(args, exec) {
|
|
707
|
+
const argsAny = args;
|
|
708
|
+
const namedId = pickString(args.resourceId, argsAny.bookId);
|
|
709
|
+
const op = args.op === "add" || args.op === "update" ? "create" : args.op;
|
|
710
|
+
if (op === "remove") {
|
|
711
|
+
if (namedId === void 0) throw new Error("library_textbook · op=remove takes the resourceId field — library_read · resources lists the ids");
|
|
712
|
+
return unwrap(await service.removeResource({
|
|
713
|
+
projectId: projectOf(args.project, exec, namedId),
|
|
714
|
+
resourceId: namedId
|
|
715
|
+
}));
|
|
716
|
+
}
|
|
717
|
+
if (op === "move") {
|
|
718
|
+
if (namedId === void 0 || typeof args.seq !== "number") throw new Error("library_textbook · op=move takes resourceId and seq (the textbook's new place, from 1)");
|
|
719
|
+
return { order: unwrap(await service.moveResource({
|
|
720
|
+
projectId: projectOf(args.project, exec, namedId),
|
|
721
|
+
resourceId: namedId,
|
|
722
|
+
seq: args.seq
|
|
723
|
+
})).resources.filter((resource) => resource.seq !== void 0).map((resource) => `${resource.seq}. ${resource.id} 《${resource.title}》`) };
|
|
724
|
+
}
|
|
725
|
+
if (op !== "create") throw new Error(`library_textbook needs op: add | update | move | remove — got ${JSON.stringify(args.op)}`);
|
|
726
|
+
const originArg = argsAny.origin ?? (argsAny.addedBy === "learner" ? "learner" : argsAny.addedBy === "coach" ? "picked" : void 0);
|
|
727
|
+
const origin = originArg === "learner" || originArg === "picked" || originArg === "authored" ? { origin: originArg } : {};
|
|
728
|
+
const projectId = projectOf(args.project, exec, namedId);
|
|
729
|
+
const from = payload(argsAny.from);
|
|
730
|
+
if (from !== void 0) {
|
|
731
|
+
const fromProject = pickString(from.project, from.projectId);
|
|
732
|
+
const fromResource = pickString(from.resource, from.resourceId, from.book, from.bookId);
|
|
733
|
+
if (fromProject === void 0 || fromResource === void 0) throw new Error("from is { project, resource } — the OTHER project's id and the entry's id there");
|
|
734
|
+
const cloned = unwrap(await service.putResource({
|
|
735
|
+
projectId,
|
|
736
|
+
title: "",
|
|
737
|
+
kind: "article",
|
|
738
|
+
...namedId === void 0 ? {} : { resourceId: namedId },
|
|
739
|
+
from: {
|
|
740
|
+
projectId: fromProject,
|
|
741
|
+
resourceId: fromResource
|
|
742
|
+
}
|
|
743
|
+
}));
|
|
744
|
+
await autoBind(exec, {
|
|
745
|
+
projectId,
|
|
746
|
+
resourceId: cloned.resource.id
|
|
747
|
+
});
|
|
748
|
+
return cloned;
|
|
749
|
+
}
|
|
750
|
+
const tocList = payloadList(args.toc);
|
|
751
|
+
const sentRows = typeof args.toc === "string" && tocList === void 0 ? splitTocText(args.toc) : tocList ?? [];
|
|
752
|
+
const shelved = namedId !== void 0 && sentRows.length === 0 && pickString(args.url) === void 0 ? await service.getResource({
|
|
753
|
+
projectId,
|
|
754
|
+
resourceId: namedId
|
|
755
|
+
}) : void 0;
|
|
756
|
+
const kept = shelved?.ok === true && isThickResource(shelved.resource) ? shelved.resource : void 0;
|
|
757
|
+
const tocRows = sentRows.length > 0 ? sentRows : (kept?.toc ?? []).map((entry) => ({ ...entry }));
|
|
758
|
+
if (kept !== void 0) {
|
|
759
|
+
if (typeof args.title !== "string" || args.title.trim() === "") argsAny.title = kept.title;
|
|
760
|
+
if (argsAny.kind === void 0) argsAny.kind = kept.kind;
|
|
761
|
+
}
|
|
762
|
+
const linkKind = typeof argsAny.kind === "string" && argsAny.kind !== "book" && argsAny.kind !== "course" && LIBRARY_RESOURCE_KINDS.includes(argsAny.kind);
|
|
763
|
+
if (tocRows.length > 0 || !linkKind) {
|
|
764
|
+
if (typeof args.title !== "string" || args.title.trim() === "" || tocRows.length === 0) throw new Error(`a textbook takes: title, toc (one chapter per line), tocSource { by, url? }, origin (+ seq?, bookKind?, edition?, author?; resourceId to update one) — ${arrival("toc", args.toc)}`);
|
|
765
|
+
const toc = tocRows.map((row) => typeof row === "string" ? { title: row } : row);
|
|
766
|
+
const written = unwrap(await service.putResource({
|
|
767
|
+
projectId,
|
|
768
|
+
...namedId === void 0 ? {} : { resourceId: namedId },
|
|
769
|
+
title: args.title,
|
|
770
|
+
kind: argsAny.kind === "course" ? "course" : "book",
|
|
771
|
+
...typeof args.url === "string" ? { url: args.url } : {},
|
|
772
|
+
...typeof args.note === "string" ? { note: args.note } : {},
|
|
773
|
+
...origin,
|
|
774
|
+
...typeof args.seq === "number" ? { seq: args.seq } : {},
|
|
775
|
+
toc,
|
|
776
|
+
tocSource: payload(args.tocSource) ?? kept?.tocSource ?? { by: "model" },
|
|
777
|
+
...args.bookKind === "knowledge" || args.bookKind === "material" ? { bookKind: args.bookKind } : {},
|
|
778
|
+
...typeof argsAny.short === "string" ? { short: argsAny.short } : {},
|
|
779
|
+
...typeof args.author === "string" ? { author: args.author } : {},
|
|
780
|
+
...typeof args.edition === "string" ? { edition: args.edition } : {},
|
|
781
|
+
...typeof argsAny.isbn === "string" ? { isbn: argsAny.isbn } : {},
|
|
782
|
+
...Array.isArray(payloadList(argsAny.extras)) ? { extras: payloadList(argsAny.extras) } : {}
|
|
783
|
+
}));
|
|
784
|
+
await autoBind(exec, {
|
|
785
|
+
projectId,
|
|
786
|
+
resourceId: written.resource.id
|
|
787
|
+
});
|
|
788
|
+
const rows = written.resource.toc?.length ?? 0;
|
|
789
|
+
return {
|
|
790
|
+
...written,
|
|
791
|
+
contentsRows: rows,
|
|
792
|
+
...rows < 3 ? { warning: `the contents has only ${rows} row(s) — one row per chapter is expected (station chapters count these rows). If the chapters got joined, resend toc with one chapter per line and resourceId ${written.resource.id}` } : {}
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
const title = pickString(args.title);
|
|
796
|
+
const url = pickString(args.url);
|
|
797
|
+
if (title === void 0 || url === void 0) throw new Error(`a link takes: title, url, kind ("article"|"video"|"repo"|"tool"|"hardware"), note? — got title ${title === void 0 ? "✗ MISSING" : "✓"} · url ${url === void 0 ? "✗ MISSING" : "✓"} (a textbook takes kind "book" | "course" + toc)`);
|
|
798
|
+
const added = unwrap(await service.putResource({
|
|
799
|
+
projectId,
|
|
800
|
+
...namedId === void 0 ? {} : { resourceId: namedId },
|
|
801
|
+
title,
|
|
802
|
+
kind: linkKind ? argsAny.kind : "article",
|
|
803
|
+
url,
|
|
804
|
+
...typeof args.note === "string" ? { note: args.note } : {},
|
|
805
|
+
...origin
|
|
806
|
+
}));
|
|
807
|
+
await autoBind(exec, { projectId });
|
|
808
|
+
return added;
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
//#endregion
|
|
813
|
+
//#region lib/types/tools/roadmap.js
|
|
814
|
+
/**
|
|
815
|
+
* The `library_roadmap` tool (moved verbatim from tool-definition.ts; the shared
|
|
816
|
+
* helpers and schemas are in `tools/kit.ts`).
|
|
817
|
+
*
|
|
818
|
+
* @module @khorsheed/dsh-library/tools/roadmap
|
|
819
|
+
*/
|
|
820
|
+
/** Build the `library_roadmap` tool over the shared kit. */
|
|
821
|
+
function libraryRoadmapTool(kit) {
|
|
822
|
+
const { service, projectOf, autoBind, textbookList } = kit;
|
|
823
|
+
return defineTool({
|
|
824
|
+
name: "library_roadmap",
|
|
825
|
+
description: "Draft one textbook's path (replaces its previous draft); the learner adopts it in the sidebar. One station = one lesson: 3–5 knowledge points, simple → hard. A real book's long chapter splits into several stations; a textbook you authored teaches one chapter per station.",
|
|
826
|
+
parameters: {
|
|
827
|
+
resource: {
|
|
828
|
+
type: "string",
|
|
829
|
+
required: true,
|
|
830
|
+
description: "The textbook."
|
|
831
|
+
},
|
|
832
|
+
nodes: {
|
|
833
|
+
...NODES_SCHEMA,
|
|
834
|
+
required: true,
|
|
835
|
+
description: "The stations, in study order."
|
|
836
|
+
},
|
|
837
|
+
note: {
|
|
838
|
+
type: "string",
|
|
839
|
+
description: "One line of pacing for the draft card."
|
|
840
|
+
}
|
|
841
|
+
},
|
|
842
|
+
output: jsonOutput(),
|
|
843
|
+
isConcurrencySafe: () => false,
|
|
844
|
+
async execute(args, exec) {
|
|
845
|
+
const list = payloadList(args.nodes);
|
|
846
|
+
if (list === void 0) throw new Error(`library_roadmap takes resource and nodes: [{ id, title, chapters: "3-4", points: [{ id, title }] (3–5), goals?, acceptance? }] — ${arrival("nodes", args.nodes)}`);
|
|
847
|
+
const argsAny = args;
|
|
848
|
+
const resourceId = pickString(args.resource, argsAny.book);
|
|
849
|
+
if (resourceId === void 0) throw new Error(`a path belongs to one textbook — pass resource${textbookList(exec) || " (shelve the textbook first with library_textbook)"}`);
|
|
850
|
+
const adjusted = [];
|
|
851
|
+
const missingPoints = [];
|
|
852
|
+
const nodes = list.map((node) => {
|
|
853
|
+
const { chapters, also, chapterRanges, covers, kind, gate, points, ...rest } = node;
|
|
854
|
+
const gateValue = typeof gate === "string" ? gate : kind === "practice" ? "acceptance" : typeof kind === "string" ? kind : "quiz";
|
|
855
|
+
if (gateValue === "quiz" && Array.isArray(rest.acceptance) && rest.acceptance.length > 0) {
|
|
856
|
+
const lines = rest.acceptance.flatMap((line) => typeof line === "string" ? [line] : typeof payload(line)?.line === "string" ? [String(payload(line).line)] : []);
|
|
857
|
+
rest.goals = [...Array.isArray(rest.goals) ? rest.goals : typeof rest.goals === "string" ? [rest.goals] : [], ...lines];
|
|
858
|
+
delete rest.acceptance;
|
|
859
|
+
}
|
|
860
|
+
if (gateValue === "quiz" && (!Array.isArray(points) || points.length === 0)) missingPoints.push(String(rest.id ?? rest.title ?? "?"));
|
|
861
|
+
const keepPoints = gateValue === "quiz" || points === void 0 || Array.isArray(points) && points.length === 0;
|
|
862
|
+
if (!keepPoints) adjusted.push(`points dropped on ${String(rest.title ?? rest.id)} — a ${String(gateValue)} station has no quiz; what it checks belongs in ${gateValue === "acceptance" ? "acceptance lines" : "goals"}`);
|
|
863
|
+
const main = [];
|
|
864
|
+
const spelled = typeof chapters === "string" && chapters.trim() !== "" ? chaptersLocator(chapters) : void 0;
|
|
865
|
+
if (spelled?.rest !== void 0) adjusted.push(`${String(rest.title ?? rest.id)}: "${spelled.rest}" in chapters is not a row number — kept as a side reference (topic: ${spelled.rest})`);
|
|
866
|
+
const locator = typeof chapters === "number" ? String(chapters) : spelled !== void 0 ? spelled.locator : Array.isArray(chapters) ? chapters.filter((entry) => typeof entry === "number" || typeof entry === "string").join(",") : Array.isArray(chapterRanges) ? formatLocator(chapterRanges.flatMap((range) => {
|
|
867
|
+
const row = payload(range);
|
|
868
|
+
return typeof row?.from === "number" && typeof row.to === "number" ? [{
|
|
869
|
+
from: row.from,
|
|
870
|
+
to: row.to
|
|
871
|
+
}] : [];
|
|
872
|
+
})) : void 0;
|
|
873
|
+
if (locator !== void 0 && locator !== "") main.push({
|
|
874
|
+
ref: resourceId,
|
|
875
|
+
locator
|
|
876
|
+
});
|
|
877
|
+
const side = [
|
|
878
|
+
...Array.isArray(covers) ? covers : [],
|
|
879
|
+
...Array.isArray(also) ? also : typeof also === "string" ? [also] : [],
|
|
880
|
+
...spelled?.rest === void 0 ? [] : [`topic: ${spelled.rest}`]
|
|
881
|
+
].map((cover) => normalizeCoverArg(cover) ?? cover);
|
|
882
|
+
const all = main.length === 0 && side.length === 0 ? [{ ref: resourceId }] : [...main, ...side];
|
|
883
|
+
return {
|
|
884
|
+
...rest,
|
|
885
|
+
...gateValue === void 0 ? {} : { gate: gateValue },
|
|
886
|
+
...keepPoints && points !== void 0 ? { points } : {},
|
|
887
|
+
covers: all
|
|
888
|
+
};
|
|
889
|
+
});
|
|
890
|
+
if (missingPoints.length > 0) throw new Error(`every station needs its 3–5 knowledge points (points: [{ id, title }]) — the chapter quiz asks them: ${missingPoints.join(", ")}`);
|
|
891
|
+
const projectId = projectOf(argsAny.project, exec, resourceId);
|
|
892
|
+
const replaced = unwrap(await service.getProject({ id: projectId })).project.resources.find((row) => row.id === resourceId)?.hasDraft === true;
|
|
893
|
+
const drafted = unwrap(await service.draftRoadmap({
|
|
894
|
+
projectId,
|
|
895
|
+
resourceId,
|
|
896
|
+
nodes,
|
|
897
|
+
...typeof argsAny.passRatio === "number" ? { passRatio: argsAny.passRatio } : {},
|
|
898
|
+
...typeof args.note === "string" ? { note: args.note } : {}
|
|
899
|
+
}));
|
|
900
|
+
await autoBind(exec, {
|
|
901
|
+
projectId,
|
|
902
|
+
resourceId
|
|
903
|
+
});
|
|
904
|
+
const after = unwrap(await service.getProject({ id: projectId }));
|
|
905
|
+
const textbook = after.project.resources.find((row) => row.id === resourceId);
|
|
906
|
+
return {
|
|
907
|
+
ok: true,
|
|
908
|
+
placedOn: `${textbook?.seq ?? "-"}. 《${textbook?.title ?? resourceId}》 (${resourceId})`,
|
|
909
|
+
stations: drafted.draft.nodes.length,
|
|
910
|
+
...replaced ? { replacedDraft: true } : {},
|
|
911
|
+
waitingForAdoption: after.project.resources.filter((row) => row.hasDraft).map((row) => `${row.seq ?? "-"}. 《${row.title}》 ${row.draftTotal ?? 0} stations`),
|
|
912
|
+
...adjusted.length === 0 ? {} : { adjusted },
|
|
913
|
+
next: "Describe the path to the learner in two lines and ask them to review and adopt it in the sidebar (采用). Then stop: before they adopt it, write no 讲义, teach nothing, set no quiz."
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
//#endregion
|
|
919
|
+
//#region lib/types/tools/notes.js
|
|
920
|
+
/**
|
|
921
|
+
* The `library_notes` tool (moved verbatim from tool-definition.ts; the shared
|
|
922
|
+
* helpers and schemas are in `tools/kit.ts`).
|
|
923
|
+
*
|
|
924
|
+
* @module @khorsheed/dsh-library/tools/notes
|
|
925
|
+
*/
|
|
926
|
+
/** Build the `library_notes` tool over the shared kit. */
|
|
927
|
+
function libraryNotesTool(kit) {
|
|
928
|
+
const { service, projectOf, resourceOf, textbookParams } = kit;
|
|
929
|
+
return defineTool({
|
|
930
|
+
name: "library_notes",
|
|
931
|
+
description: "Write or rewrite a station's 讲义 (markdown, math in $…$), for THIS learner, when they start the station. The 讲义 are the lesson itself — the learner learns from them without leaving the page, whatever the textbook: open with the core question and 「你将学到」 (the points, numbered) and one or two warm-up questions; then one ## section per knowledge point, headed by its title — explain it plainly, give a concrete example or the steps to try, a common mistake, and a 思考题; then 小结. Outside material (a manual page, a video, the learner's book pages) goes only in a final ## 参考 section, never as the lesson. No change logs or internal words.",
|
|
932
|
+
parameters: {
|
|
933
|
+
...textbookParams,
|
|
934
|
+
node: {
|
|
935
|
+
type: "string",
|
|
936
|
+
required: true,
|
|
937
|
+
description: "The station id."
|
|
938
|
+
},
|
|
939
|
+
text: {
|
|
940
|
+
type: "string",
|
|
941
|
+
required: true,
|
|
942
|
+
description: "The 讲义, markdown."
|
|
943
|
+
}
|
|
944
|
+
},
|
|
945
|
+
output: jsonOutput(),
|
|
946
|
+
isConcurrencySafe: () => false,
|
|
947
|
+
async execute(args, exec) {
|
|
948
|
+
const resourceId = resourceOf(args, exec);
|
|
949
|
+
const written = unwrap(await service.putNotes({
|
|
950
|
+
projectId: projectOf(args.project, exec, resourceId),
|
|
951
|
+
resourceId,
|
|
952
|
+
nodeId: String(args.node),
|
|
953
|
+
text: String(args.text)
|
|
954
|
+
}));
|
|
955
|
+
return {
|
|
956
|
+
...written,
|
|
957
|
+
next: written.notes.revisions === 1 ? "Tell the learner in one line that the 讲义 are ready in the sidebar, then wait — they read first and ask." : "Tell the learner in one line what changed in the 讲义."
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
//#endregion
|
|
963
|
+
//#region lib/types/tools/page.js
|
|
964
|
+
/**
|
|
965
|
+
* The `library_page` tool (moved verbatim from tool-definition.ts; the shared
|
|
966
|
+
* helpers and schemas are in `tools/kit.ts`).
|
|
967
|
+
*
|
|
968
|
+
* @module @khorsheed/dsh-library/tools/page
|
|
969
|
+
*/
|
|
970
|
+
/** Build the `library_page` tool over the shared kit. */
|
|
971
|
+
function libraryPageTool(kit) {
|
|
972
|
+
const { service, images, projectOf, resourceOf, textbookParams } = kit;
|
|
973
|
+
return defineTool({
|
|
974
|
+
name: "library_page",
|
|
975
|
+
description: "Record a photographed page in one call: its printed text and every mark the learner made (each certain mark becomes a card). Omit attachment for the latest photo. If you cannot see images, say so instead.",
|
|
976
|
+
parameters: {
|
|
977
|
+
...textbookParams,
|
|
978
|
+
text: {
|
|
979
|
+
type: "string",
|
|
980
|
+
required: true,
|
|
981
|
+
description: "The printed text."
|
|
982
|
+
},
|
|
983
|
+
marks: {
|
|
984
|
+
oneOf: [{
|
|
985
|
+
type: "array",
|
|
986
|
+
items: {
|
|
987
|
+
type: "object",
|
|
988
|
+
additionalProperties: true,
|
|
989
|
+
properties: {
|
|
990
|
+
text: {
|
|
991
|
+
type: "string",
|
|
992
|
+
description: "As printed."
|
|
993
|
+
},
|
|
994
|
+
sentence: { type: "string" },
|
|
995
|
+
handNote: {
|
|
996
|
+
type: "string",
|
|
997
|
+
description: "The learner's handwriting beside it."
|
|
998
|
+
},
|
|
999
|
+
gloss: {
|
|
1000
|
+
type: "string",
|
|
1001
|
+
description: "In-context meaning."
|
|
1002
|
+
},
|
|
1003
|
+
uncertain: {
|
|
1004
|
+
type: "boolean",
|
|
1005
|
+
description: "You could not tell (kept, not carded)."
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}, {
|
|
1010
|
+
type: "string",
|
|
1011
|
+
description: "The same list as JSON text."
|
|
1012
|
+
}],
|
|
1013
|
+
required: true,
|
|
1014
|
+
description: "Every mark on the page."
|
|
1015
|
+
},
|
|
1016
|
+
attachment: {
|
|
1017
|
+
type: "string",
|
|
1018
|
+
description: "The image id (omit for the latest photo in this conversation)."
|
|
1019
|
+
},
|
|
1020
|
+
label: {
|
|
1021
|
+
type: "string",
|
|
1022
|
+
description: "What the learner calls the page (「p.16」)."
|
|
1023
|
+
},
|
|
1024
|
+
node: {
|
|
1025
|
+
type: "string",
|
|
1026
|
+
description: "The station id, when the page belongs to one."
|
|
1027
|
+
}
|
|
1028
|
+
},
|
|
1029
|
+
output: jsonOutput(),
|
|
1030
|
+
isConcurrencySafe: () => false,
|
|
1031
|
+
async execute(args, exec) {
|
|
1032
|
+
const marks = payloadList(args.marks);
|
|
1033
|
+
if (marks === void 0) throw new Error(`library_page takes: text (the page's printed text), marks: [{ text, sentence?, handNote?, gloss?, uncertain? }] (+ attachment?, label?, node?) — ${arrival("marks", args.marks)}`);
|
|
1034
|
+
const attachment = images.resolve(sessionIdOf(exec), typeof args.attachment === "string" ? args.attachment : void 0);
|
|
1035
|
+
if (attachment === void 0) throw new Error("no photo to record: this conversation has no image the library saw arrive (or the id names none) — ask the learner to send the page photo here, then record it");
|
|
1036
|
+
const pageResource = resourceOf(args, exec);
|
|
1037
|
+
return unwrap(await service.recordPage({
|
|
1038
|
+
projectId: projectOf(args.project, exec, pageResource),
|
|
1039
|
+
resourceId: pageResource,
|
|
1040
|
+
...typeof args.node === "string" && args.node !== "" ? { nodeId: args.node } : {},
|
|
1041
|
+
...typeof args.label === "string" ? { label: args.label } : {},
|
|
1042
|
+
attachment,
|
|
1043
|
+
text: args.text,
|
|
1044
|
+
marks
|
|
1045
|
+
}));
|
|
1046
|
+
}
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
//#endregion
|
|
1050
|
+
//#region lib/types/tools/cards.js
|
|
1051
|
+
/**
|
|
1052
|
+
* The `library_cards` tool (moved verbatim from tool-definition.ts; the shared
|
|
1053
|
+
* helpers and schemas are in `tools/kit.ts`).
|
|
1054
|
+
*
|
|
1055
|
+
* @module @khorsheed/dsh-library/tools/cards
|
|
1056
|
+
*/
|
|
1057
|
+
/** Build the `library_cards` tool over the shared kit. */
|
|
1058
|
+
function libraryCardsTool(kit) {
|
|
1059
|
+
const { service, bindingOf, projectOf, scopeParams } = kit;
|
|
1060
|
+
return defineTool({
|
|
1061
|
+
name: "library_cards",
|
|
1062
|
+
description: "The learner is stuck on the material (mixes it up, asks the same thing again) → note it now, then say in one line what you noted. Subject projects: { point, gloss } — the note lands on the current station's knowledge point. Language projects: { term, gloss, sentence? } becomes a word card; a near-duplicate comes back pending — resend it with mergeInto or asNew. Not a weak point: curiosity, your own slip (fix the quiz), or feedback on a question or the tools.",
|
|
1063
|
+
parameters: {
|
|
1064
|
+
...scopeParams,
|
|
1065
|
+
resource: {
|
|
1066
|
+
type: "string",
|
|
1067
|
+
description: "The textbook (defaults to the binding's)."
|
|
1068
|
+
},
|
|
1069
|
+
cards: {
|
|
1070
|
+
oneOf: [{
|
|
1071
|
+
type: "array",
|
|
1072
|
+
items: {
|
|
1073
|
+
type: "object",
|
|
1074
|
+
additionalProperties: true,
|
|
1075
|
+
properties: {
|
|
1076
|
+
point: {
|
|
1077
|
+
type: "string",
|
|
1078
|
+
description: "Subject: the knowledge point id."
|
|
1079
|
+
},
|
|
1080
|
+
term: {
|
|
1081
|
+
type: "string",
|
|
1082
|
+
description: "What they stuck on, in a few words."
|
|
1083
|
+
},
|
|
1084
|
+
gloss: {
|
|
1085
|
+
type: "string",
|
|
1086
|
+
description: "What they got wrong, or the meaning."
|
|
1087
|
+
},
|
|
1088
|
+
sentence: {
|
|
1089
|
+
type: "string",
|
|
1090
|
+
description: "Language: the sentence it came from."
|
|
1091
|
+
},
|
|
1092
|
+
mergeInto: { type: "string" },
|
|
1093
|
+
asNew: { type: "boolean" }
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
}, {
|
|
1097
|
+
type: "string",
|
|
1098
|
+
description: "The same list as JSON text."
|
|
1099
|
+
}],
|
|
1100
|
+
required: true
|
|
1101
|
+
}
|
|
1102
|
+
},
|
|
1103
|
+
output: jsonOutput(),
|
|
1104
|
+
isConcurrencySafe: () => false,
|
|
1105
|
+
async execute(args, exec) {
|
|
1106
|
+
const list = payloadList(args.cards);
|
|
1107
|
+
if (list === void 0) throw new Error(`library_cards takes cards: [{ point, gloss }] (subject) or [{ term, gloss, sentence? }] (language) — ${arrival("cards", args.cards)}`);
|
|
1108
|
+
const sessionId = sessionIdOf(exec);
|
|
1109
|
+
const resource = pickString(args.resource, args.book) ?? bindingOf(exec)?.resourceId;
|
|
1110
|
+
const projectId = projectOf(args.project, exec, resource);
|
|
1111
|
+
const project = profileOf(unwrap(await service.getProject({ id: projectId })).project.template);
|
|
1112
|
+
const cards = list.map((card) => {
|
|
1113
|
+
const source = payload(card.source) ?? { type: "chat" };
|
|
1114
|
+
const context = payload(card.context) ?? (pickString(card.sentence) === void 0 ? void 0 : { sentence: card.sentence });
|
|
1115
|
+
return {
|
|
1116
|
+
...card,
|
|
1117
|
+
kind: card.kind === "word" || card.kind === "concept" ? card.kind : project.markKind,
|
|
1118
|
+
...card.weaken === void 0 && source.type === "chat" ? { weaken: true } : {},
|
|
1119
|
+
source: {
|
|
1120
|
+
...source,
|
|
1121
|
+
...source.type === "chat" && source.ref === void 0 && sessionId !== void 0 ? { ref: sessionId } : {}
|
|
1122
|
+
},
|
|
1123
|
+
...context === void 0 ? {} : { context: {
|
|
1124
|
+
sentence: String(context.sentence ?? ""),
|
|
1125
|
+
...typeof context.node === "string" ? { nodeId: context.node } : {}
|
|
1126
|
+
} }
|
|
1127
|
+
};
|
|
1128
|
+
});
|
|
1129
|
+
const pointed = project.pointsDriven ? list.filter((card) => pickString(card.point) !== void 0) : [];
|
|
1130
|
+
const noted = [];
|
|
1131
|
+
for (const card of pointed) {
|
|
1132
|
+
const station = pickString(card.node, payload(card.context)?.node) ?? (sessionId === void 0 ? void 0 : service.bindingSummary(sessionId, 0).station?.id);
|
|
1133
|
+
if (station === void 0) throw new Error("a point note needs its station: node (the per-turn context names the current station and its points)");
|
|
1134
|
+
const term = pickString(card.term);
|
|
1135
|
+
const text = [term, pickString(card.gloss, card.userGloss)].filter(Boolean).join(" — ");
|
|
1136
|
+
const written = unwrap(await service.notePoint({
|
|
1137
|
+
projectId,
|
|
1138
|
+
...resource === void 0 ? {} : { resourceId: resource },
|
|
1139
|
+
nodeId: station,
|
|
1140
|
+
pointId: pickString(card.point),
|
|
1141
|
+
text: text === "" ? String(card.gloss ?? "") : text,
|
|
1142
|
+
...term === void 0 ? {} : { term },
|
|
1143
|
+
...sessionId === void 0 ? {} : { session: sessionId }
|
|
1144
|
+
}));
|
|
1145
|
+
noted.push({
|
|
1146
|
+
point: written.point.title,
|
|
1147
|
+
state: written.point.state,
|
|
1148
|
+
...written.changes.length === 0 ? {} : { changed: written.changes.map((change) => `${change.title}: ${change.from} → ${change.to}`) }
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
const rest = cards.filter((_card, index) => !pointed.includes(list[index]));
|
|
1152
|
+
if (project.pointsDriven) {
|
|
1153
|
+
const loose = rest.filter((card) => card.source?.type === "chat");
|
|
1154
|
+
if (loose.length > 0) {
|
|
1155
|
+
const points = sessionId === void 0 ? [] : service.bindingSummary(sessionId, 0).station?.mastery.points ?? [];
|
|
1156
|
+
throw new Error(`in a subject project a weak point from the conversation lands on the station's knowledge point — add point (+ node) to: ${loose.map((card) => card.term ?? "?").join(", ")}${points.length === 0 ? "" : `; this station's points: ${points.map((point) => `${point.id} (${point.title})`).join(", ")}`}. Record only what the LEARNER got wrong about the material — never your own slip (a wrong answer key: fix the quiz) and never feedback on a question or the tools. If none of that applies, record nothing.`);
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
return {
|
|
1160
|
+
...(rest.length === 0 ? void 0 : unwrap(await service.writeCards({
|
|
1161
|
+
projectId,
|
|
1162
|
+
...resource === void 0 ? {} : { resourceId: resource },
|
|
1163
|
+
cards: rest
|
|
1164
|
+
}))) ?? { ok: true },
|
|
1165
|
+
...noted.length === 0 ? {} : { pointNotes: noted }
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
//#endregion
|
|
1171
|
+
//#region lib/types/tools/quiz.js
|
|
1172
|
+
/**
|
|
1173
|
+
* The `library_quiz` tool (moved verbatim from tool-definition.ts; the shared
|
|
1174
|
+
* helpers and schemas are in `tools/kit.ts`).
|
|
1175
|
+
*
|
|
1176
|
+
* @module @khorsheed/dsh-library/tools/quiz
|
|
1177
|
+
*/
|
|
1178
|
+
/** Build the `library_quiz` tool over the shared kit. */
|
|
1179
|
+
function libraryQuizTool(kit) {
|
|
1180
|
+
const { service, bindingOf, projectOf, autoBind, scopeParams } = kit;
|
|
1181
|
+
return defineTool({
|
|
1182
|
+
name: "library_quiz",
|
|
1183
|
+
description: "Store a quiz, then reply with one line — the learner answers in the sidebar. node = the station it tests (omit: a whole-textbook quiz; resource null: a free quiz). focus is the learner's choice: mine = only their cards; book = the chapter only; mixed = their cards, then the chapter. Every question names its point; quiz weak and untested points first, then practicing ones. Solve each question again before storing; fix any disagreement. choice: answer = the right option verbatim. fill: a short value (1/2), several blanks comma-separated, alternatives \" / \". free: say what a good answer covers. Math only in $…$. The prompt carries no kind label or chapter reference. A material textbook needs quote (the page text). Replace voided (「这题有问题」) questions on the same points.",
|
|
1184
|
+
parameters: {
|
|
1185
|
+
...scopeParams,
|
|
1186
|
+
resource: {
|
|
1187
|
+
type: "json",
|
|
1188
|
+
description: "The textbook (defaults to the binding's); null = a free quiz."
|
|
1189
|
+
},
|
|
1190
|
+
node: {
|
|
1191
|
+
type: "string",
|
|
1192
|
+
description: "The quiz-gated station it gates."
|
|
1193
|
+
},
|
|
1194
|
+
focus: {
|
|
1195
|
+
type: "string",
|
|
1196
|
+
enum: [
|
|
1197
|
+
"mine",
|
|
1198
|
+
"book",
|
|
1199
|
+
"mixed"
|
|
1200
|
+
]
|
|
1201
|
+
},
|
|
1202
|
+
questions: {
|
|
1203
|
+
...QUESTIONS_SCHEMA,
|
|
1204
|
+
required: true,
|
|
1205
|
+
description: "The questions."
|
|
1206
|
+
}
|
|
1207
|
+
},
|
|
1208
|
+
output: jsonOutput(),
|
|
1209
|
+
isConcurrencySafe: () => false,
|
|
1210
|
+
async execute(args, exec) {
|
|
1211
|
+
const questions = payloadList(args.questions);
|
|
1212
|
+
if (questions === void 0) throw new Error(`library_quiz takes questions: [{ point, kind: "choice"|"fill"|"free", prompt, options?, answer, explanation?, quote? }] (+ node?, focus?) — ${arrival("questions", args.questions)}`);
|
|
1213
|
+
const node = typeof args.node === "string" && args.node !== "" ? args.node : void 0;
|
|
1214
|
+
const argsAny = args;
|
|
1215
|
+
const resourceId = args.resource === null || argsAny.book === null ? void 0 : pickString(args.resource, argsAny.book) ?? bindingOf(exec)?.resourceId;
|
|
1216
|
+
const cleaned = questions.map((raw, index) => {
|
|
1217
|
+
const text = (value) => typeof value === "string" ? value : typeof value === "number" ? String(value) : "";
|
|
1218
|
+
let next = {
|
|
1219
|
+
...raw,
|
|
1220
|
+
id: text(raw.id) === "" ? `q${index + 1}` : text(raw.id),
|
|
1221
|
+
kind: text(raw.kind),
|
|
1222
|
+
prompt: text(raw.prompt),
|
|
1223
|
+
answer: text(raw.answer)
|
|
1224
|
+
};
|
|
1225
|
+
if (next.options !== void 0) {
|
|
1226
|
+
const options = normalizeQuizOptions(next.options);
|
|
1227
|
+
if (options !== void 0) next = {
|
|
1228
|
+
...next,
|
|
1229
|
+
options
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
return normalizeQuestionAxes(next);
|
|
1233
|
+
});
|
|
1234
|
+
const projectId = projectOf(args.project, exec, resourceId);
|
|
1235
|
+
const written = unwrap(await service.putQuiz({
|
|
1236
|
+
projectId,
|
|
1237
|
+
quiz: {
|
|
1238
|
+
...args.focus === "mine" || args.focus === "book" || args.focus === "mixed" ? { focus: args.focus } : {},
|
|
1239
|
+
scope: {
|
|
1240
|
+
...resourceId === void 0 ? {} : { resourceId },
|
|
1241
|
+
...node === void 0 ? {} : { nodeId: node }
|
|
1242
|
+
},
|
|
1243
|
+
questions: cleaned,
|
|
1244
|
+
...Array.isArray(argsAny.addPoints) ? { addPoints: argsAny.addPoints } : {}
|
|
1245
|
+
}
|
|
1246
|
+
}));
|
|
1247
|
+
await autoBind(exec, {
|
|
1248
|
+
projectId,
|
|
1249
|
+
...resourceId === void 0 ? {} : { resourceId }
|
|
1250
|
+
});
|
|
1251
|
+
return written;
|
|
1252
|
+
}
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
//#endregion
|
|
1256
|
+
//#region lib/types/tools/binding.js
|
|
1257
|
+
/**
|
|
1258
|
+
* The `library_binding` tool (moved verbatim from tool-definition.ts; the shared
|
|
1259
|
+
* helpers and schemas are in `tools/kit.ts`).
|
|
1260
|
+
*
|
|
1261
|
+
* @module @khorsheed/dsh-library/tools/binding
|
|
1262
|
+
*/
|
|
1263
|
+
/** Build the `library_binding` tool over the shared kit. */
|
|
1264
|
+
function libraryBindingTool(kit) {
|
|
1265
|
+
const { service, textbookList } = kit;
|
|
1266
|
+
return defineTool({
|
|
1267
|
+
name: "library_binding",
|
|
1268
|
+
description: "Bind this session to a project (and optionally a textbook) — every tool's default. Writes bind an unbound session by themselves; use this to switch, or unbind: true.",
|
|
1269
|
+
parameters: {
|
|
1270
|
+
project: { type: "string" },
|
|
1271
|
+
resource: {
|
|
1272
|
+
type: "string",
|
|
1273
|
+
description: "A textbook in it."
|
|
1274
|
+
},
|
|
1275
|
+
unbind: { type: "boolean" }
|
|
1276
|
+
},
|
|
1277
|
+
output: jsonOutput(),
|
|
1278
|
+
isConcurrencySafe: () => false,
|
|
1279
|
+
async execute(args, exec) {
|
|
1280
|
+
const sessionId = sessionIdOf(exec);
|
|
1281
|
+
if (sessionId === void 0) throw new Error("binding needs a session — this call came from none");
|
|
1282
|
+
const argsAny = args;
|
|
1283
|
+
if (args.unbind === true || argsAny.binding === null) return unwrap(await service.setBinding(sessionId, null));
|
|
1284
|
+
const body = payload(argsAny.binding) ?? {};
|
|
1285
|
+
const projectId = pickString(body.project, body.projectId, args.project);
|
|
1286
|
+
const resourceId = pickString(body.resource, body.resourceId, body.book, body.bookId, args.resource, args.book);
|
|
1287
|
+
if (projectId === void 0) throw new Error("binding needs the project: { project, resource? } (library_read · projects lists them); unbind: true to unbind");
|
|
1288
|
+
if (resourceId !== void 0 && !(await service.getResource({
|
|
1289
|
+
projectId,
|
|
1290
|
+
resourceId
|
|
1291
|
+
})).ok) return {
|
|
1292
|
+
...unwrap(await service.setBinding(sessionId, { projectId })),
|
|
1293
|
+
note: `no resource ${JSON.stringify(resourceId)} in this project — bound the project only. Resource ids are minted (r_…); registering the textbook (library_textbook) binds this session to it by itself${textbookList(exec)}`
|
|
1294
|
+
};
|
|
1295
|
+
return unwrap(await service.setBinding(sessionId, {
|
|
1296
|
+
projectId,
|
|
1297
|
+
...resourceId === void 0 ? {} : { resourceId }
|
|
1298
|
+
}));
|
|
1299
|
+
}
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
//#endregion
|
|
1303
|
+
//#region lib/types/tools/assess.js
|
|
1304
|
+
/**
|
|
1305
|
+
* The `library_assess` tool (moved verbatim from tool-definition.ts; the shared
|
|
1306
|
+
* helpers and schemas are in `tools/kit.ts`).
|
|
1307
|
+
*
|
|
1308
|
+
* @module @khorsheed/dsh-library/tools/assess
|
|
1309
|
+
*/
|
|
1310
|
+
/** A passed station's result carries the next step: name what follows and ask (the learner starts it). */
|
|
1311
|
+
function withNext(result) {
|
|
1312
|
+
if (result.completed === void 0) return result;
|
|
1313
|
+
return {
|
|
1314
|
+
...result,
|
|
1315
|
+
next: `Congratulate the learner, name the next station${result.unlocked !== void 0 && result.unlocked.length > 0 ? ` (${result.unlocked.join(", ")})` : ""} and ask whether to start it — start only when they say so.`
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
/** Build the `library_assess` tool over the shared kit. */
|
|
1319
|
+
function libraryAssessTool(kit) {
|
|
1320
|
+
const { service, projectOf } = kit;
|
|
1321
|
+
return defineTool({
|
|
1322
|
+
name: "library_assess",
|
|
1323
|
+
description: "Record a graded sitting after judging it in the chat: one verdict per question. The stored answer can be wrong — judge by the question, and say so when it was. A hands-on station: pass node instead of quiz; each task is a line id (L1, L2…). Then tell the learner what moved and what unlocked.",
|
|
1324
|
+
parameters: {
|
|
1325
|
+
project: {
|
|
1326
|
+
type: "string",
|
|
1327
|
+
description: "Project id (defaults to the binding)."
|
|
1328
|
+
},
|
|
1329
|
+
quiz: {
|
|
1330
|
+
type: "string",
|
|
1331
|
+
description: "The quiz id."
|
|
1332
|
+
},
|
|
1333
|
+
node: {
|
|
1334
|
+
type: "string",
|
|
1335
|
+
description: "A hands-on station, instead of quiz."
|
|
1336
|
+
},
|
|
1337
|
+
resource: {
|
|
1338
|
+
type: "string",
|
|
1339
|
+
description: "Its textbook (defaults to wherever the node is)."
|
|
1340
|
+
},
|
|
1341
|
+
verdicts: {
|
|
1342
|
+
type: "array",
|
|
1343
|
+
required: true,
|
|
1344
|
+
items: {
|
|
1345
|
+
type: "object",
|
|
1346
|
+
additionalProperties: true,
|
|
1347
|
+
properties: {
|
|
1348
|
+
task: {
|
|
1349
|
+
type: "string",
|
|
1350
|
+
description: "The question id, or the line id (L1…)."
|
|
1351
|
+
},
|
|
1352
|
+
pass: { type: "boolean" },
|
|
1353
|
+
ratio: {
|
|
1354
|
+
type: "number",
|
|
1355
|
+
description: "Partial credit 0..1."
|
|
1356
|
+
},
|
|
1357
|
+
why: {
|
|
1358
|
+
type: "string",
|
|
1359
|
+
description: "One line on the judgment."
|
|
1360
|
+
},
|
|
1361
|
+
void: {
|
|
1362
|
+
type: "boolean",
|
|
1363
|
+
description: "The learner flagged it broken."
|
|
1364
|
+
},
|
|
1365
|
+
missedCard: {
|
|
1366
|
+
type: "object",
|
|
1367
|
+
additionalProperties: false,
|
|
1368
|
+
description: "Language projects: the word they missed.",
|
|
1369
|
+
properties: {
|
|
1370
|
+
term: { type: "string" },
|
|
1371
|
+
gloss: { type: "string" }
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
},
|
|
1378
|
+
output: jsonOutput(),
|
|
1379
|
+
isConcurrencySafe: () => false,
|
|
1380
|
+
async execute(args, exec) {
|
|
1381
|
+
if (!Array.isArray(args.verdicts) || args.verdicts.length === 0) throw new Error("verdicts is required — one per question: [{ task: <question id>, pass, why, ratio?, void? }]");
|
|
1382
|
+
const verdicts = args.verdicts.map((verdict) => {
|
|
1383
|
+
const why = pickString(verdict.why, verdict.criterion, verdict.evidence) ?? "";
|
|
1384
|
+
return {
|
|
1385
|
+
...verdict,
|
|
1386
|
+
criterion: pickString(verdict.criterion) ?? why,
|
|
1387
|
+
evidence: pickString(verdict.evidence) ?? why,
|
|
1388
|
+
by: verdict.by === "self" || verdict.by === "user" ? verdict.by : "llm"
|
|
1389
|
+
};
|
|
1390
|
+
});
|
|
1391
|
+
const node = pickString(args.node);
|
|
1392
|
+
if (node !== void 0) {
|
|
1393
|
+
const resource = pickString(args.resource, args.book);
|
|
1394
|
+
return withNext(unwrap(await service.putAttempt({
|
|
1395
|
+
projectId: projectOf(args.project, exec, resource),
|
|
1396
|
+
nodeId: node,
|
|
1397
|
+
...resource === void 0 ? {} : { resourceId: resource },
|
|
1398
|
+
verdicts
|
|
1399
|
+
})));
|
|
1400
|
+
}
|
|
1401
|
+
const quiz = pickString(args.quiz);
|
|
1402
|
+
if (quiz === void 0) throw new Error("quiz is the id library_quiz returned — or pass node (an acceptance station's id) for acceptance");
|
|
1403
|
+
return withNext(unwrap(await service.putAttempt({
|
|
1404
|
+
projectId: projectOf(args.project, exec),
|
|
1405
|
+
assessmentId: quiz,
|
|
1406
|
+
verdicts
|
|
1407
|
+
})));
|
|
1408
|
+
}
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
//#endregion
|
|
1412
|
+
//#region lib/types/tool-definition.js
|
|
1413
|
+
/** The names this module registers, in registration (workflow) order. */
|
|
1414
|
+
const LIBRARY_TOOL_NAMES = [
|
|
1415
|
+
"library_read",
|
|
1416
|
+
"library_project",
|
|
1417
|
+
"library_textbook",
|
|
1418
|
+
"library_roadmap",
|
|
1419
|
+
"library_notes",
|
|
1420
|
+
"library_page",
|
|
1421
|
+
"library_cards",
|
|
1422
|
+
"library_quiz",
|
|
1423
|
+
"library_binding",
|
|
1424
|
+
"library_assess"
|
|
1425
|
+
];
|
|
1426
|
+
/**
|
|
1427
|
+
* Build the tools. Creating them is the package's `./tool` row, which applies
|
|
1428
|
+
* the origin tag; this module never registers.
|
|
1429
|
+
* @param service - the library service the adapters translate to.
|
|
1430
|
+
* @param images - the session image references the tool row remembers (a
|
|
1431
|
+
* page's photo is resolved from them, never trusted to the model's copy).
|
|
1432
|
+
* @returns the definitions, untagged and unregistered, in workflow order.
|
|
1433
|
+
*/
|
|
1434
|
+
function libraryToolDefinitions(service, images = new ImageRefCache()) {
|
|
1435
|
+
const kit = createToolKit(service, images);
|
|
1436
|
+
return [
|
|
1437
|
+
libraryReadTool(kit),
|
|
1438
|
+
libraryProjectTool(kit),
|
|
1439
|
+
libraryResourceTool(kit),
|
|
1440
|
+
libraryRoadmapTool(kit),
|
|
1441
|
+
libraryNotesTool(kit),
|
|
1442
|
+
libraryPageTool(kit),
|
|
1443
|
+
libraryCardsTool(kit),
|
|
1444
|
+
libraryQuizTool(kit),
|
|
1445
|
+
libraryBindingTool(kit),
|
|
1446
|
+
libraryAssessTool(kit)
|
|
1447
|
+
];
|
|
1448
|
+
}
|
|
1449
|
+
//#endregion
|
|
1450
|
+
//#region lib/types/tool.js
|
|
1451
|
+
const PACKAGE_NAME = "@khorsheed/dsh-library";
|
|
1452
|
+
/**
|
|
1453
|
+
* Tag the model-visible tools with their origin (AGENTS.md § Tool origin
|
|
1454
|
+
* tagging; seam S12): the capability catalog reads this `Symbol.for`-keyed
|
|
1455
|
+
* tag back through `ctx.tools.get()`, so the library tools attribute to THIS
|
|
1456
|
+
* package (the row the preset mounts). The tag is host-side only and never
|
|
1457
|
+
* travels on the model wire. Inlined (the eval precedent) — importing the
|
|
1458
|
+
* helper from capability-catalog would be a cross-package edge.
|
|
1459
|
+
*/
|
|
1460
|
+
const definePluginTool = (def) => Object.assign(def, { [Symbol.for("dsh.tool.origin")]: {
|
|
1461
|
+
channel: "plugin",
|
|
1462
|
+
owner: PACKAGE_NAME
|
|
1463
|
+
} });
|
|
1464
|
+
const Config = z.object({
|
|
1465
|
+
tools: z.union([z.const("all"), z.const("none")]).default("all"),
|
|
1466
|
+
hideSections: z.array(z.string()).default([]),
|
|
1467
|
+
denyTools: z.array(z.string()).default([])
|
|
1468
|
+
});
|
|
1469
|
+
/** Cordis plugin name used by loader diagnostics. */
|
|
1470
|
+
const name = "library-tool";
|
|
1471
|
+
/**
|
|
1472
|
+
* The `library` core is a declared inject (the owning-family companion
|
|
1473
|
+
* exception to the community-service probe rule): a preset's standing scope
|
|
1474
|
+
* mounts at registry-activation time, BEFORE the profile's later bundle rows
|
|
1475
|
+
* provide the core, so a one-shot ctx.get probe at apply saw ABSENT there and
|
|
1476
|
+
* nothing re-ran the row (rc.1 boot order; 3080 production 2026-09-27). The
|
|
1477
|
+
* declared inject pends the row until the core provides, then the body
|
|
1478
|
+
* applies. The tools registry and the prompt assembly still join through
|
|
1479
|
+
* deferred injection so their mount order cannot strand the registrations.
|
|
1480
|
+
*/
|
|
1481
|
+
const inject = ["library"];
|
|
1482
|
+
/**
|
|
1483
|
+
* The coach's guidance, registered beside the tools it describes (order 114,
|
|
1484
|
+
* the eval line). Per-`what` payloads are in the tool descriptions.
|
|
1485
|
+
*/
|
|
1486
|
+
const LIBRARY_PROMPT = `The library holds the learner's projects, textbooks, paths and quizzes — record them there, never only in the chat. A refusal says what to fix: fix it and resend. Quote with 「」 inside any text you pass.
|
|
1487
|
+
|
|
1488
|
+
The learner leads; follow this order and stop where it says:
|
|
1489
|
+
1. A goal → ask one or two questions (level, direction), then library_project.
|
|
1490
|
+
2. Textbooks → library_textbook: the learner's book (verify edition and contents online), one you pick, or one you author.
|
|
1491
|
+
3. A path → library_roadmap. Then describe it in two lines and STOP: the learner reviews and adopts it in the sidebar. Before that: no 讲义, no teaching, no quiz.
|
|
1492
|
+
4. A station starts only when the learner says so (「开始学这一站」 or naming it). Then write its 讲义 for this learner (library_notes), say in one line that they are ready, and wait: the learner reads first and asks. Answer in the chat; if a question shows the 讲义 missed or got something wrong, fix that section.
|
|
1493
|
+
5. Stuck on the material → library_cards on that point. A quiz when asked → library_quiz, then one line. Grade → library_assess.
|
|
1494
|
+
6. A station passed → name the next one and ask whether to start it.
|
|
1495
|
+
|
|
1496
|
+
The per-turn "Library:" context is the current state; trust it over the chat.`;
|
|
1497
|
+
/** The bound-context line's order: right after the sandbox / approval snapshots. */
|
|
1498
|
+
const CONTEXT_ORDER = 650;
|
|
1499
|
+
/** How many weak cards the per-turn context names. */
|
|
1500
|
+
const CONTEXT_CARDS = 12;
|
|
1501
|
+
/**
|
|
1502
|
+
* Render the per-turn context for one session: its binding and the weakest
|
|
1503
|
+
* cards there. Kept short — it rides every turn.
|
|
1504
|
+
*/
|
|
1505
|
+
function renderBindingContext(service, sessionId) {
|
|
1506
|
+
const summary = service.bindingSummary(sessionId, CONTEXT_CARDS);
|
|
1507
|
+
if (summary.binding === void 0) return "Library: this session is not bound to a project yet.";
|
|
1508
|
+
if (summary.project === void 0) return "Library: this session's bound project no longer exists — ask the learner which one to use.";
|
|
1509
|
+
const { project, resource, textbooks, weakCards } = summary;
|
|
1510
|
+
const lines = [`Library: bound to project 「${project.name}」 (${project.id}, template ${project.template})` + (resource === void 0 ? "." : `, textbook 《${resource.title}》 (${resource.id}).`)];
|
|
1511
|
+
if (summary.binding.previousProjectId !== void 0 && summary.binding.previousProjectId !== project.id) {
|
|
1512
|
+
const previous = service.projectName(summary.binding.previousProjectId);
|
|
1513
|
+
lines.push(`This session worked on another project${previous === void 0 ? "" : ` 「${previous}」`} before switching here: nothing said about it is 「${project.name}」's material — never quiz, teach or note from it here.`);
|
|
1514
|
+
}
|
|
1515
|
+
if (textbooks.length === 0) lines.push("Textbooks: none yet — shelve them first (library_textbook).");
|
|
1516
|
+
else {
|
|
1517
|
+
lines.push("Textbooks (study order):");
|
|
1518
|
+
for (const textbook of textbooks) {
|
|
1519
|
+
const state = textbook.roadmap === void 0 ? textbook.hasDraft ? `draft ${textbook.draftTotal ?? 0} stations, waiting for the learner to adopt` : "no path yet" : `path adopted, ${textbook.roadmap.done}/${textbook.roadmap.total} done` + (textbook.current === void 0 ? "" : `, current: ${textbook.current.title} (${textbook.current.id})`) + (textbook.hasDraft ? `; a new draft (${textbook.draftTotal ?? 0} stations) waits to replace it` : "");
|
|
1520
|
+
lines.push(` ${textbook.seq ?? "-"}. 《${textbook.title}》 (${textbook.id}) ${state}`);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
const profile = profileOf(project.template);
|
|
1524
|
+
if (profile.pointsDriven && summary.station !== void 0) {
|
|
1525
|
+
const { station } = summary;
|
|
1526
|
+
lines.push(`Current station ${station.title} (${station.id}) — its points:`);
|
|
1527
|
+
for (const point of station.mastery.points) {
|
|
1528
|
+
const reason = masteryReason(point);
|
|
1529
|
+
lines.push(` - ${point.title} (${point.id}): ${point.state}${reason === "" ? "" : ` — ${reason}`}`);
|
|
1530
|
+
}
|
|
1531
|
+
if (station.mastery.open.length > 0) lines.push(`Open misses here: ${station.mastery.open.length} (the learner retakes them in the sidebar; they count for points, not for passing).`);
|
|
1532
|
+
}
|
|
1533
|
+
if (weakCards.length > 0) lines.push(`Weak cards: ${weakCards.map((card) => `${card.term} [${card.weakness}]`).join("; ")}.`);
|
|
1534
|
+
lines.push(`Subject: ${profile.coachHint}`);
|
|
1535
|
+
return lines.join("\n");
|
|
1536
|
+
}
|
|
1537
|
+
/**
|
|
1538
|
+
* Plugin body: register the three tools, their guidance, and the per-turn
|
|
1539
|
+
* bound context; quiet the scope when configured. The declared `library`
|
|
1540
|
+
* inject pends the row until the core provides; the in-body guard stays as
|
|
1541
|
+
* the defensive direct-call path.
|
|
1542
|
+
* @param ctx - Cordis context (the preset's agent-plane mount).
|
|
1543
|
+
* @param config - validated plugin config.
|
|
1544
|
+
*/
|
|
1545
|
+
function apply(ctx, config = {}) {
|
|
1546
|
+
const service = ctx.get("library");
|
|
1547
|
+
if (service === void 0) {
|
|
1548
|
+
ctx.logger.info(`${PACKAGE_NAME}: the global library service is absent — the library tools are not registered`);
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
if ((config.tools ?? "all") === "none") return;
|
|
1552
|
+
const images = new ImageRefCache();
|
|
1553
|
+
ctx.on("session/event", ((session, event) => {
|
|
1554
|
+
images.observe(String(session.id), event);
|
|
1555
|
+
}));
|
|
1556
|
+
ctx.inject(["tools"], (toolsCtx) => {
|
|
1557
|
+
for (const definition of libraryToolDefinitions(service, images)) toolsCtx.effect(() => toolsCtx.tools.register(definePluginTool(definition)), `${PACKAGE_NAME}: ${definition.name} tool`);
|
|
1558
|
+
for (const toolName of config.denyTools ?? []) try {
|
|
1559
|
+
toolsCtx.effect(() => toolsCtx.tools.restrict({ deny: [toolName] }), `${PACKAGE_NAME}: mask ${toolName}`);
|
|
1560
|
+
} catch (error) {
|
|
1561
|
+
ctx.logger.info(`${PACKAGE_NAME}: could not mask ${toolName} here — ${error instanceof Error ? error.message : String(error)}`);
|
|
1562
|
+
}
|
|
1563
|
+
});
|
|
1564
|
+
ctx.inject(["systemPrompt"], (promptCtx) => {
|
|
1565
|
+
const prompt = promptCtx.get("systemPrompt");
|
|
1566
|
+
if (prompt === void 0) return;
|
|
1567
|
+
prompt.section({
|
|
1568
|
+
name: "tool:library",
|
|
1569
|
+
order: 114,
|
|
1570
|
+
text: LIBRARY_PROMPT
|
|
1571
|
+
});
|
|
1572
|
+
for (const sectionName of config.hideSections ?? []) try {
|
|
1573
|
+
prompt.section({
|
|
1574
|
+
name: sectionName,
|
|
1575
|
+
order: 0,
|
|
1576
|
+
text: ""
|
|
1577
|
+
});
|
|
1578
|
+
} catch (error) {
|
|
1579
|
+
ctx.logger.info(`${PACKAGE_NAME}: could not blank ${sectionName} — ${error instanceof Error ? error.message : String(error)}`);
|
|
1580
|
+
}
|
|
1581
|
+
prompt.context({
|
|
1582
|
+
name: "library:binding",
|
|
1583
|
+
order: CONTEXT_ORDER,
|
|
1584
|
+
text: (context) => {
|
|
1585
|
+
const id = context.agent?.session?.id;
|
|
1586
|
+
if (id === void 0) return "";
|
|
1587
|
+
try {
|
|
1588
|
+
return renderBindingContext(service, String(id));
|
|
1589
|
+
} catch {
|
|
1590
|
+
return "";
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
});
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
1596
|
+
//#endregion
|
|
1597
|
+
export { Config, LIBRARY_PROMPT, LIBRARY_TOOL_NAMES, apply, inject, libraryToolDefinitions, name, renderBindingContext };
|