@deepseek-ai/dsh-api-session-controller 0.1.2-alpha.5 → 0.1.3-alpha.2
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.i18n.yaml +2 -2
- package/README.md +6 -6
- package/README.zh.md +6 -6
- package/lib/client.js +592 -28
- package/lib/index.js +269 -214
- package/lib/typert.host.js +183 -141
- package/lib/typert.remote-client.js +79 -97
- package/lib/types/assistant-stream.d.ts +26 -0
- package/lib/types/assistant-stream.js +83 -0
- package/lib/types/client/contract/events.d.ts +38 -7
- package/lib/types/client/contract/events.js +21 -0
- package/lib/types/client/contract/session.d.ts +7 -7
- package/lib/types/client/contract/snapshot.d.ts +15 -2
- package/lib/types/client/index.d.ts +2 -2
- package/lib/types/client/index.js +2 -0
- package/lib/types/client/sessions/assistant-stream.d.ts +51 -0
- package/lib/types/client/sessions/assistant-stream.js +168 -0
- package/lib/types/client/sessions/history-records.d.ts +2 -2
- package/lib/types/client/sessions/history-records.js +3 -8
- package/lib/types/client/sessions/queue-mirror.js +3 -3
- package/lib/types/client/sessions/remotes.d.ts +2 -2
- package/lib/types/client/sessions/session.d.ts +3 -1
- package/lib/types/client/sessions/session.js +63 -15
- package/lib/types/client/transport.d.ts +7 -3
- package/lib/types/client/transport.js +18 -3
- package/lib/types/commands.js +98 -28
- package/lib/types/history.d.ts +2 -1
- package/lib/types/history.js +66 -37
- package/lib/types/index.d.ts +4 -4
- package/lib/types/index.js +13 -5
- package/lib/types/list.d.ts +2 -9
- package/lib/types/list.js +10 -124
- package/lib/types/types.d.ts +71 -29
- package/package.json +63 -57
|
@@ -7,6 +7,8 @@ export { SessionCreateError, SessionForkError } from "./sessions/service.js";
|
|
|
7
7
|
export { MutableSessionEventSource } from "./contract/events.js";
|
|
8
8
|
/** Required Remote and Context projection services. */
|
|
9
9
|
export const inject = [
|
|
10
|
+
'connection',
|
|
11
|
+
'fileUpload',
|
|
10
12
|
'typert',
|
|
11
13
|
'remote',
|
|
12
14
|
'remote.commands',
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** Web presentation fold joining transient Assistant frames to one durable v2 settlement. */
|
|
2
|
+
import type { SessionAssistantStreamBaseline, SessionAssistantStreamFrame } from '../../types.ts';
|
|
3
|
+
import type { LlmAttemptId } from '@deepseek-ai/dsh-llm/brand';
|
|
4
|
+
import type { SessionAssistantSettlementEntry, SessionEventLikeEntry, SessionLiveEventEntry, SessionTransientEventEntry } from '../contract/events.ts';
|
|
5
|
+
/** One Web publication decision from the assistant stream fold. */
|
|
6
|
+
export type ClientAssistantStreamResult = {
|
|
7
|
+
readonly type: 'publish';
|
|
8
|
+
readonly entry: SessionLiveEventEntry;
|
|
9
|
+
} | {
|
|
10
|
+
readonly type: 'settlement';
|
|
11
|
+
readonly attemptId: LlmAttemptId;
|
|
12
|
+
readonly entry: SessionAssistantSettlementEntry;
|
|
13
|
+
} | {
|
|
14
|
+
readonly type: 'abandonment';
|
|
15
|
+
readonly attemptId: LlmAttemptId;
|
|
16
|
+
} | {
|
|
17
|
+
readonly type: 'transient';
|
|
18
|
+
readonly entry: SessionTransientEventEntry;
|
|
19
|
+
} | {
|
|
20
|
+
readonly type: 'rebaseline';
|
|
21
|
+
} | undefined;
|
|
22
|
+
/** Keeps transient Assistant presentation behind one settlement-aware interface. */
|
|
23
|
+
export declare class ClientAssistantStream {
|
|
24
|
+
private activeAttempt;
|
|
25
|
+
private readonly pending;
|
|
26
|
+
private publishedSeqs;
|
|
27
|
+
private durableCursor;
|
|
28
|
+
private transientInGap;
|
|
29
|
+
/**
|
|
30
|
+
* Replace the durable Web window and adopt an optional reconnect baseline.
|
|
31
|
+
* @param entries - durable entries in the replacement window.
|
|
32
|
+
* @param baseline - compact prefix for an Assistant attempt that is still live.
|
|
33
|
+
* @returns immediately visible durable entries plus reconstructed transient chunks.
|
|
34
|
+
*/
|
|
35
|
+
replace(entries: readonly SessionEventLikeEntry[], baseline?: SessionAssistantStreamBaseline): readonly SessionEventLikeEntry[];
|
|
36
|
+
/**
|
|
37
|
+
* Stage one durable v2 settlement while its matching live attempt is open.
|
|
38
|
+
* @param entry - newly followed durable entry.
|
|
39
|
+
* @returns a publication decision, or `undefined` when no entry becomes visible.
|
|
40
|
+
*/
|
|
41
|
+
acceptDurable(entry: SessionLiveEventEntry): ClientAssistantStreamResult;
|
|
42
|
+
/**
|
|
43
|
+
* Fold one dense transient frame and release its named durable settlement.
|
|
44
|
+
* @param frame - next Assistant stream frame received by the follow connection.
|
|
45
|
+
* @returns a transient, publication, or rebaseline decision, or `undefined` when no entry becomes visible.
|
|
46
|
+
*/
|
|
47
|
+
acceptFrame(frame: SessionAssistantStreamFrame): ClientAssistantStreamResult;
|
|
48
|
+
private attemptForSettlement;
|
|
49
|
+
private publish;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=assistant-stream.d.ts.map
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/** Web presentation fold joining transient Assistant frames to one durable v2 settlement. */
|
|
2
|
+
import { expandAssistantStream } from '@deepseek-ai/dsh-llm/assistant-stream';
|
|
3
|
+
/** Keeps transient Assistant presentation behind one settlement-aware interface. */
|
|
4
|
+
export class ClientAssistantStream {
|
|
5
|
+
activeAttempt;
|
|
6
|
+
pending = new Map();
|
|
7
|
+
publishedSeqs = new Set();
|
|
8
|
+
durableCursor = -1;
|
|
9
|
+
transientInGap = 0;
|
|
10
|
+
/**
|
|
11
|
+
* Replace the durable Web window and adopt an optional reconnect baseline.
|
|
12
|
+
* @param entries - durable entries in the replacement window.
|
|
13
|
+
* @param baseline - compact prefix for an Assistant attempt that is still live.
|
|
14
|
+
* @returns immediately visible durable entries plus reconstructed transient chunks.
|
|
15
|
+
*/
|
|
16
|
+
replace(entries, baseline) {
|
|
17
|
+
this.pending.clear();
|
|
18
|
+
this.transientInGap = 0;
|
|
19
|
+
this.activeAttempt = undefined;
|
|
20
|
+
const opening = baseline?.activeAttempt;
|
|
21
|
+
if (opening !== undefined) {
|
|
22
|
+
this.activeAttempt = {
|
|
23
|
+
attemptId: opening.attemptId,
|
|
24
|
+
startedAfterSeq: opening.startedAfterSeq,
|
|
25
|
+
turn: opening.turn,
|
|
26
|
+
step: opening.step,
|
|
27
|
+
nextIndex: opening.nextIndex,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const visible = [...entries];
|
|
31
|
+
this.publishedSeqs = new Set(visible.map(entry => entry.event.seq));
|
|
32
|
+
this.durableCursor = visible.reduce((cursor, entry) => Math.max(cursor, entry.event.seq), -1);
|
|
33
|
+
if (opening !== undefined) {
|
|
34
|
+
for (const [index, member] of expandAssistantStream(opening.stream).entries()) {
|
|
35
|
+
this.transientInGap += 1;
|
|
36
|
+
visible.push({
|
|
37
|
+
type: 'transient',
|
|
38
|
+
event: {
|
|
39
|
+
type: 'assistant/live-chunk',
|
|
40
|
+
seq: this.durableCursor + 1 - 1 / (this.transientInGap + 1),
|
|
41
|
+
time: member.time,
|
|
42
|
+
data: {
|
|
43
|
+
attemptId: opening.attemptId,
|
|
44
|
+
turn: opening.turn,
|
|
45
|
+
step: opening.step,
|
|
46
|
+
chunk: member.chunk,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
if (index + 1 >= opening.nextIndex)
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return visible;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Stage one durable v2 settlement while its matching live attempt is open.
|
|
58
|
+
* @param entry - newly followed durable entry.
|
|
59
|
+
* @returns a publication decision, or `undefined` when no entry becomes visible.
|
|
60
|
+
*/
|
|
61
|
+
acceptDurable(entry) {
|
|
62
|
+
const event = entry.event;
|
|
63
|
+
this.durableCursor = Math.max(this.durableCursor, event.seq);
|
|
64
|
+
this.transientInGap = 0;
|
|
65
|
+
const settlement = assistantSettlementEntry(entry);
|
|
66
|
+
if (settlement !== undefined && this.attemptForSettlement(settlement.event) !== undefined) {
|
|
67
|
+
if (this.pending.has(event.seq))
|
|
68
|
+
return { type: 'rebaseline' };
|
|
69
|
+
this.pending.set(event.seq, settlement);
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
return this.publish(entry);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Fold one dense transient frame and release its named durable settlement.
|
|
76
|
+
* @param frame - next Assistant stream frame received by the follow connection.
|
|
77
|
+
* @returns a transient, publication, or rebaseline decision, or `undefined` when no entry becomes visible.
|
|
78
|
+
*/
|
|
79
|
+
acceptFrame(frame) {
|
|
80
|
+
switch (frame.type) {
|
|
81
|
+
case 'start':
|
|
82
|
+
if (this.activeAttempt !== undefined || this.pending.size > 0)
|
|
83
|
+
return { type: 'rebaseline' };
|
|
84
|
+
this.pending.clear();
|
|
85
|
+
this.activeAttempt = {
|
|
86
|
+
attemptId: frame.attemptId,
|
|
87
|
+
startedAfterSeq: frame.startedAfterSeq,
|
|
88
|
+
turn: frame.turn,
|
|
89
|
+
step: frame.step,
|
|
90
|
+
nextIndex: 0,
|
|
91
|
+
};
|
|
92
|
+
return undefined;
|
|
93
|
+
case 'chunk': {
|
|
94
|
+
const attempt = this.activeAttempt;
|
|
95
|
+
// A controller mounted after the Host saw this attempt has no start
|
|
96
|
+
// frame to reconstruct. Its durable settlement publishes directly;
|
|
97
|
+
// ignore the transient suffix until the next known start.
|
|
98
|
+
if (attempt === undefined || attempt.attemptId !== frame.attemptId)
|
|
99
|
+
return undefined;
|
|
100
|
+
if (frame.index !== attempt.nextIndex)
|
|
101
|
+
return { type: 'rebaseline' };
|
|
102
|
+
attempt.nextIndex += 1;
|
|
103
|
+
this.transientInGap += 1;
|
|
104
|
+
return {
|
|
105
|
+
type: 'transient',
|
|
106
|
+
entry: {
|
|
107
|
+
type: 'transient',
|
|
108
|
+
event: {
|
|
109
|
+
type: 'assistant/live-chunk',
|
|
110
|
+
seq: this.durableCursor + 1 - 1 / (this.transientInGap + 1),
|
|
111
|
+
time: frame.time,
|
|
112
|
+
data: {
|
|
113
|
+
attemptId: frame.attemptId,
|
|
114
|
+
turn: attempt.turn,
|
|
115
|
+
step: attempt.step,
|
|
116
|
+
chunk: frame.chunk,
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
case 'end': {
|
|
123
|
+
const attempt = this.activeAttempt;
|
|
124
|
+
if (attempt === undefined || attempt.attemptId !== frame.attemptId) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
this.activeAttempt = undefined;
|
|
128
|
+
if (frame.index !== attempt.nextIndex)
|
|
129
|
+
return { type: 'rebaseline' };
|
|
130
|
+
if (frame.outcome.kind === 'abandoned') {
|
|
131
|
+
return this.pending.size === 0
|
|
132
|
+
? { type: 'abandonment', attemptId: attempt.attemptId }
|
|
133
|
+
: { type: 'rebaseline' };
|
|
134
|
+
}
|
|
135
|
+
if (this.publishedSeqs.has(frame.outcome.seq))
|
|
136
|
+
return undefined;
|
|
137
|
+
const entry = this.pending.get(frame.outcome.seq);
|
|
138
|
+
if (entry === undefined
|
|
139
|
+
|| entry.event.type !== frame.outcome.eventType) {
|
|
140
|
+
return { type: 'rebaseline' };
|
|
141
|
+
}
|
|
142
|
+
this.pending.delete(frame.outcome.seq);
|
|
143
|
+
this.publishedSeqs.add(entry.event.seq);
|
|
144
|
+
return { type: 'settlement', attemptId: attempt.attemptId, entry };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
attemptForSettlement(event) {
|
|
149
|
+
const attempt = this.activeAttempt;
|
|
150
|
+
if (attempt === undefined
|
|
151
|
+
|| (event.type === 'assistant/message' && event.surfaceOp !== 'append')
|
|
152
|
+
|| event.seq <= attempt.startedAfterSeq
|
|
153
|
+
|| attempt.turn !== event.data.turn
|
|
154
|
+
|| attempt.step !== event.data.step)
|
|
155
|
+
return undefined;
|
|
156
|
+
return attempt;
|
|
157
|
+
}
|
|
158
|
+
publish(entry) {
|
|
159
|
+
this.publishedSeqs.add(entry.event.seq);
|
|
160
|
+
return { type: 'publish', entry };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function assistantSettlementEntry(entry) {
|
|
164
|
+
return entry.event.type === 'assistant/message' || entry.event.type === 'assistant/attempt'
|
|
165
|
+
? entry
|
|
166
|
+
: undefined;
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=assistant-stream.js.map
|
|
@@ -9,13 +9,13 @@ import type { SessionEventLikeEntry } from '../contract/events.ts';
|
|
|
9
9
|
export declare function historyEntries(records: readonly SessionHistoryRecord[]): readonly SessionEventLikeEntry[];
|
|
10
10
|
/**
|
|
11
11
|
* Read the first logical sequence represented by one wire record.
|
|
12
|
-
* @param record - validated
|
|
12
|
+
* @param record - validated Session event.
|
|
13
13
|
* @returns inclusive first Session sequence.
|
|
14
14
|
*/
|
|
15
15
|
export declare function historyRecordFirstSeq(record: SessionHistoryRecord): number;
|
|
16
16
|
/**
|
|
17
17
|
* Read the final logical sequence represented by one wire record.
|
|
18
|
-
* @param record - validated
|
|
18
|
+
* @param record - validated Session event.
|
|
19
19
|
* @returns inclusive final Session sequence.
|
|
20
20
|
*/
|
|
21
21
|
export declare function historyRecordLastSeq(record: SessionHistoryRecord): number;
|
|
@@ -9,7 +9,7 @@ export function historyEntries(records) {
|
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
11
11
|
* Read the first logical sequence represented by one wire record.
|
|
12
|
-
* @param record - validated
|
|
12
|
+
* @param record - validated Session event.
|
|
13
13
|
* @returns inclusive first Session sequence.
|
|
14
14
|
*/
|
|
15
15
|
export function historyRecordFirstSeq(record) {
|
|
@@ -17,15 +17,10 @@ export function historyRecordFirstSeq(record) {
|
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
19
|
* Read the final logical sequence represented by one wire record.
|
|
20
|
-
* @param record - validated
|
|
20
|
+
* @param record - validated Session event.
|
|
21
21
|
* @returns inclusive final Session sequence.
|
|
22
22
|
*/
|
|
23
23
|
export function historyRecordLastSeq(record) {
|
|
24
|
-
|
|
25
|
-
return record.event.seq;
|
|
26
|
-
const length = record.event.type === 'chunkrow/tool-call-chunks'
|
|
27
|
-
? record.event.data.args.length
|
|
28
|
-
: record.event.data.texts.length;
|
|
29
|
-
return record.event.seq + length - 1;
|
|
24
|
+
return record.event.seq;
|
|
30
25
|
}
|
|
31
26
|
//# sourceMappingURL=history-records.js.map
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
const QUEUE_PREVIEW_CHARS = 200;
|
|
2
|
-
//
|
|
3
|
-
//
|
|
2
|
+
// Attachment blocks are excluded: queue presentation renders them from
|
|
3
|
+
// `content`, so the text preview covers only what has no visual form.
|
|
4
4
|
function previewOf(content) {
|
|
5
5
|
const flat = content
|
|
6
|
-
.filter(block => block.type !== 'image')
|
|
6
|
+
.filter(block => block.type !== 'image' && block.type !== 'file')
|
|
7
7
|
.map(block => (block.type === 'text' ? block.text : `[${block.type}]`))
|
|
8
8
|
.join(' ').replace(/\s+/g, ' ').trim();
|
|
9
9
|
const chars = Array.from(flat);
|
|
@@ -4,15 +4,15 @@
|
|
|
4
4
|
*
|
|
5
5
|
* @module @deepseek-ai/dsh-api-session-controller/client/sessions/remotes
|
|
6
6
|
*/
|
|
7
|
-
import type { EncodedImageAttachment } from '@deepseek-ai/dsh-attachment/types';
|
|
8
7
|
import type { ClientRemote } from '@deepseek-ai/dsh-api-gateway/client';
|
|
8
|
+
import type { CommandSubmitAttachment } from '@deepseek-ai/dsh-commands/types';
|
|
9
9
|
import type { SessionId } from '@deepseek-ai/dsh-session/types';
|
|
10
10
|
import type { SubagentCatalog, SubagentInterruptReceipt, SubagentPromptReceipt, SubagentPromptRequest } from '@deepseek-ai/dsh-subagent/client';
|
|
11
11
|
import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol';
|
|
12
12
|
import type { SessionRemote } from '../transport.ts';
|
|
13
13
|
/** Narrow Commands namespace consumed by a Client Session. */
|
|
14
14
|
export interface SessionCommandsRemote {
|
|
15
|
-
execute(agentId: SessionId, line: string,
|
|
15
|
+
execute(agentId: SessionId, line: string, attachments: readonly CommandSubmitAttachment[], signal?: AbortSignal): Promise<RemoteResult<object | undefined>>;
|
|
16
16
|
}
|
|
17
17
|
/** Narrow subagent namespace consumed by a Client Session and its manager. */
|
|
18
18
|
export interface SessionSubagentsRemote {
|
|
@@ -61,6 +61,7 @@ export declare class Session implements SessionFace {
|
|
|
61
61
|
private jumpPromise;
|
|
62
62
|
/** Authoritative stream-only inbox snapshot; pending work never hits history. */
|
|
63
63
|
private readonly queueMirror;
|
|
64
|
+
private readonly assistantStream;
|
|
64
65
|
private running;
|
|
65
66
|
private address;
|
|
66
67
|
private parentAvailable;
|
|
@@ -136,7 +137,7 @@ export declare class Session implements SessionFace {
|
|
|
136
137
|
beginSubmission(input: BeginSubmissionInput): SubmissionHandle;
|
|
137
138
|
/**
|
|
138
139
|
* Send (queue/steer passed through 1:1); failures land in the snapshot's promptError.
|
|
139
|
-
* @param content - text
|
|
140
|
+
* @param content - text, browser-owned temporary image uploads, and staged-file receipts.
|
|
140
141
|
* @param mode - queue appends after the current turn; steer interrupts it.
|
|
141
142
|
* @param signal - optional caller cancellation for the complete admission round-trip.
|
|
142
143
|
* @param requestId - identity from {@link beginSubmission}; a failed identified prompt retires its echo.
|
|
@@ -267,6 +268,7 @@ export declare class Session implements SessionFace {
|
|
|
267
268
|
private acceptEventChange;
|
|
268
269
|
/** Replace the complete contiguous window and apply page-owned projection metadata. */
|
|
269
270
|
private installWindow;
|
|
271
|
+
private publishAssistantEntry;
|
|
270
272
|
/** Prepend one stream-validated history page. */
|
|
271
273
|
private prependWindow;
|
|
272
274
|
/** Append one stream-validated live event. */
|
|
@@ -5,9 +5,11 @@ import { SessionEventStream } from "../transport.js";
|
|
|
5
5
|
import { MutableSessionEventSource } from "../contract/events.js";
|
|
6
6
|
import { Notifier } from "./notifier.js";
|
|
7
7
|
import { isRemoteFailure } from '@deepseek-ai/dsh-api-gateway/client';
|
|
8
|
+
import { RemoteError } from '@deepseek-ai/dsh-typert-protocol';
|
|
8
9
|
import { ProjectionValueStore } from "./projection-store.js";
|
|
9
10
|
import { resolvedClientTimeZone } from "../time-zone.js";
|
|
10
11
|
import { SessionQueueMirror } from "./queue-mirror.js";
|
|
12
|
+
import { ClientAssistantStream, } from "./assistant-stream.js";
|
|
11
13
|
function projectionsBaseline(value) {
|
|
12
14
|
return {
|
|
13
15
|
...value,
|
|
@@ -44,6 +46,7 @@ export class Session {
|
|
|
44
46
|
jumpPromise = null;
|
|
45
47
|
/** Authoritative stream-only inbox snapshot; pending work never hits history. */
|
|
46
48
|
queueMirror = new SessionQueueMirror();
|
|
49
|
+
assistantStream = new ClientAssistantStream();
|
|
47
50
|
running = false;
|
|
48
51
|
address;
|
|
49
52
|
parentAvailable;
|
|
@@ -143,7 +146,7 @@ export class Session {
|
|
|
143
146
|
: 'transcript',
|
|
144
147
|
time: Date.now(),
|
|
145
148
|
text: input.text,
|
|
146
|
-
|
|
149
|
+
attachments: input.attachments,
|
|
147
150
|
}];
|
|
148
151
|
this.submissionSettlements.set(requestId, { onRetire: input.onRetire, retiring: false });
|
|
149
152
|
// The blank → engaging edge flips here, ahead of prompt(): the composer
|
|
@@ -154,7 +157,7 @@ export class Session {
|
|
|
154
157
|
}
|
|
155
158
|
/**
|
|
156
159
|
* Send (queue/steer passed through 1:1); failures land in the snapshot's promptError.
|
|
157
|
-
* @param content - text
|
|
160
|
+
* @param content - text, browser-owned temporary image uploads, and staged-file receipts.
|
|
158
161
|
* @param mode - queue appends after the current turn; steer interrupts it.
|
|
159
162
|
* @param signal - optional caller cancellation for the complete admission round-trip.
|
|
160
163
|
* @param requestId - identity from {@link beginSubmission}; a failed identified prompt retires its echo.
|
|
@@ -181,13 +184,23 @@ export class Session {
|
|
|
181
184
|
clientTimeZone,
|
|
182
185
|
}, signal);
|
|
183
186
|
}
|
|
187
|
+
else if (content.some(part => part.type === 'file')) {
|
|
188
|
+
result = {
|
|
189
|
+
ok: false,
|
|
190
|
+
error: new RemoteError('subagent/attachment-invalid', 'subagent continuation does not accept files', { reason: 'SUBAGENT_FILE_UNSUPPORTED' }),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
184
193
|
else {
|
|
194
|
+
// The preceding branch rejects file parts before the narrower subagent
|
|
195
|
+
// wire type is used; this array is not filtered or reordered.
|
|
196
|
+
const routedContent = content;
|
|
185
197
|
const routed = await this.remote.subagents.prompt({
|
|
186
198
|
requestId: randomUUID(),
|
|
187
199
|
parentSessionId: this.address.parentSessionId,
|
|
188
200
|
childSessionId: this.address.childSessionId,
|
|
189
201
|
mode: 'continuable',
|
|
190
|
-
|
|
202
|
+
delivery: mode,
|
|
203
|
+
content: routedContent,
|
|
191
204
|
clientTimeZone: resolvedClientTimeZone(),
|
|
192
205
|
}, signal);
|
|
193
206
|
result = routed.ok ? { ok: true, value: { accepted: true } } : routed;
|
|
@@ -553,29 +566,63 @@ export class Session {
|
|
|
553
566
|
acceptEventChange(change) {
|
|
554
567
|
switch (change.type) {
|
|
555
568
|
case 'replace':
|
|
556
|
-
this.installWindow(change.entries, change.hasMore, change.page.projections === undefined ? undefined : projectionsBaseline(change.page.projections));
|
|
569
|
+
this.installWindow(change.entries, change.hasMore, change.page.projections === undefined ? undefined : projectionsBaseline(change.page.projections), change.page.assistantStream);
|
|
557
570
|
return;
|
|
558
571
|
case 'prepend':
|
|
559
572
|
this.prependWindow(change.entries, change.hasMore);
|
|
560
573
|
return;
|
|
561
574
|
case 'append':
|
|
562
|
-
|
|
563
|
-
|
|
575
|
+
this.publishAssistantEntry(this.assistantStream.acceptDurable(change.entry));
|
|
576
|
+
return;
|
|
577
|
+
case 'assistant-stream':
|
|
578
|
+
this.publishAssistantEntry(this.assistantStream.acceptFrame(change.frame));
|
|
564
579
|
}
|
|
565
580
|
}
|
|
566
581
|
/** Replace the complete contiguous window and apply page-owned projection metadata. */
|
|
567
|
-
installWindow(entries, hasMore, projections) {
|
|
582
|
+
installWindow(entries, hasMore, projections, assistantStream) {
|
|
583
|
+
// A durable gap-repair page has no assistant baseline. Clearing transient
|
|
584
|
+
// attempts makes a held notification reopen follow once for an atomic
|
|
585
|
+
// page/baseline pair instead of applying it to an unrelated repair cut.
|
|
586
|
+
const visible = this.assistantStream.replace(entries, assistantStream);
|
|
568
587
|
this.baseSeq = SessionLogOffset(entries[0]?.event.seq ?? 0);
|
|
569
588
|
this.hasMore = hasMore;
|
|
570
|
-
if (
|
|
589
|
+
if (visible.some(entry => entry.event.type === 'turn/start'))
|
|
571
590
|
this.firstPromptPendingTurn = false;
|
|
572
591
|
if (projections !== undefined)
|
|
573
592
|
this.projections.seed(projections);
|
|
574
|
-
this.eventSource.replace(
|
|
575
|
-
for (const entry of
|
|
593
|
+
this.eventSource.replace(visible, hasMore);
|
|
594
|
+
for (const entry of visible)
|
|
576
595
|
this.observeSubmissionEvent(entry.event);
|
|
577
596
|
this.notifier.markDirty();
|
|
578
597
|
}
|
|
598
|
+
publishAssistantEntry(result) {
|
|
599
|
+
if (result?.type === 'rebaseline') {
|
|
600
|
+
const events = this.events;
|
|
601
|
+
queueMicrotask(() => {
|
|
602
|
+
if (events !== undefined && this.events === events)
|
|
603
|
+
events.restart();
|
|
604
|
+
});
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
if (result?.type === 'settlement') {
|
|
608
|
+
this.eventSource.settleAssistant(result.attemptId, result.entry);
|
|
609
|
+
this.observeSubmissionEvent(result.entry.event);
|
|
610
|
+
this.notifier.markDirty();
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
if (result?.type === 'abandonment') {
|
|
614
|
+
this.eventSource.settleAssistant(result.attemptId);
|
|
615
|
+
this.notifier.markDirty();
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
if (result?.type === 'publish' && this.appendLive(result.entry)) {
|
|
619
|
+
this.notifier.markDirty();
|
|
620
|
+
}
|
|
621
|
+
else if (result?.type === 'transient') {
|
|
622
|
+
this.eventSource.append(result.entry);
|
|
623
|
+
this.notifier.markDirty();
|
|
624
|
+
}
|
|
625
|
+
}
|
|
579
626
|
/** Prepend one stream-validated history page. */
|
|
580
627
|
prependWindow(entries, hasMore) {
|
|
581
628
|
this.baseSeq = entries[0] === undefined ? this.baseSeq : SessionLogOffset(entries[0].event.seq);
|
|
@@ -607,7 +654,7 @@ export class Session {
|
|
|
607
654
|
const source = data?.source;
|
|
608
655
|
if (source?.kind !== 'user' || typeof source.rpcId !== 'string')
|
|
609
656
|
return;
|
|
610
|
-
this.scheduleObservedRetirement(source.rpcId,
|
|
657
|
+
this.scheduleObservedRetirement(source.rpcId, attachmentRefsIn(data?.content));
|
|
611
658
|
}
|
|
612
659
|
/** Retire echoes whose prompts landed in the host inbox instead of the log (running-turn submissions). */
|
|
613
660
|
observeSubmissionQueue(items) {
|
|
@@ -615,7 +662,7 @@ export class Session {
|
|
|
615
662
|
return;
|
|
616
663
|
for (const item of items) {
|
|
617
664
|
if (item.rpcId !== undefined) {
|
|
618
|
-
this.scheduleObservedRetirement(item.rpcId,
|
|
665
|
+
this.scheduleObservedRetirement(item.rpcId, attachmentRefsIn(item.message.content));
|
|
619
666
|
}
|
|
620
667
|
}
|
|
621
668
|
}
|
|
@@ -702,8 +749,8 @@ function scheduleFrame(fn) {
|
|
|
702
749
|
else
|
|
703
750
|
setTimeout(fn, 0);
|
|
704
751
|
}
|
|
705
|
-
/**
|
|
706
|
-
function
|
|
752
|
+
/** Attachment references in one structurally-read content block list, in block order. */
|
|
753
|
+
function attachmentRefsIn(content) {
|
|
707
754
|
if (!Array.isArray(content))
|
|
708
755
|
return [];
|
|
709
756
|
const refs = [];
|
|
@@ -711,7 +758,8 @@ function imageRefsIn(content) {
|
|
|
711
758
|
if (typeof block !== 'object' || block === null)
|
|
712
759
|
continue;
|
|
713
760
|
const candidate = block;
|
|
714
|
-
if (candidate.type === 'image'
|
|
761
|
+
if ((candidate.type === 'image' || candidate.type === 'file')
|
|
762
|
+
&& typeof candidate.attachment === 'object' && candidate.attachment !== null) {
|
|
715
763
|
refs.push(candidate.attachment);
|
|
716
764
|
}
|
|
717
765
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Session-specific adapters for Gateway-owned Remote stream lifecycles. */
|
|
2
2
|
import { RemoteJournalStream, RemoteSnapshotStream, RemoteStreamCarrierError, type ClientRemote, type RemoteJournalFrame } from '@deepseek-ai/dsh-api-gateway/client';
|
|
3
|
-
import type { SessionAddress, SessionControlFrame, SessionHistoryRecord, SessionPage, SessionPageRequest, SessionProjectionBaseline } from '../types.ts';
|
|
3
|
+
import type { SessionAddress, SessionAssistantStreamBaseline, SessionAssistantStreamFrame, SessionControlFrame, SessionHistoryRecord, SessionPage, SessionPageRequest, SessionProjectionBaseline } from '../types.ts';
|
|
4
4
|
import type { SessionEventLikeEntry, SessionLiveEventEntry } from './contract/events.ts';
|
|
5
5
|
import type { SessionRemotes } from './sessions/remotes.ts';
|
|
6
6
|
export { SESSION_SEARCH_RESULT_LIMIT, SESSION_SEARCH_SNIPPET_MAX_CODE_POINTS, } from '../types.ts';
|
|
@@ -11,6 +11,7 @@ export type SessionRemote = ClientRemote['session'];
|
|
|
11
11
|
/** Opening metadata carried only by a follow snapshot, never by loadOlder pages. */
|
|
12
12
|
interface SessionJournalPage extends SessionPage {
|
|
13
13
|
readonly projections?: SessionProjectionBaseline;
|
|
14
|
+
readonly assistantStream?: SessionAssistantStreamBaseline;
|
|
14
15
|
}
|
|
15
16
|
/** One complete publication from the Session journal stream. */
|
|
16
17
|
export type SessionJournalChange = {
|
|
@@ -21,6 +22,9 @@ export type SessionJournalChange = {
|
|
|
21
22
|
} | {
|
|
22
23
|
readonly type: 'append';
|
|
23
24
|
readonly entry: SessionLiveEventEntry;
|
|
25
|
+
} | {
|
|
26
|
+
readonly type: 'assistant-stream';
|
|
27
|
+
readonly frame: SessionAssistantStreamFrame;
|
|
24
28
|
};
|
|
25
29
|
type SessionControlBaselineFrame = Extract<SessionControlFrame, {
|
|
26
30
|
type: 'baseline';
|
|
@@ -54,7 +58,7 @@ export interface SessionEventStreamOptions {
|
|
|
54
58
|
*/
|
|
55
59
|
export declare function createSessionControlStream(remote: SessionRemotes, options: SessionControlStreamOptions): SessionControlStream;
|
|
56
60
|
/** Gateway-owned event journal bound to one ordinary or direct-subagent Session address. */
|
|
57
|
-
export declare class SessionEventStream extends RemoteJournalStream<SessionJournalPage, SessionHistoryRecord, number, ClientSessionPageRequest> {
|
|
61
|
+
export declare class SessionEventStream extends RemoteJournalStream<SessionJournalPage, SessionHistoryRecord, number, ClientSessionPageRequest, SessionAssistantStreamFrame> {
|
|
58
62
|
private readonly remote;
|
|
59
63
|
private readonly address;
|
|
60
64
|
/**
|
|
@@ -64,7 +68,7 @@ export declare class SessionEventStream extends RemoteJournalStream<SessionJourn
|
|
|
64
68
|
*/
|
|
65
69
|
constructor(remote: SessionRemotes, address: SessionAddress, options: SessionEventStreamOptions);
|
|
66
70
|
/** @inheritdoc */
|
|
67
|
-
protected follow(request: ClientSessionPageRequest, signal: AbortSignal): AsyncIterable<RemoteJournalFrame<SessionHistoryRecord, number, SessionJournalPage>>;
|
|
71
|
+
protected follow(request: ClientSessionPageRequest, signal: AbortSignal): AsyncIterable<RemoteJournalFrame<SessionHistoryRecord, number, SessionJournalPage, SessionAssistantStreamFrame>>;
|
|
68
72
|
/** @inheritdoc */
|
|
69
73
|
protected readPage(request: ClientSessionPageRequest, throughSeq: number, signal: AbortSignal): Promise<SessionJournalPage>;
|
|
70
74
|
/** @inheritdoc */
|
|
@@ -9,14 +9,13 @@ function toSessionJournalChange(change) {
|
|
|
9
9
|
case 'prepend':
|
|
10
10
|
return { ...change, entries: historyEntries(change.entries) };
|
|
11
11
|
case 'append': {
|
|
12
|
-
if (change.entry.type !== 'event') {
|
|
13
|
-
throw new RemoteError('gateway/internal', 'session live stream emitted a packed history record', {});
|
|
14
|
-
}
|
|
15
12
|
return {
|
|
16
13
|
type: 'append',
|
|
17
14
|
entry: change.entry,
|
|
18
15
|
};
|
|
19
16
|
}
|
|
17
|
+
case 'notification':
|
|
18
|
+
return { type: 'assistant-stream', frame: change.notification };
|
|
20
19
|
}
|
|
21
20
|
}
|
|
22
21
|
/**
|
|
@@ -72,11 +71,17 @@ export class SessionEventStream extends RemoteJournalStream {
|
|
|
72
71
|
}
|
|
73
72
|
/** @inheritdoc */
|
|
74
73
|
async *follow(request, signal) {
|
|
74
|
+
let assistantRevision;
|
|
75
75
|
for await (const frame of this.remote.session.follow({
|
|
76
76
|
address: this.address,
|
|
77
|
+
assistantStream: true,
|
|
77
78
|
...(request.maxMessages === undefined ? {} : { maxMessages: request.maxMessages }),
|
|
78
79
|
}, signal)) {
|
|
79
80
|
if (frame.type === 'snapshot') {
|
|
81
|
+
if (frame.assistantStream === undefined) {
|
|
82
|
+
throw new RemoteError('gateway/internal', 'session assistant stream omitted its opted-in opening baseline', {});
|
|
83
|
+
}
|
|
84
|
+
assistantRevision = frame.assistantStream.revision;
|
|
80
85
|
yield {
|
|
81
86
|
type: 'opened',
|
|
82
87
|
cursor: frame.cursor,
|
|
@@ -84,10 +89,20 @@ export class SessionEventStream extends RemoteJournalStream {
|
|
|
84
89
|
records: frame.records,
|
|
85
90
|
hasMore: frame.hasMore,
|
|
86
91
|
projections: frame.projections,
|
|
92
|
+
assistantStream: frame.assistantStream,
|
|
87
93
|
},
|
|
88
94
|
};
|
|
89
95
|
continue;
|
|
90
96
|
}
|
|
97
|
+
if (frame.type === 'assistant-stream') {
|
|
98
|
+
const expected = (assistantRevision ?? 0) + 1;
|
|
99
|
+
if (frame.frame.revision !== expected) {
|
|
100
|
+
throw new RemoteStreamCarrierError(`session assistant stream skipped revision ${String(expected)}`);
|
|
101
|
+
}
|
|
102
|
+
assistantRevision = frame.frame.revision;
|
|
103
|
+
yield { type: 'notification', notification: frame.frame };
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
91
106
|
yield { type: 'entry', entry: frame };
|
|
92
107
|
}
|
|
93
108
|
}
|