@khorsheed/dsh-local-agent-dsh 0.2.0-rc.3 → 0.2.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 +7 -1
- package/README.en.md +23 -139
- package/README.i18n.yaml +2 -2
- package/README.md +23 -139
- package/lib/client.js +46 -46
- package/lib/dsh-cli-provider-DYRuRpVz.js +1920 -0
- package/lib/dsh-cli-provider.js +1 -1
- package/lib/index.js +1 -1
- package/lib/types/client/icons.d.ts +1 -1
- package/lib/types/dsh-cli-provider.js +46 -24
- package/lib/types/live-driver.js +47 -41
- package/lib/types/session-mirror.d.ts +55 -0
- package/lib/types/session-mirror.js +155 -1
- package/package.json +3 -3
package/lib/dsh-cli-provider.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as MEMBER_BRIDGE_ENTRY_ENV, c as dshTextTask, d as startDshCliRun, i as DshCliProvider, l as registerMemberRun, n as DEFAULT_DISPOSE_GRACE_MS, o as dshCliVersion, r as DEFAULT_LIVE_MIRROR_INTERVAL_MS, s as dshLaunchArgv, t as CONTAINER_NODE_OPTIONS, u as resolveApiKey } from "./dsh-cli-provider-
|
|
1
|
+
import { a as MEMBER_BRIDGE_ENTRY_ENV, c as dshTextTask, d as startDshCliRun, i as DshCliProvider, l as registerMemberRun, n as DEFAULT_DISPOSE_GRACE_MS, o as dshCliVersion, r as DEFAULT_LIVE_MIRROR_INTERVAL_MS, s as dshLaunchArgv, t as CONTAINER_NODE_OPTIONS, u as resolveApiKey } from "./dsh-cli-provider-DYRuRpVz.js";
|
|
2
2
|
import "./provision.js";
|
|
3
3
|
export { CONTAINER_NODE_OPTIONS, DEFAULT_DISPOSE_GRACE_MS, DEFAULT_LIVE_MIRROR_INTERVAL_MS, DshCliProvider, MEMBER_BRIDGE_ENTRY_ENV, dshCliVersion, dshLaunchArgv, dshTextTask, registerMemberRun, resolveApiKey, startDshCliRun };
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { f as DEFAULT_LIVE_IDLE_MS, h as localModelLaunch, i as DshCliProvider, m as canInheritModelProvider, o as dshCliVersion, p as DshLiveDriver, t as CONTAINER_NODE_OPTIONS } from "./dsh-cli-provider-
|
|
1
|
+
import { f as DEFAULT_LIVE_IDLE_MS, h as localModelLaunch, i as DshCliProvider, m as canInheritModelProvider, o as dshCliVersion, p as DshLiveDriver, t as CONTAINER_NODE_OPTIONS } from "./dsh-cli-provider-DYRuRpVz.js";
|
|
2
2
|
import { DEFAULT_SUB_PROFILE_NAME, provisionDshScope } from "./provision.js";
|
|
3
3
|
import { listDshSessions } from "./records.js";
|
|
4
4
|
import { dirname } from "node:path";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Generated by scripts/sync-icon-artwork.mts from @deepseek-ai/dsh-client-ui-primitives
|
|
3
|
-
* (v0.1
|
|
3
|
+
* (v0.2.1-rc.1, harness 1600d8a1e1) — do not edit by hand. The 0.1.5 and rc.1 host lines
|
|
4
4
|
* share no icon export name, so the rc.1 artwork is inlined here: pure SVG,
|
|
5
5
|
* no host runtime identity, identical on every line. Re-run the generator
|
|
6
6
|
* after adding an icon import or bumping the harness checkout.
|
|
@@ -15,11 +15,11 @@ import { randomUUID } from 'node:crypto';
|
|
|
15
15
|
import { credentialRef } from '@deepseek-ai/dsh-credentials';
|
|
16
16
|
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
17
17
|
import { NO_START_CAPABILITIES, SUBAGENT_DESCRIPTOR_VERSION, settleRunResult, subprocessRunHandle, } from '@deepseek-ai/dsh-subagent';
|
|
18
|
-
import { assertResumeCwdUnchanged, assertResumeScopeUnchanged, resolveRoundModel, containerExecSpawn, containerScopedHome, delegationEnv, establishSubagentCatalogChild, probeCliVersion, resolveChildCwd, subagentDelegationLabel, } from '@khorsheed/dsh-local-agent';
|
|
18
|
+
import { assertResumeCwdUnchanged, assertResumeScopeUnchanged, resolveRoundModel, containerExecSpawn, containerScopedHome, delegationEnv, establishSubagentCatalogChild, persistChildSession, probeCliVersion, resolveChildCwd, subagentDelegationLabel, } from '@khorsheed/dsh-local-agent';
|
|
19
19
|
import { MEMBER_BRIDGE_SOCKET_ENV, MEMBER_BRIDGE_TOKEN_ENV } from '@khorsheed/dsh-local-agent/types';
|
|
20
20
|
import { LiveChannelUnavailableError } from './live-driver.js';
|
|
21
21
|
import { DEFAULT_SUB_PROFILE_NAME, provisionDshScope, unitSubProfileName } from './provision.js';
|
|
22
|
-
import { mirrorDshSession } from './session-mirror.js';
|
|
22
|
+
import { closeMirroredOpenStep, mirrorDshSession } from './session-mirror.js';
|
|
23
23
|
/**
|
|
24
24
|
* Spawn-env name carrying the bridge entry path into the sub-dsh process,
|
|
25
25
|
* where the headless bundle's mcp-client row reads it (`!!js`). The name is a
|
|
@@ -685,39 +685,61 @@ export async function startDshCliRun(request, spec) {
|
|
|
685
685
|
// reached its endpoint and produced an answer.
|
|
686
686
|
if (settled.stopReason === 'completed')
|
|
687
687
|
markCredentialVerified(ctx, spec.scope);
|
|
688
|
-
// The turn
|
|
689
|
-
//
|
|
690
|
-
//
|
|
691
|
-
//
|
|
692
|
-
if (spec.childSession !== undefined) {
|
|
693
|
-
if (settled.stopReason === 'completed') {
|
|
694
|
-
spec.childSession.append('turn/end', { turn, reason: { kind: 'completed' } });
|
|
695
|
-
}
|
|
696
|
-
else if (settled.stopReason === 'aborted') {
|
|
697
|
-
spec.childSession.append('turn/end', { turn, reason: { kind: 'aborted', reason: { kind: 'parent' } } });
|
|
698
|
-
}
|
|
699
|
-
else {
|
|
700
|
-
spec.childSession.append('turn/end', {
|
|
701
|
-
turn,
|
|
702
|
-
reason: { kind: 'error', error: { message: 'dsh exited before the run completed', code: 'UNKNOWN' } },
|
|
703
|
-
});
|
|
704
|
-
}
|
|
705
|
-
}
|
|
688
|
+
// The turn does NOT close here: turn/end lands in the mirror queue after
|
|
689
|
+
// the settle pass (see below), so every mirrored event precedes it in the
|
|
690
|
+
// stored log — the released-log reader rejects content outside an open
|
|
691
|
+
// turn on every cold read.
|
|
706
692
|
return settled;
|
|
707
693
|
});
|
|
708
694
|
// After the child EXITS — however it ended (completed, killed by dispose, or
|
|
709
695
|
// crashed) — mirror whatever the sub-dsh session already produced into the
|
|
710
696
|
// dsh subagent session, so every round preserves its conversation and real
|
|
711
697
|
// token usage instead of showing turn boundaries only. Waits for the settle
|
|
712
|
-
// chain first
|
|
713
|
-
//
|
|
714
|
-
//
|
|
698
|
+
// chain first AND for the process to actually exit (the sub-dsh flushed its
|
|
699
|
+
// session before exiting) before reading. The turn closes INSIDE the mirror
|
|
700
|
+
// queue, after the settle pass: the copied tail's open step first receives
|
|
701
|
+
// its closure (an interrupted sub-dsh leaves its step — and any advertised
|
|
702
|
+
// tool calls — open, and the released-log reader rejects a turn/end over
|
|
703
|
+
// it), then turn/end lands and is persisted explicitly (the mirror's own
|
|
704
|
+
// persist skips a no-op pass, which is exactly the case that needs the
|
|
705
|
+
// boundary written). Every terminal path closes the window — a failed or
|
|
706
|
+
// cancelled run settles 'error'/'aborted' instead of leaving the turn open.
|
|
715
707
|
void result.then(() => child.done).then(() => {
|
|
716
708
|
if (spec.childSession !== undefined) {
|
|
717
709
|
const childSession = spec.childSession;
|
|
718
710
|
// The settle pass reports the final mirror count even when polling
|
|
719
711
|
// already covered the round (empty delta): it is authoritative.
|
|
720
|
-
return enqueueMirror(() =>
|
|
712
|
+
return enqueueMirror(async () => {
|
|
713
|
+
const settled = await result;
|
|
714
|
+
try {
|
|
715
|
+
await mirrorAndReport(childSession, true);
|
|
716
|
+
}
|
|
717
|
+
catch (error) {
|
|
718
|
+
// The mirror degrades to a warn; the turn boundary still closes —
|
|
719
|
+
// an open turn at EOF reads as an interrupted tail only until the
|
|
720
|
+
// next resume round tries to open its turn over it.
|
|
721
|
+
ctx.logger.warn(`subagent-dsh: settle mirror failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
722
|
+
}
|
|
723
|
+
closeMirroredOpenStep(childSession);
|
|
724
|
+
// The turn closes at the real settle moment (plus the settle-mirror
|
|
725
|
+
// quiescence), so the timing projection's duration equals the actual
|
|
726
|
+
// CLI runtime.
|
|
727
|
+
if (settled.stopReason === 'completed') {
|
|
728
|
+
childSession.append('turn/end', { turn, reason: { kind: 'completed' } });
|
|
729
|
+
}
|
|
730
|
+
else if (settled.stopReason === 'aborted') {
|
|
731
|
+
childSession.append('turn/end', { turn, reason: { kind: 'aborted', reason: { kind: 'parent' } } });
|
|
732
|
+
}
|
|
733
|
+
else {
|
|
734
|
+
childSession.append('turn/end', {
|
|
735
|
+
turn,
|
|
736
|
+
reason: { kind: 'error', error: { message: 'dsh exited before the run completed', code: 'UNKNOWN' } },
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
await persistChildSession(ctx, childSession).catch((error) => {
|
|
740
|
+
ctx.logger.warn(`subagent-dsh: turn/end persistence failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
741
|
+
});
|
|
742
|
+
});
|
|
721
743
|
}
|
|
722
744
|
return undefined;
|
|
723
745
|
}, () => { });
|
package/lib/types/live-driver.js
CHANGED
|
@@ -28,7 +28,7 @@ import { LIVE_SERVER_NAME, LIVE_WIRE_PROTOCOL_VERSION, } from '@khorsheed/dsh-lo
|
|
|
28
28
|
import { DEFAULT_DISPOSE_GRACE_MS, dshLaunchArgv, dshTextTask, registerMemberRun, } from './dsh-cli-provider.js';
|
|
29
29
|
import { localModelLaunch } from './model-launch.js';
|
|
30
30
|
import { DEFAULT_SUB_PROFILE_NAME, provisionDshScope } from './provision.js';
|
|
31
|
-
import { mirrorDshLiveEvent, mirrorDshSession } from './session-mirror.js';
|
|
31
|
+
import { mirrorDshLiveEvent, mirrorDshSession, closeMirroredOpenStep, mirroredOpenStep } from './session-mirror.js';
|
|
32
32
|
/** Default idle lifetime of an unused resident runtime before reclaim. */
|
|
33
33
|
export const DEFAULT_LIVE_IDLE_MS = 30 * 60_000;
|
|
34
34
|
/** Default timeout for a wire request acknowledgement. */
|
|
@@ -785,7 +785,13 @@ export class DshLiveDriver {
|
|
|
785
785
|
}).then(async (settled) => {
|
|
786
786
|
roundSettled = true;
|
|
787
787
|
liveFlush.dispose();
|
|
788
|
-
|
|
788
|
+
// The interrupted stream's partial content folds durably only while its
|
|
789
|
+
// step is still OPEN: a graceful sub-dsh unwind writes its own
|
|
790
|
+
// interrupted assistant/message and step/end (both crossed the wire),
|
|
791
|
+
// and an append at the closed step would fail the released-log reader.
|
|
792
|
+
const openStep = turnOpened ? mirroredOpenStep(childSession) : undefined;
|
|
793
|
+
if (turnOpened && streaming !== undefined && streamPublisher !== undefined
|
|
794
|
+
&& openStep !== undefined && openStep.step === streaming.step) {
|
|
789
795
|
const blocks = streaming.assembler.interruptedBlocks();
|
|
790
796
|
if (blocks.length > 0) {
|
|
791
797
|
childSession.append('assistant/message', {
|
|
@@ -798,22 +804,6 @@ export class DshLiveDriver {
|
|
|
798
804
|
}
|
|
799
805
|
}
|
|
800
806
|
streamPublisher?.dispose();
|
|
801
|
-
// Identical turn/end bookkeeping to the exec path — but only for a turn
|
|
802
|
-
// that actually opened (a pre-accept cancel records no turn at all).
|
|
803
|
-
if (turnOpened) {
|
|
804
|
-
if (settled.stopReason === 'completed') {
|
|
805
|
-
childSession.append('turn/end', { turn, reason: { kind: 'completed' } });
|
|
806
|
-
}
|
|
807
|
-
else if (settled.stopReason === 'aborted') {
|
|
808
|
-
childSession.append('turn/end', { turn, reason: { kind: 'aborted', reason: { kind: 'parent' } } });
|
|
809
|
-
}
|
|
810
|
-
else {
|
|
811
|
-
childSession.append('turn/end', {
|
|
812
|
-
turn,
|
|
813
|
-
reason: { kind: 'error', error: nativeFailure ?? { message: 'the live round did not complete', code: 'UNKNOWN' } },
|
|
814
|
-
});
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
807
|
// Settlement clears the round's handlers: a late notification drops at
|
|
818
808
|
// the runtime frame instead of landing in a dead closure.
|
|
819
809
|
if (runtime !== undefined) {
|
|
@@ -821,24 +811,18 @@ export class DshLiveDriver {
|
|
|
821
811
|
runtime.onEvent = undefined;
|
|
822
812
|
runtime.onIdle = undefined;
|
|
823
813
|
}
|
|
824
|
-
// A completed answer is not a durable turn until its final boundary
|
|
825
|
-
// and any closing stream checkpoints have reached the child store.
|
|
826
814
|
if (turnOpened) {
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
void result.then(async () => {
|
|
839
|
-
try {
|
|
840
|
-
if (turnOpened) {
|
|
841
|
-
await persistQueue;
|
|
815
|
+
// Settle reconciliation IN-CHAIN: the file-based mirror pass (the serve
|
|
816
|
+
// side flushed before its idle notification) dedupes against the
|
|
817
|
+
// live-mirrored prefix and catches anything the wire dropped; the
|
|
818
|
+
// final mirror progress report is authoritative, exactly as in the
|
|
819
|
+
// exec path. It runs before turn/end so every mirrored event lands
|
|
820
|
+
// inside the turn window — the released-log reader rejects content
|
|
821
|
+
// outside an open turn on every cold read — and inside the result
|
|
822
|
+
// chain so the round serializer (and a resume round's turn/start)
|
|
823
|
+
// never overtakes the boundary. A mirror fault degrades to a warn;
|
|
824
|
+
// the boundary still closes.
|
|
825
|
+
try {
|
|
842
826
|
if (mirroredMessages > 0)
|
|
843
827
|
persist();
|
|
844
828
|
await persistQueue.catch(() => { });
|
|
@@ -863,13 +847,35 @@ export class DshLiveDriver {
|
|
|
863
847
|
registry?.recordRoundSettled?.(childSession.id, round);
|
|
864
848
|
}
|
|
865
849
|
}
|
|
850
|
+
catch (error) {
|
|
851
|
+
this.ctx.logger.warn(`subagent-dsh: live settle mirror failed: ${thrown(error).message}`);
|
|
852
|
+
}
|
|
853
|
+
// The copied tail's open step (an interrupted round leaves it open —
|
|
854
|
+
// the reader rejects a turn/end over it) receives its closure first.
|
|
855
|
+
closeMirroredOpenStep(childSession);
|
|
856
|
+
// Identical turn/end bookkeeping to the exec path — a completed,
|
|
857
|
+
// interrupted, or failed round each closes its window.
|
|
858
|
+
if (settled.stopReason === 'completed') {
|
|
859
|
+
childSession.append('turn/end', { turn, reason: { kind: 'completed' } });
|
|
860
|
+
}
|
|
861
|
+
else if (settled.stopReason === 'aborted') {
|
|
862
|
+
childSession.append('turn/end', { turn, reason: { kind: 'aborted', reason: { kind: 'parent' } } });
|
|
863
|
+
}
|
|
864
|
+
else {
|
|
865
|
+
childSession.append('turn/end', {
|
|
866
|
+
turn,
|
|
867
|
+
reason: { kind: 'error', error: nativeFailure ?? { message: 'the live round did not complete', code: 'UNKNOWN' } },
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
// A completed answer is not a durable turn until its final boundary
|
|
871
|
+
// and any closing stream checkpoints have reached the child store.
|
|
872
|
+
persist();
|
|
873
|
+
await persistQueue;
|
|
866
874
|
}
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
this.armIdleTimer(spec.sessionId);
|
|
872
|
-
}
|
|
875
|
+
// Re-arm the idle reaper for every settled round — including one that
|
|
876
|
+
// never opened its turn (the runtime is up and must not leak).
|
|
877
|
+
this.armIdleTimer(spec.sessionId);
|
|
878
|
+
return settled;
|
|
873
879
|
});
|
|
874
880
|
// Publication gate: hold start() until the turn is accepted (channel
|
|
875
881
|
// errors still throw for the provider's exec fallback), but NEVER hold it
|
|
@@ -29,6 +29,19 @@
|
|
|
29
29
|
* passes never duplicate an identical snapshot and the child's `todos`
|
|
30
30
|
* projection (and the official TodoPanel in non-takeover views) reads the
|
|
31
31
|
* member's task list for free.
|
|
32
|
+
*
|
|
33
|
+
* Interrupted-tail closure: a sub-dsh that dies mid-step (the exec kill
|
|
34
|
+
* ladder, a live-runtime crash) leaves its log's step — and any advertised
|
|
35
|
+
* tool calls — OPEN, and the verbatim copy carries that open tail into the
|
|
36
|
+
* child session. The released-log reader (harness
|
|
37
|
+
* session-format-v3-to-v4/relationships.ts) rejects a `turn/end` over an open
|
|
38
|
+
* step on every cold read, so the provider's settle flow calls
|
|
39
|
+
* {@link closeMirroredOpenStep} after the settle mirror pass and before its
|
|
40
|
+
* own `turn/end`: each advertised-but-unresolved call receives a synthesized
|
|
41
|
+
* interrupted `tool/result` (the family's shared placeholder for a started
|
|
42
|
+
* call, the validator's exact TOOL_NOT_STARTED repair for one that never
|
|
43
|
+
* started) and the copied step receives its `step/end`. Mid-round the open
|
|
44
|
+
* pair still crosses as-is — the sub-dsh may yet write its end.
|
|
32
45
|
* @module @khorsheed/dsh-local-agent-dsh/session-mirror
|
|
33
46
|
*/
|
|
34
47
|
import type { DshSessionLog } from './session-log.ts';
|
|
@@ -98,6 +111,48 @@ export interface DshMirrorDelta {
|
|
|
98
111
|
}
|
|
99
112
|
/** How much of the live event stream crosses into the child session. */
|
|
100
113
|
export type DshLiveMirrorGranularity = 'event' | 'token';
|
|
114
|
+
/**
|
|
115
|
+
* The placeholder text of the synthesized result for a tool call the sub-dsh
|
|
116
|
+
* started but never resolved (its process died between `tool/call` and
|
|
117
|
+
* `tool/result`). Shared verbatim with the kimi family's fold.
|
|
118
|
+
*/
|
|
119
|
+
export declare const DSH_INTERRUPTED_TOOL_RESULT = "\uFF08\u5DE5\u5177\u8C03\u7528\u88AB\u4E2D\u65AD\uFF0C\u672A\u80FD\u8BB0\u5F55\u8FD4\u56DE\u7ED3\u679C\uFF09";
|
|
120
|
+
/** What {@link closeMirroredOpenStep} synthesized, when it closed anything. */
|
|
121
|
+
export interface DshMirrorTailClosure {
|
|
122
|
+
/** The copied open step this closure ended. */
|
|
123
|
+
readonly step: {
|
|
124
|
+
turn: number;
|
|
125
|
+
step: number;
|
|
126
|
+
};
|
|
127
|
+
/** The advertised calls that received a synthesized interrupted result, in advertisement order. */
|
|
128
|
+
readonly repairedCalls: readonly string[];
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The child session's currently-open copied step: the (turn, step) of a
|
|
132
|
+
* `step/start` whose `step/end` has not crossed, inside a turn the provider
|
|
133
|
+
* has not closed. Undefined when nothing is open. The live driver's
|
|
134
|
+
* interrupted-stream fold consults it: the fold's assistant/message may land
|
|
135
|
+
* only while its step is still open (a graceful sub-dsh unwind writes its own
|
|
136
|
+
* message and step/end, and the fold must not append at the closed step).
|
|
137
|
+
*/
|
|
138
|
+
export declare function mirroredOpenStep(childSession: Session): {
|
|
139
|
+
turn: number;
|
|
140
|
+
step: number;
|
|
141
|
+
} | undefined;
|
|
142
|
+
/**
|
|
143
|
+
* Close the copied open tail of the child session's current turn at settle,
|
|
144
|
+
* before the provider's `turn/end`: the released-log reader rejects a
|
|
145
|
+
* `turn/end` over an open step ('turn/end does not match the open turn with
|
|
146
|
+
* no open step') and a step/end leaving an advertised call unresolved, so an
|
|
147
|
+
* interrupted sub-dsh's open step is ended here — each advertised-but-
|
|
148
|
+
* unresolved tool call first receiving its synthesized interrupted result
|
|
149
|
+
* (the family's placeholder for a started call; the validator's exact
|
|
150
|
+
* TOOL_NOT_STARTED repair for one that never started). Mid-round transports
|
|
151
|
+
* never call this: the sub-dsh may yet write its own end, and a synthesized
|
|
152
|
+
* step/end would collide with it on a later pass. Returns what it
|
|
153
|
+
* synthesized, or undefined when the copied tail was already balanced.
|
|
154
|
+
*/
|
|
155
|
+
export declare function closeMirroredOpenStep(childSession: Session): DshMirrorTailClosure | undefined;
|
|
101
156
|
/**
|
|
102
157
|
* Mirror ONE live-pushed sub-dsh session event (the serve mode's
|
|
103
158
|
* `session/event` wire notification) into the child session. This is the
|
|
@@ -29,6 +29,19 @@
|
|
|
29
29
|
* passes never duplicate an identical snapshot and the child's `todos`
|
|
30
30
|
* projection (and the official TodoPanel in non-takeover views) reads the
|
|
31
31
|
* member's task list for free.
|
|
32
|
+
*
|
|
33
|
+
* Interrupted-tail closure: a sub-dsh that dies mid-step (the exec kill
|
|
34
|
+
* ladder, a live-runtime crash) leaves its log's step — and any advertised
|
|
35
|
+
* tool calls — OPEN, and the verbatim copy carries that open tail into the
|
|
36
|
+
* child session. The released-log reader (harness
|
|
37
|
+
* session-format-v3-to-v4/relationships.ts) rejects a `turn/end` over an open
|
|
38
|
+
* step on every cold read, so the provider's settle flow calls
|
|
39
|
+
* {@link closeMirroredOpenStep} after the settle mirror pass and before its
|
|
40
|
+
* own `turn/end`: each advertised-but-unresolved call receives a synthesized
|
|
41
|
+
* interrupted `tool/result` (the family's shared placeholder for a started
|
|
42
|
+
* call, the validator's exact TOOL_NOT_STARTED repair for one that never
|
|
43
|
+
* started) and the copied step receives its `step/end`. Mid-round the open
|
|
44
|
+
* pair still crosses as-is — the sub-dsh may yet write its end.
|
|
32
45
|
* @module @khorsheed/dsh-local-agent-dsh/session-mirror
|
|
33
46
|
*/
|
|
34
47
|
import { readFile, readdir } from 'node:fs/promises';
|
|
@@ -247,7 +260,9 @@ function appendMirroredToolEvent(childSession, event) {
|
|
|
247
260
|
* index registers the same step the sub-dsh wrote. Shared by the file
|
|
248
261
|
* mirror's span loop and the live driver's per-event mirror. An interrupted
|
|
249
262
|
* step crosses as an OPEN pair (start without end): the location resolves to
|
|
250
|
-
* the step either way, and no boundary is
|
|
263
|
+
* the step either way, and mid-round no boundary is synthesized — the sub-dsh
|
|
264
|
+
* may yet write its end. At settle the provider closes the copied tail with
|
|
265
|
+
* {@link closeMirroredOpenStep} before its own `turn/end`.
|
|
251
266
|
*/
|
|
252
267
|
function appendMirroredStepBoundary(childSession, event) {
|
|
253
268
|
if (event.type === 'step/start') {
|
|
@@ -274,6 +289,145 @@ function appendMirroredMessageEvent(childSession, event) {
|
|
|
274
289
|
}
|
|
275
290
|
return mirroredEventText(event);
|
|
276
291
|
}
|
|
292
|
+
/**
|
|
293
|
+
* The placeholder text of the synthesized result for a tool call the sub-dsh
|
|
294
|
+
* started but never resolved (its process died between `tool/call` and
|
|
295
|
+
* `tool/result`). Shared verbatim with the kimi family's fold.
|
|
296
|
+
*/
|
|
297
|
+
export const DSH_INTERRUPTED_TOOL_RESULT = '(工具调用被中断,未能记录返回结果)';
|
|
298
|
+
/**
|
|
299
|
+
* The released-log reader's exact repair text for a call that was advertised
|
|
300
|
+
* but never recorded as started: relationships.ts's notStartedRepair admits
|
|
301
|
+
* only that shape (the `interrupted-tool-result-<callId>-<n>` id, the
|
|
302
|
+
* ToolNotStartedError/TOOL_NOT_STARTED error, no sourceEventSeqs, and this
|
|
303
|
+
* single text block) — no other wording resolves the call.
|
|
304
|
+
*/
|
|
305
|
+
const TOOL_NOT_STARTED_REPAIR_TEXT = 'The tool call was interrupted before the Harness recorded it as started. Retry it if it is still needed.';
|
|
306
|
+
/**
|
|
307
|
+
* The child session's currently-open copied step: the (turn, step) of a
|
|
308
|
+
* `step/start` whose `step/end` has not crossed, inside a turn the provider
|
|
309
|
+
* has not closed. Undefined when nothing is open. The live driver's
|
|
310
|
+
* interrupted-stream fold consults it: the fold's assistant/message may land
|
|
311
|
+
* only while its step is still open (a graceful sub-dsh unwind writes its own
|
|
312
|
+
* message and step/end, and the fold must not append at the closed step).
|
|
313
|
+
*/
|
|
314
|
+
export function mirroredOpenStep(childSession) {
|
|
315
|
+
const events = childSession.snapshotEvents();
|
|
316
|
+
// Backward: the latest boundary event says the state — a step/start opens,
|
|
317
|
+
// a step/end or any turn boundary closes the question.
|
|
318
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
319
|
+
const type = events[index]?.type;
|
|
320
|
+
if (type === 'step/start')
|
|
321
|
+
return events[index]?.data;
|
|
322
|
+
if (type === 'step/end' || type === 'turn/start' || type === 'turn/end')
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Close the copied open tail of the child session's current turn at settle,
|
|
329
|
+
* before the provider's `turn/end`: the released-log reader rejects a
|
|
330
|
+
* `turn/end` over an open step ('turn/end does not match the open turn with
|
|
331
|
+
* no open step') and a step/end leaving an advertised call unresolved, so an
|
|
332
|
+
* interrupted sub-dsh's open step is ended here — each advertised-but-
|
|
333
|
+
* unresolved tool call first receiving its synthesized interrupted result
|
|
334
|
+
* (the family's placeholder for a started call; the validator's exact
|
|
335
|
+
* TOOL_NOT_STARTED repair for one that never started). Mid-round transports
|
|
336
|
+
* never call this: the sub-dsh may yet write its own end, and a synthesized
|
|
337
|
+
* step/end would collide with it on a later pass. Returns what it
|
|
338
|
+
* synthesized, or undefined when the copied tail was already balanced.
|
|
339
|
+
*/
|
|
340
|
+
export function closeMirroredOpenStep(childSession) {
|
|
341
|
+
const events = childSession.snapshotEvents();
|
|
342
|
+
// Only the current (last) turn can hold the copied open tail: an earlier
|
|
343
|
+
// turn closed with the provider's turn/end, which no valid prefix crossed
|
|
344
|
+
// with open state.
|
|
345
|
+
let turnStart = -1;
|
|
346
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
347
|
+
const type = events[index]?.type;
|
|
348
|
+
if (type === 'turn/end')
|
|
349
|
+
return undefined;
|
|
350
|
+
if (type === 'turn/start') {
|
|
351
|
+
turnStart = index;
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (turnStart < 0)
|
|
356
|
+
return undefined;
|
|
357
|
+
let openStep;
|
|
358
|
+
// The turn's tool-call ledger: an assistant/message's tool-call blocks
|
|
359
|
+
// advertise, a tool/call starts, a tool/result resolves — its message names
|
|
360
|
+
// the call at the top level (the V4 tool-role shape) or in its single
|
|
361
|
+
// content block (the pre-V4 user-role shape); read both, a mirrored log may
|
|
362
|
+
// predate the upgrade.
|
|
363
|
+
const advertised = new Set();
|
|
364
|
+
const started = new Map();
|
|
365
|
+
const resolved = new Set();
|
|
366
|
+
for (const event of events.slice(turnStart + 1)) {
|
|
367
|
+
if (event.type === 'step/start') {
|
|
368
|
+
openStep = event.data;
|
|
369
|
+
}
|
|
370
|
+
else if (event.type === 'step/end') {
|
|
371
|
+
openStep = undefined;
|
|
372
|
+
}
|
|
373
|
+
else if (event.type === 'assistant/message') {
|
|
374
|
+
const content = event.data.message?.content ?? [];
|
|
375
|
+
for (const block of content) {
|
|
376
|
+
if (block?.type === 'tool-call' && typeof block.id === 'string')
|
|
377
|
+
advertised.add(block.id);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
else if (event.type === 'tool/call') {
|
|
381
|
+
const callId = event.data.callId;
|
|
382
|
+
if (typeof callId === 'string')
|
|
383
|
+
started.set(callId, event.seq);
|
|
384
|
+
}
|
|
385
|
+
else if (event.type === 'tool/result') {
|
|
386
|
+
const message = event.data.message;
|
|
387
|
+
const id = message?.toolCallId ?? message?.content?.[0]?.toolCallId;
|
|
388
|
+
if (typeof id === 'string')
|
|
389
|
+
resolved.add(id);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (openStep === undefined)
|
|
393
|
+
return undefined;
|
|
394
|
+
const step = openStep;
|
|
395
|
+
const repaired = [];
|
|
396
|
+
// The seq the next appended event occupies (seq == log length), so the
|
|
397
|
+
// synthesized ids carry the harness's repair-id convention.
|
|
398
|
+
let nextSeq = events.length;
|
|
399
|
+
for (const callId of advertised) {
|
|
400
|
+
if (resolved.has(callId))
|
|
401
|
+
continue;
|
|
402
|
+
const callSeq = started.get(callId);
|
|
403
|
+
const message = {
|
|
404
|
+
id: `interrupted-tool-result-${callId}-${nextSeq}`,
|
|
405
|
+
role: 'tool',
|
|
406
|
+
toolCallId: callId,
|
|
407
|
+
isError: true,
|
|
408
|
+
source: { kind: 'tool', callId },
|
|
409
|
+
content: [{ type: 'text', text: callSeq === undefined ? TOOL_NOT_STARTED_REPAIR_TEXT : DSH_INTERRUPTED_TOOL_RESULT }],
|
|
410
|
+
};
|
|
411
|
+
nextSeq += 1;
|
|
412
|
+
childSession.append('tool/result', {
|
|
413
|
+
turn: step.turn,
|
|
414
|
+
step: step.step,
|
|
415
|
+
message,
|
|
416
|
+
error: callSeq === undefined
|
|
417
|
+
? { name: 'ToolNotStartedError', code: 'TOOL_NOT_STARTED' }
|
|
418
|
+
: { name: 'ToolOutcomeUnknownError', code: 'TOOL_OUTCOME_UNKNOWN' },
|
|
419
|
+
}, {
|
|
420
|
+
surfaceOp: 'append',
|
|
421
|
+
// The not-started repair must carry NO sourceEventSeqs (the reader
|
|
422
|
+
// admits exactly that shape); the started repair pairs with the copied
|
|
423
|
+
// call event, exactly as a real mirrored result does.
|
|
424
|
+
...callSeq === undefined ? {} : { sourceEventSeqs: [callSeq] },
|
|
425
|
+
});
|
|
426
|
+
repaired.push(callId);
|
|
427
|
+
}
|
|
428
|
+
childSession.append('step/end', { turn: step.turn, step: step.step });
|
|
429
|
+
return { step, repairedCalls: repaired };
|
|
430
|
+
}
|
|
277
431
|
/**
|
|
278
432
|
* Mirror ONE live-pushed sub-dsh session event (the serve mode's
|
|
279
433
|
* `session/event` wire notification) into the child session. This is the
|
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"local-agent",
|
|
9
9
|
"subagent"
|
|
10
10
|
],
|
|
11
|
-
"version": "0.2.0
|
|
11
|
+
"version": "0.2.0",
|
|
12
12
|
"type": "module",
|
|
13
13
|
"main": "lib/index.js",
|
|
14
14
|
"types": "lib/types/index.d.ts",
|
|
@@ -75,8 +75,8 @@
|
|
|
75
75
|
"dsh": ">=0.1.5-rc.1 <0.3.0-0"
|
|
76
76
|
},
|
|
77
77
|
"dependencies": {
|
|
78
|
-
"@khorsheed/dsh-local-agent": "^0.2.0
|
|
79
|
-
"@khorsheed/dsh-local-agent-dsh-headless": "^0.2.0
|
|
78
|
+
"@khorsheed/dsh-local-agent": "^0.2.0",
|
|
79
|
+
"@khorsheed/dsh-local-agent-dsh-headless": "^0.2.0",
|
|
80
80
|
"js-yaml": "^4.3.1"
|
|
81
81
|
},
|
|
82
82
|
"peerDependencies": {
|