dsh-diagnostic-tutor 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +658 -0
- package/cordis.patch.yml +23 -0
- package/lib/api.js +413 -0
- package/lib/api.js.map +1 -0
- package/lib/client.js +2029 -0
- package/lib/client.js.map +1 -0
- package/lib/contract.js +14 -0
- package/lib/contract.js.map +1 -0
- package/lib/diagnosis.js +224 -0
- package/lib/diagnosis.js.map +1 -0
- package/lib/handoff.js +194 -0
- package/lib/handoff.js.map +1 -0
- package/lib/index.js +186 -0
- package/lib/index.js.map +1 -0
- package/lib/lesson.js +285 -0
- package/lib/lesson.js.map +1 -0
- package/lib/prompt.js +96 -0
- package/lib/prompt.js.map +1 -0
- package/lib/state.js +500 -0
- package/lib/state.js.map +1 -0
- package/lib/tools.js +994 -0
- package/lib/tools.js.map +1 -0
- package/lib/trust-fence.js +101 -0
- package/lib/trust-fence.js.map +1 -0
- package/lib/types/api.d.ts +62 -0
- package/lib/types/api.d.ts.map +1 -0
- package/lib/types/contract.d.ts +147 -0
- package/lib/types/contract.d.ts.map +1 -0
- package/lib/types/diagnosis.d.ts +116 -0
- package/lib/types/diagnosis.d.ts.map +1 -0
- package/lib/types/handoff.d.ts +141 -0
- package/lib/types/handoff.d.ts.map +1 -0
- package/lib/types/index.d.ts +71 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/lesson.d.ts +295 -0
- package/lib/types/lesson.d.ts.map +1 -0
- package/lib/types/prompt.d.ts +85 -0
- package/lib/types/prompt.d.ts.map +1 -0
- package/lib/types/state.d.ts +627 -0
- package/lib/types/state.d.ts.map +1 -0
- package/lib/types/tools.d.ts +38 -0
- package/lib/types/tools.d.ts.map +1 -0
- package/lib/types/trust-fence.d.ts +53 -0
- package/lib/types/trust-fence.d.ts.map +1 -0
- package/lib/types/udt.d.ts +95 -0
- package/lib/types/udt.d.ts.map +1 -0
- package/lib/types/vocabulary.d.ts +162 -0
- package/lib/types/vocabulary.d.ts.map +1 -0
- package/lib/udt.js +141 -0
- package/lib/udt.js.map +1 -0
- package/lib/vocabulary.js +182 -0
- package/lib/vocabulary.js.map +1 -0
- package/package.json +104 -0
package/lib/handoff.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The handoff: what happens between pressing Continue and seeing a lesson.
|
|
3
|
+
*
|
|
4
|
+
* Moving from one node to another is not instant — the runtime records a focus,
|
|
5
|
+
* an agent is woken, a model turn runs, and only then does a lesson exist. That
|
|
6
|
+
* gap used to be silent, which is the worst thing a gap can be: the learner
|
|
7
|
+
* cannot tell "working" from "broken", and a seven-minute turn is
|
|
8
|
+
* indistinguishable from a dead button.
|
|
9
|
+
*
|
|
10
|
+
* So the handoff is a **persisted record with timestamps**, and everything the
|
|
11
|
+
* UI shows about progress is derived from it. Persisting it is what makes the
|
|
12
|
+
* behaviours that matter possible at all:
|
|
13
|
+
*
|
|
14
|
+
* - **idempotent** — keyed by target node, so pressing Continue twice is one
|
|
15
|
+
* handoff, not two;
|
|
16
|
+
* - **refresh-proof** — a reload re-reads the same record;
|
|
17
|
+
* - **restart-proof** — the record outlives the process;
|
|
18
|
+
* - **retryable** — the attempt count and timestamps say whether the tutor
|
|
19
|
+
* ever answered, so a retry is informed rather than blind;
|
|
20
|
+
* - **non-destructive on timeout** — a timeout is a statement about the
|
|
21
|
+
* *wait*, never about the focus. The record goes stale; the focus stands.
|
|
22
|
+
*
|
|
23
|
+
* The timestamps are also the measurement. `requestedAt → focusRecordedAt →
|
|
24
|
+
* promptedAt → firstActivityAt → lessonAt → observedAt` is the whole chain, so
|
|
25
|
+
* "where did the time go" is a question with an answer rather than an
|
|
26
|
+
* argument.
|
|
27
|
+
*/
|
|
28
|
+
import { z } from 'zod';
|
|
29
|
+
/**
|
|
30
|
+
* Where a handoff has got to.
|
|
31
|
+
*
|
|
32
|
+
* `requested` the button was pressed; nothing has been persisted yet
|
|
33
|
+
* `prompted` the focus is recorded and the tutor was woken
|
|
34
|
+
* `working` the agent has produced its first event since being woken
|
|
35
|
+
* `ready` a lesson exists for the target node
|
|
36
|
+
* `failed` the tutor could not be reached
|
|
37
|
+
*
|
|
38
|
+
* There is deliberately no `timeout` status: a timeout is the *reader's*
|
|
39
|
+
* judgement about a record that has stopped moving, and storing it would mean
|
|
40
|
+
* writing to a record because time passed. The UI derives it from `updatedAt`.
|
|
41
|
+
*/
|
|
42
|
+
export const HANDOFF_STATUSES = ['requested', 'prompted', 'working', 'ready', 'failed'];
|
|
43
|
+
export const HandoffStatusSchema = z.enum(HANDOFF_STATUSES);
|
|
44
|
+
export const HandoffSchema = z.object({
|
|
45
|
+
/** The node being moved to. The key, which is what makes this idempotent. */
|
|
46
|
+
targetNodeId: z.string().min(1),
|
|
47
|
+
courseId: z.string().min(1),
|
|
48
|
+
fromNodeId: z.string().min(1),
|
|
49
|
+
status: HandoffStatusSchema,
|
|
50
|
+
/** How many times the tutor has been asked. A retry increments this. */
|
|
51
|
+
attempts: z.number().int().min(1),
|
|
52
|
+
/**
|
|
53
|
+
* The session the tutor was asked in.
|
|
54
|
+
*
|
|
55
|
+
* Kept so the first-activity listener can ignore events from every other
|
|
56
|
+
* conversation: a busy session elsewhere must not make this handoff look like
|
|
57
|
+
* it has started.
|
|
58
|
+
*/
|
|
59
|
+
sessionId: z.string().optional(),
|
|
60
|
+
/** When the request reached the host. */
|
|
61
|
+
requestedAt: z.string(),
|
|
62
|
+
/** When the focus was durably recorded. */
|
|
63
|
+
focusRecordedAt: z.string().optional(),
|
|
64
|
+
/** When `followup` was accepted. */
|
|
65
|
+
promptedAt: z.string().optional(),
|
|
66
|
+
/** The first session event after the prompt — the tutor actually started. */
|
|
67
|
+
firstActivityAt: z.string().optional(),
|
|
68
|
+
/** When a lesson for the target node was written. */
|
|
69
|
+
lessonAt: z.string().optional(),
|
|
70
|
+
/** When a surface first rendered that lesson. */
|
|
71
|
+
observedAt: z.string().optional(),
|
|
72
|
+
failedAt: z.string().optional(),
|
|
73
|
+
failureReason: z.string().optional(),
|
|
74
|
+
updatedAt: z.string(),
|
|
75
|
+
});
|
|
76
|
+
/* -------------------------------------------------------------------------- */
|
|
77
|
+
/* Pure transitions */
|
|
78
|
+
/* -------------------------------------------------------------------------- */
|
|
79
|
+
/** Start (or restart) a handoff for a target node. */
|
|
80
|
+
export function beginHandoff(input) {
|
|
81
|
+
return {
|
|
82
|
+
courseId: input.courseId,
|
|
83
|
+
fromNodeId: input.fromNodeId,
|
|
84
|
+
targetNodeId: input.targetNodeId,
|
|
85
|
+
status: 'requested',
|
|
86
|
+
attempts: (input.previous?.attempts ?? 0) + 1,
|
|
87
|
+
requestedAt: input.now,
|
|
88
|
+
updatedAt: input.now,
|
|
89
|
+
...(input.sessionId === undefined ? {} : { sessionId: input.sessionId }),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export function withFocusRecorded(record, now) {
|
|
93
|
+
return { ...record, focusRecordedAt: now, updatedAt: now };
|
|
94
|
+
}
|
|
95
|
+
export function withPrompted(record, now) {
|
|
96
|
+
return { ...record, status: 'prompted', promptedAt: now, updatedAt: now };
|
|
97
|
+
}
|
|
98
|
+
export function withPromptFailure(record, now, reason) {
|
|
99
|
+
return { ...record, status: 'failed', failedAt: now, failureReason: reason, updatedAt: now };
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Record the tutor's first sign of activity.
|
|
103
|
+
*
|
|
104
|
+
* Only the first one is kept: this answers "how long until it started", and a
|
|
105
|
+
* later event would answer a different question.
|
|
106
|
+
*/
|
|
107
|
+
export function withActivity(record, now) {
|
|
108
|
+
if (record.firstActivityAt !== undefined)
|
|
109
|
+
return record;
|
|
110
|
+
return { ...record, status: 'working', firstActivityAt: now, updatedAt: now };
|
|
111
|
+
}
|
|
112
|
+
export function withLesson(record, now) {
|
|
113
|
+
return {
|
|
114
|
+
...record,
|
|
115
|
+
status: 'ready',
|
|
116
|
+
lessonAt: record.lessonAt ?? now,
|
|
117
|
+
firstActivityAt: record.firstActivityAt ?? now,
|
|
118
|
+
updatedAt: now,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
export function withObserved(record, now) {
|
|
122
|
+
if (record.observedAt !== undefined)
|
|
123
|
+
return record;
|
|
124
|
+
return { ...record, observedAt: now, updatedAt: now };
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* How long a surface waits before calling a quiet handoff stalled.
|
|
128
|
+
*
|
|
129
|
+
* Long, on purpose: a real model turn on a large node has taken minutes here,
|
|
130
|
+
* and telling a learner the tutor is broken while it is still thinking is worse
|
|
131
|
+
* than a longer wait. A stall is a prompt to retry, never a verdict.
|
|
132
|
+
*/
|
|
133
|
+
export const STALL_AFTER_MS = 150_000;
|
|
134
|
+
function offset(from, requestedMs) {
|
|
135
|
+
if (from === undefined)
|
|
136
|
+
return undefined;
|
|
137
|
+
const at = Date.parse(from);
|
|
138
|
+
return Number.isNaN(at) ? undefined : Math.max(0, at - requestedMs);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Derive what a surface should show.
|
|
142
|
+
*
|
|
143
|
+
* `stalled` is computed, not stored: a record that has stopped moving is not a
|
|
144
|
+
* different record, and writing to it because time passed would make the store
|
|
145
|
+
* a clock.
|
|
146
|
+
*
|
|
147
|
+
* @param record - the handoff.
|
|
148
|
+
* @param nowMs - current time, injected so this is testable.
|
|
149
|
+
* @param hasLesson - whether a lesson for the target node already exists.
|
|
150
|
+
* @returns the phase, a label, the elapsed time and the stage breakdown.
|
|
151
|
+
*/
|
|
152
|
+
export function handoffView(record, nowMs, hasLesson) {
|
|
153
|
+
const stages = {};
|
|
154
|
+
const requested = Date.parse(record.requestedAt);
|
|
155
|
+
const add = (name, from) => {
|
|
156
|
+
if (Number.isNaN(requested))
|
|
157
|
+
return;
|
|
158
|
+
const offsetMs = offset(from, requested);
|
|
159
|
+
if (offsetMs !== undefined)
|
|
160
|
+
stages[name] = offsetMs;
|
|
161
|
+
};
|
|
162
|
+
add('toFocusRecorded', record.focusRecordedAt);
|
|
163
|
+
add('toPrompted', record.promptedAt);
|
|
164
|
+
add('toFirstActivity', record.firstActivityAt);
|
|
165
|
+
add('toLesson', record.lessonAt);
|
|
166
|
+
add('toObserved', record.observedAt);
|
|
167
|
+
const elapsedMs = Math.max(0, nowMs - (Number.isNaN(requested) ? nowMs : requested));
|
|
168
|
+
const base = { targetNodeId: record.targetNodeId, elapsedMs, attempts: record.attempts, stages };
|
|
169
|
+
if (record.status === 'failed') {
|
|
170
|
+
return {
|
|
171
|
+
...base,
|
|
172
|
+
phase: 'failed',
|
|
173
|
+
label: 'Could not reach the tutor',
|
|
174
|
+
...(record.failureReason === undefined ? {} : { detail: record.failureReason }),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
if (hasLesson || record.status === 'ready') {
|
|
178
|
+
return { ...base, phase: 'lesson-ready', label: 'Lesson ready' };
|
|
179
|
+
}
|
|
180
|
+
if (record.status === 'working' || record.firstActivityAt !== undefined) {
|
|
181
|
+
if (nowMs - Date.parse(record.updatedAt) > STALL_AFTER_MS) {
|
|
182
|
+
return { ...base, phase: 'stalled', label: 'The tutor has gone quiet', detail: 'Still working, or stopped — you can ask again.' };
|
|
183
|
+
}
|
|
184
|
+
return { ...base, phase: 'tutor-working', label: 'Tutor working…' };
|
|
185
|
+
}
|
|
186
|
+
if (record.status === 'prompted') {
|
|
187
|
+
if (nowMs - Date.parse(record.updatedAt) > STALL_AFTER_MS) {
|
|
188
|
+
return { ...base, phase: 'stalled', label: 'No answer from the tutor yet', detail: 'You can ask again without losing your place.' };
|
|
189
|
+
}
|
|
190
|
+
return { ...base, phase: 'tutor-requested', label: 'Tutor requested…' };
|
|
191
|
+
}
|
|
192
|
+
return { ...base, phase: 'focus-recorded', label: 'Focus recorded' };
|
|
193
|
+
}
|
|
194
|
+
//# sourceMappingURL=handoff.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handoff.js","sourceRoot":"","sources":["../src/handoff.ts"],"names":[],"mappings":"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"}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-diagnostic-tutor — DeepSeek Harness plugin entry point.
|
|
3
|
+
*
|
|
4
|
+
* A DSH plugin is a module exporting `apply(ctx, config)`. Cordis calls it at
|
|
5
|
+
* load time and everything registered through `ctx` is disposed automatically
|
|
6
|
+
* on unload.
|
|
7
|
+
*
|
|
8
|
+
* ---------------------------------------------------------------------------
|
|
9
|
+
* v0.0.3 scope (see docs/planning/PLAN.md §7)
|
|
10
|
+
* ---------------------------------------------------------------------------
|
|
11
|
+
* Goal → detection → diagnosis map. The runtime now carries real product
|
|
12
|
+
* semantics: it records a learning goal in the learner's words, plants a map
|
|
13
|
+
* root, and lets a map grow one diagnosis at a time under rules that make
|
|
14
|
+
* unverified mastery impossible to store.
|
|
15
|
+
*
|
|
16
|
+
* Still deliberately absent:
|
|
17
|
+
* - no lesson generation, no quiz system -> v0.0.4+
|
|
18
|
+
* - no client half / UI -> v0.0.4
|
|
19
|
+
* - no resource ingestion, no RAG -> out of scope for v0.1
|
|
20
|
+
*
|
|
21
|
+
* ---------------------------------------------------------------------------
|
|
22
|
+
* Cross-version discipline (see PLAN.md 2.12 #9 / #16)
|
|
23
|
+
* ---------------------------------------------------------------------------
|
|
24
|
+
* Three harness cohorts can be resolvable at once (the running host, the
|
|
25
|
+
* shared profile fallback, and this package's own store). Therefore:
|
|
26
|
+
*
|
|
27
|
+
* 1. never `export default` — Cordis' `unwrapExports` prefers `.default` and
|
|
28
|
+
* would silently DROP `inject`, so only named exports appear here.
|
|
29
|
+
* 2. every *type* coming from `@deepseek-ai/*` is imported with `import type`
|
|
30
|
+
* (enforced by `verbatimModuleSyntax`), so no second runtime copy is
|
|
31
|
+
* resolved for anything that merely describes a shape.
|
|
32
|
+
* 3. services and instances — `tools`, `storageDomain`, `skills`,
|
|
33
|
+
* `systemPrompt`, the opened `Domain` — are always taken from `ctx` via
|
|
34
|
+
* `ctx.get(...)`, never constructed and never `instanceof`-checked. A
|
|
35
|
+
* cross-cohort copy would make such a check silently false.
|
|
36
|
+
*
|
|
37
|
+
* Pure *builder* helpers (`defineTool`, `defineDomain`, `domainTable`) are the
|
|
38
|
+
* one documented exception: they take plain data and return plain data, so
|
|
39
|
+
* they are imported as values. See the header of `state.ts`.
|
|
40
|
+
*/
|
|
41
|
+
import { API_PREFIX, registerApi } from './api.js';
|
|
42
|
+
import { withActivity } from './handoff.js';
|
|
43
|
+
import { promptSession } from './prompt.js';
|
|
44
|
+
import { UDT_DOMAIN_NAME, openUdState } from './state.js';
|
|
45
|
+
import { registerTools } from './tools.js';
|
|
46
|
+
import { describeUdtStatus, detectUdtSkill } from './udt.js';
|
|
47
|
+
/**
|
|
48
|
+
* Plugin module name. Stable kebab-case, equal to the loader row `id` in
|
|
49
|
+
* cordis.patch.yml and distinct from the npm package name.
|
|
50
|
+
*/
|
|
51
|
+
export const name = 'diagnostic-tutor';
|
|
52
|
+
/**
|
|
53
|
+
* Services this plugin requires before it may load.
|
|
54
|
+
*
|
|
55
|
+
* Only services the standard profiles guarantee are declared here — both come
|
|
56
|
+
* from `@deepseek-ai/dsh-base`. Everything optional (the skill catalog, the
|
|
57
|
+
* system prompt) is resolved lazily with `ctx.get(...)`, so a profile without
|
|
58
|
+
* them loses the corresponding enhancement instead of leaving the whole plugin
|
|
59
|
+
* tree pending. A pending plugin prints nothing at all, which is hard to
|
|
60
|
+
* diagnose.
|
|
61
|
+
*/
|
|
62
|
+
export const inject = ['tools', 'storageDomain'];
|
|
63
|
+
/**
|
|
64
|
+
* Load the plugin.
|
|
65
|
+
*
|
|
66
|
+
* `apply` is async, and Cordis keeps the fiber in `LOADING` until the returned
|
|
67
|
+
* promise settles — so `await ctx.plugin(...)` genuinely waits for the storage
|
|
68
|
+
* domain to be open, for first-run initialization to be durable, and for the
|
|
69
|
+
* tools to be registered. A nested `ctx.inject(...)` would return its own
|
|
70
|
+
* fiber, letting the outer fiber report ACTIVE while the domain was still
|
|
71
|
+
* opening.
|
|
72
|
+
*
|
|
73
|
+
* @param ctx - the Cordis context the loader hands to this plugin.
|
|
74
|
+
*/
|
|
75
|
+
export async function apply(ctx) {
|
|
76
|
+
ctx.logger.debug('[diagnostic-tutor] plugin loading');
|
|
77
|
+
const tools = ctx.get('tools');
|
|
78
|
+
const facility = ctx.get('storageDomain');
|
|
79
|
+
if (!tools || !facility) {
|
|
80
|
+
// Never degrade silently: a missing seam is a configuration error.
|
|
81
|
+
ctx.logger.error(`[diagnostic-tutor] missing required service(s):${tools ? '' : ' tools'}${facility ? '' : ' storageDomain'}`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
// A storage failure must not take the whole plugin tree down with it. The
|
|
85
|
+
// loader treats a rejection from `apply` as a fatal composition error, so an
|
|
86
|
+
// unreadable, partial or version-mismatched store file would otherwise stop
|
|
87
|
+
// every unrelated plugin in the profile from loading — and would do it
|
|
88
|
+
// silently, because nothing is registered to report it. Degrade instead:
|
|
89
|
+
// report the cause loudly, register nothing, and stay inert.
|
|
90
|
+
//
|
|
91
|
+
// First-run initialization is inside this guard on purpose. It is the first
|
|
92
|
+
// *read* of the stored document, so it is where a partial file actually
|
|
93
|
+
// throws — a domain can open successfully and still fail on the first record
|
|
94
|
+
// that does not match its schema.
|
|
95
|
+
let state;
|
|
96
|
+
try {
|
|
97
|
+
state = await openUdState(facility);
|
|
98
|
+
// First run writes the learner record; later runs leave it untouched.
|
|
99
|
+
await state.ensureLearner(new Date().toISOString());
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
ctx.logger.error(`[diagnostic-tutor] could not open storage domain "${UDT_DOMAIN_NAME}": ${error.message}. ` +
|
|
103
|
+
'The plugin is loaded but inert — no tools were registered. ' +
|
|
104
|
+
'This usually means a stored document written by an incompatible version, or one edited by hand.');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
// Unloading must leave no residue: this disposer closes the domain (which
|
|
108
|
+
// rejects new writes, drains queued ones, and releases the backend unit) and
|
|
109
|
+
// Cordis awaits the returned promise before the plugin counts as unloaded.
|
|
110
|
+
ctx.effect(() => () => state.close());
|
|
111
|
+
// Is the teaching brain present? Resolved lazily and never fatal: the
|
|
112
|
+
// runtime is useful without it, and a missing skill is a degraded mode, not
|
|
113
|
+
// an error. The result stays internal — it is logged, never surfaced to a
|
|
114
|
+
// learner through a tool, because the skill's own protocol forbids naming
|
|
115
|
+
// its files and versions in learner-facing text.
|
|
116
|
+
//
|
|
117
|
+
// Nothing is installed on the strength of it. Until v0.0.8 this gated a
|
|
118
|
+
// runtime-semantics system-prompt section that explained the storage model to
|
|
119
|
+
// a skill whose guardrails read as forbidding it; UDT v2.1's
|
|
120
|
+
// `learning_runtime_contract.md` now says all of that in the skill's own
|
|
121
|
+
// words, so the bridge was deleted rather than kept as a second voice.
|
|
122
|
+
const udt = await detectUdtSkill(ctx.get('skills'));
|
|
123
|
+
if (udt.available) {
|
|
124
|
+
ctx.logger.debug(`[diagnostic-tutor] teaching brain: ${describeUdtStatus(udt)}`);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
// Not debug: without the skill nothing will ever be taught, and a learner
|
|
128
|
+
// staring at an empty panel deserves a reason in the log. The panel says
|
|
129
|
+
// the same thing in its own words; neither names the skill's files or
|
|
130
|
+
// version, because its protocol forbids that in learner-facing text.
|
|
131
|
+
ctx.logger.warn(`[diagnostic-tutor] no teaching brain in this profile: ${describeUdtStatus(udt)}. ` +
|
|
132
|
+
'The runtime will record and display state, but no lesson will be written until the ' +
|
|
133
|
+
'Universal Diagnostic Tutor skill is installed for this workspace.');
|
|
134
|
+
}
|
|
135
|
+
// Mount the browser API if this profile has a web surface.
|
|
136
|
+
//
|
|
137
|
+
// Resolved eagerly first: a profile's web server is mounted by the base and
|
|
138
|
+
// web-app bundles, which always precede a user bundle, so it is present by
|
|
139
|
+
// the time this runs. The deferred `ctx.inject` is the fallback for the rare
|
|
140
|
+
// case where the service arrives later — awaiting the outer fiber would not
|
|
141
|
+
// wait for that nested fiber, so preferring the eager path also keeps the
|
|
142
|
+
// load deterministic for callers and tests.
|
|
143
|
+
const mountApi = (server) => {
|
|
144
|
+
if (!server)
|
|
145
|
+
return;
|
|
146
|
+
ctx.effect(() => registerApi(server, {
|
|
147
|
+
state,
|
|
148
|
+
prompt: (sessionId, text) => promptSession(ctx, sessionId, text),
|
|
149
|
+
// Three states, because two would be a lie. `null` means this scope
|
|
150
|
+
// cannot see the catalog well enough to say — the normal case in a web
|
|
151
|
+
// profile, where skills are mounted per agent — and a surface must not
|
|
152
|
+
// turn that into "no tutor installed".
|
|
153
|
+
teachingBrain: udt.available ? true : udt.catalogVisible ? false : null,
|
|
154
|
+
}));
|
|
155
|
+
ctx.logger.debug(`[diagnostic-tutor] browser API mounted at ${API_PREFIX}`);
|
|
156
|
+
};
|
|
157
|
+
const webServer = ctx.get('webServer');
|
|
158
|
+
if (webServer)
|
|
159
|
+
mountApi(webServer);
|
|
160
|
+
else
|
|
161
|
+
ctx.inject(['webServer'], (webCtx) => mountApi(webCtx.get('webServer')));
|
|
162
|
+
// First sign of life from the tutor.
|
|
163
|
+
//
|
|
164
|
+
// The handoff records when the tutor was asked; this records when it actually
|
|
165
|
+
// started, which is the difference between "waiting" and "nothing is
|
|
166
|
+
// happening". Only the first event counts, and only from the session the
|
|
167
|
+
// handoff asked in, so a busy conversation elsewhere cannot make a quiet
|
|
168
|
+
// handoff look alive.
|
|
169
|
+
ctx.on('session/event', (session, event) => {
|
|
170
|
+
const handoff = state.activeHandoff();
|
|
171
|
+
if (handoff === undefined || handoff.status !== 'prompted')
|
|
172
|
+
return;
|
|
173
|
+
if (handoff.sessionId !== undefined && String(session.id) !== handoff.sessionId)
|
|
174
|
+
return;
|
|
175
|
+
void event;
|
|
176
|
+
// Atomic, and re-checked inside the transform: the lesson write races this
|
|
177
|
+
// listener, and a plain read-modify-write here would clobber a handoff that
|
|
178
|
+
// had already become ready.
|
|
179
|
+
void state
|
|
180
|
+
.updateHandoff(handoff.targetNodeId, (current) => current.status === 'prompted' ? withActivity(current, new Date().toISOString()) : current)
|
|
181
|
+
.catch(() => { });
|
|
182
|
+
});
|
|
183
|
+
registerTools({ tools }, state);
|
|
184
|
+
ctx.logger.debug(`[diagnostic-tutor] ready — domain "${state.name}" v${state.version}, 4 tools registered`);
|
|
185
|
+
}
|
|
186
|
+
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|