@deepwatch/dsh-trajectory 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 +71 -0
- package/lib/compare.d.ts +186 -0
- package/lib/compare.js +314 -0
- package/lib/definition.d.ts +91 -0
- package/lib/definition.js +176 -0
- package/lib/events.d.ts +172 -0
- package/lib/events.js +244 -0
- package/lib/index.d.ts +22 -0
- package/lib/index.js +22 -0
- package/lib/projection.d.ts +72 -0
- package/lib/projection.js +174 -0
- package/lib/selection-store.d.ts +63 -0
- package/lib/selection-store.js +108 -0
- package/lib/selection.d.ts +106 -0
- package/lib/selection.js +223 -0
- package/package.json +47 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Watch records registered into DeepSeek Harness's own event system.
|
|
3
|
+
*
|
|
4
|
+
* This is the seam the plan insists on. Watch does **not** build a second
|
|
5
|
+
* tracker: it registers a `ConversationNodeDefinition` into
|
|
6
|
+
* `ctx.conversationEvents` — the same registry Trajectory itself uses — and a
|
|
7
|
+
* view target into `ctx.conversationViews`. Both read the single session event
|
|
8
|
+
* log DSH already owns.
|
|
9
|
+
*
|
|
10
|
+
* What that buys is not tidiness. It means a Watch record and the Tool record
|
|
11
|
+
* it came from cannot disagree about when something happened, because they are
|
|
12
|
+
* two projections of the same event at the same sequence number. A separate
|
|
13
|
+
* Watch ledger would eventually drift, and the drift would be invisible.
|
|
14
|
+
*
|
|
15
|
+
* The Trajectory *contribution* union is closed in upstream's own package, so
|
|
16
|
+
* Watch rows cannot be injected into the existing Trajectory ledger without an
|
|
17
|
+
* upstream patch. Rather than take one, Watch registers its own view target
|
|
18
|
+
* over the same events — the arrangement upstream already uses for `chat` and
|
|
19
|
+
* `trajectory`, and additive by construction.
|
|
20
|
+
*
|
|
21
|
+
* @module @deepwatch/dsh-trajectory/definition
|
|
22
|
+
*/
|
|
23
|
+
import { emptyProjection } from './projection.js';
|
|
24
|
+
import { isWatchTool, recordsFromToolResult, toolResultValue } from './events.js';
|
|
25
|
+
/** The view target Watch publishes. Distinct from `trajectory` and `chat`. */
|
|
26
|
+
export const WATCH_TARGET = 'watchEvidence';
|
|
27
|
+
function str(value) {
|
|
28
|
+
return typeof value === 'string' && value !== '' ? value : null;
|
|
29
|
+
}
|
|
30
|
+
function num(value) {
|
|
31
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The Watch event Definition.
|
|
35
|
+
*
|
|
36
|
+
* Typed structurally rather than against upstream's exported generics: this
|
|
37
|
+
* package must build outside the DSH monorepo, where those types are reachable
|
|
38
|
+
* only through the packages the browser bundle is allowed to import. The shape
|
|
39
|
+
* is checked against the real contract by the round-trip tests, which run the
|
|
40
|
+
* same extraction over real event shapes.
|
|
41
|
+
*/
|
|
42
|
+
export function watchTrajectoryDefinition(sessionId) {
|
|
43
|
+
return {
|
|
44
|
+
kind: 'watch-evidence',
|
|
45
|
+
target: WATCH_TARGET,
|
|
46
|
+
/**
|
|
47
|
+
* Claim the Watch tool calls and their results.
|
|
48
|
+
*
|
|
49
|
+
* Keyed by call id, so a call and its result assemble into one context the
|
|
50
|
+
* way every other business definition in DSH does.
|
|
51
|
+
*/
|
|
52
|
+
match(event) {
|
|
53
|
+
if (event.type === 'tool/call') {
|
|
54
|
+
const callId = str(event.data['callId']);
|
|
55
|
+
return callId !== null && isWatchTool(event.data['name'])
|
|
56
|
+
? { id: callId, role: 'start' }
|
|
57
|
+
: null;
|
|
58
|
+
}
|
|
59
|
+
if (event.type === 'tool/result') {
|
|
60
|
+
const message = event.data['message'];
|
|
61
|
+
const source = message?.['source'];
|
|
62
|
+
const callId = str(source?.['callId']);
|
|
63
|
+
return callId === null ? null : { id: callId, role: 'update' };
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
},
|
|
67
|
+
start(_context, match) {
|
|
68
|
+
const event = match.event;
|
|
69
|
+
const args = event.data['arguments'];
|
|
70
|
+
return {
|
|
71
|
+
callId: str(event.data['callId']) ?? '',
|
|
72
|
+
toolName: str(event.data['name']) ?? '',
|
|
73
|
+
turn: num(event.data['turn']),
|
|
74
|
+
step: num(event.data['step']),
|
|
75
|
+
correlationId: str(args?.['correlationId']),
|
|
76
|
+
records: [],
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
update(context, match) {
|
|
80
|
+
if (match.event.type !== 'tool/result')
|
|
81
|
+
return context.state;
|
|
82
|
+
const records = recordsFromToolResult(match.event, {
|
|
83
|
+
sessionId,
|
|
84
|
+
turn: context.state.turn,
|
|
85
|
+
step: context.state.step,
|
|
86
|
+
callId: context.state.callId,
|
|
87
|
+
toolName: context.state.toolName,
|
|
88
|
+
correlationId: context.state.correlationId,
|
|
89
|
+
}, toolResultValue(match.event));
|
|
90
|
+
return records.length === 0 ? context.state : { ...context.state, records };
|
|
91
|
+
},
|
|
92
|
+
/**
|
|
93
|
+
* Publish this context's records, or nothing.
|
|
94
|
+
*
|
|
95
|
+
* A Watch tool that produced no record — a refusal, an unreadable result —
|
|
96
|
+
* contributes no row at all. DSH's own Tool record already shows that the
|
|
97
|
+
* call happened and how it ended; adding an empty Watch row beside it
|
|
98
|
+
* would put a claim in the ledger that nothing backs.
|
|
99
|
+
*/
|
|
100
|
+
buildViewNode(context) {
|
|
101
|
+
const state = context.state;
|
|
102
|
+
if (state === undefined || state.records.length === 0)
|
|
103
|
+
return null;
|
|
104
|
+
return {
|
|
105
|
+
key: context.key,
|
|
106
|
+
kind: context.kind,
|
|
107
|
+
id: context.id,
|
|
108
|
+
target: WATCH_TARGET,
|
|
109
|
+
anchorSeq: context.start?.event.seq ?? state.records[0]?.seq ?? 0,
|
|
110
|
+
data: { records: state.records },
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The Watch view target's incremental builder.
|
|
117
|
+
*
|
|
118
|
+
* Rebuilds the whole projection on change rather than patching it. The record
|
|
119
|
+
* set for one session is small — tens of rows, not thousands — and a full fold
|
|
120
|
+
* is the same code path replay uses, so the live view and a reopened one
|
|
121
|
+
* cannot diverge. Patching would be faster and would introduce exactly the
|
|
122
|
+
* class of bug this whole design exists to rule out.
|
|
123
|
+
*/
|
|
124
|
+
class WatchViewBuilder {
|
|
125
|
+
sessionId;
|
|
126
|
+
empty;
|
|
127
|
+
nodes = new Map();
|
|
128
|
+
constructor(sessionId) {
|
|
129
|
+
this.sessionId = sessionId;
|
|
130
|
+
this.empty = emptyProjection(sessionId);
|
|
131
|
+
}
|
|
132
|
+
replace(input) {
|
|
133
|
+
this.nodes = new Map(input.nodes.map(node => [node.key, node]));
|
|
134
|
+
return this.build();
|
|
135
|
+
}
|
|
136
|
+
patch(input) {
|
|
137
|
+
for (const node of input.nodes)
|
|
138
|
+
this.nodes.set(node.key, node);
|
|
139
|
+
return this.build();
|
|
140
|
+
}
|
|
141
|
+
build() {
|
|
142
|
+
const records = [...this.nodes.values()]
|
|
143
|
+
.flatMap(node => node.data.records)
|
|
144
|
+
.sort((a, b) => {
|
|
145
|
+
const bySeq = a.seq - b.seq;
|
|
146
|
+
return bySeq !== 0 ? bySeq : a.recordId.localeCompare(b.recordId);
|
|
147
|
+
});
|
|
148
|
+
const byEvidence = new Map();
|
|
149
|
+
const byRecord = new Map();
|
|
150
|
+
for (const record of records) {
|
|
151
|
+
byRecord.set(record.recordId, record);
|
|
152
|
+
for (const evidenceId of record.refs.evidenceIds) {
|
|
153
|
+
if (!byEvidence.has(evidenceId))
|
|
154
|
+
byEvidence.set(evidenceId, record);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return { sessionId: this.sessionId, records, byEvidence, byRecord };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Register the Watch definition and view target.
|
|
162
|
+
*
|
|
163
|
+
* Both registrations ride Cordis effects, so unloading the plugin removes them
|
|
164
|
+
* and the workspace returns to stock DSH with no Watch rows and no leftover
|
|
165
|
+
* target — which is the uninstall path the bundle promises.
|
|
166
|
+
*/
|
|
167
|
+
export function registerWatchTrajectory(ctx, sessionId) {
|
|
168
|
+
const registries = ctx;
|
|
169
|
+
registries.conversationViews.register({
|
|
170
|
+
target: WATCH_TARGET,
|
|
171
|
+
create: () => new WatchViewBuilder(sessionId),
|
|
172
|
+
});
|
|
173
|
+
registries.conversationEvents.register(watchTrajectoryDefinition(sessionId));
|
|
174
|
+
}
|
|
175
|
+
export { WatchViewBuilder };
|
|
176
|
+
//# sourceMappingURL=definition.js.map
|
package/lib/events.d.ts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Watch records, derived from DeepSeek Harness's own session event log.
|
|
3
|
+
*
|
|
4
|
+
* The single most important thing about this module is what it does *not* do:
|
|
5
|
+
* it does not store anything. DSH already records every Watch tool call and
|
|
6
|
+
* result in the session log it owns, and these are a projection over that log.
|
|
7
|
+
* There is one event store, and it is DSH's.
|
|
8
|
+
*
|
|
9
|
+
* That constraint is what keeps the authority boundaries honest. A second
|
|
10
|
+
* Watch-owned ledger for UI tracking would immediately become a place where
|
|
11
|
+
* evidence could be copied, then edited, then disagree with Watch Core — and
|
|
12
|
+
* the disagreement would be invisible, because both would look authoritative.
|
|
13
|
+
*
|
|
14
|
+
* So what a record carries is **stable foreign identifiers plus presentation
|
|
15
|
+
* metadata**, never a mutable evidence payload. An `evidenceId` is a handle
|
|
16
|
+
* that Watch Core resolves; the text of the evidence lives at Watch Core and
|
|
17
|
+
* is fetched when someone opens it.
|
|
18
|
+
*
|
|
19
|
+
* @module @deepwatch/dsh-trajectory/events
|
|
20
|
+
*/
|
|
21
|
+
import type { Verdict } from '@deepwatch/dsh-contracts';
|
|
22
|
+
/**
|
|
23
|
+
* The Watch event family.
|
|
24
|
+
*
|
|
25
|
+
* Deliberately small and capability-driven. Each one exists because something
|
|
26
|
+
* in the vertical slice needs to select it, deep-link to it, or replay it.
|
|
27
|
+
*/
|
|
28
|
+
export type WatchEventType =
|
|
29
|
+
/** A source was attached to this session. */
|
|
30
|
+
'source.bound'
|
|
31
|
+
/** Watch looked at something and reported what it saw. */
|
|
32
|
+
| 'observation.created'
|
|
33
|
+
/** Watch Core minted an evidence record. */
|
|
34
|
+
| 'evidence.created'
|
|
35
|
+
/** A verification contract was submitted. */
|
|
36
|
+
| 'verification.requested'
|
|
37
|
+
/** A verdict came back. */
|
|
38
|
+
| 'verification.completed'
|
|
39
|
+
/** A browser action was sent to the page. */
|
|
40
|
+
| 'browser.action.dispatched'
|
|
41
|
+
/** A receipt settled for a browser action. */
|
|
42
|
+
| 'browser.action.receipt'
|
|
43
|
+
/** Memory was selected for a turn. */
|
|
44
|
+
| 'memory.context.injected'
|
|
45
|
+
/** A memory was corrected. */
|
|
46
|
+
| 'memory.record.corrected'
|
|
47
|
+
/** A memory was forgotten. */
|
|
48
|
+
| 'memory.record.forgotten';
|
|
49
|
+
/** Every event type, for exhaustive checks and registration. */
|
|
50
|
+
export declare const WATCH_EVENT_TYPES: readonly WatchEventType[];
|
|
51
|
+
/**
|
|
52
|
+
* Stable references a Watch record carries into Trajectory.
|
|
53
|
+
*
|
|
54
|
+
* All identifiers, no payloads. Every field here is a handle that resolves
|
|
55
|
+
* somewhere with its own authority: DSH owns the session and turn ids, Watch
|
|
56
|
+
* Core owns the source, evidence, verification and receipt ids, and Watch
|
|
57
|
+
* Memory owns the memory id.
|
|
58
|
+
*/
|
|
59
|
+
export interface WatchRefs {
|
|
60
|
+
/** DSH session this happened in. */
|
|
61
|
+
readonly sessionId: string;
|
|
62
|
+
/** DSH turn, when the event happened inside one. */
|
|
63
|
+
readonly turn: number | null;
|
|
64
|
+
/** DSH step within the turn. */
|
|
65
|
+
readonly step: number | null;
|
|
66
|
+
/** The tool call this record came from. */
|
|
67
|
+
readonly callId: string | null;
|
|
68
|
+
/** Travels with the Bridge request; the same id appears in Core's logs. */
|
|
69
|
+
readonly correlationId: string | null;
|
|
70
|
+
readonly sourceId: string | null;
|
|
71
|
+
/** Which revision of the source. A source that changed is a different one. */
|
|
72
|
+
readonly sourceRevisionId: string | null;
|
|
73
|
+
readonly evidenceIds: readonly string[];
|
|
74
|
+
readonly verificationId: string | null;
|
|
75
|
+
readonly verdict: Verdict | null;
|
|
76
|
+
readonly receiptId: string | null;
|
|
77
|
+
/** Half-open range on the source's own clock, in milliseconds. */
|
|
78
|
+
readonly temporalRange: {
|
|
79
|
+
readonly startMs: number;
|
|
80
|
+
readonly endMs: number;
|
|
81
|
+
} | null;
|
|
82
|
+
/** Artifact handle, resolved by Watch Core when someone opens it. */
|
|
83
|
+
readonly artifactId: string | null;
|
|
84
|
+
readonly memoryIds: readonly string[];
|
|
85
|
+
}
|
|
86
|
+
/** An empty reference set, so a record never carries `undefined` fields. */
|
|
87
|
+
export declare const NO_REFS: WatchRefs;
|
|
88
|
+
/**
|
|
89
|
+
* One Watch record as Trajectory shows it.
|
|
90
|
+
*
|
|
91
|
+
* `summary` is the only free text, and it is presentation metadata: a short
|
|
92
|
+
* line a person reads in a ledger. It is derived, never authoritative, and
|
|
93
|
+
* nothing resolves anything from it.
|
|
94
|
+
*/
|
|
95
|
+
export interface WatchTrajectoryRecord {
|
|
96
|
+
/** Stable within a session; what a deep link points at. */
|
|
97
|
+
readonly recordId: string;
|
|
98
|
+
readonly type: WatchEventType;
|
|
99
|
+
/** DSH log sequence, for ordering against every other Trajectory row. */
|
|
100
|
+
readonly seq: number;
|
|
101
|
+
/** Wall clock, in epoch milliseconds. */
|
|
102
|
+
readonly time: number;
|
|
103
|
+
readonly refs: WatchRefs;
|
|
104
|
+
/** One line for the ledger. Presentation only. */
|
|
105
|
+
readonly summary: string;
|
|
106
|
+
/**
|
|
107
|
+
* Whether this record's detail may be shown without further permission.
|
|
108
|
+
*
|
|
109
|
+
* Memory records about a person can be sensitive, and a ledger is a place
|
|
110
|
+
* people scroll past rather than read carefully. A redacted record still
|
|
111
|
+
* appears — hiding that memory influenced a turn would be worse — but its
|
|
112
|
+
* content is withheld until someone asks for it.
|
|
113
|
+
*/
|
|
114
|
+
readonly redacted: boolean;
|
|
115
|
+
}
|
|
116
|
+
/** The minimum of a DSH session event this module reads. */
|
|
117
|
+
export interface SessionEventLike {
|
|
118
|
+
readonly type: string;
|
|
119
|
+
readonly seq: number;
|
|
120
|
+
readonly time: number;
|
|
121
|
+
readonly data: Record<string, unknown>;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Pull the JSON a Watch tool returned out of a `tool/result` event.
|
|
125
|
+
*
|
|
126
|
+
* Returns null on anything unexpected. A result this cannot read produces no
|
|
127
|
+
* Watch record at all, which is correct: an unreadable result is not evidence
|
|
128
|
+
* that something happened, and inventing a row for it would put a claim in the
|
|
129
|
+
* ledger that nothing backs.
|
|
130
|
+
*/
|
|
131
|
+
export declare function toolResultValue(event: SessionEventLike): unknown;
|
|
132
|
+
/** Whether a tool name belongs to Watch. */
|
|
133
|
+
export declare function isWatchTool(name: unknown): boolean;
|
|
134
|
+
/** Everything one extraction needs from its surrounding DSH context. */
|
|
135
|
+
export interface ExtractionContext {
|
|
136
|
+
readonly sessionId: string;
|
|
137
|
+
readonly turn: number | null;
|
|
138
|
+
readonly step: number | null;
|
|
139
|
+
readonly callId: string | null;
|
|
140
|
+
readonly toolName: string;
|
|
141
|
+
readonly correlationId: string | null;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Derive Watch records from one Watch tool result.
|
|
145
|
+
*
|
|
146
|
+
* One tool call can produce several records — a query that returns evidence
|
|
147
|
+
* and a verification that returns a verdict are different things to select and
|
|
148
|
+
* to deep-link to, even when they arrived together.
|
|
149
|
+
*/
|
|
150
|
+
export declare function recordsFromToolResult(event: SessionEventLike, context: ExtractionContext, value: unknown): readonly WatchTrajectoryRecord[];
|
|
151
|
+
/**
|
|
152
|
+
* Derive a Watch record from a memory event.
|
|
153
|
+
*
|
|
154
|
+
* Memory is *not* an evidence plane, and these records carry no memory text.
|
|
155
|
+
* What they establish is that memory influenced a turn and which record did
|
|
156
|
+
* it, so a person can follow the id to the Memory surface and correct it
|
|
157
|
+
* there. Sensitive records are marked redacted: the row still appears, because
|
|
158
|
+
* hiding that memory influenced a turn would be worse than showing a withheld
|
|
159
|
+
* one.
|
|
160
|
+
*/
|
|
161
|
+
export declare function recordFromMemoryEvent(input: {
|
|
162
|
+
readonly kind: 'injected' | 'corrected' | 'forgotten';
|
|
163
|
+
readonly memoryId: string;
|
|
164
|
+
readonly sessionId: string;
|
|
165
|
+
readonly seq: number;
|
|
166
|
+
readonly time: number;
|
|
167
|
+
readonly turn?: number | null;
|
|
168
|
+
/** Why this memory was included. Presentation metadata, never the content. */
|
|
169
|
+
readonly reason?: string;
|
|
170
|
+
readonly sensitive?: boolean;
|
|
171
|
+
}): WatchTrajectoryRecord;
|
|
172
|
+
//# sourceMappingURL=events.d.ts.map
|
package/lib/events.js
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Watch records, derived from DeepSeek Harness's own session event log.
|
|
3
|
+
*
|
|
4
|
+
* The single most important thing about this module is what it does *not* do:
|
|
5
|
+
* it does not store anything. DSH already records every Watch tool call and
|
|
6
|
+
* result in the session log it owns, and these are a projection over that log.
|
|
7
|
+
* There is one event store, and it is DSH's.
|
|
8
|
+
*
|
|
9
|
+
* That constraint is what keeps the authority boundaries honest. A second
|
|
10
|
+
* Watch-owned ledger for UI tracking would immediately become a place where
|
|
11
|
+
* evidence could be copied, then edited, then disagree with Watch Core — and
|
|
12
|
+
* the disagreement would be invisible, because both would look authoritative.
|
|
13
|
+
*
|
|
14
|
+
* So what a record carries is **stable foreign identifiers plus presentation
|
|
15
|
+
* metadata**, never a mutable evidence payload. An `evidenceId` is a handle
|
|
16
|
+
* that Watch Core resolves; the text of the evidence lives at Watch Core and
|
|
17
|
+
* is fetched when someone opens it.
|
|
18
|
+
*
|
|
19
|
+
* @module @deepwatch/dsh-trajectory/events
|
|
20
|
+
*/
|
|
21
|
+
/** Every event type, for exhaustive checks and registration. */
|
|
22
|
+
export const WATCH_EVENT_TYPES = [
|
|
23
|
+
'source.bound',
|
|
24
|
+
'observation.created',
|
|
25
|
+
'evidence.created',
|
|
26
|
+
'verification.requested',
|
|
27
|
+
'verification.completed',
|
|
28
|
+
'browser.action.dispatched',
|
|
29
|
+
'browser.action.receipt',
|
|
30
|
+
'memory.context.injected',
|
|
31
|
+
'memory.record.corrected',
|
|
32
|
+
'memory.record.forgotten',
|
|
33
|
+
];
|
|
34
|
+
/** An empty reference set, so a record never carries `undefined` fields. */
|
|
35
|
+
export const NO_REFS = Object.freeze({
|
|
36
|
+
sessionId: '',
|
|
37
|
+
turn: null,
|
|
38
|
+
step: null,
|
|
39
|
+
callId: null,
|
|
40
|
+
correlationId: null,
|
|
41
|
+
sourceId: null,
|
|
42
|
+
sourceRevisionId: null,
|
|
43
|
+
evidenceIds: [],
|
|
44
|
+
verificationId: null,
|
|
45
|
+
verdict: null,
|
|
46
|
+
receiptId: null,
|
|
47
|
+
temporalRange: null,
|
|
48
|
+
artifactId: null,
|
|
49
|
+
memoryIds: [],
|
|
50
|
+
});
|
|
51
|
+
/** Watch tools whose results carry evidence, verdicts or receipts. */
|
|
52
|
+
const WATCH_TOOL_PREFIX = 'watch_';
|
|
53
|
+
/** Read a string field, or null. */
|
|
54
|
+
function str(value) {
|
|
55
|
+
return typeof value === 'string' && value !== '' ? value : null;
|
|
56
|
+
}
|
|
57
|
+
/** Read a plain object, or null. */
|
|
58
|
+
function obj(value) {
|
|
59
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
60
|
+
return null;
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
/** Read an array of ids, dropping anything that is not one. */
|
|
64
|
+
function ids(value) {
|
|
65
|
+
return Array.isArray(value)
|
|
66
|
+
? value.map(str).filter((entry) => entry !== null)
|
|
67
|
+
: [];
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Pull the JSON a Watch tool returned out of a `tool/result` event.
|
|
71
|
+
*
|
|
72
|
+
* Returns null on anything unexpected. A result this cannot read produces no
|
|
73
|
+
* Watch record at all, which is correct: an unreadable result is not evidence
|
|
74
|
+
* that something happened, and inventing a row for it would put a claim in the
|
|
75
|
+
* ledger that nothing backs.
|
|
76
|
+
*/
|
|
77
|
+
export function toolResultValue(event) {
|
|
78
|
+
const message = obj(obj(event.data['message'])?.['content']);
|
|
79
|
+
const content = Array.isArray(obj(event.data['message'])?.['content'])
|
|
80
|
+
? obj(event.data['message'])?.['content']
|
|
81
|
+
: null;
|
|
82
|
+
if (content === null)
|
|
83
|
+
return message;
|
|
84
|
+
const first = obj(content[0]);
|
|
85
|
+
const inner = first?.['content'];
|
|
86
|
+
const text = Array.isArray(inner)
|
|
87
|
+
? inner.map(part => str(obj(part)?.['text'])).filter(Boolean).join('')
|
|
88
|
+
: str(inner);
|
|
89
|
+
if (text === null)
|
|
90
|
+
return null;
|
|
91
|
+
try {
|
|
92
|
+
return JSON.parse(text);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** Whether a tool name belongs to Watch. */
|
|
99
|
+
export function isWatchTool(name) {
|
|
100
|
+
return typeof name === 'string' && name.startsWith(WATCH_TOOL_PREFIX);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Derive Watch records from one Watch tool result.
|
|
104
|
+
*
|
|
105
|
+
* One tool call can produce several records — a query that returns evidence
|
|
106
|
+
* and a verification that returns a verdict are different things to select and
|
|
107
|
+
* to deep-link to, even when they arrived together.
|
|
108
|
+
*/
|
|
109
|
+
export function recordsFromToolResult(event, context, value) {
|
|
110
|
+
const payload = obj(value);
|
|
111
|
+
if (payload === null)
|
|
112
|
+
return [];
|
|
113
|
+
// A refusal is not a Watch record. It is a tool that did not do anything,
|
|
114
|
+
// and DSH's own tool row already shows that it failed.
|
|
115
|
+
if (payload['ok'] === false)
|
|
116
|
+
return [];
|
|
117
|
+
const base = {
|
|
118
|
+
seq: event.seq,
|
|
119
|
+
time: event.time,
|
|
120
|
+
redacted: false,
|
|
121
|
+
};
|
|
122
|
+
const refs = {
|
|
123
|
+
...NO_REFS,
|
|
124
|
+
sessionId: context.sessionId,
|
|
125
|
+
turn: context.turn,
|
|
126
|
+
step: context.step,
|
|
127
|
+
callId: context.callId,
|
|
128
|
+
correlationId: context.correlationId,
|
|
129
|
+
};
|
|
130
|
+
const records = [];
|
|
131
|
+
const evidence = Array.isArray(payload['evidence']) ? payload['evidence'] : [];
|
|
132
|
+
if (evidence.length > 0) {
|
|
133
|
+
const first = obj(evidence[0]);
|
|
134
|
+
const range = obj(first?.['temporalRange']);
|
|
135
|
+
records.push({
|
|
136
|
+
...base,
|
|
137
|
+
recordId: `${context.callId ?? String(event.seq)}:evidence`,
|
|
138
|
+
type: 'evidence.created',
|
|
139
|
+
summary: `${String(evidence.length)} evidence record(s)`,
|
|
140
|
+
refs: {
|
|
141
|
+
...refs,
|
|
142
|
+
sourceId: str(first?.['sourceRevisionId']),
|
|
143
|
+
sourceRevisionId: str(first?.['sourceRevisionId']),
|
|
144
|
+
evidenceIds: evidence
|
|
145
|
+
.map(entry => str(obj(entry)?.['evidenceId']))
|
|
146
|
+
.filter((entry) => entry !== null),
|
|
147
|
+
temporalRange: range === null ? null : {
|
|
148
|
+
startMs: Number(range['startMs'] ?? 0),
|
|
149
|
+
endMs: Number(range['endMs'] ?? 0),
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
const verdict = str(payload['verdict']);
|
|
155
|
+
if (verdict !== null) {
|
|
156
|
+
records.push({
|
|
157
|
+
...base,
|
|
158
|
+
recordId: `${context.callId ?? String(event.seq)}:verification`,
|
|
159
|
+
type: 'verification.completed',
|
|
160
|
+
// The verdict itself, verbatim. Never widened, never defaulted.
|
|
161
|
+
summary: verdict,
|
|
162
|
+
refs: {
|
|
163
|
+
...refs,
|
|
164
|
+
verificationId: str(payload['verificationId']),
|
|
165
|
+
verdict: verdict,
|
|
166
|
+
evidenceIds: ids(payload['evidenceRefs']),
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
const receiptVerdict = str(payload['verdict'] ?? payload['status']);
|
|
171
|
+
if (str(payload['idempotencyKey']) !== null && verdict === null) {
|
|
172
|
+
records.push({
|
|
173
|
+
...base,
|
|
174
|
+
recordId: `${context.callId ?? String(event.seq)}:receipt`,
|
|
175
|
+
type: 'browser.action.receipt',
|
|
176
|
+
summary: receiptVerdict ?? 'receipt',
|
|
177
|
+
refs: {
|
|
178
|
+
...refs,
|
|
179
|
+
receiptId: str(payload['idempotencyKey']),
|
|
180
|
+
verdict: null,
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
const sources = Array.isArray(payload['sources']) ? payload['sources'] : [];
|
|
185
|
+
if (sources.length > 0) {
|
|
186
|
+
records.push({
|
|
187
|
+
...base,
|
|
188
|
+
recordId: `${context.callId ?? String(event.seq)}:sources`,
|
|
189
|
+
type: 'source.bound',
|
|
190
|
+
summary: `${String(sources.length)} source(s)`,
|
|
191
|
+
refs: { ...refs, sourceId: str(obj(sources[0])?.['sourceId']) },
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
// An answer with no evidence is still an observation worth showing: it says
|
|
195
|
+
// Watch looked and found nothing to cite, which is different from Watch not
|
|
196
|
+
// having been asked.
|
|
197
|
+
if (records.length === 0 && str(payload['answer']) !== null) {
|
|
198
|
+
records.push({
|
|
199
|
+
...base,
|
|
200
|
+
recordId: `${context.callId ?? String(event.seq)}:observation`,
|
|
201
|
+
type: 'observation.created',
|
|
202
|
+
summary: 'observation with no citations',
|
|
203
|
+
refs,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
return records;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Derive a Watch record from a memory event.
|
|
210
|
+
*
|
|
211
|
+
* Memory is *not* an evidence plane, and these records carry no memory text.
|
|
212
|
+
* What they establish is that memory influenced a turn and which record did
|
|
213
|
+
* it, so a person can follow the id to the Memory surface and correct it
|
|
214
|
+
* there. Sensitive records are marked redacted: the row still appears, because
|
|
215
|
+
* hiding that memory influenced a turn would be worse than showing a withheld
|
|
216
|
+
* one.
|
|
217
|
+
*/
|
|
218
|
+
export function recordFromMemoryEvent(input) {
|
|
219
|
+
const type = input.kind === 'injected'
|
|
220
|
+
? 'memory.context.injected'
|
|
221
|
+
: input.kind === 'corrected'
|
|
222
|
+
? 'memory.record.corrected'
|
|
223
|
+
: 'memory.record.forgotten';
|
|
224
|
+
const redacted = input.sensitive === true;
|
|
225
|
+
return {
|
|
226
|
+
recordId: `mem:${input.memoryId}:${String(input.seq)}`,
|
|
227
|
+
type,
|
|
228
|
+
seq: input.seq,
|
|
229
|
+
time: input.time,
|
|
230
|
+
refs: {
|
|
231
|
+
...NO_REFS,
|
|
232
|
+
sessionId: input.sessionId,
|
|
233
|
+
turn: input.turn ?? null,
|
|
234
|
+
memoryIds: [input.memoryId],
|
|
235
|
+
},
|
|
236
|
+
summary: redacted
|
|
237
|
+
// Withheld, not omitted. The reason text can quote a preference, and a
|
|
238
|
+
// ledger is somewhere people scroll rather than read carefully.
|
|
239
|
+
? 'memory used (details withheld)'
|
|
240
|
+
: input.reason ?? type,
|
|
241
|
+
redacted,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
//# sourceMappingURL=events.js.map
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Watch records inside the DeepSeek Harness Trajectory.
|
|
3
|
+
*
|
|
4
|
+
* The authority boundaries this package exists to hold:
|
|
5
|
+
*
|
|
6
|
+
* - **DSH owns the session and its event log.** There is no Watch event store.
|
|
7
|
+
* Every record here is a projection over the events DSH already recorded.
|
|
8
|
+
* - **Watch Core owns evidence and verdicts.** A record carries the ids; the
|
|
9
|
+
* content is resolved from Core when someone opens it.
|
|
10
|
+
* - **Watch Memory stays separate from evidence.** Memory records show that
|
|
11
|
+
* memory influenced a turn and which record did it. They are not evidence
|
|
12
|
+
* and cannot be cited as any.
|
|
13
|
+
*
|
|
14
|
+
* @module @deepwatch/dsh-trajectory
|
|
15
|
+
*/
|
|
16
|
+
export * from './events.js';
|
|
17
|
+
export * from './selection.js';
|
|
18
|
+
export * from './projection.js';
|
|
19
|
+
export * from './selection-store.js';
|
|
20
|
+
export * from './definition.js';
|
|
21
|
+
export * from './compare.js';
|
|
22
|
+
//# sourceMappingURL=index.d.ts.map
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Watch records inside the DeepSeek Harness Trajectory.
|
|
3
|
+
*
|
|
4
|
+
* The authority boundaries this package exists to hold:
|
|
5
|
+
*
|
|
6
|
+
* - **DSH owns the session and its event log.** There is no Watch event store.
|
|
7
|
+
* Every record here is a projection over the events DSH already recorded.
|
|
8
|
+
* - **Watch Core owns evidence and verdicts.** A record carries the ids; the
|
|
9
|
+
* content is resolved from Core when someone opens it.
|
|
10
|
+
* - **Watch Memory stays separate from evidence.** Memory records show that
|
|
11
|
+
* memory influenced a turn and which record did it. They are not evidence
|
|
12
|
+
* and cannot be cited as any.
|
|
13
|
+
*
|
|
14
|
+
* @module @deepwatch/dsh-trajectory
|
|
15
|
+
*/
|
|
16
|
+
export * from './events.js';
|
|
17
|
+
export * from './selection.js';
|
|
18
|
+
export * from './projection.js';
|
|
19
|
+
export * from './selection-store.js';
|
|
20
|
+
export * from './definition.js';
|
|
21
|
+
export * from './compare.js';
|
|
22
|
+
//# sourceMappingURL=index.js.map
|