@lmzhen/dsh-evolution-core 0.26.15 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -1
- package/lib/composition/index.js +3 -0
- package/lib/composition-D8Y3-ROd.js +604 -0
- package/lib/ensure-MEYMj9jn.js +222 -0
- package/lib/errors-Br2-uUSK.js +57 -0
- package/lib/http-routes-BP-S3KV-.js +149 -0
- package/lib/http-routes.js +2 -0
- package/lib/index.js +777 -840
- package/lib/restrictor.js +37 -0
- package/lib/subagent/index.js +186 -0
- package/lib/types/citations.js +192 -0
- package/lib/types/{preset-composition.d.ts → composition/composer.d.ts} +2 -2
- package/lib/types/composition/composer.js +681 -0
- package/lib/types/composition/ensure.d.ts +10 -0
- package/lib/types/composition/ensure.js +95 -0
- package/lib/types/composition/index.d.ts +5 -0
- package/lib/types/composition/index.js +7 -0
- package/lib/types/composition/preset-ids.d.ts +17 -0
- package/lib/types/composition/preset-ids.js +21 -0
- package/lib/types/composition/restricted-preset.d.ts +36 -0
- package/lib/types/composition/restricted-preset.js +42 -0
- package/lib/types/constants.js +223 -0
- package/lib/types/cost.js +67 -0
- package/lib/types/curator.d.ts +2 -0
- package/lib/types/curator.js +346 -0
- package/lib/types/diagnostics.d.ts +26 -0
- package/lib/types/diagnostics.js +23 -0
- package/lib/types/document-body.js +91 -0
- package/lib/types/drift-signals.js +319 -0
- package/lib/types/env.js +25 -0
- package/lib/types/errors.d.ts +4 -1
- package/lib/types/errors.js +59 -0
- package/lib/types/events.js +16 -0
- package/lib/types/evidence.js +65 -0
- package/lib/types/evolution-events.js +479 -0
- package/lib/types/fold-untrusted.js +44 -0
- package/lib/types/frontmatter.js +623 -0
- package/lib/types/fuzzy-match.js +135 -0
- package/lib/types/gates.js +51 -0
- package/lib/types/guards.js +27 -0
- package/lib/types/health-baseline.js +63 -0
- package/lib/types/http-routes.d.ts +67 -35
- package/lib/types/http-routes.js +135 -0
- package/lib/types/index.d.ts +13 -2
- package/lib/types/index.js +101 -0
- package/lib/types/instance-scope.js +69 -0
- package/lib/types/io.js +675 -0
- package/lib/types/learn-prompt.js +42 -0
- package/lib/types/limits.js +63 -0
- package/lib/types/live.js +107 -0
- package/lib/types/long-runs.js +96 -0
- package/lib/types/memory-store.d.ts +0 -16
- package/lib/types/memory-store.js +606 -0
- package/lib/types/mutations.js +95 -0
- package/lib/types/namespace-migration.js +156 -0
- package/lib/types/numeric.js +47 -0
- package/lib/types/opt-in.js +101 -0
- package/lib/types/package-identity.d.ts +14 -0
- package/lib/types/package-identity.js +47 -0
- package/lib/types/params.js +328 -0
- package/lib/types/payload.js +36 -0
- package/lib/types/platform-line.generated.d.ts +10 -0
- package/lib/types/platform-line.generated.js +12 -0
- package/lib/types/platform-services.d.ts +2 -2
- package/lib/types/platform-services.js +52 -0
- package/lib/types/probe.d.ts +9 -3
- package/lib/types/probe.js +122 -0
- package/lib/types/prompts.js +409 -0
- package/lib/types/quality.js +308 -0
- package/lib/types/reachability.d.ts +46 -0
- package/lib/types/reachability.js +122 -0
- package/lib/types/redact.js +257 -0
- package/lib/types/reference-rewrite.js +144 -0
- package/lib/types/reports.js +110 -0
- package/lib/types/restrictor.d.ts +11 -0
- package/lib/types/restrictor.js +48 -0
- package/lib/types/review-channel.js +59 -0
- package/lib/types/run-registry.js +345 -0
- package/lib/types/scope.js +7 -0
- package/lib/types/serial.js +17 -0
- package/lib/types/session-fold.d.ts +15 -0
- package/lib/types/session-fold.js +28 -0
- package/lib/types/session-projection.js +295 -0
- package/lib/types/signals.js +209 -0
- package/lib/types/skill-discovery.js +66 -0
- package/lib/types/skill-health.js +134 -0
- package/lib/types/skill-history.js +745 -0
- package/lib/types/skill-lint.js +112 -0
- package/lib/types/skill-reads.js +67 -0
- package/lib/types/skill-row-cells.js +123 -0
- package/lib/types/skill-store.d.ts +34 -3
- package/lib/types/skill-store.js +3528 -0
- package/lib/types/state-store.js +60 -0
- package/lib/types/store-ledger.d.ts +44 -0
- package/lib/types/store-ledger.js +117 -0
- package/lib/types/subagent/channel.d.ts +4 -0
- package/lib/types/subagent/channel.js +131 -0
- package/lib/types/subagent/index.d.ts +4 -0
- package/lib/types/subagent/index.js +5 -0
- package/lib/types/subagent/readiness.d.ts +6 -0
- package/lib/types/subagent/readiness.js +35 -0
- package/lib/types/subagent/types.d.ts +58 -0
- package/lib/types/subagent/types.js +2 -0
- package/lib/types/threats.js +338 -0
- package/lib/types/tool-dispatch.d.ts +5 -0
- package/lib/types/tool-dispatch.js +535 -0
- package/lib/types/tree-write.d.ts +53 -0
- package/lib/types/tree-write.js +40 -0
- package/lib/types/unit-members.js +121 -0
- package/lib/types/usage.js +474 -0
- package/lib/types/write-authority.d.ts +87 -0
- package/lib/types/write-authority.js +23 -0
- package/lib/types/write-gates.d.ts +111 -0
- package/lib/types/write-gates.js +344 -0
- package/lib/types/write-inventory.js +158 -0
- package/package.json +39 -11
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The family's session projections: the per-session facts the family used to re-read the log for.
|
|
3
|
+
*
|
|
4
|
+
* 0.2.x deprecates the synchronous log reads (`Session.snapshotEvents` — "existing logic may
|
|
5
|
+
* remain unmigrated for now, but new calls are prohibited",
|
|
6
|
+
* `core/session/src/index.ts:639-658`) and drives DOMAIN-OWNED units instead: a registered unit
|
|
7
|
+
* folds every committed event once, the registry owns the subscription and the per-session
|
|
8
|
+
* watermark, and a reader asks for the current state
|
|
9
|
+
* (`session/session-projection/src/index.ts` — `register`/`stateOf`). Each unit here carries
|
|
10
|
+
* exactly what one pure family reader derives from a log, so that reader keeps its pure form and
|
|
11
|
+
* gains a projection-backed one beside it.
|
|
12
|
+
*
|
|
13
|
+
* State stays bounded. The evidence unit keeps the SPARSE boundary seqs and answers the dense
|
|
14
|
+
* question by complement: a content-seq set grows with every session event, while a turn/step
|
|
15
|
+
* boundary set grows four entries per turn.
|
|
16
|
+
*
|
|
17
|
+
* The registry is part of the platform's BASE bundle (`packages/bundle/base/cordis.patch.yml:158`),
|
|
18
|
+
* so a real host always has it; a composition without it keeps loading (`ctx.inject` probe) and
|
|
19
|
+
* its readers report "cannot classify" instead of guessing — the same posture the pure readers
|
|
20
|
+
* document for a log with no seq.
|
|
21
|
+
* @module @lmzhen/dsh-evolution-core
|
|
22
|
+
*/
|
|
23
|
+
import { z } from 'zod';
|
|
24
|
+
import { isBookkeepingFrame } from "./evidence.js";
|
|
25
|
+
import { dispatchStepOf, isSkillReadToolName, skillReadNameOf } from "./tool-dispatch.js";
|
|
26
|
+
import { foldTurnEvent, initialTurnFold } from "./signals.js";
|
|
27
|
+
// Bounds are the persisted-row validation bounds (a millisecond timestamp): the clamp guard
|
|
28
|
+
// requires every `z.number()` field to carry one on the same line.
|
|
29
|
+
const MAX_TIMESTAMP_MS = 8_640_000_000_000_000;
|
|
30
|
+
const activityStateSchema = z.object({ lastEventTime: z.number().nonnegative().max(MAX_TIMESTAMP_MS) }).strict();
|
|
31
|
+
const evidenceStateSchema = z.object({
|
|
32
|
+
sequenced: z.number().int().nonnegative(),
|
|
33
|
+
through: z.number().int().min(-1).max(Number.MAX_SAFE_INTEGER),
|
|
34
|
+
bookkeeping: z.array(z.number().int().nonnegative()),
|
|
35
|
+
}).strict();
|
|
36
|
+
/** The `evolutionActivity` unit (host-only: no client view is registered for it). */
|
|
37
|
+
export const evolutionActivityProjection = {
|
|
38
|
+
key: 'evolutionActivity',
|
|
39
|
+
stateVersion: 1,
|
|
40
|
+
stateSchema: activityStateSchema,
|
|
41
|
+
init: () => ({ lastEventTime: 0 }),
|
|
42
|
+
apply: (state, event) => {
|
|
43
|
+
const time = typeof event.time === 'number' ? event.time : state.lastEventTime;
|
|
44
|
+
// Same reference when the event repeats the previous millisecond: the change feed keys on
|
|
45
|
+
// `Object.is`, and an unchanged reference produces zero downstream work.
|
|
46
|
+
return time === state.lastEventTime ? state : { lastEventTime: time };
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
/** The `evolutionEvidence` unit (host-only: no client view is registered for it). */
|
|
50
|
+
export const evolutionEvidenceProjection = {
|
|
51
|
+
key: 'evolutionEvidence',
|
|
52
|
+
stateVersion: 1,
|
|
53
|
+
stateSchema: evidenceStateSchema,
|
|
54
|
+
init: () => ({ sequenced: 0, through: -1, bookkeeping: [] }),
|
|
55
|
+
apply: (state, event) => {
|
|
56
|
+
const seq = event.seq;
|
|
57
|
+
// Only an integer seq is citable, so only an integer seq is classifiable (the same test the
|
|
58
|
+
// pure reader makes).
|
|
59
|
+
if (typeof seq !== 'number' || !Number.isInteger(seq))
|
|
60
|
+
return state;
|
|
61
|
+
const sequenced = state.sequenced + 1;
|
|
62
|
+
const through = seq > state.through ? seq : state.through;
|
|
63
|
+
if (typeof event.type !== 'string' || !isBookkeepingFrame(event.type)) {
|
|
64
|
+
return { sequenced, through, bookkeeping: state.bookkeeping };
|
|
65
|
+
}
|
|
66
|
+
return { sequenced, through, bookkeeping: [...state.bookkeeping, seq] };
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Whether one session object can be projected at all.
|
|
71
|
+
*
|
|
72
|
+
* The family's readers also take STRUCTURAL session views — `skill-reads.ts` documents the tool
|
|
73
|
+
* path's bare stub — while the registry folds a session's own log (`snapshotEvents(from, to)`,
|
|
74
|
+
* `eventAt(seq)`, `seq`, `inheritedEventCount`) whenever it has to build or advance a cell. A
|
|
75
|
+
* view without those members has no projection state to read, and asking for one would fold a
|
|
76
|
+
* log the unit cannot index.
|
|
77
|
+
* @param session - the value a caller holds as its session.
|
|
78
|
+
* @returns true when the registry can fold this object.
|
|
79
|
+
*/
|
|
80
|
+
function isProjectableSession(session) {
|
|
81
|
+
if (session === null || typeof session !== 'object')
|
|
82
|
+
return false;
|
|
83
|
+
const candidate = session;
|
|
84
|
+
// The two accessors are named on purpose: they ARE the fold contract the registry needs, and
|
|
85
|
+
// this probe only answers whether the object carries them — it reads no session history.
|
|
86
|
+
// oxlint-disable-next-line typescript/no-deprecated -- capability probe, not a history read
|
|
87
|
+
const folds = typeof candidate.eventAt === 'function';
|
|
88
|
+
// oxlint-disable-next-line typescript/no-deprecated -- capability probe, not a history read
|
|
89
|
+
const logs = typeof candidate.snapshotEvents === 'function';
|
|
90
|
+
return folds && logs && typeof candidate.seq === 'number';
|
|
91
|
+
}
|
|
92
|
+
const readsStateSchema = z.object({
|
|
93
|
+
reads: z.array(z.object({
|
|
94
|
+
key: z.string(),
|
|
95
|
+
skill: z.string().nullable(),
|
|
96
|
+
failed: z.boolean(),
|
|
97
|
+
}).strict()),
|
|
98
|
+
}).strict();
|
|
99
|
+
const signalsStateSchema = z.object({
|
|
100
|
+
through: z.number().int().min(-1).max(Number.MAX_SAFE_INTEGER),
|
|
101
|
+
lastEventTime: z.number().nonnegative().max(MAX_TIMESTAMP_MS),
|
|
102
|
+
turnStartSeq: z.number().int().min(-1).max(Number.MAX_SAFE_INTEGER),
|
|
103
|
+
turn: z.object({
|
|
104
|
+
substantive: z.boolean(),
|
|
105
|
+
toolCalls: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER),
|
|
106
|
+
userChars: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER),
|
|
107
|
+
assistantChars: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER),
|
|
108
|
+
memorySignal: z.boolean(),
|
|
109
|
+
skillSignal: z.boolean(),
|
|
110
|
+
opened: z.array(z.string()),
|
|
111
|
+
}).strict(),
|
|
112
|
+
}).strict();
|
|
113
|
+
/** The `evolutionReads` unit (host-only: no client view is registered for it). */
|
|
114
|
+
export const evolutionReadsProjection = {
|
|
115
|
+
key: 'evolutionReads',
|
|
116
|
+
stateVersion: 1,
|
|
117
|
+
stateSchema: readsStateSchema,
|
|
118
|
+
init: () => ({ reads: [] }),
|
|
119
|
+
apply: (state, event) => {
|
|
120
|
+
const frame = event;
|
|
121
|
+
const step = dispatchStepOf(frame);
|
|
122
|
+
if (step === null)
|
|
123
|
+
return state;
|
|
124
|
+
const at = state.reads.findIndex(entry => entry.key === step.key);
|
|
125
|
+
// An event with no name only SETTLES a dispatch: it can flip the failure flag of one already
|
|
126
|
+
// tracked, and a dispatch this fold never saw open has nothing to record.
|
|
127
|
+
// One settle-or-replay transition, shared by both branches below: only a tracked dispatch with
|
|
128
|
+
// a carried outcome moves, and only when the flag actually changes.
|
|
129
|
+
const settled = () => {
|
|
130
|
+
const entry = at < 0 ? undefined : state.reads[at];
|
|
131
|
+
if (entry === undefined || step.outcome === undefined)
|
|
132
|
+
return state;
|
|
133
|
+
const failed = !step.outcome.ok;
|
|
134
|
+
if (entry.failed === failed)
|
|
135
|
+
return state;
|
|
136
|
+
const reads = [...state.reads];
|
|
137
|
+
reads[at] = { key: entry.key, skill: entry.skill, failed };
|
|
138
|
+
return { reads };
|
|
139
|
+
};
|
|
140
|
+
// An event with no name only SETTLES a dispatch: it can flip the failure flag of one already
|
|
141
|
+
// tracked, and a dispatch this fold never saw open has nothing to record.
|
|
142
|
+
if (step.name === '')
|
|
143
|
+
return settled();
|
|
144
|
+
// A replayed open of a tracked dispatch: keep the first entry, still honour its outcome.
|
|
145
|
+
if (at >= 0)
|
|
146
|
+
return settled();
|
|
147
|
+
// Only a read-capable tool is worth a state entry: the reader answers skill names, and a
|
|
148
|
+
// state that tracked every dispatch would grow with the session.
|
|
149
|
+
if (!isSkillReadToolName(step.name))
|
|
150
|
+
return state;
|
|
151
|
+
const skill = skillReadNameOf({
|
|
152
|
+
kind: step.kind,
|
|
153
|
+
callId: step.callId,
|
|
154
|
+
rootCallId: step.rootCallId,
|
|
155
|
+
name: step.name,
|
|
156
|
+
arguments: step.arguments,
|
|
157
|
+
ok: step.outcome?.ok,
|
|
158
|
+
}) ?? null;
|
|
159
|
+
return { reads: [...state.reads, { key: step.key, skill, failed: step.outcome?.ok === false }] };
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
/** The `evolutionSignals` unit (host-only: no client view is registered for it). */
|
|
163
|
+
export const evolutionSignalsProjection = {
|
|
164
|
+
key: 'evolutionSignals',
|
|
165
|
+
stateVersion: 1,
|
|
166
|
+
stateSchema: signalsStateSchema,
|
|
167
|
+
init: () => ({ through: -1, lastEventTime: 0, turnStartSeq: -1, turn: initialTurnFold() }),
|
|
168
|
+
apply: (state, event) => {
|
|
169
|
+
const start = event.type === 'turn/start';
|
|
170
|
+
const turn = foldTurnEvent(start ? initialTurnFold() : state.turn, event);
|
|
171
|
+
const seq = typeof event.seq === 'number' && Number.isInteger(event.seq) ? event.seq : state.through;
|
|
172
|
+
const time = typeof event.time === 'number' ? event.time : state.lastEventTime;
|
|
173
|
+
const turnStartSeq = start ? seq : state.turnStartSeq;
|
|
174
|
+
if (turn === state.turn && seq === state.through && time === state.lastEventTime && turnStartSeq === state.turnStartSeq)
|
|
175
|
+
return state;
|
|
176
|
+
return { through: seq, lastEventTime: time, turnStartSeq, turn };
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
/**
|
|
180
|
+
* Register the family's units on the calling context's fiber.
|
|
181
|
+
*
|
|
182
|
+
* Called by the READERS (not by a plugin's mount) and idempotent by the registry's own reference
|
|
183
|
+
* count: the first read of a foldable session installs the definitions, later reads count as
|
|
184
|
+
* co-owners, and the keys survive until the last registrant unloads
|
|
185
|
+
* (`session-projection/src/index.ts:273-292`). Registering at the read is what keeps a session
|
|
186
|
+
* view the registry cannot fold (`isProjectableSession`) from ever reaching its drive — the
|
|
187
|
+
* registry folds the in-memory log on first touch, so a unit that appears mid-session is still
|
|
188
|
+
* correct (`:602-630`).
|
|
189
|
+
*
|
|
190
|
+
* Idempotent by design, so a plugin mounting later does not need its own call.
|
|
191
|
+
* @param ctx - the mounting plugin's context.
|
|
192
|
+
*/
|
|
193
|
+
export function installEvolutionProjections(ctx) {
|
|
194
|
+
const register = (registry) => {
|
|
195
|
+
registry.register(evolutionActivityProjection);
|
|
196
|
+
registry.register(evolutionEvidenceProjection);
|
|
197
|
+
registry.register(evolutionReadsProjection);
|
|
198
|
+
registry.register(evolutionSignalsProjection);
|
|
199
|
+
};
|
|
200
|
+
// Already-mounted registry first: a reader that runs in the same tick as its own mount
|
|
201
|
+
// (a unit spec, an assembly-time probe) must see its units registered, and the injected
|
|
202
|
+
// callback of a not-yet-available service is not guaranteed to have run by then.
|
|
203
|
+
// platform-declaration anchor: the platform’s `sessionProjections` service (optional; undefined ⇒ no measurement)
|
|
204
|
+
const mounted = ctx.get('sessionProjections');
|
|
205
|
+
if (mounted !== undefined) {
|
|
206
|
+
register(mounted);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
ctx.inject(['sessionProjections'], (injected) => { register(injected.sessionProjections); });
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Time of one session's newest event, or `undefined` when the unit is not installed.
|
|
213
|
+
*
|
|
214
|
+
* The curator's idleness gate reads this instead of folding the session log; `undefined` is a
|
|
215
|
+
* measurement that did not happen, which the gate answers with the deployment's fail-open policy.
|
|
216
|
+
* @param ctx - a context of the runtime (the service is read from the global store).
|
|
217
|
+
* @param session - the session to ask about.
|
|
218
|
+
* @returns the newest event's milliseconds, or undefined with no unit or no event.
|
|
219
|
+
*/
|
|
220
|
+
export function sessionLastEventTime(ctx, session) {
|
|
221
|
+
if (!isProjectableSession(session))
|
|
222
|
+
return undefined;
|
|
223
|
+
installEvolutionProjections(ctx);
|
|
224
|
+
const state = ctx.get('sessionProjections')?.stateOf(session, 'evolutionActivity');
|
|
225
|
+
if (state === undefined || state.lastEventTime === 0)
|
|
226
|
+
return undefined;
|
|
227
|
+
return state.lastEventTime;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* The skill names one session read through a non-failed dispatch, or `undefined` when the session
|
|
231
|
+
* cannot be projected (a structural view, or a host without the registry).
|
|
232
|
+
*
|
|
233
|
+
* `undefined` is NOT an empty set: the tool path's read-before-write gate keeps its previous
|
|
234
|
+
* behavior rather than refusing every write in a composition whose session object is a bare stub
|
|
235
|
+
* (`skill-reads.ts` documents that three-state posture).
|
|
236
|
+
* @param ctx - a context of the runtime.
|
|
237
|
+
* @param session - the session whose reads are wanted.
|
|
238
|
+
* @returns the names read, or undefined when the log cannot be folded.
|
|
239
|
+
*/
|
|
240
|
+
export function sessionReadNames(ctx, session) {
|
|
241
|
+
if (!isProjectableSession(session))
|
|
242
|
+
return undefined;
|
|
243
|
+
installEvolutionProjections(ctx);
|
|
244
|
+
const state = ctx.get('sessionProjections')?.stateOf(session, 'evolutionReads');
|
|
245
|
+
if (state === undefined)
|
|
246
|
+
return undefined;
|
|
247
|
+
const names = new Set();
|
|
248
|
+
for (const entry of state.reads) {
|
|
249
|
+
if (!entry.failed && entry.skill !== null)
|
|
250
|
+
names.add(entry.skill);
|
|
251
|
+
}
|
|
252
|
+
return names;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* The open turn's window signals, or `undefined` when the session cannot be projected.
|
|
256
|
+
*
|
|
257
|
+
* The window is the events since the newest `turn/start`, folded by the same `foldTurnEvent`
|
|
258
|
+
* `observeEvent` uses; `undefined` means the caller has no measurement (a structural view), not
|
|
259
|
+
* that the turn was empty.
|
|
260
|
+
* @param ctx - a context of the runtime.
|
|
261
|
+
* @param session - the session whose open turn is being judged.
|
|
262
|
+
* @returns the window signals, or undefined when the log cannot be folded.
|
|
263
|
+
*/
|
|
264
|
+
export function sessionTurnSignals(ctx, session) {
|
|
265
|
+
if (!isProjectableSession(session))
|
|
266
|
+
return undefined;
|
|
267
|
+
installEvolutionProjections(ctx);
|
|
268
|
+
const state = ctx.get('sessionProjections')?.stateOf(session, 'evolutionSignals');
|
|
269
|
+
if (state === undefined)
|
|
270
|
+
return undefined;
|
|
271
|
+
const { substantive, toolCalls, userChars, assistantChars, memorySignal, skillSignal } = state.turn;
|
|
272
|
+
return { substantive, toolCalls, userChars, assistantChars, memorySignal, skillSignal };
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* The evidence index of one session, or `undefined` when the log cannot be classified.
|
|
276
|
+
*
|
|
277
|
+
* `undefined` covers both the missing unit and a log whose frames carry no seq: the plan path's
|
|
278
|
+
* `EVIDENCE_CLASS` row is report-only and must stay silent rather than report every op.
|
|
279
|
+
* @param ctx - a context of the runtime.
|
|
280
|
+
* @param session - the session the review plan is authored against.
|
|
281
|
+
* @returns an index answering "does this seq carry content?".
|
|
282
|
+
*/
|
|
283
|
+
export function sessionEvidenceIndex(ctx, session) {
|
|
284
|
+
if (!isProjectableSession(session))
|
|
285
|
+
return undefined;
|
|
286
|
+
installEvolutionProjections(ctx);
|
|
287
|
+
const state = ctx.get('sessionProjections')?.stateOf(session, 'evolutionEvidence');
|
|
288
|
+
if (state === undefined || state.sequenced === 0)
|
|
289
|
+
return undefined;
|
|
290
|
+
const { through, bookkeeping } = state;
|
|
291
|
+
return {
|
|
292
|
+
has: (seq) => Number.isInteger(seq) && seq >= 0 && seq <= through && !bookkeeping.includes(seq),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
//# sourceMappingURL=session-projection.js.map
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic review signal gate.
|
|
3
|
+
*
|
|
4
|
+
* Scans a DSH session event log for durable learning signals before any LLM
|
|
5
|
+
* is spent. `turn/end` calls `observeEvent`; the returned review kind is
|
|
6
|
+
* accumulated until a configured interval fires.
|
|
7
|
+
*/
|
|
8
|
+
import { dispatchStepOf, isSkillToolName } from "./tool-dispatch.js";
|
|
9
|
+
const CORRECTION_PATTERNS = [
|
|
10
|
+
/(?:don'?t|do not|stop|never)\s+(?:do|use|format|explain|write|say)/i,
|
|
11
|
+
/(?:too|very|way\s+too)\s+(?:verbose|long|detailed|short|brief)/i,
|
|
12
|
+
/(?:I|we)\s+(?:prefer|like|want|need)\b/i,
|
|
13
|
+
/remember\s+(?:this|that|to)/i,
|
|
14
|
+
];
|
|
15
|
+
const FIX_PATTERNS = [
|
|
16
|
+
/worked after|fixed by|the fix was|root cause/i,
|
|
17
|
+
/retry(?:ing)? worked|workaround/i,
|
|
18
|
+
];
|
|
19
|
+
/** Text of one persisted content block, or `''` for any other shape.
|
|
20
|
+
*
|
|
21
|
+
* Content blocks cross the durable session-log boundary, so their runtime
|
|
22
|
+
* shape is `unknown` even where the static event type promises
|
|
23
|
+
* `{ type, text }`: a persisted `content: [null]` (A2-7, v18) used to throw a
|
|
24
|
+
* TypeError here and the review catch swallowed the whole turn's remaining
|
|
25
|
+
* signals. Keeping the guard in one helper also keeps the branches free of
|
|
26
|
+
* conditions the static type already excludes.
|
|
27
|
+
* @param block - one element of a persisted message `content` array.
|
|
28
|
+
* @returns the block's text when it is a text block, otherwise an empty string.
|
|
29
|
+
*/
|
|
30
|
+
function textOfBlock(block) {
|
|
31
|
+
if (block === null || typeof block !== 'object')
|
|
32
|
+
return '';
|
|
33
|
+
const candidate = block;
|
|
34
|
+
return candidate.type === 'text' && typeof candidate.text === 'string' ? candidate.text : '';
|
|
35
|
+
}
|
|
36
|
+
/** The empty turn-window fold state. */
|
|
37
|
+
export function initialTurnFold() {
|
|
38
|
+
return {
|
|
39
|
+
substantive: false,
|
|
40
|
+
toolCalls: 0,
|
|
41
|
+
userChars: 0,
|
|
42
|
+
assistantChars: 0,
|
|
43
|
+
memorySignal: false,
|
|
44
|
+
skillSignal: false,
|
|
45
|
+
opened: [],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Fold ONE session event into a turn-window state, purely.
|
|
50
|
+
*
|
|
51
|
+
* The one fold behind all three readers: `observeEvent` (the mutating face the live `turn/end`
|
|
52
|
+
* path uses), `foldTurn` (a whole-log window fold, kept for structural test sessions) and the
|
|
53
|
+
* family's `evolutionSignals` projection unit. P1-1/N4 guards stay: events arrive from disk, so
|
|
54
|
+
* a persisted `data: null` / non-object or a malformed `content` skips instead of throwing.
|
|
55
|
+
* @param state - the window so far.
|
|
56
|
+
* @param event - the next committed session event.
|
|
57
|
+
* @returns the next state; the same reference when the event carries nothing this fold reads.
|
|
58
|
+
*/
|
|
59
|
+
export function foldTurnEvent(state, event) {
|
|
60
|
+
const data = event.data;
|
|
61
|
+
if (data === null || typeof data !== 'object' || Array.isArray(data))
|
|
62
|
+
return state;
|
|
63
|
+
if (event.type === 'user/message') {
|
|
64
|
+
// 0.3.16 (E-49): a malformed content (not an array) used to throw here and
|
|
65
|
+
// break the whole signal pipeline — guard and skip instead.
|
|
66
|
+
const content = data.content;
|
|
67
|
+
if (!Array.isArray(content))
|
|
68
|
+
return state;
|
|
69
|
+
const text = content.map(textOfBlock).join(' ');
|
|
70
|
+
return {
|
|
71
|
+
...state,
|
|
72
|
+
userChars: state.userChars + text.length,
|
|
73
|
+
memorySignal: state.memorySignal || CORRECTION_PATTERNS.some(pattern => pattern.test(text)),
|
|
74
|
+
skillSignal: state.skillSignal || FIX_PATTERNS.some(pattern => pattern.test(text)),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (event.type === 'assistant/message') {
|
|
78
|
+
// V6-21/V7-09: the same content guard the user branch got in E-49, extended to `message`
|
|
79
|
+
// itself missing — `.content` on an absent message is the same TypeError one level up.
|
|
80
|
+
const message = data.message;
|
|
81
|
+
if (!message || !Array.isArray(message.content))
|
|
82
|
+
return state;
|
|
83
|
+
const text = message.content.map(textOfBlock).join(' ');
|
|
84
|
+
return { ...state, assistantChars: state.assistantChars + text.length };
|
|
85
|
+
}
|
|
86
|
+
// Tool evidence (v37 P7a): the dispatch vocabulary lives in `tool-dispatch.ts` — the ONE reader
|
|
87
|
+
// of the platform's dispatch event types — and `dispatchStepOf` is its pure half, so this fold
|
|
88
|
+
// counts one tool call per dispatch whatever the runtime mode (native or PTC).
|
|
89
|
+
// One widening assignment instead of a conditional spread: `SessionEvent`'s own type keeps `data`
|
|
90
|
+
// required, while events arriving from disk may omit it — the vocabulary takes it optional.
|
|
91
|
+
const frame = event;
|
|
92
|
+
const dispatch = dispatchStepOf(frame);
|
|
93
|
+
if (dispatch === null || dispatch.name === '' || state.opened.includes(dispatch.key))
|
|
94
|
+
return state;
|
|
95
|
+
// `toolCalls` is the model-facing call count the cadence weights by: a program's sub-dispatches
|
|
96
|
+
// are not model calls (the `run_code` call that owns them is logged natively and counted by its
|
|
97
|
+
// own `tool/call`), so only native dispatches advance it — which, since the vocabulary's dead
|
|
98
|
+
// 'program-root' value was removed (S1-E6), is simply `kind !== 'program'`. This is the ONE
|
|
99
|
+
// modality split the family's N17 guard table registers; do not add a second copy.
|
|
100
|
+
let toolCalls = state.toolCalls;
|
|
101
|
+
if (dispatch.kind !== 'program')
|
|
102
|
+
toolCalls += 1;
|
|
103
|
+
return {
|
|
104
|
+
...state,
|
|
105
|
+
opened: [...state.opened, dispatch.key],
|
|
106
|
+
toolCalls,
|
|
107
|
+
skillSignal: state.skillSignal || isSkillToolName(dispatch.name),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Per-turn dispatch ledgers, keyed by the signal object they feed.
|
|
112
|
+
*
|
|
113
|
+
* `observeEvent` folds ONE event at a time, so the ledger has to outlive the call. One WeakMap
|
|
114
|
+
* entry per `TurnSignals`, so a finished fold's ledger is collectable with the fold itself and two
|
|
115
|
+
* concurrent turns never share one.
|
|
116
|
+
*/
|
|
117
|
+
const dispatchLedgers = new WeakMap();
|
|
118
|
+
/** The ledger of the turn this signal is folding; empty before its first dispatch. */
|
|
119
|
+
function ledgerForSignal(signal) {
|
|
120
|
+
return dispatchLedgers.get(signal) ?? [];
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Fold one session event into the current turn observation (the mutating face of
|
|
124
|
+
* {@link foldTurnEvent}: every reader shares the one fold, this one copies the result back onto
|
|
125
|
+
* the caller's signal object and keeps its ledger for the next event).
|
|
126
|
+
* @param signal - the turn observation to advance.
|
|
127
|
+
* @param event - the next committed session event.
|
|
128
|
+
*/
|
|
129
|
+
export function observeEvent(signal, event) {
|
|
130
|
+
const next = foldTurnEvent({ ...signal, opened: [...ledgerForSignal(signal)] }, event);
|
|
131
|
+
signal.substantive = next.substantive;
|
|
132
|
+
signal.toolCalls = next.toolCalls;
|
|
133
|
+
signal.userChars = next.userChars;
|
|
134
|
+
signal.assistantChars = next.assistantChars;
|
|
135
|
+
signal.memorySignal = next.memorySignal;
|
|
136
|
+
signal.skillSignal = next.skillSignal;
|
|
137
|
+
dispatchLedgers.set(signal, next.opened);
|
|
138
|
+
}
|
|
139
|
+
/** Compute review cadence after `turn/end`. */
|
|
140
|
+
export function advanceReview(state, turn, signal, config) {
|
|
141
|
+
if (turn === state.lastTurn)
|
|
142
|
+
return null;
|
|
143
|
+
state.lastTurn = turn;
|
|
144
|
+
signal.substantive = signal.toolCalls >= config.substantiveMinToolCalls
|
|
145
|
+
|| signal.userChars >= config.substantiveMinUserChars
|
|
146
|
+
|| signal.assistantChars >= config.substantiveMinAgentChars;
|
|
147
|
+
if (!signal.substantive)
|
|
148
|
+
return null;
|
|
149
|
+
// Decision point 2 (0.3.24 G4.6): turnsSinceMemory is now ACTIVITY-weighted,
|
|
150
|
+
// symmetric with turnsSinceSkill below — a turn without a memory signal
|
|
151
|
+
// advances by the turn's tool-call count, so memory fires on accumulated
|
|
152
|
+
// work rather than raw turn count. The field name is kept for on-disk
|
|
153
|
+
// record compatibility; the semantic is documented here.
|
|
154
|
+
state.turnsSinceMemory += signal.memorySignal ? 1 : Math.max(1, signal.toolCalls);
|
|
155
|
+
// P3 (v3 audit): turnsSinceSkill is an ACTIVITY-weighted counter — a turn
|
|
156
|
+
// without a skill signal advances by the turn's tool-call count (so skills
|
|
157
|
+
// fire on accumulated work, not turn count). The field name is kept for
|
|
158
|
+
// on-disk record compatibility; the semantic is documented here.
|
|
159
|
+
state.turnsSinceSkill += signal.skillSignal ? 1 : Math.max(1, signal.toolCalls);
|
|
160
|
+
const memoryDue = state.turnsSinceMemory >= config.memoryInterval;
|
|
161
|
+
const skillDue = state.turnsSinceSkill >= config.skillInterval;
|
|
162
|
+
// 0.3.40: with resetOnFire=false the counters stay monotonic — the caller
|
|
163
|
+
// resets them at its injection point (the counting window is inject→inject).
|
|
164
|
+
const zero = () => {
|
|
165
|
+
state.turnsSinceMemory = 0;
|
|
166
|
+
state.turnsSinceSkill = 0;
|
|
167
|
+
};
|
|
168
|
+
if (memoryDue && skillDue) {
|
|
169
|
+
if (config.resetOnFire !== false)
|
|
170
|
+
zero();
|
|
171
|
+
return 'combined';
|
|
172
|
+
}
|
|
173
|
+
if (memoryDue) {
|
|
174
|
+
if (config.resetOnFire !== false)
|
|
175
|
+
state.turnsSinceMemory = 0;
|
|
176
|
+
return 'memory';
|
|
177
|
+
}
|
|
178
|
+
if (skillDue) {
|
|
179
|
+
if (config.resetOnFire !== false)
|
|
180
|
+
state.turnsSinceSkill = 0;
|
|
181
|
+
return 'skill';
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Fold a log slice into one turn window, purely.
|
|
187
|
+
*
|
|
188
|
+
* The whole-log reference for {@link foldTurnEvent}: a caller that holds events (a fixture, a
|
|
189
|
+
* structural view, a replay) reads a window without touching a platform Session. Production reads
|
|
190
|
+
* the same window live from the family's `evolutionSignals` projection (`sessionTurnSignals`),
|
|
191
|
+
* which folds `foldTurnEvent` once per committed event.
|
|
192
|
+
* @param events - the log, oldest first. Pass an array, not a live iterator, when it can grow.
|
|
193
|
+
* @param fromSeq - index of the first event of the window (0 for the whole log).
|
|
194
|
+
* @returns the window's signals.
|
|
195
|
+
*/
|
|
196
|
+
export function foldTurnEvents(events, fromSeq = 0) {
|
|
197
|
+
// Materialize once: a live iterator must not be re-entered, and `Array.isArray` on an
|
|
198
|
+
// `Iterable` would narrow to `any[]`.
|
|
199
|
+
const list = [...events];
|
|
200
|
+
let state = initialTurnFold();
|
|
201
|
+
for (let index = Math.max(0, fromSeq); index < list.length; index += 1) {
|
|
202
|
+
const event = list[index];
|
|
203
|
+
if (event)
|
|
204
|
+
state = foldTurnEvent(state, event);
|
|
205
|
+
}
|
|
206
|
+
const { substantive, toolCalls, userChars, assistantChars, memorySignal, skillSignal } = state;
|
|
207
|
+
return { substantive, toolCalls, userChars, assistantChars, memorySignal, skillSignal };
|
|
208
|
+
}
|
|
209
|
+
//# sourceMappingURL=signals.js.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 技能发现形状 —— 家族里**唯一**的实现,门禁(`packages/scripts/verify-graph-edges.mjs`)
|
|
3
|
+
* 与图谱/边解析必须都调用它(R21/R69:两处各自实现=68/63 并存那类症状的根源)。
|
|
4
|
+
*
|
|
5
|
+
* 平台判定见 skill-filesystem/src/index.ts (`isPotentialSkillPath`:`<root>/<name>.md`
|
|
6
|
+
* 或 `<root>/<name>/SKILL.md`,即 depth<=2) 与 parseSkillFile:816-819 (`isSkillName`:kebab-case);
|
|
7
|
+
* 排除机制是三条而非一条前缀规则:`.archive/`、`.backups/` 靠**深度**(3 段 > 2),
|
|
8
|
+
* `._x/` 靠**名字文法**,`.system` 才是真前缀排除(:666/:680)。
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
/** 技能名文法(与平台 `isSkillName` 等价:kebab-case,点前缀与下划线一律不是技能名)。 */
|
|
13
|
+
export const SKILL_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
14
|
+
/** 名字是否是技能名 —— `._x`、`Bad_Name`、`.archive` 都返回 false。 */
|
|
15
|
+
export function isSkillName(name) {
|
|
16
|
+
return SKILL_NAME_RE.test(name);
|
|
17
|
+
}
|
|
18
|
+
/** 相对技能根的路径是否落在平台会发现的位置(depth<=2 且最后一段是 SKILL.md 或 `<name>.md`)。 */
|
|
19
|
+
export function isPotentialSkillPath(relPath) {
|
|
20
|
+
const parts = relPath.split(/[\\/]+/).filter(p => p.length > 0);
|
|
21
|
+
if (parts.length < 1 || parts.length > 2)
|
|
22
|
+
return false;
|
|
23
|
+
// `noUncheckedIndexedAccess` is on: an empty part list cannot happen (length >= 1 above), but the
|
|
24
|
+
// index expression still types as `string | undefined` -- an empty fallback fails both tests below.
|
|
25
|
+
const last = parts[parts.length - 1] ?? '';
|
|
26
|
+
if (parts.length === 2)
|
|
27
|
+
return last === 'SKILL.md' && isSkillName(parts[0] ?? '');
|
|
28
|
+
return last.endsWith('.md') && isSkillName(last.slice(0, -3));
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 扫描一个技能根,返回平台**真正会发现**的技能(非递归、depth<=2、含 `SKILL.md` 的目录、
|
|
32
|
+
* 名字过 kebab-case、跳过点前缀目录)。忽略目录里的边不得计入,也不得算悬空。
|
|
33
|
+
*
|
|
34
|
+
* @param root - 技能根目录。
|
|
35
|
+
* @returns 按名字排序的技能列表(根缺失时返回空数组)。
|
|
36
|
+
*/
|
|
37
|
+
export function discoverSkillFiles(root) {
|
|
38
|
+
if (!existsSync(root))
|
|
39
|
+
return [];
|
|
40
|
+
const found = [];
|
|
41
|
+
let entries = [];
|
|
42
|
+
try {
|
|
43
|
+
entries = readdirSync(root);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
for (const entry of entries) {
|
|
49
|
+
if (!isSkillName(entry))
|
|
50
|
+
continue;
|
|
51
|
+
const dir = join(root, entry);
|
|
52
|
+
try {
|
|
53
|
+
if (!statSync(dir).isDirectory())
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const md = join(dir, 'SKILL.md');
|
|
60
|
+
if (!existsSync(md))
|
|
61
|
+
continue;
|
|
62
|
+
found.push({ name: entry, path: md });
|
|
63
|
+
}
|
|
64
|
+
return found.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=skill-discovery.js.map
|