dsh-nerd-mode 0.4.0 → 0.4.4
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/NOTICE.md +1 -1
- package/README.md +303 -215
- package/README.zh.md +303 -217
- package/assets/calibration.svg +2 -2
- package/assets/section.md +15 -0
- package/client.js +93 -77
- package/cordis.patch.yml +10 -14
- package/lib/index.js +223 -213
- package/package.json +1 -1
- package/skills/diagram/SKILL.md +0 -2
- package/skills/study/SKILL.md +3 -3
- package/skills/study-plus/SKILL.md +26 -19
- package/skills/study-plus/scripts/book-data.mjs +3 -3
- package/skills/study-plus/scripts/build-book.mjs +5 -5
- package/skills/teach/GLOSSARY-FORMAT.md +0 -35
- package/skills/teach/LEARNING-RECORD-FORMAT.md +0 -46
- package/skills/teach/MISSION-FORMAT.md +0 -31
- package/skills/teach/RESOURCES-FORMAT.md +0 -32
- package/skills/teach/SKILL.md +0 -140
- package/skills/teach/agents/openai.yaml +0 -5
package/lib/index.js
CHANGED
|
@@ -1,213 +1,223 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Nerd Mode: a per-session switch. While it is on, one prompt section joins
|
|
3
|
-
* every request and the `study` skill becomes the default answer shape. State
|
|
4
|
-
* folds from the session log, so it survives compaction, resume, and fork. The
|
|
5
|
-
* tool set never changes.
|
|
6
|
-
*
|
|
7
|
-
* The section registers at the profile level, not inside a preset, so it
|
|
8
|
-
* reaches every agent in every preset. See docs/adr/0008.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
import
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
export const
|
|
17
|
-
|
|
18
|
-
export const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
function
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Nerd Mode: a per-session switch. While it is on, one prompt section joins
|
|
3
|
+
* every request and the `study` skill becomes the default answer shape. State
|
|
4
|
+
* folds from the session log, so it survives compaction, resume, and fork. The
|
|
5
|
+
* tool set never changes.
|
|
6
|
+
*
|
|
7
|
+
* The section registers at the profile level, not inside a preset, so it
|
|
8
|
+
* reaches every agent in every preset. See docs/adr/0008.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync } from 'node:fs'
|
|
12
|
+
import { fileURLToPath } from 'node:url'
|
|
13
|
+
import { COMMAND, KEY, fold, init } from './fold.js'
|
|
14
|
+
import z from '@deepseek-ai/schemastery'
|
|
15
|
+
|
|
16
|
+
export const name = 'dsh-nerd-mode'
|
|
17
|
+
export const PACKAGE_NAME = 'dsh-nerd-mode'
|
|
18
|
+
export const inject = ['systemPrompt', 'sessionProjections']
|
|
19
|
+
|
|
20
|
+
// The prompt text ships with the package. Keeping it out of the config stops
|
|
21
|
+
// every profile patch from carrying a stale copy of it.
|
|
22
|
+
const BUNDLED_SECTION = readFileSync(
|
|
23
|
+
fileURLToPath(new URL('../assets/section.md', import.meta.url)),
|
|
24
|
+
'utf8',
|
|
25
|
+
).trim()
|
|
26
|
+
|
|
27
|
+
export const Config = z.object({
|
|
28
|
+
section: z.string().pattern(/\S/).default(BUNDLED_SECTION),
|
|
29
|
+
recallSessionEnabled: z.boolean().default(false).volatile(),
|
|
30
|
+
unseenQuestionCount: z.number().step(1).min(0).max(10).default(3).volatile(),
|
|
31
|
+
unseenQuestionType: z.union([
|
|
32
|
+
z.const('short-answer'),
|
|
33
|
+
z.const('multiple-choice'),
|
|
34
|
+
z.const('mixed'),
|
|
35
|
+
]).default('short-answer').volatile(),
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const SECTION = 'study:policy'
|
|
39
|
+
// Between PLAN_POLICY (500) and TEAM_POLICY (600).
|
|
40
|
+
const SECTION_ORDER = 550
|
|
41
|
+
const studyPlusDefaultsBySession = new WeakMap()
|
|
42
|
+
const directStudyPlusBySession = new WeakMap()
|
|
43
|
+
// A PTC skill load leaves no message in the history, so keep its id for later
|
|
44
|
+
// turns the way an injected skill message would.
|
|
45
|
+
const ptcStudyPlusBySession = new WeakMap()
|
|
46
|
+
const SKILL_COMMAND = /(^|\s)\/([a-z0-9]+(?:-[a-z0-9]+)*)(?=\s|$)/g
|
|
47
|
+
|
|
48
|
+
// shortcut: parse-only schemas, no zod. The projection registry calls `parse`
|
|
49
|
+
// and nothing else. Upgrade to zod if the host starts introspecting schemas.
|
|
50
|
+
const stateSchema = {
|
|
51
|
+
parse: (value) => ({
|
|
52
|
+
active: value?.active === true,
|
|
53
|
+
running:
|
|
54
|
+
value?.running && typeof value.running.commandId === 'string'
|
|
55
|
+
? { commandId: value.running.commandId, wanted: value.running.wanted === true }
|
|
56
|
+
: null,
|
|
57
|
+
}),
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const viewSchema = { parse: (value) => ({ active: value?.active === true }) }
|
|
61
|
+
|
|
62
|
+
function studyPlusSettings(config, session, invocationId) {
|
|
63
|
+
let defaults = studyPlusDefaultsBySession.get(session)
|
|
64
|
+
if (defaults?.invocationId !== invocationId) {
|
|
65
|
+
defaults = {
|
|
66
|
+
invocationId,
|
|
67
|
+
recallSessionEnabled: config.recallSessionEnabled?.get?.() ?? false,
|
|
68
|
+
unseenQuestionCount: config.unseenQuestionCount?.get?.() ?? 3,
|
|
69
|
+
unseenQuestionType: config.unseenQuestionType?.get?.() ?? 'short-answer',
|
|
70
|
+
}
|
|
71
|
+
studyPlusDefaultsBySession.set(session, defaults)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return [
|
|
75
|
+
'',
|
|
76
|
+
'',
|
|
77
|
+
'Study-plus profile defaults for this tracked session:',
|
|
78
|
+
`- Recall Session: ${defaults.recallSessionEnabled ? 'enabled' : 'disabled'}.`,
|
|
79
|
+
`- Post-lecture questions: ${defaults.unseenQuestionCount}; type: ${defaults.unseenQuestionType}.`,
|
|
80
|
+
'Apply these defaults only during tracked study-plus sessions. Ignore them for ordinary study and all other requests.',
|
|
81
|
+
'An explicit preference in the current /study-plus request overrides its profile default for this session and does not update the profile.',
|
|
82
|
+
].join('\n')
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Return the message id when the message is a direct /study-plus command, else undefined. */
|
|
86
|
+
function directStudyPlusId(message) {
|
|
87
|
+
if (message?.source?.kind !== 'user') return undefined
|
|
88
|
+
const text = (message.content ?? [])
|
|
89
|
+
.filter((block) => block.type === 'text')
|
|
90
|
+
.map((block) => block.text)
|
|
91
|
+
.join('\n')
|
|
92
|
+
return [...text.matchAll(SKILL_COMMAND)].at(-1)?.[2] === 'study-plus' ? message.id : undefined
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function studyPlusInvocation(session) {
|
|
96
|
+
const directInvocation = directStudyPlusBySession.get(session)
|
|
97
|
+
if (directInvocation !== undefined) return directInvocation
|
|
98
|
+
const ptcInvocation = ptcStudyPlusBySession.get(session)
|
|
99
|
+
if (ptcInvocation !== undefined) return ptcInvocation
|
|
100
|
+
|
|
101
|
+
// The host returns the full surface history here, not just this turn. See docs/agents/dsh-session-api.md.
|
|
102
|
+
const invocation = session.deriveMessages().findLast((message) => message.source?.kind === 'skill-invocation')
|
|
103
|
+
if (invocation?.source.name === 'study-plus') return invocation.id
|
|
104
|
+
|
|
105
|
+
const completedCalls = new Set()
|
|
106
|
+
let loadedId
|
|
107
|
+
let directId
|
|
108
|
+
let skillCall
|
|
109
|
+
let ptcSkillCall
|
|
110
|
+
for (let seq = session.seq - 1; seq >= 0; seq -= 1) {
|
|
111
|
+
const event = session.eventAt(seq)
|
|
112
|
+
if (event?.type === 'turn/start') break
|
|
113
|
+
// A resumed, forked, or seeded log may carry a partial event payload, so
|
|
114
|
+
// every nested read here is guarded. One bad record must not fail a turn.
|
|
115
|
+
if (event?.type === 'tool/result') {
|
|
116
|
+
const result = event.data?.message
|
|
117
|
+
if (result !== undefined && result.isError !== true && result.source?.callId !== undefined) {
|
|
118
|
+
completedCalls.add(result.source.callId)
|
|
119
|
+
}
|
|
120
|
+
} else if (event?.type === 'tool/call' && event.data?.name === 'skill') {
|
|
121
|
+
try {
|
|
122
|
+
if (JSON.parse(event.data.arguments).name === 'study-plus') skillCall = event.data.callId
|
|
123
|
+
} catch {}
|
|
124
|
+
} else if (event?.type === 'tool/ptc-dispatch' && event.data?.name === 'skill') {
|
|
125
|
+
// PTC records a skill call made inside run_code as a dispatch, not as a
|
|
126
|
+
// `tool/call`, and the dispatch event itself carries the outcome.
|
|
127
|
+
if (event.data.isError !== true && event.data.arguments?.name === 'study-plus') {
|
|
128
|
+
ptcSkillCall = event.data.subCallId
|
|
129
|
+
}
|
|
130
|
+
} else if (event?.type === 'user/message') {
|
|
131
|
+
// `user/message` stores the message fields directly in `data`; only
|
|
132
|
+
// `tool/result` and `system/message` wrap them under `data.message`.
|
|
133
|
+
const message = event.data
|
|
134
|
+
if (message?.source?.kind === 'skill-invocation') {
|
|
135
|
+
if (message.source.name === 'study-plus') loadedId = message.id
|
|
136
|
+
} else {
|
|
137
|
+
directId = directStudyPlusId(message) ?? directId
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// A skill load counts only when its tool call completed. A typed command counts at once.
|
|
143
|
+
if (skillCall !== undefined) {
|
|
144
|
+
return completedCalls.has(skillCall) ? loadedId ?? directId ?? skillCall : undefined
|
|
145
|
+
}
|
|
146
|
+
if (ptcSkillCall !== undefined) {
|
|
147
|
+
ptcStudyPlusBySession.set(session, ptcSkillCall)
|
|
148
|
+
return loadedId ?? directId ?? ptcSkillCall
|
|
149
|
+
}
|
|
150
|
+
return loadedId ?? directId
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const projection = {
|
|
154
|
+
key: KEY,
|
|
155
|
+
stateVersion: 1,
|
|
156
|
+
stateSchema,
|
|
157
|
+
init,
|
|
158
|
+
apply: fold,
|
|
159
|
+
wire: {
|
|
160
|
+
viewSchema,
|
|
161
|
+
view: (state) => ({ active: state.active }),
|
|
162
|
+
},
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* @param config - `section` is an optional overlay text; a missing value uses
|
|
167
|
+
* the bundled `assets/section.md`.
|
|
168
|
+
*/
|
|
169
|
+
export function apply(ctx, config) {
|
|
170
|
+
const section = config?.section
|
|
171
|
+
if (typeof section !== 'string' || section.trim() === '') {
|
|
172
|
+
throw new Error('dsh-nerd-mode needs a non-empty string `section`')
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
ctx.sessionProjections.register(projection)
|
|
176
|
+
|
|
177
|
+
ctx.effect(
|
|
178
|
+
() => ctx.on('agent/inbox/claimed', ({ agent, message }) => {
|
|
179
|
+
directStudyPlusBySession.set(agent.session, directStudyPlusId(message))
|
|
180
|
+
}),
|
|
181
|
+
'dsh-nerd-mode: study-plus claim',
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
ctx.effect(
|
|
185
|
+
() =>
|
|
186
|
+
ctx.systemPrompt.section({
|
|
187
|
+
name: SECTION,
|
|
188
|
+
order: SECTION_ORDER,
|
|
189
|
+
text: (context) => {
|
|
190
|
+
if (context.agent === undefined) return ''
|
|
191
|
+
if (!ctx.sessionProjections.stateOf(context.agent.session, KEY).active) return ''
|
|
192
|
+
const session = context.agent.session
|
|
193
|
+
const invocationId = studyPlusInvocation(session)
|
|
194
|
+
if (invocationId === undefined) return section
|
|
195
|
+
return section + studyPlusSettings(config ?? {}, session, invocationId)
|
|
196
|
+
},
|
|
197
|
+
}),
|
|
198
|
+
'dsh-nerd-mode: section',
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
ctx.inject(['commands'], (commandCtx) => {
|
|
202
|
+
commandCtx.commands.register({
|
|
203
|
+
name: COMMAND,
|
|
204
|
+
description: 'Turn Nerd Mode on or off for this conversation',
|
|
205
|
+
input: { hint: '[on|off]' },
|
|
206
|
+
handler: ({ agent, rawInput }) => {
|
|
207
|
+
const arg = rawInput.trim().toLowerCase()
|
|
208
|
+
const active = ctx.sessionProjections.stateOf(agent.session, KEY).active
|
|
209
|
+
|
|
210
|
+
if (arg === 'off') {
|
|
211
|
+
return { kind: 'success', text: active ? 'Nerd Mode off.' : 'Nerd Mode is already off.' }
|
|
212
|
+
}
|
|
213
|
+
if (arg !== '' && arg !== 'on') {
|
|
214
|
+
return { kind: 'error', text: `Unknown argument "${arg}". Use /nerd-mode on or /nerd-mode off.` }
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
kind: 'success',
|
|
218
|
+
text: active ? 'Nerd Mode is already on.' : 'Nerd Mode on. /study guides a concept or a task.',
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
})
|
|
222
|
+
})
|
|
223
|
+
}
|
package/package.json
CHANGED
package/skills/diagram/SKILL.md
CHANGED
|
@@ -29,8 +29,6 @@ Write the SVG file first, then talk about it.
|
|
|
29
29
|
3. Put one caption sentence after it that states the point the picture makes. Keep the prose around it short; the figure carries the detail.
|
|
30
30
|
4. Embed the figure in the section whose step it illustrates, not in a gallery at the end.
|
|
31
31
|
|
|
32
|
-
Inside a `/teach` workspace, write the file to that workspace's `assets/` and reference it from the lesson file instead, with the same relative form.
|
|
33
|
-
|
|
34
32
|
The Markdown path is resolved against the working directory and served from disk, so it must be a file that exists when the message renders. Write the file before the sentence that shows it. No query string, no fragment, no `data:` URL: the renderer rejects all three and shows the alt text instead. To revise a figure, overwrite the file; if the learner still sees the old picture, write a new name.
|
|
35
33
|
|
|
36
34
|
## File contract
|
package/skills/study/SKILL.md
CHANGED
|
@@ -48,7 +48,7 @@ For `advanced`:
|
|
|
48
48
|
|
|
49
49
|
Accuracy rules outrank the level. No level removes a symbol definition, a justification for a derivation step, a stated assumption or approximation, or a bound. No level adds filler, repeated explanation, or a section the concept does not need. The level affects the lecture only.
|
|
50
50
|
|
|
51
|
-
For a one-off request with no graph, derive the level from the request and the supplied material. When neither states a goal or a background, ask one combined question before teaching: what the learner wants to reach and whether they have met the topic before. State the derived level in one line, then teach. Write no file.
|
|
51
|
+
For a one-off request with no graph, derive the level from the brief the upper layer supplies and from the request and the supplied material. A brief from an upper layer is final: never ask again for a goal or a background it leaves out. A bare topic, or "I want to learn X", states no goal. When no brief arrives and neither the request nor the supplied material states a goal or a background, ask one combined question before teaching: what the learner wants to reach and whether they have met the topic before. State the derived level in one line, then teach. Write no file.
|
|
52
52
|
|
|
53
53
|
## Format the answer
|
|
54
54
|
|
|
@@ -73,6 +73,6 @@ The upper layer may hand you an approved concept graph, the relevant supplied ma
|
|
|
73
73
|
|
|
74
74
|
Default to chat output. If the learner asks to save a lecture or other artifact, save it to the requested path and show the same content in chat when practical.
|
|
75
75
|
|
|
76
|
-
When an SVG would materially clarify a relationship, create it by default
|
|
76
|
+
When an SVG would materially clarify a relationship, create it by default. When the upper layer scopes a Course, write `study/<course>/diagrams/<name>.svg`. When the guide runs alone, write `diagrams/<name>.svg` and create no folder under `study/`. Load `diagram` for its visual authoring rules, but use this study-specific path instead of its generic default. Sanitize `course` and `name` as single lowercase kebab-case path segments with no slashes or dot segments. If either is unclear, choose a safe fallback. Before writing, confirm the resolved parent stays inside the intended `diagrams/` root and the target is not `study/<course>/graph.md`, `state.md`, `log.md`, or `plan.md`. Compute plotted points rather than placing them by eye. Before replacing an SVG with the same name, copy the old file to `<name>.svg.bak`, replacing any older backup.
|
|
77
77
|
|
|
78
|
-
Never read or write `study/<
|
|
78
|
+
Never read or write `study/<course>/graph.md`, `state.md`, `log.md`, or `plan.md`. Do not read or change tracker state. Study-plus owns the choice of lecture paths and the automatic lecture save during tracked sessions. Study may read or write ordinary artifacts, including lectures, drafts, and figures, at paths the user or the upper layer gives it.
|