@khorsheed/dsh-local-agent-dsh 0.1.0-rc.6
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.en.md +150 -0
- package/README.i18n.yaml +6 -0
- package/README.md +151 -0
- package/cordis.patch.yml +24 -0
- package/lib/client.js +1115 -0
- package/lib/dsh-cli-provider-UhWvivte.js +1701 -0
- package/lib/dsh-cli-provider.js +3 -0
- package/lib/index.js +624 -0
- package/lib/invariant.js +33 -0
- package/lib/provision.js +602 -0
- package/lib/records.js +109 -0
- package/lib/session-log-DOsSz56-.js +88 -0
- package/lib/types/client/SettingsCard.d.ts +78 -0
- package/lib/types/client/SettingsCard.js +200 -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 +45 -0
- package/lib/types/client/index.js +88 -0
- package/lib/types/client/locales.d.ts +76 -0
- package/lib/types/client/locales.js +67 -0
- package/lib/types/dsh-cli-provider.d.ts +213 -0
- package/lib/types/dsh-cli-provider.js +733 -0
- package/lib/types/index.d.ts +124 -0
- package/lib/types/index.js +347 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/invariant.js +37 -0
- package/lib/types/live-driver.d.ts +180 -0
- package/lib/types/live-driver.js +887 -0
- package/lib/types/live-switch.d.ts +76 -0
- package/lib/types/live-switch.js +100 -0
- package/lib/types/model-broker.d.ts +86 -0
- package/lib/types/model-broker.js +149 -0
- package/lib/types/model-catalog.d.ts +27 -0
- package/lib/types/model-catalog.js +71 -0
- package/lib/types/provision.d.ts +382 -0
- package/lib/types/provision.js +656 -0
- package/lib/types/records.d.ts +41 -0
- package/lib/types/records.js +125 -0
- package/lib/types/session-log.d.ts +58 -0
- package/lib/types/session-log.js +83 -0
- package/lib/types/session-mirror.d.ts +155 -0
- package/lib/types/session-mirror.js +451 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +121 -0
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mirror a sub-dsh session's events into the parent-side dsh subagent session,
|
|
3
|
+
* so a dsh delegation shows its full conversation (task, replies, thinking)
|
|
4
|
+
* and real token usage in the standard 子代理 surface — the same fidelity the
|
|
5
|
+
* other family harnesses get from their CLI transcript files. The sub-dsh
|
|
6
|
+
* writes a real dsh session into the harness scoped home with the SAME id as
|
|
7
|
+
* the parent-side child session and the same SessionEvent format, so the
|
|
8
|
+
* mirror copies finalized `user/message`/`assistant/message` events verbatim
|
|
9
|
+
* (no parsing, no re-encoding): reasoning blocks render natively and usage
|
|
10
|
+
* rides the assistant events' own `usage` field into the tokenUsage
|
|
11
|
+
* projection.
|
|
12
|
+
*
|
|
13
|
+
* Round scoping: the parent provider appends one `turn/start` per delegation
|
|
14
|
+
* round, and the sub-dsh session numbers its turns identically (fresh = 1,
|
|
15
|
+
* each resume +1), so a round's span is the sub-dsh events from its
|
|
16
|
+
* `turn/start` with `turn === round` to the next `turn/start`. The sub-dsh's
|
|
17
|
+
* own scaffolding user messages (agent-instructions, plugin, skill-catalog
|
|
18
|
+
* sources) are filtered — only the caller task (`source.kind === 'user'`)
|
|
19
|
+
* crosses. The sub-dsh's own `step/start`/`step/end` pairs cross verbatim
|
|
20
|
+
* (each carries its (turn, step) coordinates): the real-time conversation
|
|
21
|
+
* assembler only registers a step — and materializes its assistant message —
|
|
22
|
+
* at a boundary, so without them the live subsession view renders the prompt
|
|
23
|
+
* and tool cards but no replies. Turn boundaries are NOT copied: the parent's
|
|
24
|
+
* real-time spawn→settle boundaries stay the authoritative timing.
|
|
25
|
+
*
|
|
26
|
+
* `todo/write` passthrough: the sub-dsh's task list is a standing whole-list
|
|
27
|
+
* snapshot (last-wins). Each pass appends the round's LATEST snapshot only
|
|
28
|
+
* when it differs from the child session's last mirrored one, so repeated
|
|
29
|
+
* passes never duplicate an identical snapshot and the child's `todos`
|
|
30
|
+
* projection (and the official TodoPanel in non-takeover views) reads the
|
|
31
|
+
* member's task list for free.
|
|
32
|
+
* @module @khorsheed/dsh-local-agent-dsh/session-mirror
|
|
33
|
+
*/
|
|
34
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
35
|
+
import { resolveDshSessionLog } from './session-log.js';
|
|
36
|
+
import { join } from 'node:path';
|
|
37
|
+
import { promisify } from 'node:util';
|
|
38
|
+
import { zstdDecompress } from 'node:zlib';
|
|
39
|
+
import { persistChildSession } from '@khorsheed/dsh-local-agent';
|
|
40
|
+
/** Decompress one zstd session log (Node ≥22.15 built-in; engines require ^22.19). */
|
|
41
|
+
const decompressZstd = promisify(zstdDecompress);
|
|
42
|
+
/** Every zstd frame's little-endian magic number, marking a frame start in the stream. */
|
|
43
|
+
const ZSTD_FRAME_MAGIC = 0xfd2fb528;
|
|
44
|
+
/**
|
|
45
|
+
* Decompress a multi-frame zstd session log. The persistence layer appends
|
|
46
|
+
* one frame per flush batch, and Node's single-shot `zstdDecompress` stops at
|
|
47
|
+
* the first frame's end (a streaming decoder does the same), so each frame
|
|
48
|
+
* start is scanned and decompressed independently. A magic-looking byte
|
|
49
|
+
* sequence inside compressed data fails decompression and is skipped.
|
|
50
|
+
* @param content - the raw `.zstd` file bytes.
|
|
51
|
+
* @returns the concatenated plaintext of every frame.
|
|
52
|
+
*/
|
|
53
|
+
async function decompressZstdFrames(content) {
|
|
54
|
+
const parts = [];
|
|
55
|
+
for (let offset = 0; offset + 4 <= content.length; offset += 1) {
|
|
56
|
+
if (content.readUInt32LE(offset) !== ZSTD_FRAME_MAGIC)
|
|
57
|
+
continue;
|
|
58
|
+
try {
|
|
59
|
+
parts.push((await decompressZstd(content.subarray(offset))).toString('utf8'));
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Not a real frame start; keep scanning.
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return parts.join('');
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Read a sub-dsh session's event log from the scoped home. The session lives
|
|
69
|
+
* at `<homeDir>/sessions/<workspace>/<id>/` (the runner passes the bare uuid);
|
|
70
|
+
* WHICH file inside that directory holds the history is the host's choice —
|
|
71
|
+
* `session.jsonl` on the original generation, `session.v3.jsonl.zstd` on host
|
|
72
|
+
* 0.1.5 — so the name is resolved, never assumed (see
|
|
73
|
+
* {@link resolveDshSessionLog}). The first line is the header, the rest are
|
|
74
|
+
* events.
|
|
75
|
+
* @param homeDir - the `dsh` harness's scoped home.
|
|
76
|
+
* @param sessionId - the sub-dsh session id (same uuid as the child session).
|
|
77
|
+
* @returns the events and their source file, or undefined when the session is
|
|
78
|
+
* absent or unreadable.
|
|
79
|
+
*/
|
|
80
|
+
export async function readSubDshEvents(homeDir, sessionId) {
|
|
81
|
+
let workspaces;
|
|
82
|
+
try {
|
|
83
|
+
workspaces = await readdir(join(homeDir, 'sessions'));
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
for (const workspace of workspaces) {
|
|
89
|
+
for (const name of [sessionId, `session-${sessionId}`]) {
|
|
90
|
+
const log = await resolveDshSessionLog(join(homeDir, 'sessions', workspace, name));
|
|
91
|
+
if (log === undefined)
|
|
92
|
+
continue;
|
|
93
|
+
let text;
|
|
94
|
+
try {
|
|
95
|
+
const content = await readFile(log.path);
|
|
96
|
+
text = log.compressed ? await decompressZstdFrames(content) : content.toString('utf8');
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const events = [];
|
|
102
|
+
for (const line of text.split('\n')) {
|
|
103
|
+
if (line === '')
|
|
104
|
+
continue;
|
|
105
|
+
try {
|
|
106
|
+
const parsed = JSON.parse(line);
|
|
107
|
+
if (parsed.type === 'session')
|
|
108
|
+
continue;
|
|
109
|
+
events.push(parsed);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// A torn tail line (killed mid-flush) is skipped, not fatal.
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (events.length > 0)
|
|
116
|
+
return { events, log };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Sum one more assistant event's usage into a running round total. Each
|
|
123
|
+
* sub-dsh assistant event carries ONE LLM call's accounting; the round's
|
|
124
|
+
* windows (live polls, settle pass) recompute it from the round's span, so
|
|
125
|
+
* the total is span-derived and never double counts across passes.
|
|
126
|
+
*/
|
|
127
|
+
function addEventUsage(total, usage) {
|
|
128
|
+
const raw = usage;
|
|
129
|
+
if (raw === undefined || typeof raw !== 'object')
|
|
130
|
+
return total;
|
|
131
|
+
const num = (value) => {
|
|
132
|
+
const n = Number(value);
|
|
133
|
+
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
134
|
+
};
|
|
135
|
+
const merged = {
|
|
136
|
+
inputTokens: (total?.inputTokens ?? 0) + num(raw.inputTokens),
|
|
137
|
+
outputTokens: (total?.outputTokens ?? 0) + num(raw.outputTokens),
|
|
138
|
+
};
|
|
139
|
+
const cacheRead = (total?.cacheReadTokens ?? 0) + num(raw.cacheReadTokens);
|
|
140
|
+
const cacheWrite = (total?.cacheWriteTokens ?? 0) + num(raw.cacheWriteTokens);
|
|
141
|
+
if (cacheRead > 0)
|
|
142
|
+
merged.cacheReadTokens = cacheRead;
|
|
143
|
+
if (cacheWrite > 0)
|
|
144
|
+
merged.cacheWriteTokens = cacheWrite;
|
|
145
|
+
return merged;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Extract the round's observed model and token usage from its event span:
|
|
149
|
+
the LAST assistant event's `message.source` names the model that ran, and
|
|
150
|
+
* the assistant events' `usage` fields sum to the round's accounting.
|
|
151
|
+
*/
|
|
152
|
+
function roundObservation(span) {
|
|
153
|
+
let observedModel;
|
|
154
|
+
let usage;
|
|
155
|
+
// Tool calls counted over the same round window, keyed by the name the
|
|
156
|
+
// sub-dsh's own `tool/call` event carries — the tool's dsh name, verbatim.
|
|
157
|
+
const byName = {};
|
|
158
|
+
let toolCount = 0;
|
|
159
|
+
for (const event of span) {
|
|
160
|
+
if (event.type === 'tool/call') {
|
|
161
|
+
const name = event.data.name;
|
|
162
|
+
toolCount += 1;
|
|
163
|
+
const key = typeof name === 'string' && name !== '' ? name : 'tool';
|
|
164
|
+
byName[key] = (byName[key] ?? 0) + 1;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (event.type !== 'assistant/message')
|
|
168
|
+
continue;
|
|
169
|
+
const data = event.data;
|
|
170
|
+
const source = data.message?.source;
|
|
171
|
+
if (source !== undefined && typeof source === 'object') {
|
|
172
|
+
const model = typeof source.model === 'string' && source.model !== '' ? source.model : undefined;
|
|
173
|
+
if (model !== undefined) {
|
|
174
|
+
const provider = typeof source.provider === 'string' && source.provider !== '' ? source.provider : undefined;
|
|
175
|
+
observedModel = provider === undefined ? model : `${provider}/${model}`;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
usage = addEventUsage(usage, data.usage);
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
...observedModel === undefined ? {} : { observedModel },
|
|
182
|
+
...usage === undefined ? {} : { usage },
|
|
183
|
+
// Zero is not reported: a round that made no tool call and a round whose
|
|
184
|
+
// events were never read are different facts, and only absence can say
|
|
185
|
+
// the second one honestly.
|
|
186
|
+
...toolCount === 0 ? {} : { toolCalls: { count: toolCount, byName } },
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
/** Visible answer text for progress/results; reasoning stays in the verbatim transcript. */
|
|
190
|
+
function mirroredEventText(event) {
|
|
191
|
+
// user/message carries content directly; assistant/message wraps it in `message`.
|
|
192
|
+
const data = event.data;
|
|
193
|
+
const content = data.message?.content ?? data.content ?? [];
|
|
194
|
+
return content
|
|
195
|
+
.filter(block => block.type === 'text' && typeof block.text === 'string')
|
|
196
|
+
.map(block => block.text)
|
|
197
|
+
.join('');
|
|
198
|
+
}
|
|
199
|
+
/** The delta-progress text for one mirrored tool event. */
|
|
200
|
+
function mirroredToolText(event) {
|
|
201
|
+
if (event.type === 'tool/call') {
|
|
202
|
+
return `[工具 ${event.data.name ?? 'tool'}]`;
|
|
203
|
+
}
|
|
204
|
+
const data = event.data;
|
|
205
|
+
const result = data.message?.content?.[0]?.content?.map(block => block.text ?? '').join('') ?? '';
|
|
206
|
+
return result === '' ? '[工具结果]' : `[工具结果] ${result}`;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Find the child-session seq of the mirrored `tool/call` carrying `callId`,
|
|
210
|
+
* so a mirrored `tool/result`'s `sourceEventSeqs` points at the CHILD's call
|
|
211
|
+
* event — the source event's own seqs reference the sub-dsh session's
|
|
212
|
+
* numbering and are meaningless here.
|
|
213
|
+
*/
|
|
214
|
+
function findMirroredCallSeq(childSession, callId) {
|
|
215
|
+
for (let index = childSession.snapshotEvents().length - 1; index >= 0; index -= 1) {
|
|
216
|
+
const event = childSession.snapshotEvents()[index];
|
|
217
|
+
if (event?.type !== 'tool/call')
|
|
218
|
+
continue;
|
|
219
|
+
if (event.data.callId === callId)
|
|
220
|
+
return event.seq;
|
|
221
|
+
}
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Append one mirrored `tool/call` or `tool/result` event — the native tool
|
|
226
|
+
* card pair, so the child session renders the sub-dsh's tool activity with
|
|
227
|
+
* the standard conversation's tool rows instead of not at all. Shared by the
|
|
228
|
+
* file mirror's span loop and the live driver's per-event mirror.
|
|
229
|
+
*/
|
|
230
|
+
function appendMirroredToolEvent(childSession, event) {
|
|
231
|
+
if (event.type === 'tool/call') {
|
|
232
|
+
childSession.append('tool/call', event.data);
|
|
233
|
+
}
|
|
234
|
+
else if (event.type === 'tool/result') {
|
|
235
|
+
const data = event.data;
|
|
236
|
+
const callSeq = findMirroredCallSeq(childSession, String(data.message.source.callId));
|
|
237
|
+
childSession.append('tool/result', event.data, {
|
|
238
|
+
surfaceOp: 'append',
|
|
239
|
+
...callSeq === undefined ? {} : { sourceEventSeqs: [callSeq] },
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
return mirroredToolText(event);
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Append one mirrored `step/start` or `step/end` boundary verbatim — the event
|
|
246
|
+
* carries its own (turn, step) coordinates, so the child session's location
|
|
247
|
+
* index registers the same step the sub-dsh wrote. Shared by the file
|
|
248
|
+
* mirror's span loop and the live driver's per-event mirror. An interrupted
|
|
249
|
+
* step crosses as an OPEN pair (start without end): the location resolves to
|
|
250
|
+
* the step either way, and no boundary is ever synthesized.
|
|
251
|
+
*/
|
|
252
|
+
function appendMirroredStepBoundary(childSession, event) {
|
|
253
|
+
if (event.type === 'step/start') {
|
|
254
|
+
childSession.append('step/start', event.data);
|
|
255
|
+
}
|
|
256
|
+
else if (event.type === 'step/end') {
|
|
257
|
+
childSession.append('step/end', event.data);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Append one appendable message event verbatim and report its delta text —
|
|
262
|
+
* the single append path shared by the file mirror's span loop and the live
|
|
263
|
+
* driver's per-event mirror, so both transports produce identical child
|
|
264
|
+
* sessions.
|
|
265
|
+
*/
|
|
266
|
+
function appendMirroredMessageEvent(childSession, event) {
|
|
267
|
+
if (event.type === 'user/message') {
|
|
268
|
+
childSession.append('user/message', event.data, { surfaceOp: 'append' });
|
|
269
|
+
}
|
|
270
|
+
else if (event.type === 'assistant/message') {
|
|
271
|
+
// Verbatim copy: content blocks (text/reasoning) and usage ride the
|
|
272
|
+
// event's own fields, so the tokenUsage projection counts the round.
|
|
273
|
+
childSession.append('assistant/message', event.data, { surfaceOp: 'append' });
|
|
274
|
+
}
|
|
275
|
+
return mirroredEventText(event);
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Mirror ONE live-pushed sub-dsh session event (the serve mode's
|
|
279
|
+
* `session/event` wire notification) into the child session. This is the
|
|
280
|
+
* live driver's transport-side entry into the SAME fold the file mirror
|
|
281
|
+
* owns: the filter (only the caller task's `user/message`, every
|
|
282
|
+
* `assistant/message`, the tool events, and the `step/start`/`step/end`
|
|
283
|
+
* boundaries cross — verbatim; turn boundaries stay the parent's; scaffolding
|
|
284
|
+
* stays behind) and the verbatim append are exactly `mirrorDshSession`'s
|
|
285
|
+
* span-loop rules. The 0.1.5 sub-dsh emits no per-chunk events (the
|
|
286
|
+
* `assistant/chunk` type is retired — an interrupted attempt settles as
|
|
287
|
+
* `assistant/attempt`, which carries no surface content and never crosses),
|
|
288
|
+
* so the `token` granularity no longer streams deltas into the child log;
|
|
289
|
+
* live text rides the run-progress channel only.
|
|
290
|
+
* The caller owns offset/dedupe (the live runtime pushes
|
|
291
|
+
* each event once) and persistence batching.
|
|
292
|
+
* @param childSession - the parent-side dsh subagent session.
|
|
293
|
+
* @param event - the live event from the resident sub-dsh.
|
|
294
|
+
* @param _options - granularity; retained for caller compatibility — the
|
|
295
|
+
* retirement of per-chunk events leaves the mirror granularity-invariant.
|
|
296
|
+
* @returns the mirrored text for delta progress, or undefined when the event
|
|
297
|
+
* was filtered out or carries no progress text (a step boundary crosses but
|
|
298
|
+
* is structure, not content).
|
|
299
|
+
*/
|
|
300
|
+
export function mirrorDshLiveEvent(childSession, event, _options) {
|
|
301
|
+
if (event.type === 'user/message' && event.data.source.kind === 'user') {
|
|
302
|
+
return appendMirroredMessageEvent(childSession, event);
|
|
303
|
+
}
|
|
304
|
+
if (event.type === 'assistant/message') {
|
|
305
|
+
return appendMirroredMessageEvent(childSession, event);
|
|
306
|
+
}
|
|
307
|
+
if (event.type === 'tool/call' || event.type === 'tool/result') {
|
|
308
|
+
return appendMirroredToolEvent(childSession, event);
|
|
309
|
+
}
|
|
310
|
+
if (event.type === 'step/start' || event.type === 'step/end') {
|
|
311
|
+
appendMirroredStepBoundary(childSession, event);
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
314
|
+
return undefined;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Mirror the current round's events from the sub-dsh session into the child
|
|
318
|
+
* session, then persist (a live session syncs through the core's cached write
|
|
319
|
+
* handle, a standalone one through a one-shot handle; see
|
|
320
|
+
* {@link persistIfStandalone}). Runs both
|
|
321
|
+
* from the provider's live poll (while the
|
|
322
|
+
* sub-dsh writes its log in batches — a torn final zstd frame is skipped
|
|
323
|
+
* until the next pass) and after the child process exits (the settle pass);
|
|
324
|
+
* the round's already-mirrored prefix in the child session is the offset, so
|
|
325
|
+
* the settle pass is a no-op when polling kept up. An aborted round still
|
|
326
|
+
* preserves its partial work and real usage. All failures degrade to a warn:
|
|
327
|
+
* the delegation result is already settled.
|
|
328
|
+
* @param ctx - host context carrying session persistence.
|
|
329
|
+
* @param childSession - the parent-side dsh subagent session.
|
|
330
|
+
* @param homeDir - the `dsh` harness's scoped home.
|
|
331
|
+
* @param subSessionId - the sub-dsh session id (same uuid as the child session).
|
|
332
|
+
* @returns the newly mirrored events' texts and the round's mirrored total.
|
|
333
|
+
*/
|
|
334
|
+
export async function mirrorDshSession(ctx, childSession, homeDir, subSessionId) {
|
|
335
|
+
const empty = { texts: [], total: 0 };
|
|
336
|
+
try {
|
|
337
|
+
const read = await readSubDshEvents(homeDir, subSessionId);
|
|
338
|
+
if (read === undefined)
|
|
339
|
+
return empty;
|
|
340
|
+
const { events, log } = read;
|
|
341
|
+
// The round mirrors the sub-dsh turn with the same number: the parent's
|
|
342
|
+
// turn/start count IS this round's number (appended before spawn), and the
|
|
343
|
+
// sub-dsh numbers its turns identically across fresh and resume rounds.
|
|
344
|
+
const round = childSession.snapshotEvents().filter(event => event.type === 'turn/start').length;
|
|
345
|
+
if (round === 0)
|
|
346
|
+
return empty;
|
|
347
|
+
let start = -1;
|
|
348
|
+
let end = events.length;
|
|
349
|
+
for (let index = 0; index < events.length; index += 1) {
|
|
350
|
+
const event = events[index];
|
|
351
|
+
if (event?.type !== 'turn/start')
|
|
352
|
+
continue;
|
|
353
|
+
if (start === -1 && event.data.turn === round) {
|
|
354
|
+
start = index;
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
if (start !== -1) {
|
|
358
|
+
end = index;
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (start === -1)
|
|
363
|
+
return empty;
|
|
364
|
+
// The child session's messages after its last turn/start are this round's
|
|
365
|
+
// already-mirrored prefix (a live poll may have appended them); skip it so
|
|
366
|
+
// the settle pass never duplicates what polling mirrored. The span is
|
|
367
|
+
// filtered to APPENDABLE events first — scaffolding user messages
|
|
368
|
+
// (non-'user' sources) never cross, so they must not occupy skip
|
|
369
|
+
// positions either. The two filter sets must stay IDENTICAL — step
|
|
370
|
+
// boundaries included on both sides — or a repeated pass misaligns the
|
|
371
|
+
// prefix and re-appends (a second step/start for the same (turn, step)
|
|
372
|
+
// makes the live assembler throw).
|
|
373
|
+
let lastTurnStart = -1;
|
|
374
|
+
for (let index = 0; index < childSession.snapshotEvents().length; index += 1) {
|
|
375
|
+
if (childSession.snapshotEvents()[index]?.type === 'turn/start')
|
|
376
|
+
lastTurnStart = index;
|
|
377
|
+
}
|
|
378
|
+
const mirrored = childSession.snapshotEvents().slice(lastTurnStart + 1)
|
|
379
|
+
.filter(event => event.type === 'user/message' || event.type === 'assistant/message'
|
|
380
|
+
|| event.type === 'tool/call' || event.type === 'tool/result'
|
|
381
|
+
|| event.type === 'step/start' || event.type === 'step/end')
|
|
382
|
+
.length;
|
|
383
|
+
const roundSpan = events.slice(start, end)
|
|
384
|
+
.filter(event => (event.type === 'user/message' && event.data.source.kind === 'user')
|
|
385
|
+
|| event.type === 'assistant/message'
|
|
386
|
+
|| event.type === 'tool/call'
|
|
387
|
+
|| event.type === 'tool/result'
|
|
388
|
+
|| event.type === 'step/start'
|
|
389
|
+
|| event.type === 'step/end');
|
|
390
|
+
const span = roundSpan.slice(mirrored);
|
|
391
|
+
const texts = [];
|
|
392
|
+
let appended = 0;
|
|
393
|
+
for (const event of span) {
|
|
394
|
+
appended += 1;
|
|
395
|
+
if (event.type === 'step/start' || event.type === 'step/end') {
|
|
396
|
+
// A boundary is structure, not content: it crosses verbatim (its own
|
|
397
|
+
// (turn, step) coordinates) but contributes no delta text.
|
|
398
|
+
appendMirroredStepBoundary(childSession, event);
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
texts.push(event.type === 'tool/call' || event.type === 'tool/result'
|
|
402
|
+
? appendMirroredToolEvent(childSession, event)
|
|
403
|
+
: appendMirroredMessageEvent(childSession, event));
|
|
404
|
+
}
|
|
405
|
+
// The round's observation comes from the round's OWN window (the
|
|
406
|
+
// pre-skip roundSpan, not the delta): the last assistant event names the
|
|
407
|
+
// model that ran, and the assistant events' usage sums to the round's
|
|
408
|
+
// accounting, whatever the live-poll / settle split was.
|
|
409
|
+
const observation = roundObservation(roundSpan);
|
|
410
|
+
// todo/write passthrough, counted independently of the message prefix
|
|
411
|
+
// skip: the snapshot is a standing whole list (last-wins), so a pass
|
|
412
|
+
// appends the round's latest snapshot only when it differs from the
|
|
413
|
+
// child's last mirrored one — repeated passes never duplicate an
|
|
414
|
+
// identical snapshot, and intermediate snapshots stay out of the log.
|
|
415
|
+
let todosAppended = 0;
|
|
416
|
+
const latestTodos = events.slice(start, end).filter(event => event.type === 'todo/write').at(-1);
|
|
417
|
+
const mirroredTodos = childSession.snapshotEvents().slice(lastTurnStart + 1)
|
|
418
|
+
.filter(event => event.type === 'todo/write');
|
|
419
|
+
if (latestTodos !== undefined && latestTodos.type === 'todo/write') {
|
|
420
|
+
if (JSON.stringify(mirroredTodos.at(-1)?.data) !== JSON.stringify(latestTodos.data)) {
|
|
421
|
+
// todo/write's append takes no surface options (log-only UI state).
|
|
422
|
+
childSession.append('todo/write', latestTodos.data);
|
|
423
|
+
todosAppended = 1;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (appended > 0 || todosAppended > 0) {
|
|
427
|
+
await persistChildSession(ctx, childSession);
|
|
428
|
+
}
|
|
429
|
+
return {
|
|
430
|
+
texts,
|
|
431
|
+
total: mirrored + mirroredTodos.length + appended + todosAppended,
|
|
432
|
+
sessionLogFile: log.filename,
|
|
433
|
+
...observation,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
catch (error) {
|
|
437
|
+
ctx.logger.warn(`subagent-dsh: session mirror failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
438
|
+
return empty;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Persist the child session's mirrored events. Thin wrapper over the core's
|
|
443
|
+
* {@link persistChildSession}: a live session syncs through the core's cached
|
|
444
|
+
* per-child write handle (host 0.1.5's live write-behind never checkpoints a
|
|
445
|
+
* mirror session — it has no agent loop — so production child logs stayed
|
|
446
|
+
* header-only until the explicit sync), a standalone one through a one-shot
|
|
447
|
+
* handle flow. Kept under the historical name for the live driver.
|
|
448
|
+
*/
|
|
449
|
+
export async function persistIfStandalone(ctx, childSession) {
|
|
450
|
+
await persistChildSession(ctx, childSession);
|
|
451
|
+
}
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@khorsheed/dsh-local-agent-dsh",
|
|
3
|
+
"description": "dsh harness for dsh-local-agent: delegate to dsh itself as a local CLI — scoped DSH_HOME under the shared homes root, one-shot sub-dsh runs visible in the 子代理 surface, caller-supplied session ids, resume across rounds, DeepSeek toggle in settings",
|
|
4
|
+
"keywords": [
|
|
5
|
+
"dsh",
|
|
6
|
+
"dsh-plugin",
|
|
7
|
+
"cordis",
|
|
8
|
+
"local-agent",
|
|
9
|
+
"subagent"
|
|
10
|
+
],
|
|
11
|
+
"version": "0.1.0-rc.6",
|
|
12
|
+
"type": "module",
|
|
13
|
+
"main": "lib/index.js",
|
|
14
|
+
"types": "lib/types/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./lib/types/index.d.ts",
|
|
18
|
+
"default": "./lib/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./invariant": {
|
|
21
|
+
"types": "./lib/types/invariant.d.ts",
|
|
22
|
+
"default": "./lib/invariant.js"
|
|
23
|
+
},
|
|
24
|
+
"./records": {
|
|
25
|
+
"types": "./lib/types/records.d.ts",
|
|
26
|
+
"default": "./lib/records.js"
|
|
27
|
+
},
|
|
28
|
+
"./provision": {
|
|
29
|
+
"types": "./lib/types/provision.d.ts",
|
|
30
|
+
"default": "./lib/provision.js"
|
|
31
|
+
},
|
|
32
|
+
"./dsh-cli-provider": {
|
|
33
|
+
"types": "./lib/types/dsh-cli-provider.d.ts",
|
|
34
|
+
"default": "./lib/dsh-cli-provider.js"
|
|
35
|
+
},
|
|
36
|
+
"./client": {
|
|
37
|
+
"types": "./lib/types/client/index.d.ts",
|
|
38
|
+
"default": "./lib/client.js"
|
|
39
|
+
},
|
|
40
|
+
"./src/*": "./src/*",
|
|
41
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
42
|
+
"./package.json": "./package.json",
|
|
43
|
+
"./locale/*.json": "./locale/*.json"
|
|
44
|
+
},
|
|
45
|
+
"dsh": {
|
|
46
|
+
"references": [
|
|
47
|
+
"@khorsheed/dsh-bundle-local-agent"
|
|
48
|
+
],
|
|
49
|
+
"client": {
|
|
50
|
+
"inject": [
|
|
51
|
+
"@deepseek-ai/dsh-api-remotes",
|
|
52
|
+
"@deepseek-ai/dsh-client-locale",
|
|
53
|
+
"@deepseek-ai/dsh-client-ui-primitives",
|
|
54
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
55
|
+
"@deepseek-ai/dsh-client-ui-slots"
|
|
56
|
+
],
|
|
57
|
+
"platform": "web"
|
|
58
|
+
},
|
|
59
|
+
"bundle": {
|
|
60
|
+
"patch": "./cordis.patch.yml"
|
|
61
|
+
},
|
|
62
|
+
"compat": {
|
|
63
|
+
"minHost": "0.1.5-rc.1",
|
|
64
|
+
"notes": "liveMirrorGranularity token: host 0.1.5 retired the per-chunk session event, so token-mode deltas no longer land in the child session log — they ride the run-progress channel and the round settles as one combined assistant/message (identical final text); a late-arriving usage whose carrier message already mirrored is dropped with a warn (no usage-backfill event exists)",
|
|
65
|
+
"verifiedHost": "0.1.5-rc.1"
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"files": [
|
|
69
|
+
"lib",
|
|
70
|
+
"cordis.patch.yml",
|
|
71
|
+
"locale/*.json"
|
|
72
|
+
],
|
|
73
|
+
"license": "MIT",
|
|
74
|
+
"dependencies": {
|
|
75
|
+
"@khorsheed/dsh-local-agent": "^0.1.0-rc.6",
|
|
76
|
+
"@khorsheed/dsh-local-agent-dsh-headless": "^0.1.0-rc.6",
|
|
77
|
+
"@khorsheed/dsh-local-agent-tool-subagent": "^0.1.0-rc.6"
|
|
78
|
+
},
|
|
79
|
+
"peerDependencies": {
|
|
80
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
81
|
+
"@deepseek-ai/dsh-agent-default-model": "^0.1.0-rc.6",
|
|
82
|
+
"@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.6",
|
|
83
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
|
|
84
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.6",
|
|
85
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.6",
|
|
86
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
|
|
87
|
+
"@deepseek-ai/dsh-commands": "^0.1.0-rc.6",
|
|
88
|
+
"@deepseek-ai/dsh-credentials": "^0.1.0-rc.6",
|
|
89
|
+
"@deepseek-ai/dsh-invariants": "^0.1.0-rc.6",
|
|
90
|
+
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
|
|
91
|
+
"@deepseek-ai/dsh-session": "^0.1.0-rc.6",
|
|
92
|
+
"@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
|
|
93
|
+
"@deepseek-ai/dsh-subagent": "^0.1.0-rc.6",
|
|
94
|
+
"@deepseek-ai/dsh-subprocess": "^0.1.0-rc.6",
|
|
95
|
+
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
|
|
96
|
+
"react": "^18.2.0"
|
|
97
|
+
},
|
|
98
|
+
"peerDependenciesMeta": {
|
|
99
|
+
"@deepseek-ai/dsh-agent-default-model": {
|
|
100
|
+
"optional": true
|
|
101
|
+
},
|
|
102
|
+
"@deepseek-ai/dsh-api-remotes": {
|
|
103
|
+
"optional": true
|
|
104
|
+
},
|
|
105
|
+
"@deepseek-ai/dsh-client-locale": {
|
|
106
|
+
"optional": true
|
|
107
|
+
},
|
|
108
|
+
"@deepseek-ai/dsh-client-ui-primitives": {
|
|
109
|
+
"optional": true
|
|
110
|
+
},
|
|
111
|
+
"@deepseek-ai/dsh-client-ui-settings": {
|
|
112
|
+
"optional": true
|
|
113
|
+
},
|
|
114
|
+
"@deepseek-ai/dsh-client-ui-slots": {
|
|
115
|
+
"optional": true
|
|
116
|
+
},
|
|
117
|
+
"react": {
|
|
118
|
+
"optional": true
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|