@khorsheed/dsh-context-lens 0.0.0-stage → 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/CHANGELOG.md +16 -0
- package/README.en.md +53 -0
- package/README.i18n.yaml +6 -0
- package/README.md +52 -2
- package/cordis.patch.yml +13 -0
- package/lib/client.js +26953 -0
- package/lib/index.js +3943 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +1569 -0
- package/lib/typert.remote-client.d.ts +40 -0
- package/lib/typert.remote-client.js +1202 -0
- package/lib/types/changes.d.ts +72 -0
- package/lib/types/changes.js +328 -0
- package/lib/types/client/AgentTabs.d.ts +34 -0
- package/lib/types/client/AgentTabs.js +89 -0
- package/lib/types/client/ChangeList.d.ts +28 -0
- package/lib/types/client/ChangeList.js +109 -0
- package/lib/types/client/ConfigFacts.d.ts +21 -0
- package/lib/types/client/ConfigFacts.js +30 -0
- package/lib/types/client/HeaderUtility.d.ts +12 -0
- package/lib/types/client/HeaderUtility.js +8 -0
- package/lib/types/client/InspectorPanel.d.ts +5 -0
- package/lib/types/client/InspectorPanel.js +486 -0
- package/lib/types/client/RawView.d.ts +18 -0
- package/lib/types/client/RawView.js +8 -0
- package/lib/types/client/RequestFacts.d.ts +19 -0
- package/lib/types/client/RequestFacts.js +39 -0
- package/lib/types/client/RoomDecisionsView.d.ts +24 -0
- package/lib/types/client/RoomDecisionsView.js +52 -0
- package/lib/types/client/SchemaFields.d.ts +13 -0
- package/lib/types/client/SchemaFields.js +96 -0
- package/lib/types/client/SegCtl.d.ts +32 -0
- package/lib/types/client/SegCtl.js +12 -0
- package/lib/types/client/SegmentBlocks.d.ts +28 -0
- package/lib/types/client/SegmentBlocks.js +225 -0
- package/lib/types/client/TimingView.d.ts +17 -0
- package/lib/types/client/TimingView.js +98 -0
- package/lib/types/client/TokenView.d.ts +21 -0
- package/lib/types/client/TokenView.js +22 -0
- package/lib/types/client/ToolCatalog.d.ts +9 -0
- package/lib/types/client/ToolCatalog.js +51 -0
- package/lib/types/client/Trend.d.ts +25 -0
- package/lib/types/client/Trend.js +100 -0
- package/lib/types/client/TurnAction.d.ts +14 -0
- package/lib/types/client/TurnAction.js +31 -0
- package/lib/types/client/TurnMenu.d.ts +36 -0
- package/lib/types/client/TurnMenu.js +50 -0
- package/lib/types/client/aggregate.d.ts +141 -0
- package/lib/types/client/aggregate.js +213 -0
- package/lib/types/client/contract.d.ts +58 -0
- package/lib/types/client/contract.js +1 -0
- package/lib/types/client/definition.d.ts +47 -0
- package/lib/types/client/definition.js +25 -0
- package/lib/types/client/icons-local.d.ts +19 -0
- package/lib/types/client/icons-local.js +3 -0
- package/lib/types/client/icons.d.ts +18 -0
- package/lib/types/client/icons.js +2 -0
- package/lib/types/client/index.d.ts +42 -0
- package/lib/types/client/index.js +139 -0
- package/lib/types/client/locales.d.ts +365 -0
- package/lib/types/client/locales.js +705 -0
- package/lib/types/client/preset-visibility.d.ts +99 -0
- package/lib/types/client/preset-visibility.js +108 -0
- package/lib/types/client/scroll-sync.d.ts +54 -0
- package/lib/types/client/scroll-sync.js +118 -0
- package/lib/types/client/timing.d.ts +29 -0
- package/lib/types/client/timing.js +117 -0
- package/lib/types/client/wire.d.ts +13 -0
- package/lib/types/client/wire.js +11 -0
- package/lib/types/index.d.ts +32 -0
- package/lib/types/index.js +25 -0
- package/lib/types/log-lane.d.ts +198 -0
- package/lib/types/log-lane.js +841 -0
- package/lib/types/messages.d.ts +96 -0
- package/lib/types/messages.js +254 -0
- package/lib/types/prefix-diff.d.ts +4 -0
- package/lib/types/prefix-diff.js +20 -0
- package/lib/types/remote.d.ts +45 -0
- package/lib/types/remote.js +99 -0
- package/lib/types/room.d.ts +66 -0
- package/lib/types/room.js +101 -0
- package/lib/types/segmenter.d.ts +100 -0
- package/lib/types/segmenter.js +184 -0
- package/lib/types/service.d.ts +145 -0
- package/lib/types/service.js +493 -0
- package/lib/types/store.d.ts +424 -0
- package/lib/types/store.js +1920 -0
- package/lib/types/tool-summary.d.ts +6 -0
- package/lib/types/tool-summary.js +7 -0
- package/lib/types/types.d.ts +703 -0
- package/lib/types/types.js +18 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +133 -3
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
export function emptyRoom() {
|
|
2
|
+
return { members: new Map(), dispatches: [], runs: new Map(), handoffs: [], decisions: [], seen: new Set() };
|
|
3
|
+
}
|
|
4
|
+
const str = (value) => typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
5
|
+
const num = (value) => typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
6
|
+
/**
|
|
7
|
+
* Apply one `room/*` event. Unknown types and malformed payloads are ignored.
|
|
8
|
+
* @returns whether the event was a room event this module reads.
|
|
9
|
+
*/
|
|
10
|
+
export function applyRoomEvent(room, type, data, seq, at) {
|
|
11
|
+
if (!type.startsWith('room/'))
|
|
12
|
+
return false;
|
|
13
|
+
if (seq !== undefined) {
|
|
14
|
+
if (room.seen.has(seq))
|
|
15
|
+
return true;
|
|
16
|
+
room.seen.add(seq);
|
|
17
|
+
}
|
|
18
|
+
const d = (typeof data === 'object' && data !== null ? data : {});
|
|
19
|
+
switch (type) {
|
|
20
|
+
case 'room/member-added':
|
|
21
|
+
case 'room/member-updated': {
|
|
22
|
+
const id = str(d['id']), name = str(d['name']);
|
|
23
|
+
if (id !== undefined && name !== undefined)
|
|
24
|
+
room.members.set(id, name);
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
case 'room/dispatch': {
|
|
28
|
+
if (d['origin'] !== 'human' || seq === undefined || at === undefined)
|
|
29
|
+
return true;
|
|
30
|
+
const targets = Array.isArray(d['targets']) ? d['targets'].filter((t) => typeof t === 'string') : [];
|
|
31
|
+
room.dispatches.push({ seq, at, text: str(d['text']) ?? '', targets });
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
case 'room/run-state': {
|
|
35
|
+
const runId = str(d['runId']);
|
|
36
|
+
if (runId === undefined)
|
|
37
|
+
return true;
|
|
38
|
+
const [head, ...rest] = runId.split(':');
|
|
39
|
+
const dispatchSeq = Number(head);
|
|
40
|
+
if (!Number.isSafeInteger(dispatchSeq))
|
|
41
|
+
return true;
|
|
42
|
+
const run = room.runs.get(runId) ?? { runId, dispatchSeq, memberId: rest.join(':') };
|
|
43
|
+
const member = str(d['member']), state = str(d['state']), child = str(d['childSessionId']);
|
|
44
|
+
const startedAt = num(d['startedAt']), elapsed = num(d['elapsedMs']), childTurn = num(d['childTurn']);
|
|
45
|
+
if (member !== undefined)
|
|
46
|
+
run.member = member;
|
|
47
|
+
if (state !== undefined)
|
|
48
|
+
run.state = state;
|
|
49
|
+
if (startedAt !== undefined)
|
|
50
|
+
run.startedAt = startedAt;
|
|
51
|
+
if (startedAt !== undefined && elapsed !== undefined)
|
|
52
|
+
run.endedAt = startedAt + elapsed;
|
|
53
|
+
else if ((state === 'done' || state === 'failed') && at !== undefined)
|
|
54
|
+
run.endedAt ??= at;
|
|
55
|
+
if (child !== undefined)
|
|
56
|
+
run.childSessionId = child;
|
|
57
|
+
if (childTurn !== undefined)
|
|
58
|
+
run.childTurn = childTurn;
|
|
59
|
+
room.runs.set(runId, run);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
case 'room/decision': {
|
|
63
|
+
const call = (typeof d['call'] === 'object' && d['call'] !== null ? d['call'] : undefined);
|
|
64
|
+
const id = str(d['id']), scenario = str(d['scenario']), outcome = str(d['outcome']);
|
|
65
|
+
if (call === undefined || id === undefined || scenario === undefined || outcome === undefined || seq === undefined || at === undefined)
|
|
66
|
+
return true;
|
|
67
|
+
const request = (typeof call['request'] === 'object' && call['request'] !== null ? call['request'] : {});
|
|
68
|
+
const failure = call['failure'];
|
|
69
|
+
const about = (typeof d['about'] === 'object' && d['about'] !== null ? d['about'] : {});
|
|
70
|
+
room.decisions.push({
|
|
71
|
+
id, seq, at, scenario, outcome, model: str(call['model']) ?? 'typesafe', about,
|
|
72
|
+
request: { state: (request['state'] ?? null), questions: (request['questions'] ?? null) },
|
|
73
|
+
...str(d['rule']) === undefined ? {} : { rule: str(d['rule']) },
|
|
74
|
+
...num(d['probability']) === undefined ? {} : { probability: num(d['probability']) },
|
|
75
|
+
...num(call['latencyMs']) === undefined ? {} : { latencyMs: num(call['latencyMs']) },
|
|
76
|
+
...typeof call['cached'] === 'boolean' ? { cached: call['cached'] } : {},
|
|
77
|
+
...num(call['inputTokens']) === undefined ? {} : { inputTokens: num(call['inputTokens']) },
|
|
78
|
+
...num(call['outputTokens']) === undefined ? {} : { outputTokens: num(call['outputTokens']) },
|
|
79
|
+
...call['response'] === undefined ? {} : { response: call['response'] },
|
|
80
|
+
...typeof failure?.reason === 'string' ? { failure: { reason: failure.reason, ...typeof failure.detail === 'string' ? { detail: failure.detail } : {} } } : {},
|
|
81
|
+
});
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
case 'room/coordinator': {
|
|
85
|
+
const memberId = str(d['memberId']);
|
|
86
|
+
if (memberId !== undefined && seq !== undefined && at !== undefined)
|
|
87
|
+
room.handoffs.push({ seq, at, memberId });
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
default:
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** The runs a dispatch started, in start order. */
|
|
95
|
+
export function runsOf(room, dispatchSeq) {
|
|
96
|
+
return [...room.runs.values()].filter(run => run.dispatchSeq === dispatchSeq)
|
|
97
|
+
.sort((a, b) => (a.startedAt ?? 0) - (b.startedAt ?? 0));
|
|
98
|
+
}
|
|
99
|
+
/** A synthetic, stable panel address for a room round (far below any purpose bucket). */
|
|
100
|
+
export const ROOM_TURN_BASE = -1_000_000;
|
|
101
|
+
export const roomTurnSeq = (dispatchSeq) => ROOM_TURN_BASE - dispatchSeq;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The four-segment split of one captured request: system / tools / history /
|
|
3
|
+
* added. History vs added compares message identity AND content against the previous
|
|
4
|
+
* request IN THE SAME SCOPE (durable message ids and content hashes
|
|
5
|
+
* otherwise); "added" therefore includes the previous response and its tool
|
|
6
|
+
* results, and a compaction that rewrote the surface shows up as new content —
|
|
7
|
+
* exactly what the model saw change.
|
|
8
|
+
*
|
|
9
|
+
* System rows are attributed to `system-prompt/assemble` sections by content:
|
|
10
|
+
* the capture renders each observed assembly (same interpolate-filter-join
|
|
11
|
+
* rules as the host's `renderPrompt`) and a system message whose text equals a
|
|
12
|
+
* rendered assembly splits into that assembly's named sections. Anything that
|
|
13
|
+
* fails to match stays one unattributed row — attribution is presentation,
|
|
14
|
+
* never worth faking.
|
|
15
|
+
*
|
|
16
|
+
* @module @khorsheed/dsh-context-lens/segmenter
|
|
17
|
+
*/
|
|
18
|
+
import type { CapturedRequest, SegmentItem, ToolDefItem } from './types.ts';
|
|
19
|
+
import { type CapturedMessage } from './messages.ts';
|
|
20
|
+
/** One tool definition as the waterfall carries it (host `ToolSchema`, structurally). */
|
|
21
|
+
export interface CapturedToolSchema {
|
|
22
|
+
name: string;
|
|
23
|
+
description: string;
|
|
24
|
+
parameters: Record<string, unknown>;
|
|
25
|
+
deferLoading?: true;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* One rendered `system-prompt/assemble` output, kept for content-matching
|
|
29
|
+
* against later requests' system messages.
|
|
30
|
+
*/
|
|
31
|
+
export interface AssemblyRecord {
|
|
32
|
+
/** The whole rendered prompt (sections interpolated, empties dropped, `\n\n`-joined). */
|
|
33
|
+
rendered: string;
|
|
34
|
+
/** Per-section rendered text, in assembly order. */
|
|
35
|
+
sections: {
|
|
36
|
+
name: string;
|
|
37
|
+
text: string;
|
|
38
|
+
}[];
|
|
39
|
+
at: number;
|
|
40
|
+
}
|
|
41
|
+
/** The previous request's diff baseline within one scope. */
|
|
42
|
+
export interface ScopeFingerprint {
|
|
43
|
+
messageKeys: string[];
|
|
44
|
+
toolNames: string[];
|
|
45
|
+
/** The one-shot system slot text, for its freshness on the next request. */
|
|
46
|
+
systemSlot?: string;
|
|
47
|
+
}
|
|
48
|
+
/** Everything the split needs beyond the request's own payload. */
|
|
49
|
+
export interface SegmentContext {
|
|
50
|
+
/** Previous request's fingerprint in this scope; absent for the scope's first request. */
|
|
51
|
+
previous?: ScopeFingerprint;
|
|
52
|
+
/** Recently observed assemblies, oldest first. */
|
|
53
|
+
assemblies: readonly AssemblyRecord[];
|
|
54
|
+
/** Resolve a tool-call id to its tool name (tool rows label themselves with it). */
|
|
55
|
+
resolveToolName?: (callId: string) => string | undefined;
|
|
56
|
+
/** Resolve a durable assistant message id to its producing request's generation time. */
|
|
57
|
+
turnFor?: (messageId: string) => number | undefined;
|
|
58
|
+
durationFor?: (messageId: string) => number | undefined;
|
|
59
|
+
}
|
|
60
|
+
/** The split result plus the fingerprint the scope's NEXT request diffs against. */
|
|
61
|
+
export interface SegmentResult {
|
|
62
|
+
system: SegmentItem[];
|
|
63
|
+
tools: ToolDefItem[];
|
|
64
|
+
history: SegmentItem[];
|
|
65
|
+
added: SegmentItem[];
|
|
66
|
+
fingerprint: ScopeFingerprint;
|
|
67
|
+
/** 请求体总字符数(明细头右上)— system + tools + history + added 之和。 */
|
|
68
|
+
totalChars: number;
|
|
69
|
+
}
|
|
70
|
+
/** A `system-prompt/assemble` waterfall value, structurally (host `PromptAssembly`). */
|
|
71
|
+
export interface CapturedAssembly {
|
|
72
|
+
sections: {
|
|
73
|
+
name: string;
|
|
74
|
+
text: string;
|
|
75
|
+
interpolate?: boolean;
|
|
76
|
+
}[];
|
|
77
|
+
variables: Record<string, string | undefined>;
|
|
78
|
+
}
|
|
79
|
+
/** Cap on retained assemblies; assemblies older than the last 32 never match again in practice. */
|
|
80
|
+
export declare const ASSEMBLY_RING_CAP = 32;
|
|
81
|
+
/**
|
|
82
|
+
* Record one post-waterfall assembly into the ring. Adjacent duplicates (a
|
|
83
|
+
* listener-free re-assembly between steps) collapse to one entry.
|
|
84
|
+
*/
|
|
85
|
+
export declare function recordAssembly(ring: AssemblyRecord[], assembly: CapturedAssembly, at: number): void;
|
|
86
|
+
/** Split one request's payload into the four segments. Pure: no I/O, no clock. */
|
|
87
|
+
export declare function splitRequest(input: {
|
|
88
|
+
/** All `options.messages`, normalized, in wire order. */
|
|
89
|
+
messages: CapturedMessage[];
|
|
90
|
+
/** `options.system` (one-shot callers), when present. */
|
|
91
|
+
systemSlot?: string;
|
|
92
|
+
/** `options.tools`, normalized. */
|
|
93
|
+
tools?: CapturedToolSchema[];
|
|
94
|
+
context: SegmentContext;
|
|
95
|
+
}): SegmentResult;
|
|
96
|
+
/** A late log backfill can supply the baseline that was missing at capture. */
|
|
97
|
+
export declare function rebaseSegments(segments: CapturedRequest['segments'], current: ScopeFingerprint, previous: ScopeFingerprint): CapturedRequest['segments'];
|
|
98
|
+
/** FNV-1a 32-bit — enough to keep id-less messages diffable, no crypto import needed. */
|
|
99
|
+
export declare function contentHash(text: string): string;
|
|
100
|
+
//# sourceMappingURL=segmenter.d.ts.map
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { classifyMessage, messageKey, messageSourceLabel, previewLine, previewMessage, serializeMessage, toolCallNames, } from "./messages.js";
|
|
2
|
+
/** Cap on retained assemblies; assemblies older than the last 32 never match again in practice. */
|
|
3
|
+
export const ASSEMBLY_RING_CAP = 32;
|
|
4
|
+
const VARIABLE_REFERENCE = /\{\{([a-z][a-z0-9_]*)\}\}/g;
|
|
5
|
+
/**
|
|
6
|
+
* Interpolate one section the way the host's renderer does, minus its error
|
|
7
|
+
* paths: a reference whose variable is missing keeps its literal form so the
|
|
8
|
+
* rendered text still lines up with whatever the host produced (or simply
|
|
9
|
+
* fails to match, degrading attribution to a single row).
|
|
10
|
+
*/
|
|
11
|
+
function interpolateSection(text, variables) {
|
|
12
|
+
return text.replace(VARIABLE_REFERENCE, (raw, name) => variables[name] ?? raw);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Record one post-waterfall assembly into the ring. Adjacent duplicates (a
|
|
16
|
+
* listener-free re-assembly between steps) collapse to one entry.
|
|
17
|
+
*/
|
|
18
|
+
export function recordAssembly(ring, assembly, at) {
|
|
19
|
+
const sections = assembly.sections
|
|
20
|
+
.map(section => ({
|
|
21
|
+
name: section.name,
|
|
22
|
+
text: section.interpolate === false ? section.text : interpolateSection(section.text, assembly.variables),
|
|
23
|
+
}))
|
|
24
|
+
.filter(section => section.text.length > 0);
|
|
25
|
+
const rendered = sections.map(section => section.text).join('\n\n');
|
|
26
|
+
const last = ring[ring.length - 1];
|
|
27
|
+
if (last !== undefined && last.rendered === rendered) {
|
|
28
|
+
last.at = at;
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
ring.push({ rendered, sections, at });
|
|
32
|
+
if (ring.length > ASSEMBLY_RING_CAP)
|
|
33
|
+
ring.splice(0, ring.length - ASSEMBLY_RING_CAP);
|
|
34
|
+
}
|
|
35
|
+
/** Split one request's payload into the four segments. Pure: no I/O, no clock. */
|
|
36
|
+
export function splitRequest(input) {
|
|
37
|
+
const { messages, context } = input;
|
|
38
|
+
const keys = messages.map(message => messageKey(message, contentHash));
|
|
39
|
+
const freshnessByIndex = messageFreshness(keys, context.previous);
|
|
40
|
+
const previousToolNames = new Set(context.previous?.toolNames ?? []);
|
|
41
|
+
const system = [];
|
|
42
|
+
if (input.systemSlot !== undefined && input.systemSlot.length > 0) {
|
|
43
|
+
system.push({
|
|
44
|
+
// The system slot sits ahead of the message array on the wire; -1 marks
|
|
45
|
+
// "not a message-slot position".
|
|
46
|
+
index: -1,
|
|
47
|
+
category: 'system',
|
|
48
|
+
source: 'system',
|
|
49
|
+
preview: previewLine(input.systemSlot),
|
|
50
|
+
full: input.systemSlot,
|
|
51
|
+
chars: input.systemSlot.length,
|
|
52
|
+
freshness: context.previous?.systemSlot === input.systemSlot ? 'history' : 'new',
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
const history = [];
|
|
56
|
+
const added = [];
|
|
57
|
+
messages.forEach((message, index) => {
|
|
58
|
+
const freshness = freshnessByIndex[index];
|
|
59
|
+
if (message.role === 'system') {
|
|
60
|
+
system.push(...attributeSystem(message, index, freshness, context.assemblies));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const full = serializeMessage(message);
|
|
64
|
+
const toolCalls = message.role === 'assistant' ? toolCallNames(message) : [];
|
|
65
|
+
const durationMs = message.role === 'assistant' && message.id !== undefined
|
|
66
|
+
? context.durationFor?.(message.id)
|
|
67
|
+
: undefined;
|
|
68
|
+
const sourceTurn = message.sourceTurn ?? (message.id === undefined ? undefined : context.turnFor?.(message.id));
|
|
69
|
+
const item = {
|
|
70
|
+
index,
|
|
71
|
+
...message.id === undefined ? {} : { messageId: message.id },
|
|
72
|
+
...message.compactionId === undefined ? {} : { compactionId: message.compactionId },
|
|
73
|
+
...message.pruning === undefined ? {} : { pruning: message.pruning },
|
|
74
|
+
category: classifyMessage(message),
|
|
75
|
+
source: messageSourceLabel(message, message.toolCallId === undefined ? undefined : context.resolveToolName?.(message.toolCallId)),
|
|
76
|
+
...toolCalls.length > 0 ? { toolCalls } : {},
|
|
77
|
+
preview: previewMessage(message),
|
|
78
|
+
full,
|
|
79
|
+
chars: full.length,
|
|
80
|
+
...durationMs === undefined ? {} : { durationMs },
|
|
81
|
+
...message.isError === undefined ? {} : { isError: message.isError },
|
|
82
|
+
...message.toolCallId === undefined ? {} : { toolCallId: message.toolCallId },
|
|
83
|
+
...sourceTurn === undefined ? {} : { sourceTurn },
|
|
84
|
+
freshness,
|
|
85
|
+
};
|
|
86
|
+
(freshness === 'history' ? history : added).push(item);
|
|
87
|
+
});
|
|
88
|
+
const tools = (input.tools ?? []).map(tool => {
|
|
89
|
+
const parameters = JSON.stringify(tool.parameters, null, 1);
|
|
90
|
+
return {
|
|
91
|
+
name: tool.name,
|
|
92
|
+
description: tool.description,
|
|
93
|
+
parameters,
|
|
94
|
+
chars: tool.name.length + tool.description.length + parameters.length,
|
|
95
|
+
added: context.previous !== undefined && !previousToolNames.has(tool.name),
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
const totalChars = system.reduce((n, item) => n + item.chars, 0)
|
|
99
|
+
+ tools.reduce((n, item) => n + item.chars, 0)
|
|
100
|
+
+ history.reduce((n, item) => n + item.chars, 0)
|
|
101
|
+
+ added.reduce((n, item) => n + item.chars, 0);
|
|
102
|
+
return {
|
|
103
|
+
system,
|
|
104
|
+
tools,
|
|
105
|
+
history,
|
|
106
|
+
added,
|
|
107
|
+
fingerprint: {
|
|
108
|
+
messageKeys: keys,
|
|
109
|
+
toolNames: (input.tools ?? []).map(tool => tool.name),
|
|
110
|
+
...input.systemSlot === undefined ? {} : { systemSlot: input.systemSlot },
|
|
111
|
+
},
|
|
112
|
+
totalChars,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/** Match occurrences, preserving unchanged tails across surface replacements. */
|
|
116
|
+
function messageFreshness(keys, previous) {
|
|
117
|
+
const remaining = new Map();
|
|
118
|
+
for (const key of previous?.messageKeys ?? [])
|
|
119
|
+
remaining.set(key, (remaining.get(key) ?? 0) + 1);
|
|
120
|
+
return keys.map(key => {
|
|
121
|
+
const count = remaining.get(key) ?? 0;
|
|
122
|
+
if (count > 0)
|
|
123
|
+
remaining.set(key, count - 1);
|
|
124
|
+
return count > 0 ? 'history' : 'new';
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
/** A late log backfill can supply the baseline that was missing at capture. */
|
|
128
|
+
export function rebaseSegments(segments, current, previous) {
|
|
129
|
+
const freshness = messageFreshness(current.messageKeys, previous);
|
|
130
|
+
const refresh = (item) => ({ ...item, freshness: item.index === -1
|
|
131
|
+
? current.systemSlot === previous.systemSlot ? 'history' : 'new'
|
|
132
|
+
: freshness[item.index] ?? item.freshness });
|
|
133
|
+
const messages = [...segments.history, ...segments.added].sort((a, b) => a.index - b.index).map(refresh);
|
|
134
|
+
const tools = new Set(previous.toolNames);
|
|
135
|
+
return {
|
|
136
|
+
system: segments.system.map(refresh),
|
|
137
|
+
tools: segments.tools.map(tool => ({ ...tool, added: !tools.has(tool.name) })),
|
|
138
|
+
history: messages.filter(item => item.freshness === 'history'),
|
|
139
|
+
added: messages.filter(item => item.freshness === 'new'),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/** Attribute one system message to assembly sections when its text matches a rendered assembly exactly. */
|
|
143
|
+
function attributeSystem(message, index, freshness, assemblies) {
|
|
144
|
+
const full = serializeMessage(message);
|
|
145
|
+
for (let i = assemblies.length - 1; i >= 0; i -= 1) {
|
|
146
|
+
const assembly = assemblies[i];
|
|
147
|
+
if (assembly === undefined || assembly.rendered !== full || assembly.sections.length === 0)
|
|
148
|
+
continue;
|
|
149
|
+
return assembly.sections.map(section => ({
|
|
150
|
+
index,
|
|
151
|
+
...message.id === undefined ? {} : { messageId: message.id },
|
|
152
|
+
...message.compactionId === undefined ? {} : { compactionId: message.compactionId },
|
|
153
|
+
...message.pruning === undefined ? {} : { pruning: message.pruning },
|
|
154
|
+
category: 'system',
|
|
155
|
+
source: section.name,
|
|
156
|
+
preview: previewLine(section.text),
|
|
157
|
+
full: section.text,
|
|
158
|
+
chars: section.text.length,
|
|
159
|
+
...message.isError === undefined ? {} : { isError: message.isError },
|
|
160
|
+
...message.toolCallId === undefined ? {} : { toolCallId: message.toolCallId },
|
|
161
|
+
...message.sourceTurn === undefined ? {} : { sourceTurn: message.sourceTurn },
|
|
162
|
+
freshness,
|
|
163
|
+
}));
|
|
164
|
+
}
|
|
165
|
+
return [{
|
|
166
|
+
index,
|
|
167
|
+
...message.id === undefined ? {} : { messageId: message.id },
|
|
168
|
+
category: 'system',
|
|
169
|
+
source: message.sourceKind,
|
|
170
|
+
preview: previewLine(full),
|
|
171
|
+
full,
|
|
172
|
+
chars: full.length,
|
|
173
|
+
freshness,
|
|
174
|
+
}];
|
|
175
|
+
}
|
|
176
|
+
/** FNV-1a 32-bit — enough to keep id-less messages diffable, no crypto import needed. */
|
|
177
|
+
export function contentHash(text) {
|
|
178
|
+
let hash = 0x811c9dc5;
|
|
179
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
180
|
+
hash ^= text.charCodeAt(i);
|
|
181
|
+
hash = Math.imul(hash, 0x01000193);
|
|
182
|
+
}
|
|
183
|
+
return (hash >>> 0).toString(36);
|
|
184
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { SessionTrend } from './types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* The context-lens service core: wires the three read-only capture lanes
|
|
4
|
+
* into the host and answers the Remote face's queries from the store.
|
|
5
|
+
*
|
|
6
|
+
* Lanes:
|
|
7
|
+
* 1. **`llm/stream`** (primary, full fidelity) — a waterfall tee that snapshots
|
|
8
|
+
* the frozen `GenerateOptions` at dispatch and forwards every chunk
|
|
9
|
+
* unchanged while recording block timing, usage, and tool-call identity.
|
|
10
|
+
* 2. **`system-prompt/assemble`** — records each post-waterfall assembly into
|
|
11
|
+
* the section-attribution ring (read-only: always returns `next()`'s value).
|
|
12
|
+
* 3. **`session/event`** — turn brackets, the user query preview, closing
|
|
13
|
+
* assistant message ids, tool exec pairing, and `subagent/catalog` scope
|
|
14
|
+
* correlation.
|
|
15
|
+
*
|
|
16
|
+
* The fourth input is the **log lane** (`sessionPersistence`, lazy): child
|
|
17
|
+
* scopes of fresh-process `subagent_dsh` runs fill from the child's on-disk
|
|
18
|
+
* log (the parent's waterfall never sees those requests), and a session's
|
|
19
|
+
* pre-mount turns reconstruct from its own log — once per session at first
|
|
20
|
+
* query, budgeted behind the live turns (the history backfill).
|
|
21
|
+
*
|
|
22
|
+
* Capture is observation, never interception: any capture-internal failure is
|
|
23
|
+
* logged and the model path forwards untouched (the tee degrades to a bare
|
|
24
|
+
* `next()`; listener bodies are individually contained).
|
|
25
|
+
*
|
|
26
|
+
* @module @khorsheed/dsh-context-lens/service
|
|
27
|
+
*/
|
|
28
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
29
|
+
import type { CapturedRequest, ListTurnsResult, ScopeFilter, ScopedRefusal, TurnOverview, TurnTiming } from './types.ts';
|
|
30
|
+
import { type SessionLogReader } from './log-lane.ts';
|
|
31
|
+
declare module '@deepseek-ai/cordis' {
|
|
32
|
+
interface Context {
|
|
33
|
+
/** The context-lens service core. */
|
|
34
|
+
requestInspector: InspectorService;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Plugin config: capture retention knobs. */
|
|
38
|
+
export interface InspectorConfig {
|
|
39
|
+
/** Turns retained per session (oldest evicted first, backfilled before live). Default 30. */
|
|
40
|
+
maxTurnsPerSession?: number;
|
|
41
|
+
/** Log-reconstructed requests retained per session. Default 500. */
|
|
42
|
+
maxBackfillRequestsPerSession?: number;
|
|
43
|
+
/** Global budget of stored segment text characters. Default 150M (~150MB). */
|
|
44
|
+
maxStoredCharsTotal?: number;
|
|
45
|
+
}
|
|
46
|
+
/** Test seams: the clock, the persistence probe, and the warning sink. */
|
|
47
|
+
export interface InspectorDeps {
|
|
48
|
+
now?: () => number;
|
|
49
|
+
/** Overrides the `sessionPersistence` probe (tests inject the fixture here). */
|
|
50
|
+
persistence?: SessionLogReader;
|
|
51
|
+
logger?: {
|
|
52
|
+
warn(message: string): void;
|
|
53
|
+
};
|
|
54
|
+
/** Overrides the instance epoch stamped on query results (restart invalidation tests). */
|
|
55
|
+
generation?: string;
|
|
56
|
+
}
|
|
57
|
+
/** The capture store's query face (Remote delegates here). */
|
|
58
|
+
export declare class InspectorService {
|
|
59
|
+
readonly ctx: Context;
|
|
60
|
+
readonly config: InspectorConfig;
|
|
61
|
+
private readonly store;
|
|
62
|
+
private readonly disposers;
|
|
63
|
+
/** childId → open read handle (persistent: reads are offset-incremental). */
|
|
64
|
+
private readonly handles;
|
|
65
|
+
private readonly cursors;
|
|
66
|
+
/** In-flight refresh per childId — concurrent panel queries share one read. */
|
|
67
|
+
private readonly refreshing;
|
|
68
|
+
/** sessionId → the metering event (`compaction/summary` | `compaction/prune`) just seen; the host appends its replacement next. */
|
|
69
|
+
private readonly meters;
|
|
70
|
+
private readonly now;
|
|
71
|
+
private readonly logWarn;
|
|
72
|
+
private readonly persistenceOverride?;
|
|
73
|
+
constructor(ctx: Context, config?: InspectorConfig, deps?: InspectorDeps);
|
|
74
|
+
/**
|
|
75
|
+
* The waterfall tee: snapshot the (possibly deep-frozen) request at dispatch,
|
|
76
|
+
* then forward every chunk unchanged while observing. A capture-side failure
|
|
77
|
+
* at setup degrades to the bare downstream stream; mid-stream failures only
|
|
78
|
+
* lose the remaining observations.
|
|
79
|
+
*/
|
|
80
|
+
private onStream;
|
|
81
|
+
private tee;
|
|
82
|
+
/**
|
|
83
|
+
* Record the post-waterfall assembly for section attribution. The returned
|
|
84
|
+
* value passes through unchanged — this listener observes, it never rewrites
|
|
85
|
+
* a prompt.
|
|
86
|
+
*/
|
|
87
|
+
private onAssemble;
|
|
88
|
+
private onSessionEvent;
|
|
89
|
+
private dispatchSessionEvent;
|
|
90
|
+
/** The probed `sessionPersistence` service, when the composition mounts one. */
|
|
91
|
+
private reader;
|
|
92
|
+
/**
|
|
93
|
+
* Refresh the log-sourced child scopes of one session (query-time,
|
|
94
|
+
* incremental). A scope the query's {@link ScopeFilter} excludes is skipped
|
|
95
|
+
* outright — its log is not opened on an unauthorized query's behalf.
|
|
96
|
+
*/
|
|
97
|
+
private refreshChildren;
|
|
98
|
+
private refreshChild;
|
|
99
|
+
private doRefreshChild;
|
|
100
|
+
/** Turn eviction drops the child cursors/handles whose scopes lived on it. */
|
|
101
|
+
private dropEvictedChildState;
|
|
102
|
+
/**
|
|
103
|
+
* Reconstruct the session's pre-mount history from its own log, once per
|
|
104
|
+
* session (the store's watermark holds). The read is one full pass at first
|
|
105
|
+
* query; the store keeps the newest turns that fit the budget behind the
|
|
106
|
+
* live ones. A not-yet-materialized log is transient (retry next query); any
|
|
107
|
+
* other read failure marks the session broken and stops hammering it.
|
|
108
|
+
*/
|
|
109
|
+
/** Pair a surface replacement with what it shadowed, typed by the metering event right before it. */
|
|
110
|
+
private noteReplacement;
|
|
111
|
+
private backfillMainSession;
|
|
112
|
+
private doBackfillMainSession;
|
|
113
|
+
/**
|
|
114
|
+
* A session opened on its own whose log header names a `parentSession`:
|
|
115
|
+
* read the parent's log once for its title, the turn whose catalog row
|
|
116
|
+
* started this child, and that row's label. Best effort — an unreadable
|
|
117
|
+
* parent leaves just its id.
|
|
118
|
+
*/
|
|
119
|
+
private resolveParent;
|
|
120
|
+
/**
|
|
121
|
+
* Light turn list of one session plus its backfill watermark. The optional
|
|
122
|
+
* {@link ScopeFilter} restricts rows and aggregates to the authorized
|
|
123
|
+
* scopes; absent is the unfiltered local semantics.
|
|
124
|
+
*/
|
|
125
|
+
listTurns(sessionId: string, filter?: ScopeFilter): Promise<ListTurnsResult>;
|
|
126
|
+
/**
|
|
127
|
+
* Move the session's backfill window to the turns ending at `end` (the
|
|
128
|
+
* latest turns when null) and answer the refreshed turn list. Live turns
|
|
129
|
+
* stay; the window refills from the log in the same call.
|
|
130
|
+
*/
|
|
131
|
+
loadTurns(sessionId: string, end: number | null, filter?: ScopeFilter): Promise<ListTurnsResult>;
|
|
132
|
+
/** One turn's scope/request overview, without segments. */
|
|
133
|
+
getTurn(sessionId: string, turnSeq: number, filter?: ScopeFilter): Promise<TurnOverview | undefined>;
|
|
134
|
+
/** One request's full four-segment breakdown (a {@link ScopedRefusal} when the filter excludes its scope). */
|
|
135
|
+
getRequest(sessionId: string, seq: number, filter?: ScopeFilter): Promise<CapturedRequest | ScopedRefusal | undefined>;
|
|
136
|
+
/** The declaring package from a registered tool's host-side origin tag (capability-catalog convention); absent when untagged. */
|
|
137
|
+
private toolOwner;
|
|
138
|
+
/** Read the session map through the same backfill and scope-filtered path. */
|
|
139
|
+
getSessionTrend(sessionId: string, filter?: ScopeFilter): Promise<SessionTrend>;
|
|
140
|
+
/** One turn's whole-turn timing aggregation. */
|
|
141
|
+
getTurnTiming(sessionId: string, turnSeq: number, filter?: ScopeFilter): Promise<TurnTiming | undefined>;
|
|
142
|
+
/** Release every session buffer, listener, and open child-log handle (plugin dispose). */
|
|
143
|
+
dispose(): void;
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=service.d.ts.map
|