dsh-loop-engine-allinone 0.0.0-stage → 0.4.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 +100 -0
- package/CONTRIBUTING.md +78 -0
- package/LICENSE +23 -0
- package/NOTICE +45 -0
- package/README.md +221 -2
- package/README.zh.md +89 -0
- package/SECURITY.md +39 -0
- package/cordis.patch.yml +3 -0
- package/lib/client.js +2166 -0
- package/lib/index.js +11621 -0
- package/lib/invariant.js +453 -0
- package/lib/types/agent-preset-ids.d.ts +276 -0
- package/lib/types/client/LoopEngineBadge.d.ts +71 -0
- package/lib/types/client/LoopEngineComposerSelect.d.ts +126 -0
- package/lib/types/client/LoopEngineSection.d.ts +35 -0
- package/lib/types/client/engine-marks.d.ts +87 -0
- package/lib/types/client/index.d.ts +39 -0
- package/lib/types/client/locales.d.ts +182 -0
- package/lib/types/client/reload.d.ts +135 -0
- package/lib/types/client/session-engine.d.ts +508 -0
- package/lib/types/client/store.d.ts +103 -0
- package/lib/types/client/turn-status.d.ts +234 -0
- package/lib/types/client/use-session-engine.d.ts +74 -0
- package/lib/types/commands.d.ts +77 -0
- package/lib/types/compat.d.ts +25 -0
- package/lib/types/driver-acp/agent.d.ts +289 -0
- package/lib/types/driver-acp/approval.d.ts +27 -0
- package/lib/types/driver-acp/capabilities.d.ts +200 -0
- package/lib/types/driver-acp/client.d.ts +297 -0
- package/lib/types/driver-acp/executable.d.ts +53 -0
- package/lib/types/driver-acp/mapping.d.ts +76 -0
- package/lib/types/driver-acp/profile.d.ts +126 -0
- package/lib/types/driver-acp/runtime.d.ts +45 -0
- package/lib/types/driver-acp/subprocess.d.ts +19 -0
- package/lib/types/driver-acp/terminal.d.ts +98 -0
- package/lib/types/driver-acp/transport.d.ts +43 -0
- package/lib/types/driver-acp/types.d.ts +152 -0
- package/lib/types/driver-cli/capability.d.ts +42 -0
- package/lib/types/driver-cli/cli-agent.d.ts +258 -0
- package/lib/types/driver-cli/cli-loop.d.ts +46 -0
- package/lib/types/driver-cli/flatten.d.ts +35 -0
- package/lib/types/driver-cli/frames.d.ts +128 -0
- package/lib/types/driver-cli/ndjson.d.ts +106 -0
- package/lib/types/driver-cli/session-sink.d.ts +333 -0
- package/lib/types/driver-cli/stream-json.d.ts +61 -0
- package/lib/types/driver-cli/text.d.ts +66 -0
- package/lib/types/driver-cli/transport.d.ts +84 -0
- package/lib/types/driver-core/agents-md-skill-provider.d.ts +72 -0
- package/lib/types/driver-core/assistant-stream.d.ts +68 -0
- package/lib/types/driver-core/context-files.d.ts +62 -0
- package/lib/types/driver-core/host-servers.d.ts +119 -0
- package/lib/types/driver-core/hosted-engine-runtime.d.ts +190 -0
- package/lib/types/driver-core/hosted-tool-vocabulary.d.ts +78 -0
- package/lib/types/driver-core/idle-child.d.ts +57 -0
- package/lib/types/driver-core/image-access.d.ts +25 -0
- package/lib/types/driver-core/inbox.d.ts +104 -0
- package/lib/types/driver-core/model-handover.d.ts +134 -0
- package/lib/types/driver-core/ownership.d.ts +41 -0
- package/lib/types/driver-core/permission-knobs.d.ts +26 -0
- package/lib/types/driver-core/prompt.d.ts +64 -0
- package/lib/types/driver-core/session-lifetime.d.ts +62 -0
- package/lib/types/driver-core/session-model.d.ts +128 -0
- package/lib/types/driver-core/skill-inject.d.ts +59 -0
- package/lib/types/driver-core/step-close.d.ts +49 -0
- package/lib/types/driver-core/step-images.d.ts +62 -0
- package/lib/types/driver-core/step-tools.d.ts +43 -0
- package/lib/types/driver-core/system-head.d.ts +43 -0
- package/lib/types/engine-agy/agent.d.ts +67 -0
- package/lib/types/engine-agy/frames.d.ts +90 -0
- package/lib/types/engine-agy/loop.d.ts +64 -0
- package/lib/types/engine-agy/process.d.ts +173 -0
- package/lib/types/engine-claude/agent.d.ts +186 -0
- package/lib/types/engine-claude/loop.d.ts +73 -0
- package/lib/types/engine-claude/mapping.d.ts +108 -0
- package/lib/types/engine-claude/permission.d.ts +41 -0
- package/lib/types/engine-claude/process.d.ts +59 -0
- package/lib/types/engine-claude/sdk.d.ts +67 -0
- package/lib/types/engine-claude/types.d.ts +28 -0
- package/lib/types/engine-codex/agent.d.ts +216 -0
- package/lib/types/engine-codex/appserver/client.d.ts +80 -0
- package/lib/types/engine-codex/appserver/mapping.d.ts +67 -0
- package/lib/types/engine-codex/appserver/thread.d.ts +66 -0
- package/lib/types/engine-codex/appserver/types.d.ts +225 -0
- package/lib/types/engine-codex/loop.d.ts +75 -0
- package/lib/types/engine-codex/model-handover.d.ts +56 -0
- package/lib/types/engine-codex/permission.d.ts +126 -0
- package/lib/types/engine-codex/skills.d.ts +28 -0
- package/lib/types/engine-codex/types.d.ts +36 -0
- package/lib/types/engine-drivers.d.ts +132 -0
- package/lib/types/engine-grok/agent.d.ts +24 -0
- package/lib/types/engine-grok/loop.d.ts +61 -0
- package/lib/types/engine-grok/process.d.ts +46 -0
- package/lib/types/engine-grok/profile.d.ts +82 -0
- package/lib/types/engine-hermes/agent.d.ts +47 -0
- package/lib/types/engine-hermes/loop.d.ts +56 -0
- package/lib/types/engine-hermes/process.d.ts +92 -0
- package/lib/types/engine-kimi/acp/client.d.ts +123 -0
- package/lib/types/engine-kimi/acp/mapping.d.ts +73 -0
- package/lib/types/engine-kimi/acp/types.d.ts +143 -0
- package/lib/types/engine-kimi/agent.d.ts +24 -0
- package/lib/types/engine-kimi/commands.d.ts +44 -0
- package/lib/types/engine-kimi/loop.d.ts +62 -0
- package/lib/types/engine-kimi/model-handover.d.ts +32 -0
- package/lib/types/engine-kimi/permission.d.ts +28 -0
- package/lib/types/engine-kimi/process.d.ts +47 -0
- package/lib/types/engine-kimi/profile.d.ts +19 -0
- package/lib/types/engine-kimi/skills.d.ts +52 -0
- package/lib/types/engine-kimi/types.d.ts +25 -0
- package/lib/types/engine-minimaxcode/agent.d.ts +24 -0
- package/lib/types/engine-minimaxcode/loop.d.ts +83 -0
- package/lib/types/engine-minimaxcode/process.d.ts +53 -0
- package/lib/types/engine-minimaxcode/profile.d.ts +72 -0
- package/lib/types/engine-of-session.d.ts +97 -0
- package/lib/types/engine-pi/agent.d.ts +230 -0
- package/lib/types/engine-pi/loop.d.ts +72 -0
- package/lib/types/engine-pi/model-handover.d.ts +84 -0
- package/lib/types/engine-pi/permission.d.ts +47 -0
- package/lib/types/engine-pi/rpc/client.d.ts +107 -0
- package/lib/types/engine-pi/rpc/mapping.d.ts +37 -0
- package/lib/types/engine-pi/rpc/types.d.ts +249 -0
- package/lib/types/engine-pi/skills.d.ts +47 -0
- package/lib/types/engine-pi/types.d.ts +36 -0
- package/lib/types/engine-qoderclicn/agent.d.ts +59 -0
- package/lib/types/engine-qoderclicn/loop.d.ts +51 -0
- package/lib/types/engine-qoderclicn/process.d.ts +113 -0
- package/lib/types/engine-registry.d.ts +584 -0
- package/lib/types/engine-remote.d.ts +192 -0
- package/lib/types/engine-surface.d.ts +36 -0
- package/lib/types/engine-zcode/agent.d.ts +50 -0
- package/lib/types/engine-zcode/loop.d.ts +47 -0
- package/lib/types/engine-zcode/process.d.ts +111 -0
- package/lib/types/index.d.ts +205 -0
- package/lib/types/invariant.d.ts +26 -0
- package/lib/types/model-selection-reset.d.ts +271 -0
- package/lib/types/namespace.d.ts +25 -0
- package/lib/types/patch-manager.d.ts +104 -0
- package/lib/types/preset.d.ts +254 -0
- package/lib/types/provider-route.d.ts +245 -0
- package/lib/types/router-loop.d.ts +458 -0
- package/lib/types/session-engine-store.d.ts +138 -0
- package/lib/types/settings.d.ts +60 -0
- package/lib/types/skills.d.ts +85 -0
- package/package.json +150 -4
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The normalized frame vocabulary every CLI protocol adapter maps onto.
|
|
3
|
+
*
|
|
4
|
+
* A hosted CLI speaks one of four wire families — line-delimited JSON, JSON-RPC
|
|
5
|
+
* 2.0, the Agent Client Protocol, or plain text — and each family spells the
|
|
6
|
+
* same handful of facts differently. What a driver actually has to do with those
|
|
7
|
+
* facts is identical in every case: open a turn, stream text and reasoning,
|
|
8
|
+
* announce and settle tool calls, and say how the turn ended.
|
|
9
|
+
*
|
|
10
|
+
* So the split is: a PROTOCOL ADAPTER turns wire frames into the events below,
|
|
11
|
+
* and {@link SessionSink} (`./session-sink.ts`) turns these events into the
|
|
12
|
+
* durable dsh session log. Neither half knows the other's vocabulary, and an
|
|
13
|
+
* engine driver is the small mapping in between plus its own transport.
|
|
14
|
+
*
|
|
15
|
+
* The vocabulary is deliberately narrow. Anything a protocol reports that dsh has
|
|
16
|
+
* no durable place for (a queue update, a compaction notice, an extension UI
|
|
17
|
+
* request) is simply not part of it: an adapter drops what it cannot express
|
|
18
|
+
* rather than inventing a frame the sink would have to ignore.
|
|
19
|
+
*
|
|
20
|
+
* @module dsh-loop-engine-allinone/driver-cli/frames
|
|
21
|
+
*/
|
|
22
|
+
/** One tool call the engine announced, before its result is known. */
|
|
23
|
+
export interface ToolCallFrame {
|
|
24
|
+
/** The engine's own call id; dsh keys its pending-tool map by this. */
|
|
25
|
+
readonly id: string;
|
|
26
|
+
/** The engine's own tool name, before projection onto dsh's vocabulary. */
|
|
27
|
+
readonly name: string;
|
|
28
|
+
/** The call's arguments, as the engine sent them (a JSON string or a value). */
|
|
29
|
+
readonly arguments: unknown;
|
|
30
|
+
}
|
|
31
|
+
/** One tool result the engine reported for a call it announced. */
|
|
32
|
+
export interface ToolResultFrame {
|
|
33
|
+
/** The call id this settles; must match an announced {@link ToolCallFrame}. */
|
|
34
|
+
readonly id: string;
|
|
35
|
+
/** The result text, already flattened to a string. */
|
|
36
|
+
readonly text: string;
|
|
37
|
+
/** Whether the engine reported the call as failed. */
|
|
38
|
+
readonly isError: boolean;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* A protocol event, normalized.
|
|
42
|
+
*
|
|
43
|
+
* `delta` events are ordered and only meaningful inside a segment; `settle`
|
|
44
|
+
* ends the run. An adapter that cannot stream emits `text` once and then
|
|
45
|
+
* `settle` — that is exactly what the plain-text adapter does, and it is why
|
|
46
|
+
* {@link EngineCapabilities.streaming} is a declaration rather than a guess.
|
|
47
|
+
*/
|
|
48
|
+
export type CliFrame =
|
|
49
|
+
/** A new assistant segment began; deltas after this belong to it. */
|
|
50
|
+
{
|
|
51
|
+
readonly kind: 'segment';
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Streamed answer text.
|
|
55
|
+
*
|
|
56
|
+
* `index` is the protocol's own content-block index when it reports one, so a
|
|
57
|
+
* message with several text blocks keeps the model's own layout. An adapter
|
|
58
|
+
* that has no index leaves it out, and the sink assigns one.
|
|
59
|
+
*/
|
|
60
|
+
| {
|
|
61
|
+
readonly kind: 'text';
|
|
62
|
+
readonly text: string;
|
|
63
|
+
readonly index?: number;
|
|
64
|
+
}
|
|
65
|
+
/** Streamed reasoning text; `index` as for the `text` member. */
|
|
66
|
+
| {
|
|
67
|
+
readonly kind: 'reasoning';
|
|
68
|
+
readonly text: string;
|
|
69
|
+
readonly index?: number;
|
|
70
|
+
}
|
|
71
|
+
/** The engine announced a tool call. */
|
|
72
|
+
| {
|
|
73
|
+
readonly kind: 'tool-call';
|
|
74
|
+
readonly call: ToolCallFrame;
|
|
75
|
+
}
|
|
76
|
+
/** The engine reported a tool result. */
|
|
77
|
+
| {
|
|
78
|
+
readonly kind: 'tool-result';
|
|
79
|
+
readonly result: ToolResultFrame;
|
|
80
|
+
}
|
|
81
|
+
/** Token usage for the turn, as the engine reported it. */
|
|
82
|
+
| {
|
|
83
|
+
readonly kind: 'usage';
|
|
84
|
+
readonly usage: CliUsage;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The engine's own verdict on the turn.
|
|
88
|
+
*
|
|
89
|
+
* A verdict is recorded rather than thrown on sight because an engine may
|
|
90
|
+
* retry: a run can report an errored attempt and still succeed, so only the
|
|
91
|
+
* verdict still standing when the stream ends is a failure
|
|
92
|
+
* ({@link CliRunOutcome}).
|
|
93
|
+
*/
|
|
94
|
+
| {
|
|
95
|
+
readonly kind: 'error';
|
|
96
|
+
readonly message: string;
|
|
97
|
+
}
|
|
98
|
+
/** The run is over; no further frames will arrive. */
|
|
99
|
+
| {
|
|
100
|
+
readonly kind: 'settle';
|
|
101
|
+
};
|
|
102
|
+
/** Token usage in dsh's shape, as far as a CLI reports it. */
|
|
103
|
+
export interface CliUsage {
|
|
104
|
+
readonly input: number;
|
|
105
|
+
readonly output: number;
|
|
106
|
+
readonly cacheRead?: number;
|
|
107
|
+
readonly cacheWrite?: number;
|
|
108
|
+
readonly thinking?: number;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* How one CLI run ended, after the sink folded every frame.
|
|
112
|
+
*
|
|
113
|
+
* `failed` is separate from a thrown error on purpose: an engine that reports a
|
|
114
|
+
* failed turn has still run correctly from the driver's point of view — the
|
|
115
|
+
* driver's job is to report the engine's answer, including a bad one. Only a
|
|
116
|
+
* protocol failure (a malformed stream, a child that died mid-frame) throws.
|
|
117
|
+
*/
|
|
118
|
+
export type CliRunOutcome =
|
|
119
|
+
/** The engine produced an answer. */
|
|
120
|
+
{
|
|
121
|
+
readonly kind: 'completed';
|
|
122
|
+
}
|
|
123
|
+
/** The engine reported a failed turn; `message` is its own words. */
|
|
124
|
+
| {
|
|
125
|
+
readonly kind: 'failed';
|
|
126
|
+
readonly message: string;
|
|
127
|
+
};
|
|
128
|
+
//# sourceMappingURL=frames.d.ts.map
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The line-delimited-JSON protocol skeleton.
|
|
3
|
+
*
|
|
4
|
+
* Three of the four wire families are "one JSON document per line": the newline
|
|
5
|
+
* framing is identical, and only the FIELD NAMES inside each document differ.
|
|
6
|
+
* So the reader — split a byte stream into lines, parse each, hand it over,
|
|
7
|
+
* and surface a parse failure as a protocol error rather than silently skipping
|
|
8
|
+
* the line — lives here once, and an engine supplies only the mapping from a
|
|
9
|
+
* parsed document to {@link CliFrame}s.
|
|
10
|
+
*
|
|
11
|
+
* The reader also owns the two framing rules that are easy to get wrong and
|
|
12
|
+
* expensive to debug:
|
|
13
|
+
*
|
|
14
|
+
* - **A trailing partial line is still a line.** A child that exits without a
|
|
15
|
+
* final newline has still said something, so the last buffer is flushed on
|
|
16
|
+
* close. Dropping it loses the engine's answer.
|
|
17
|
+
* - **A blank line is not a document.** Engines interleave progress lines and
|
|
18
|
+
* keep-alives; an empty line is skipped rather than reported as malformed.
|
|
19
|
+
*
|
|
20
|
+
* @module dsh-loop-engine-allinone/driver-cli/ndjson
|
|
21
|
+
*/
|
|
22
|
+
import type { Readable, Writable } from 'node:stream';
|
|
23
|
+
import type { CliFrame } from './frames.ts';
|
|
24
|
+
import type { CliTransport } from './transport.ts';
|
|
25
|
+
/**
|
|
26
|
+
* Split a byte stream into lines.
|
|
27
|
+
*
|
|
28
|
+
* Yields each complete line WITHOUT its terminator, plus a trailing partial line
|
|
29
|
+
* when the stream ends. A line is decoded as UTF-8: every engine this plugin
|
|
30
|
+
* speaks to emits UTF-8 on its protocol channel (their own locale or OEM
|
|
31
|
+
* encodings, if any, apply to human-facing output rather than the wire).
|
|
32
|
+
* @param stream - the child's stdout.
|
|
33
|
+
* @returns an async iterator of lines.
|
|
34
|
+
*/
|
|
35
|
+
export declare function readLines(stream: Readable): AsyncIterable<string>;
|
|
36
|
+
/**
|
|
37
|
+
* Parse one NDJSON line.
|
|
38
|
+
* @param line - the raw line.
|
|
39
|
+
* @returns the parsed document, or undefined when the line is blank or not JSON.
|
|
40
|
+
*/
|
|
41
|
+
export declare function parseJsonLine(line: string): unknown;
|
|
42
|
+
/** What one NDJSON run needs. */
|
|
43
|
+
export interface NdjsonRunSpec {
|
|
44
|
+
/** The spawned child. */
|
|
45
|
+
readonly transport: CliTransport;
|
|
46
|
+
/**
|
|
47
|
+
* A payload to write to the child's stdin before reading, when the prompt
|
|
48
|
+
* travels that way (`agy --input-format stream-json`). Omitted when the
|
|
49
|
+
* prompt is an argv argument.
|
|
50
|
+
*/
|
|
51
|
+
readonly stdin?: string;
|
|
52
|
+
/**
|
|
53
|
+
* Map one parsed document to frames.
|
|
54
|
+
*
|
|
55
|
+
* Returning an empty array is normal: most protocol lines are progress,
|
|
56
|
+
* session bookkeeping or telemetry that has no durable place. Returning
|
|
57
|
+
* nothing at all is also normal for a document the adapter does not
|
|
58
|
+
* recognize — an unknown frame is DROPPED rather than raised, because a
|
|
59
|
+
* protocol that grows a new notification kind must not break a working
|
|
60
|
+
* engine.
|
|
61
|
+
* @param document - the parsed JSON document.
|
|
62
|
+
* @returns the frames it expresses, in order.
|
|
63
|
+
*/
|
|
64
|
+
map(document: unknown): readonly CliFrame[];
|
|
65
|
+
/**
|
|
66
|
+
* Called when a non-blank line could not be parsed as JSON.
|
|
67
|
+
*
|
|
68
|
+
* Counted rather than raised, and only reported at the end: a child that
|
|
69
|
+
* prints a warning on its protocol channel is common, and failing the whole
|
|
70
|
+
* turn over one stray line would be worse than reporting it. If NO line ever
|
|
71
|
+
* parsed, the run fails and this is what says why.
|
|
72
|
+
* @param line - the unparseable line.
|
|
73
|
+
*/
|
|
74
|
+
onUnparsed?(line: string): void;
|
|
75
|
+
}
|
|
76
|
+
/** What one NDJSON run produced. */
|
|
77
|
+
export interface NdjsonRunResult {
|
|
78
|
+
/** Documents that parsed. */
|
|
79
|
+
readonly parsed: number;
|
|
80
|
+
/** Non-blank lines that did not parse. */
|
|
81
|
+
readonly unparsed: number;
|
|
82
|
+
/** The last unparseable line, for the diagnostic when nothing parsed. */
|
|
83
|
+
readonly lastUnparsed: string | undefined;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Drive one NDJSON run: write the prompt, decode every line, feed the adapter.
|
|
87
|
+
*
|
|
88
|
+
* Frames are handed to `accept` as they arrive rather than collected, so a
|
|
89
|
+
* long answer streams instead of materializing.
|
|
90
|
+
* @param spec - the run's transport, stdin payload and frame mapper.
|
|
91
|
+
* @param accept - sink for the frames.
|
|
92
|
+
* @returns what the run saw.
|
|
93
|
+
*/
|
|
94
|
+
export declare function runNdjson(spec: NdjsonRunSpec, accept: (frame: CliFrame) => void): Promise<NdjsonRunResult>;
|
|
95
|
+
/**
|
|
96
|
+
* Write a payload to the child's stdin and close it.
|
|
97
|
+
*
|
|
98
|
+
* Closing matters: an engine reading `stream-json` from stdin runs one turn per
|
|
99
|
+
* line and waits, so a stdin left open hangs the run until the process is
|
|
100
|
+
* killed. A write failure is swallowed — the child may have exited already, and
|
|
101
|
+
* the exit code is the more useful diagnostic.
|
|
102
|
+
* @param stdin - the child's stdin, when the spawn requested a pipe.
|
|
103
|
+
* @param payload - the text to write.
|
|
104
|
+
*/
|
|
105
|
+
export declare function writeStdin(stdin: Writable | undefined, payload: string): void;
|
|
106
|
+
//# sourceMappingURL=ndjson.d.ts.map
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared session sink: one implementation of "turn these frames into the
|
|
3
|
+
* durable dsh session log", used by every CLI driver.
|
|
4
|
+
*
|
|
5
|
+
* This is the half of a driver that is genuinely hard to get right, and it is
|
|
6
|
+
* identical for every engine, so it lives in exactly one place. What it owns:
|
|
7
|
+
*
|
|
8
|
+
* - **The assistant-message lifecycle.** Content arrives as deltas, is held,
|
|
9
|
+
* and is committed as a durable `assistant/message` at a boundary. The live
|
|
10
|
+
* attempt's frames are published as `agent/assistant-stream` and its compact
|
|
11
|
+
* stream travels with the message ({@link DriverAssistantStream}).
|
|
12
|
+
* - **The tool-call/tool-result pairing Session V4 enforces.** A `tool/result`
|
|
13
|
+
* must follow the `assistant/message` that carries its `tool-call` block, and
|
|
14
|
+
* each call may be settled EXACTLY once — a second result for one id is
|
|
15
|
+
* refused on load and bricks the whole session log. Engines report results
|
|
16
|
+
* inconsistently (twice for one execution, or before the message that
|
|
17
|
+
* requested it), so this class synthesizes the owning message when it has to
|
|
18
|
+
* and drops the repeat.
|
|
19
|
+
* - **Step rotation.** A settled tool result means the segment that ran it is
|
|
20
|
+
* complete, so the next assistant content belongs to a new step. Rotating at
|
|
21
|
+
* a message boundary keeps the calls one model turn announced together in one
|
|
22
|
+
* step. A step that still OWES a result never rotates — that would strand the
|
|
23
|
+
* call in a step that can never resolve it, which is itself a log V4 refuses.
|
|
24
|
+
* - **Closing a step honestly.** `closeStep` writes an explicit failure result
|
|
25
|
+
* for every call still unsettled, because a `step/end` carrying an unresolved
|
|
26
|
+
* announcement is a log that will not load.
|
|
27
|
+
*
|
|
28
|
+
* What it deliberately does NOT own: the turn/step loop, cancellation, the
|
|
29
|
+
* inbox, the prompt, and the protocol. Those stay in the driver, because they
|
|
30
|
+
* are where engines actually differ.
|
|
31
|
+
*
|
|
32
|
+
* @module dsh-loop-engine-allinone/driver-cli/session-sink
|
|
33
|
+
*/
|
|
34
|
+
import type { AgentEventDispatch } from '@deepseek-ai/dsh-agent';
|
|
35
|
+
import type { ContentBlock, Message, TokenUsage } from '@deepseek-ai/dsh-llm';
|
|
36
|
+
import { ToolCallId } from '@deepseek-ai/dsh-llm';
|
|
37
|
+
import type { Session } from '@deepseek-ai/dsh-session';
|
|
38
|
+
import type { SessionId } from '@deepseek-ai/dsh-session';
|
|
39
|
+
import type { CliFrame, CliUsage } from './frames.ts';
|
|
40
|
+
import type { HostedEngineId } from '../engine-registry.ts';
|
|
41
|
+
/** Where one run's frames land: the session position they belong to. */
|
|
42
|
+
export interface SinkPosition {
|
|
43
|
+
/** The durable turn owning this run. */
|
|
44
|
+
turn: number;
|
|
45
|
+
/** The durable step this run opened on; the sink may rotate it. */
|
|
46
|
+
step: number;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* What the sink needs from the driver it serves.
|
|
50
|
+
*
|
|
51
|
+
* Passed as a small interface rather than as the driver itself, so the sink can
|
|
52
|
+
* be tested without a driver and so it is obvious what it depends on. Every
|
|
53
|
+
* member is a question the sink cannot answer itself: which engine is speaking
|
|
54
|
+
* (tool-name projection is per-engine), which provider label to log, and where
|
|
55
|
+
* the live attempt's frames go.
|
|
56
|
+
*/
|
|
57
|
+
export interface SinkHost {
|
|
58
|
+
/** The session being written. */
|
|
59
|
+
readonly session: Session;
|
|
60
|
+
/** This agent's session id, for the attempt identity of a live stream. */
|
|
61
|
+
readonly sessionId: SessionId;
|
|
62
|
+
/** The engine whose vocabulary tool names are projected from. */
|
|
63
|
+
readonly engine: HostedEngineId;
|
|
64
|
+
/** The provider route label logged into message provenance. */
|
|
65
|
+
readonly provider: string;
|
|
66
|
+
/** The model label logged into message provenance. */
|
|
67
|
+
readonly model: string;
|
|
68
|
+
/** Agent-scoped notification publisher, for live stream frames. */
|
|
69
|
+
readonly dispatch: AgentEventDispatch;
|
|
70
|
+
/**
|
|
71
|
+
* Called when the sink rotates to a new step, so the driver can record the
|
|
72
|
+
* rotation and re-run whatever it owes a fresh step (announcing the step,
|
|
73
|
+
* re-checking the system head).
|
|
74
|
+
*
|
|
75
|
+
* The sink rotates; the DRIVER owns what a rotation means for its own
|
|
76
|
+
* bookkeeping, which is why this is a callback rather than something the sink
|
|
77
|
+
* does silently.
|
|
78
|
+
* @param step - the step number now open.
|
|
79
|
+
*/
|
|
80
|
+
onStepRotated(step: number): void;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The shared sink. One instance per agent lifecycle.
|
|
84
|
+
*
|
|
85
|
+
* Not re-entrant and not concurrent: a session runs one step at a time, and the
|
|
86
|
+
* sink's buffers describe that one run. A second concurrent run on one session
|
|
87
|
+
* is refused upstream (the router never hands a live session to two machines).
|
|
88
|
+
*/
|
|
89
|
+
export declare class SessionSink {
|
|
90
|
+
private readonly host;
|
|
91
|
+
private readonly stepTools;
|
|
92
|
+
/** Calls announced in this step whose results have landed. */
|
|
93
|
+
private settledTools;
|
|
94
|
+
/** This run's live attempt, opened on the first chunk and settled by a message. */
|
|
95
|
+
private live;
|
|
96
|
+
/** Agent-lifecycle-local attempt counter, naming each streamed attempt. */
|
|
97
|
+
private streamAttempts;
|
|
98
|
+
/** The assistant message being assembled, if any. */
|
|
99
|
+
private held;
|
|
100
|
+
/** Text blocks already block-start-ed, by content index. */
|
|
101
|
+
private readonly startedText;
|
|
102
|
+
/** Reasoning blocks already block-start-ed, by content index. */
|
|
103
|
+
private readonly startedReasoning;
|
|
104
|
+
/** Reasoning accumulated by content index, for engines that stream it without a final block. */
|
|
105
|
+
private readonly thinkingByIndex;
|
|
106
|
+
/**
|
|
107
|
+
* Answer text accumulated by content index.
|
|
108
|
+
*
|
|
109
|
+
* Present so a message can be built from the DELTAS when the protocol never
|
|
110
|
+
* hands over a finished message — and so a whole-message commit can replace
|
|
111
|
+
* the accumulation instead of double-counting it.
|
|
112
|
+
*/
|
|
113
|
+
private readonly textByIndex;
|
|
114
|
+
/**
|
|
115
|
+
* The content-block index each kind is currently appending to, and the next
|
|
116
|
+
* index to hand out when a kind starts a new block.
|
|
117
|
+
*
|
|
118
|
+
* Kept per kind rather than shared because text and reasoning interleave: a
|
|
119
|
+
* reasoning chunk between two text chunks must not split the text.
|
|
120
|
+
*/
|
|
121
|
+
private readonly openIndex;
|
|
122
|
+
private nextIndex;
|
|
123
|
+
/** Call ids already announced, so a protocol that repeats one announces it once. */
|
|
124
|
+
private readonly announced;
|
|
125
|
+
/** Call ids already written a result, so a repeated report is dropped. */
|
|
126
|
+
private readonly settled;
|
|
127
|
+
/** Tool-call blocks not yet folded into a flushed assistant message. */
|
|
128
|
+
private pendingToolCalls;
|
|
129
|
+
/** `tool/call` events owed by the pending blocks, appended once their message lands. */
|
|
130
|
+
private pendingCallLog;
|
|
131
|
+
/** Calls released by the flush that just folded their owner message. */
|
|
132
|
+
private callsToLog;
|
|
133
|
+
/** Last usage snapshot, folded onto the message that closes the run. */
|
|
134
|
+
private lastUsage;
|
|
135
|
+
/** The engine's standing verdict on this run; cleared by a later successful report. */
|
|
136
|
+
private failure;
|
|
137
|
+
/** Whether an assistant message was already committed for this run. */
|
|
138
|
+
private assistantFlushed;
|
|
139
|
+
/** Whether a segment has been opened (so a `segment` frame before content is not double-counted). */
|
|
140
|
+
private segmentOpen;
|
|
141
|
+
constructor(host: SinkHost);
|
|
142
|
+
/**
|
|
143
|
+
* @param host - the driver-side facts the sink cannot answer itself.
|
|
144
|
+
*/
|
|
145
|
+
/** Reset per-step bookkeeping. Called by the driver when a step opens. */
|
|
146
|
+
beginStep(): void;
|
|
147
|
+
/**
|
|
148
|
+
* Reset the per-RUN bookkeeping that must not leak between steps.
|
|
149
|
+
*
|
|
150
|
+
* `announced` and `settled` are deliberately NOT reset: call ids are unique
|
|
151
|
+
* for the life of a session, so retaining them only widens duplicate
|
|
152
|
+
* suppression and can never hide a legitimate result. A protocol that reuses
|
|
153
|
+
* an id across steps would be malformed anyway.
|
|
154
|
+
*/
|
|
155
|
+
beginRun(): void;
|
|
156
|
+
/** The step number currently open, as the sink has rotated it. */
|
|
157
|
+
stepOf(position: SinkPosition): number;
|
|
158
|
+
/** The calls this step still owes a result, in announcement order. */
|
|
159
|
+
get pendingCalls(): readonly ToolCallId[];
|
|
160
|
+
/**
|
|
161
|
+
* The engine's standing verdict on the run so far.
|
|
162
|
+
* @returns the failure message, or undefined when the run is not (or no longer) failed.
|
|
163
|
+
*/
|
|
164
|
+
get runFailure(): string | undefined;
|
|
165
|
+
/**
|
|
166
|
+
* Consume one normalized frame.
|
|
167
|
+
*
|
|
168
|
+
* The position object is mutated in place when the sink rotates a step, so the
|
|
169
|
+
* caller's own view of "which step is open" follows — that is what keeps the
|
|
170
|
+
* turn loop's fail-safe step close on the step that is actually open rather
|
|
171
|
+
* than the one the run started under.
|
|
172
|
+
* @param frame - the normalized frame.
|
|
173
|
+
* @param position - the turn/step this run belongs to; `step` may be bumped.
|
|
174
|
+
*/
|
|
175
|
+
accept(frame: CliFrame, position: SinkPosition): void;
|
|
176
|
+
/**
|
|
177
|
+
* Open a new assistant segment.
|
|
178
|
+
*
|
|
179
|
+
* A segment after a settled tool result is the next step: the previous
|
|
180
|
+
* segment is complete and the content about to arrive belongs to the next one.
|
|
181
|
+
* A step that still owes a result does not rotate (see the module note).
|
|
182
|
+
* @param position - the open position, mutated when a rotation happens.
|
|
183
|
+
*/
|
|
184
|
+
private openSegment;
|
|
185
|
+
/** Close the step currently open, writing explicit failures for anything unsettled. */
|
|
186
|
+
private closeOpenStep;
|
|
187
|
+
/**
|
|
188
|
+
* Close the step the sink currently holds, for the driver's step teardown.
|
|
189
|
+
* @param position - the open position.
|
|
190
|
+
*/
|
|
191
|
+
closeStep(position: SinkPosition): void;
|
|
192
|
+
/** Open the step's own surface head, once, when the driver opens a step. */
|
|
193
|
+
openStep(position: SinkPosition): void;
|
|
194
|
+
/**
|
|
195
|
+
* Stream one chunk: publish its live frame AND accumulate it into the durable
|
|
196
|
+
* message being built.
|
|
197
|
+
*
|
|
198
|
+
* The two halves are both required and neither implies the other. The live
|
|
199
|
+
* frames are what the UI paints while the answer arrives; the accumulated
|
|
200
|
+
* blocks are what the session log stores. Accumulating only the frames (or
|
|
201
|
+
* only the blocks) produces a session whose durable message is missing the
|
|
202
|
+
* text the user watched being written — which is why this is one function.
|
|
203
|
+
*
|
|
204
|
+
* Blocks are keyed by content index, so a protocol that reports its own index
|
|
205
|
+
* keeps the model's own block layout, and one that does not gets a fresh block
|
|
206
|
+
* per switch between text and reasoning. Either way the durable message folds
|
|
207
|
+
* reasoning before text, matching the ordering the in-process loop produces.
|
|
208
|
+
* @param kind - whether this is answer text or reasoning.
|
|
209
|
+
* @param text - the delta.
|
|
210
|
+
* @param position - the open position, for the live attempt's framing.
|
|
211
|
+
* @param explicitIndex - the protocol's own content index, when it reports one.
|
|
212
|
+
*/
|
|
213
|
+
private streamChunk;
|
|
214
|
+
/**
|
|
215
|
+
* The live attempt for the current segment, opened on first use.
|
|
216
|
+
*
|
|
217
|
+
* Chunks open one; the durable assistant message they built settles it and the
|
|
218
|
+
* next chunk opens a fresh attempt, so every message carries exactly its own
|
|
219
|
+
* timed stream.
|
|
220
|
+
* @param position - the open position, for the attempt's turn/step framing.
|
|
221
|
+
* @returns the live attempt.
|
|
222
|
+
*/
|
|
223
|
+
private currentStream;
|
|
224
|
+
/**
|
|
225
|
+
* Record one announced tool call.
|
|
226
|
+
*
|
|
227
|
+
* The call is projected onto dsh's vocabulary ONCE, and the same projected
|
|
228
|
+
* name and arguments go to both the assistant message's `tool-call` block and
|
|
229
|
+
* the durable `tool/call` event — Session V4 pairs the two by id and refuses a
|
|
230
|
+
* log whose block and event disagree.
|
|
231
|
+
* @param call - the announced call.
|
|
232
|
+
*/
|
|
233
|
+
private emitToolCall;
|
|
234
|
+
/**
|
|
235
|
+
* Settle one tool call, exactly once.
|
|
236
|
+
*
|
|
237
|
+
* The owning assistant message is flushed first when the call has none yet:
|
|
238
|
+
* the durable surface must always pair a result with a preceding assistant
|
|
239
|
+
* `tool-call` block, and an engine that executed a call without streaming an
|
|
240
|
+
* assistant message first would otherwise leave the result orphaned.
|
|
241
|
+
* @param id - the call being settled.
|
|
242
|
+
* @param text - the result text.
|
|
243
|
+
* @param isError - whether the engine reported a failure.
|
|
244
|
+
* @param position - the open position.
|
|
245
|
+
*/
|
|
246
|
+
private settleTool;
|
|
247
|
+
/**
|
|
248
|
+
* Flush a synthesized assistant message carrying any tool-call blocks that have
|
|
249
|
+
* no committed owner yet.
|
|
250
|
+
* @param position - the open position.
|
|
251
|
+
*/
|
|
252
|
+
private ensureToolCallOwner;
|
|
253
|
+
/**
|
|
254
|
+
* Commit the held assistant message, if any.
|
|
255
|
+
* @param position - the open position.
|
|
256
|
+
* @param usage - usage to attach, when this message closes the run.
|
|
257
|
+
*/
|
|
258
|
+
private flushHeld;
|
|
259
|
+
/**
|
|
260
|
+
* Drop the deltas accumulated for the segment being replaced.
|
|
261
|
+
*
|
|
262
|
+
* A protocol that streams partials AND hands over a finished message has said
|
|
263
|
+
* the same thing twice; the finished message is authoritative, so the partials
|
|
264
|
+
* are discarded rather than committed as a second message.
|
|
265
|
+
*/
|
|
266
|
+
private clearAccumulation;
|
|
267
|
+
/**
|
|
268
|
+
* Build the content blocks the streamed deltas accumulated.
|
|
269
|
+
*
|
|
270
|
+
* Reasoning first, then text: that is the ordering the in-process loop's own
|
|
271
|
+
* messages use, and a durable message that put the answer before the model's
|
|
272
|
+
* reasoning would read wrong on replay.
|
|
273
|
+
* @returns the accumulated blocks, in durable order.
|
|
274
|
+
*/
|
|
275
|
+
private accumulatedBlocks;
|
|
276
|
+
/** Fold any not-yet-flushed tool-call blocks into the message being committed. */
|
|
277
|
+
private foldPendingCalls;
|
|
278
|
+
/**
|
|
279
|
+
* Commit the assistant message a whole-message protocol reports.
|
|
280
|
+
*
|
|
281
|
+
* A text-protocol engine has no deltas: it hands over a finished answer, and
|
|
282
|
+
* this is where it becomes the same durable shape a streaming engine produces.
|
|
283
|
+
* Reasoning is folded in before the text, matching the ordering the in-process
|
|
284
|
+
* loop produces, and any reasoning the protocol streamed but omitted from its
|
|
285
|
+
* final message is preserved rather than lost.
|
|
286
|
+
* @param text - the answer text.
|
|
287
|
+
* @param reasoning - the reasoning text, when the engine reported it separately.
|
|
288
|
+
* @param position - the open position.
|
|
289
|
+
*/
|
|
290
|
+
commitWholeMessage(text: string, reasoning: string | undefined, position: SinkPosition): void;
|
|
291
|
+
/**
|
|
292
|
+
* Commit a message built from an authoritative content-block list.
|
|
293
|
+
*
|
|
294
|
+
* The streaming engines' path: the protocol's own final message is the source
|
|
295
|
+
* of truth, and anything it omits that was streamed (reasoning, pending tool
|
|
296
|
+
* calls) is folded back in so the durable log never loses content the model
|
|
297
|
+
* produced.
|
|
298
|
+
* @param content - the blocks the protocol reported.
|
|
299
|
+
* @param position - the open position.
|
|
300
|
+
* @param usage - usage to attach when this closes the run.
|
|
301
|
+
*/
|
|
302
|
+
commitBlocks(content: ContentBlock[], position: SinkPosition, usage?: TokenUsage): void;
|
|
303
|
+
/**
|
|
304
|
+
* Finish the run: commit anything still held and report the engine's verdict.
|
|
305
|
+
*
|
|
306
|
+
* The verdict is checked before "did we commit a message", because an engine
|
|
307
|
+
* can settle cleanly with every turn failed (and send no reply at all);
|
|
308
|
+
* reporting that as success would hand the user a silent, empty turn. Its own
|
|
309
|
+
* words are the more useful thing to report in that case.
|
|
310
|
+
* @param position - the open position.
|
|
311
|
+
* @returns how the run ended.
|
|
312
|
+
*/
|
|
313
|
+
finish(position: SinkPosition): {
|
|
314
|
+
kind: 'completed';
|
|
315
|
+
} | {
|
|
316
|
+
kind: 'failed';
|
|
317
|
+
message: string;
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Project one engine-reported usage record onto dsh's `TokenUsage`.
|
|
322
|
+
*
|
|
323
|
+
* Absent fields are omitted rather than defaulted to zero: dsh distinguishes
|
|
324
|
+
* "reported zero" from "not reported", and a fabricated zero would corrupt the
|
|
325
|
+
* context accounting the compaction path reads.
|
|
326
|
+
* @param usage - the engine's usage record.
|
|
327
|
+
* @returns dsh's usage shape.
|
|
328
|
+
*/
|
|
329
|
+
export declare function mapUsage(usage: CliUsage): TokenUsage;
|
|
330
|
+
export { flattenToolResult } from './flatten.ts';
|
|
331
|
+
/** A message from any source, for the drivers that re-read the transcript. */
|
|
332
|
+
export type SinkMessage = Message;
|
|
333
|
+
//# sourceMappingURL=session-sink.d.ts.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Claude-Code-compatible `stream-json` adapter.
|
|
3
|
+
*
|
|
4
|
+
* A whole family of CLIs speak one shared `--output-format stream-json`
|
|
5
|
+
* dialect: Claude Code itself, and the forks built on it (Qoder's CLI among
|
|
6
|
+
* them — verified by probing `qoderclicn -p --output-format stream-json`, whose
|
|
7
|
+
* frames carry the same `type`/`subtype`/`session_id` vocabulary). The dialect
|
|
8
|
+
* is NDJSON, one document per line, with four document kinds that matter:
|
|
9
|
+
*
|
|
10
|
+
* - `system` / `init` — the session opened; carries `session_id`, the model
|
|
11
|
+
* and the tool list. Useful for resume and diagnostics, no durable content.
|
|
12
|
+
* - `assistant` — one assistant message, whose `message.content` is a list of
|
|
13
|
+
* Anthropic-shaped blocks (`text`, `thinking`, `tool_use`). This is where
|
|
14
|
+
* the answer and the tool calls come from.
|
|
15
|
+
* - `user` — a tool RESULT being fed back (`tool_result` blocks). A CLI that
|
|
16
|
+
* runs its own tools reports the outcome here.
|
|
17
|
+
* - `result` — the run ended; carries `result` (the final text), `is_error`,
|
|
18
|
+
* `usage` and the session id.
|
|
19
|
+
*
|
|
20
|
+
* Because the dialect is shared, the mapping lives here once and each engine
|
|
21
|
+
* supplies only its transport. That is also why tool names are NOT projected
|
|
22
|
+
* here: projection is per-engine (`normalizeHostedToolCall`), so this module
|
|
23
|
+
* reports the engine's own spelling and the sink projects it.
|
|
24
|
+
*
|
|
25
|
+
* Everything the dialect can carry that has no durable place — `hook_started`,
|
|
26
|
+
* `hook_progress`, `hook_response`, `artifacts_update`, `stream_event` and any
|
|
27
|
+
* future notification — is DROPPED rather than raised. A CLI that grows a new
|
|
28
|
+
* notification must not break a working driver.
|
|
29
|
+
*
|
|
30
|
+
* @module dsh-loop-engine-allinone/driver-cli/stream-json
|
|
31
|
+
*/
|
|
32
|
+
import type { CliFrame, CliUsage } from './frames.ts';
|
|
33
|
+
/** What one stream-json run's frames were accepted by. */
|
|
34
|
+
export type FrameSink = (frame: CliFrame) => void;
|
|
35
|
+
/** What the adapter learned about the run beyond its frames. */
|
|
36
|
+
export interface StreamJsonRun {
|
|
37
|
+
/** The session id the CLI reported, when it reported one. */
|
|
38
|
+
readonly sessionId: string | undefined;
|
|
39
|
+
/** Whether the CLI's own `result` document said the run failed. */
|
|
40
|
+
readonly reportedError: string | undefined;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Project one stream-json usage record onto {@link CliUsage}.
|
|
44
|
+
* @param usage - the engine's usage record.
|
|
45
|
+
* @returns the usage, omitting everything not reported.
|
|
46
|
+
*/
|
|
47
|
+
export declare function streamJsonUsage(usage: unknown): CliUsage | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* Map one parsed stream-json document to frames.
|
|
50
|
+
*
|
|
51
|
+
* The returned `run` is the adapter's accumulated knowledge (the session id and
|
|
52
|
+
* the CLI's own verdict), which the caller needs on top of the frames.
|
|
53
|
+
* @param document - the parsed document.
|
|
54
|
+
* @param accept - frame sink.
|
|
55
|
+
* @param run - the accumulator, updated in place.
|
|
56
|
+
*/
|
|
57
|
+
export declare function mapStreamJsonDocument(document: unknown, accept: FrameSink, run: {
|
|
58
|
+
sessionId: string | undefined;
|
|
59
|
+
reportedError: string | undefined;
|
|
60
|
+
}): void;
|
|
61
|
+
//# sourceMappingURL=stream-json.d.ts.map
|