@deepseek-ai/dsh-api-session-controller 0.1.2-rc.1 → 0.1.5-alpha.1
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 +16 -6
- package/README.zh.md +16 -6
- package/lib/client.js +838 -58
- package/lib/index.js +376 -243
- package/lib/typert.host.js +236 -178
- package/lib/typert.remote-client.js +97 -115
- package/lib/types/agent.js +6 -8
- 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/session-wire-event.d.ts +11 -0
- package/lib/types/client/session-wire-event.js +43 -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 +24 -3
- package/lib/types/commands.d.ts +1 -1
- package/lib/types/commands.js +112 -32
- package/lib/types/control.d.ts +0 -1
- package/lib/types/control.js +19 -22
- 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 +15 -5
- package/lib/types/list.d.ts +2 -9
- package/lib/types/list.js +10 -124
- package/lib/types/media-references.d.ts +16 -0
- package/lib/types/media-references.js +77 -0
- package/lib/types/types.d.ts +85 -35
- package/package.json +71 -58
|
@@ -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 */
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { RemoteError } from '@deepseek-ai/dsh-typert-protocol';
|
|
3
3
|
import { RemoteJournalStream, RemoteSnapshotStream, RemoteStreamCarrierError, } from '@deepseek-ai/dsh-api-gateway/client';
|
|
4
4
|
import { historyEntries, historyRecordFirstSeq, historyRecordLastSeq, } from "./sessions/history-records.js";
|
|
5
|
+
import { assertSessionWireEvent } from "./session-wire-event.js";
|
|
5
6
|
export { SESSION_SEARCH_RESULT_LIMIT, SESSION_SEARCH_SNIPPET_MAX_CODE_POINTS, } from "../types.js";
|
|
6
7
|
function toSessionJournalChange(change) {
|
|
7
8
|
switch (change.type) {
|
|
@@ -9,14 +10,13 @@ function toSessionJournalChange(change) {
|
|
|
9
10
|
case 'prepend':
|
|
10
11
|
return { ...change, entries: historyEntries(change.entries) };
|
|
11
12
|
case 'append': {
|
|
12
|
-
if (change.entry.type !== 'event') {
|
|
13
|
-
throw new RemoteError('gateway/internal', 'session live stream emitted a packed history record', {});
|
|
14
|
-
}
|
|
15
13
|
return {
|
|
16
14
|
type: 'append',
|
|
17
15
|
entry: change.entry,
|
|
18
16
|
};
|
|
19
17
|
}
|
|
18
|
+
case 'notification':
|
|
19
|
+
return { type: 'assistant-stream', frame: change.notification };
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
@@ -72,11 +72,19 @@ export class SessionEventStream extends RemoteJournalStream {
|
|
|
72
72
|
}
|
|
73
73
|
/** @inheritdoc */
|
|
74
74
|
async *follow(request, signal) {
|
|
75
|
+
let assistantRevision;
|
|
75
76
|
for await (const frame of this.remote.session.follow({
|
|
76
77
|
address: this.address,
|
|
78
|
+
assistantStream: true,
|
|
77
79
|
...(request.maxMessages === undefined ? {} : { maxMessages: request.maxMessages }),
|
|
78
80
|
}, signal)) {
|
|
79
81
|
if (frame.type === 'snapshot') {
|
|
82
|
+
for (const record of frame.records)
|
|
83
|
+
assertSessionWireEvent(record.event);
|
|
84
|
+
if (frame.assistantStream === undefined) {
|
|
85
|
+
throw new RemoteError('gateway/internal', 'session assistant stream omitted its opted-in opening baseline', {});
|
|
86
|
+
}
|
|
87
|
+
assistantRevision = frame.assistantStream.revision;
|
|
80
88
|
yield {
|
|
81
89
|
type: 'opened',
|
|
82
90
|
cursor: frame.cursor,
|
|
@@ -84,10 +92,21 @@ export class SessionEventStream extends RemoteJournalStream {
|
|
|
84
92
|
records: frame.records,
|
|
85
93
|
hasMore: frame.hasMore,
|
|
86
94
|
projections: frame.projections,
|
|
95
|
+
assistantStream: frame.assistantStream,
|
|
87
96
|
},
|
|
88
97
|
};
|
|
89
98
|
continue;
|
|
90
99
|
}
|
|
100
|
+
if (frame.type === 'assistant-stream') {
|
|
101
|
+
const expected = (assistantRevision ?? 0) + 1;
|
|
102
|
+
if (frame.frame.revision !== expected) {
|
|
103
|
+
throw new RemoteStreamCarrierError(`session assistant stream skipped revision ${String(expected)}`);
|
|
104
|
+
}
|
|
105
|
+
assistantRevision = frame.frame.revision;
|
|
106
|
+
yield { type: 'notification', notification: frame.frame };
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
assertSessionWireEvent(frame.event);
|
|
91
110
|
yield { type: 'entry', entry: frame };
|
|
92
111
|
}
|
|
93
112
|
}
|
|
@@ -96,6 +115,8 @@ export class SessionEventStream extends RemoteJournalStream {
|
|
|
96
115
|
const result = await this.remote.session.page({ address: this.address, throughSeq, ...request }, signal);
|
|
97
116
|
if (!result.ok)
|
|
98
117
|
throw result.error;
|
|
118
|
+
for (const record of result.value.records)
|
|
119
|
+
assertSessionWireEvent(record.event);
|
|
99
120
|
return result.value;
|
|
100
121
|
}
|
|
101
122
|
/** @inheritdoc */
|
package/lib/types/commands.d.ts
CHANGED
|
@@ -38,7 +38,7 @@ export declare class SessionCommandController {
|
|
|
38
38
|
*/
|
|
39
39
|
fork(request: SessionForkRequest): Promise<SessionForkValue>;
|
|
40
40
|
/**
|
|
41
|
-
*
|
|
41
|
+
* Reject empty content, then admit one prompt after Agent and attachment validation.
|
|
42
42
|
* @param request - Session identity, prompt content, source metadata, and delivery mode.
|
|
43
43
|
* @returns acknowledgement that the Agent accepted the prompt.
|
|
44
44
|
*/
|
package/lib/types/commands.js
CHANGED
|
@@ -53,14 +53,18 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
|
|
|
53
53
|
});
|
|
54
54
|
import { randomUUID } from 'node:crypto';
|
|
55
55
|
import { brandString } from '@deepseek-ai/dsh-brand';
|
|
56
|
-
import { AttachmentError
|
|
57
|
-
import { ReasoningEffortId, createUserMessage, freezeMessage, } from '@deepseek-ai/dsh-llm';
|
|
56
|
+
import { AttachmentError } from '@deepseek-ai/dsh-attachment';
|
|
57
|
+
import { ReasoningEffortId, assistantStreamChunks, createUserMessage, freezeMessage, } from '@deepseek-ai/dsh-llm';
|
|
58
58
|
import { SessionLogOffset, SessionSeq } from '@deepseek-ai/dsh-session';
|
|
59
59
|
import { SessionQueryError } from '@deepseek-ai/dsh-session-query';
|
|
60
60
|
import { SessionTitleInvalidError } from '@deepseek-ai/dsh-session-title';
|
|
61
61
|
import { canonicalClientTimeZone } from '@deepseek-ai/dsh-util-time';
|
|
62
|
+
import { assertNever } from '@deepseek-ai/dsh-util-values';
|
|
62
63
|
import { RemoteError, remoteErrorOf } from '@deepseek-ai/dsh-typert-protocol';
|
|
63
64
|
import { ApiSessionCwdConflict, ApiSessionNotFound, ApiSessionPresetConflict, ApiSessionSubagentOwnership, apiSessionSubagentOwnershipError, hasApiSessionSubagentOwner, inspectApiSession, } from "./agent.js";
|
|
65
|
+
function hasPromptContent(content) {
|
|
66
|
+
return content.some(part => part.type !== 'text' || part.text.trim().length > 0);
|
|
67
|
+
}
|
|
64
68
|
/** Implements Session business commands delegated by the Session Controller Remote service. */
|
|
65
69
|
export class SessionCommandController {
|
|
66
70
|
ctx;
|
|
@@ -270,11 +274,14 @@ export class SessionCommandController {
|
|
|
270
274
|
}
|
|
271
275
|
}
|
|
272
276
|
/**
|
|
273
|
-
*
|
|
277
|
+
* Reject empty content, then admit one prompt after Agent and attachment validation.
|
|
274
278
|
* @param request - Session identity, prompt content, source metadata, and delivery mode.
|
|
275
279
|
* @returns acknowledgement that the Agent accepted the prompt.
|
|
276
280
|
*/
|
|
277
281
|
async prompt(request) {
|
|
282
|
+
if (!hasPromptContent(request.content)) {
|
|
283
|
+
throw new RemoteError('gateway/bad-request', 'prompt content must include non-whitespace text or an attachment', {});
|
|
284
|
+
}
|
|
278
285
|
const clientTimeZone = request.clientTimeZone === undefined
|
|
279
286
|
? undefined
|
|
280
287
|
: canonicalClientTimeZone(request.clientTimeZone);
|
|
@@ -282,6 +289,8 @@ export class SessionCommandController {
|
|
|
282
289
|
throw new RemoteError('session/invalid-time-zone', 'clientTimeZone must be UTC or a valid IANA Area/Location name', { value: request.clientTimeZone });
|
|
283
290
|
}
|
|
284
291
|
const agent = await this.resolveAgent(request.sessionId);
|
|
292
|
+
if (hasPromptRequest(agent, request.requestId))
|
|
293
|
+
return { accepted: true };
|
|
285
294
|
const selection = this.agents.selectionFor(agent).current;
|
|
286
295
|
if (!routeServed(this.ctx, selection.provider)) {
|
|
287
296
|
throw new RemoteError('session/model-unavailable', `no adapter serves provider "${selection.provider}"; select a model for this session`, { provider: selection.provider, model: selection.model });
|
|
@@ -294,19 +303,35 @@ export class SessionCommandController {
|
|
|
294
303
|
const hasImage = request.content.some(part => part.type === 'image');
|
|
295
304
|
const admit = async () => {
|
|
296
305
|
try {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
306
|
+
const env_2 = { stack: [], error: void 0, hasError: false };
|
|
307
|
+
try {
|
|
308
|
+
if (hasImage) {
|
|
309
|
+
const current = this.agents.selectionFor(agent).current;
|
|
310
|
+
const model = await this.ctx.llm.resolveModelInfo(current.provider, current.model);
|
|
311
|
+
if (model.inputModalities !== undefined && !model.inputModalities.includes('image')) {
|
|
312
|
+
throw new RemoteError('session/attachment-invalid', `Model "${current.model}" does not support image input.`, { reason: 'MODEL_DOES_NOT_SUPPORT_IMAGES' });
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
const admission = resolvePromptFileReceipts(request.content, receiptId => this.ctx.fileUploads.resolve(agent, receiptId));
|
|
316
|
+
const content = await this.ctx.attachments.admitPromptContent(admission.content);
|
|
317
|
+
const message = createUserMessage({ content, source });
|
|
318
|
+
if (this.ctx.agents.get(agent.id) !== agent) {
|
|
319
|
+
throw new RemoteError('session/not-found', `session "${agent.id}" was disposed during prompt admission`, { sessionId: agent.id });
|
|
302
320
|
}
|
|
321
|
+
const binding = __addDisposableResource(env_2, this.ctx.fileUploads.bindPrompt(agent, admission.receiptIds, request.requestId), false);
|
|
322
|
+
if (request.mode === 'steer')
|
|
323
|
+
agent.steer(message);
|
|
324
|
+
else
|
|
325
|
+
agent.followup(message);
|
|
326
|
+
binding.commit();
|
|
327
|
+
}
|
|
328
|
+
catch (e_2) {
|
|
329
|
+
env_2.error = e_2;
|
|
330
|
+
env_2.hasError = true;
|
|
331
|
+
}
|
|
332
|
+
finally {
|
|
333
|
+
__disposeResources(env_2);
|
|
303
334
|
}
|
|
304
|
-
const content = await admitPromptContent(this.ctx.attachments, request.content);
|
|
305
|
-
const message = createUserMessage({ content, source });
|
|
306
|
-
if (request.mode === 'steer')
|
|
307
|
-
agent.steer(message);
|
|
308
|
-
else
|
|
309
|
-
agent.followup(message);
|
|
310
335
|
}
|
|
311
336
|
catch (error) {
|
|
312
337
|
if (remoteErrorOf(error) !== undefined)
|
|
@@ -360,17 +385,27 @@ export class SessionCommandController {
|
|
|
360
385
|
* @returns acknowledgement that the queue mutation was applied.
|
|
361
386
|
*/
|
|
362
387
|
updateQueue(request) {
|
|
363
|
-
if (request.action.kind === 'edit'
|
|
364
|
-
|
|
365
|
-
|
|
388
|
+
if (request.action.kind === 'edit') {
|
|
389
|
+
if (request.action.content.some(block => block.type !== 'text')) {
|
|
390
|
+
throw new RemoteError('session/attachment-invalid', 'queue edits accept text content only', { reason: 'QUEUE_EDIT_NON_TEXT' });
|
|
391
|
+
}
|
|
392
|
+
if (!hasPromptContent(request.action.content)) {
|
|
393
|
+
throw new RemoteError('gateway/bad-request', 'queue edit content must include non-whitespace text', {});
|
|
394
|
+
}
|
|
366
395
|
}
|
|
367
396
|
const agent = this.ctx.agents.get(request.sessionId);
|
|
368
|
-
if (agent !== undefined && hasApiSessionSubagentOwner(this.ctx, agent.session, agent)) {
|
|
369
|
-
throw apiSessionSubagentOwnershipError(request.sessionId);
|
|
370
|
-
}
|
|
371
397
|
if (agent === undefined) {
|
|
372
398
|
throw new RemoteError('session/queue-item-not-found', 'queued item is no longer pending', { itemId: request.itemId });
|
|
373
399
|
}
|
|
400
|
+
if (hasApiSessionSubagentOwner(this.ctx, agent.session, agent)) {
|
|
401
|
+
const identity = this.ctx.sessionProjections
|
|
402
|
+
.snapshot(agent.session, ['subagent'])
|
|
403
|
+
.values.subagent;
|
|
404
|
+
if (identity?.mode !== 'continuable'
|
|
405
|
+
|| !agent.session.isOwnSeq(identity.seq)) {
|
|
406
|
+
throw apiSessionSubagentOwnershipError(request.sessionId);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
374
409
|
const nextTurn = agent.inbox.nextTurn.find(message => message.id === request.itemId);
|
|
375
410
|
const nextStep = agent.inbox.nextStep.find(message => message.id === request.itemId);
|
|
376
411
|
const located = nextTurn === undefined
|
|
@@ -383,16 +418,28 @@ export class SessionCommandController {
|
|
|
383
418
|
if (request.action.kind === 'steer' && (target !== 'next-turn' || agent.status !== 'running')) {
|
|
384
419
|
throw new RemoteError('session/steer-unavailable', 'current turn no longer accepts steering', { itemId: request.itemId });
|
|
385
420
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
421
|
+
switch (request.action.kind) {
|
|
422
|
+
case 'edit':
|
|
423
|
+
agent.inbox.replace(request.itemId, freezeMessage({
|
|
424
|
+
...message,
|
|
425
|
+
content: [...request.action.content],
|
|
426
|
+
}));
|
|
427
|
+
break;
|
|
428
|
+
case 'remove': {
|
|
429
|
+
agent.inbox.remove(request.itemId);
|
|
430
|
+
const source = message.source;
|
|
431
|
+
if (source.kind === 'user' && 'rpcId' in source) {
|
|
432
|
+
this.ctx.fileUploads.retirePrompt(agent, source.rpcId);
|
|
433
|
+
}
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
case 'steer':
|
|
437
|
+
agent.inbox.remove(request.itemId);
|
|
395
438
|
agent.steer(message);
|
|
439
|
+
break;
|
|
440
|
+
/* v8 ignore next 2 -- closed-union exhaustiveness guard */
|
|
441
|
+
default:
|
|
442
|
+
assertNever(request.action, 'queue action');
|
|
396
443
|
}
|
|
397
444
|
return { accepted: true };
|
|
398
445
|
}
|
|
@@ -462,6 +509,34 @@ export class SessionCommandController {
|
|
|
462
509
|
return undefined;
|
|
463
510
|
}
|
|
464
511
|
}
|
|
512
|
+
function resolvePromptFileReceipts(content, stagedFile) {
|
|
513
|
+
const receiptIds = new Set();
|
|
514
|
+
const resolved = content.map((part) => {
|
|
515
|
+
if (part.type !== 'file')
|
|
516
|
+
return part;
|
|
517
|
+
const attachment = stagedFile(part.receiptId);
|
|
518
|
+
if (attachment === undefined) {
|
|
519
|
+
throw new RemoteError('session/attachment-invalid', 'File was not uploaded for this session.', { reason: 'FILE_NOT_STAGED' });
|
|
520
|
+
}
|
|
521
|
+
receiptIds.add(part.receiptId);
|
|
522
|
+
return { type: 'file', attachment };
|
|
523
|
+
});
|
|
524
|
+
return { content: resolved, receiptIds: [...receiptIds] };
|
|
525
|
+
}
|
|
526
|
+
function hasPromptRequest(agent, requestId) {
|
|
527
|
+
const matches = (message) => {
|
|
528
|
+
const source = message.source;
|
|
529
|
+
return source.kind === 'user' && 'rpcId' in source && source.rpcId === requestId;
|
|
530
|
+
};
|
|
531
|
+
if (agent.inbox.nextTurn.some(matches) || agent.inbox.nextStep.some(matches))
|
|
532
|
+
return true;
|
|
533
|
+
return agent.session.snapshotEvents().some((event) => {
|
|
534
|
+
if (event.type !== 'user/message')
|
|
535
|
+
return false;
|
|
536
|
+
const source = event.data.source;
|
|
537
|
+
return source.kind === 'user' && 'rpcId' in source && source.rpcId === requestId;
|
|
538
|
+
});
|
|
539
|
+
}
|
|
465
540
|
function imageBlockIn(content, match) {
|
|
466
541
|
if (!Array.isArray(content))
|
|
467
542
|
return undefined;
|
|
@@ -495,9 +570,14 @@ function imageInEvent(event, match) {
|
|
|
495
570
|
if (found !== undefined)
|
|
496
571
|
return found;
|
|
497
572
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
573
|
+
if (event.type === 'assistant/message' || event.type === 'assistant/attempt') {
|
|
574
|
+
for (const chunk of assistantStreamChunks(event.data.stream, 'block-end')) {
|
|
575
|
+
const found = imageBlockIn([chunk.block], match);
|
|
576
|
+
if (found !== undefined)
|
|
577
|
+
return found;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return undefined;
|
|
501
581
|
}
|
|
502
582
|
function referencedImage(events, attachmentId) {
|
|
503
583
|
for (const event of events) {
|
package/lib/types/control.d.ts
CHANGED
|
@@ -15,7 +15,6 @@ export declare class SessionControlController {
|
|
|
15
15
|
control(signal: AbortSignal): AsyncIterable<SessionControlFrame>;
|
|
16
16
|
private baseline;
|
|
17
17
|
private projectionBaseline;
|
|
18
|
-
private onSessionEvent;
|
|
19
18
|
private onJobsChanged;
|
|
20
19
|
private jobsFor;
|
|
21
20
|
private broadcast;
|
package/lib/types/control.js
CHANGED
|
@@ -7,7 +7,6 @@ export class SessionControlController {
|
|
|
7
7
|
/** @param ctx - Host context carrying live Agent, projection, and jobs services. */
|
|
8
8
|
constructor(ctx) {
|
|
9
9
|
this.ctx = ctx;
|
|
10
|
-
ctx.on('session/event', (session, event) => { this.onSessionEvent(session, event); });
|
|
11
10
|
ctx.sessionProjections.onChanged((session, key, value, seq) => {
|
|
12
11
|
this.broadcast({
|
|
13
12
|
type: 'projection',
|
|
@@ -16,6 +15,16 @@ export class SessionControlController {
|
|
|
16
15
|
value: value,
|
|
17
16
|
seq,
|
|
18
17
|
});
|
|
18
|
+
if (key !== 'inbox')
|
|
19
|
+
return;
|
|
20
|
+
const agent = this.ctx.agents.get(session.id);
|
|
21
|
+
if (agent?.session !== session)
|
|
22
|
+
return;
|
|
23
|
+
this.broadcast({
|
|
24
|
+
type: 'queue',
|
|
25
|
+
sessionId: session.id,
|
|
26
|
+
items: queueItemsFromInbox(value),
|
|
27
|
+
});
|
|
19
28
|
});
|
|
20
29
|
ctx.inject(['jobs'], (jobsCtx) => {
|
|
21
30
|
jobsCtx.jobs.onJobsChanged((owner) => { this.onJobsChanged(owner); });
|
|
@@ -76,18 +85,6 @@ export class SessionControlController {
|
|
|
76
85
|
}
|
|
77
86
|
return blocks;
|
|
78
87
|
}
|
|
79
|
-
onSessionEvent(session, event) {
|
|
80
|
-
if (event.type !== 'agent/inbox/spliced')
|
|
81
|
-
return;
|
|
82
|
-
const agent = this.ctx.agents.get(session.id);
|
|
83
|
-
if (agent?.session !== session)
|
|
84
|
-
return;
|
|
85
|
-
this.broadcast({
|
|
86
|
-
type: 'queue',
|
|
87
|
-
sessionId: session.id,
|
|
88
|
-
items: queueItems(agent, event.data),
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
88
|
onJobsChanged(owner) {
|
|
92
89
|
if (owner !== undefined) {
|
|
93
90
|
this.broadcast({ type: 'jobs', sessionId: owner.id, jobs: this.jobsFor(owner) });
|
|
@@ -151,21 +148,21 @@ class ControlQueue {
|
|
|
151
148
|
}
|
|
152
149
|
}
|
|
153
150
|
}
|
|
154
|
-
function queueItems(agent
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
151
|
+
function queueItems(agent) {
|
|
152
|
+
return queueItemsFromInbox({
|
|
153
|
+
'next-turn': agent.inbox.nextTurn,
|
|
154
|
+
'next-step': agent.inbox.nextStep,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
function queueItemsFromInbox(inbox) {
|
|
161
158
|
return [
|
|
162
|
-
...
|
|
159
|
+
...inbox['next-turn'].map(message => ({
|
|
163
160
|
id: message.id,
|
|
164
161
|
placement: 'queued',
|
|
165
162
|
...promptRpcId(message),
|
|
166
163
|
message: { id: message.id, content: message.content },
|
|
167
164
|
})),
|
|
168
|
-
...
|
|
165
|
+
...inbox['next-step'].map(message => ({
|
|
169
166
|
id: message.id,
|
|
170
167
|
placement: message.source.kind === 'user' ? 'steering' : 'context',
|
|
171
168
|
...promptRpcId(message),
|
package/lib/types/history.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export declare class SessionHistoryController {
|
|
|
7
7
|
private readonly ctx;
|
|
8
8
|
private readonly promote;
|
|
9
9
|
private readonly closeFollowers;
|
|
10
|
+
private readonly assistantStreams;
|
|
10
11
|
/**
|
|
11
12
|
* @param ctx - Host context carrying Session query and projection services.
|
|
12
13
|
* @param promote - starts ordinary Session activation after snapshot delivery.
|
|
@@ -23,7 +24,7 @@ export declare class SessionHistoryController {
|
|
|
23
24
|
* Follow events appended after an initial cursor on one durable address.
|
|
24
25
|
* @param request - durable address and last committed sequence already held by the caller.
|
|
25
26
|
* @param signal - stream cancellation owned by the Remote carrier.
|
|
26
|
-
* @returns a complete opening snapshot followed by gap-free
|
|
27
|
+
* @returns a complete opening snapshot followed by gap-free durable events and opted-in assistant frames.
|
|
27
28
|
*/
|
|
28
29
|
follow(request: SessionFollowRequest, signal: AbortSignal): AsyncIterable<SessionFollowFrame>;
|
|
29
30
|
private sourceFor;
|
package/lib/types/history.js
CHANGED
|
@@ -53,9 +53,9 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
|
|
|
53
53
|
});
|
|
54
54
|
import { Deque } from '@deepseek-ai/dsh-deque';
|
|
55
55
|
import { isAppendSurfaceEvent, SessionLogOffset, SessionSeq, } from '@deepseek-ai/dsh-session';
|
|
56
|
-
import { isChunkRow, packChunkRuns } from '@deepseek-ai/dsh-session/chunk-rows';
|
|
57
56
|
import { SessionQueryError } from '@deepseek-ai/dsh-session-query';
|
|
58
57
|
import { RemoteError } from '@deepseek-ai/dsh-typert-protocol';
|
|
58
|
+
import { SessionAssistantStreamAccumulator } from "./assistant-stream.js";
|
|
59
59
|
const DEFAULT_MAX_MESSAGES = 50;
|
|
60
60
|
const MESSAGE_TYPES = new Set(['user/message', 'assistant/message']);
|
|
61
61
|
/** Implements cold-safe history operations delegated by the Session Controller. */
|
|
@@ -63,6 +63,7 @@ export class SessionHistoryController {
|
|
|
63
63
|
ctx;
|
|
64
64
|
promote;
|
|
65
65
|
closeFollowers = new Set();
|
|
66
|
+
assistantStreams = new Map();
|
|
66
67
|
/**
|
|
67
68
|
* @param ctx - Host context carrying Session query and projection services.
|
|
68
69
|
* @param promote - starts ordinary Session activation after snapshot delivery.
|
|
@@ -70,6 +71,17 @@ export class SessionHistoryController {
|
|
|
70
71
|
constructor(ctx, promote) {
|
|
71
72
|
this.ctx = ctx;
|
|
72
73
|
this.promote = promote;
|
|
74
|
+
ctx.on('agent/assistant-stream', ({ agent, frame }) => {
|
|
75
|
+
let stream = this.assistantStreams.get(agent.session.id);
|
|
76
|
+
if (stream === undefined) {
|
|
77
|
+
stream = new SessionAssistantStreamAccumulator();
|
|
78
|
+
this.assistantStreams.set(agent.session.id, stream);
|
|
79
|
+
}
|
|
80
|
+
stream.accept(frame, cursorBeforeNext(agent.session.seq));
|
|
81
|
+
}, { global: true });
|
|
82
|
+
ctx.on('agent/disposed', ({ agent }) => {
|
|
83
|
+
this.assistantStreams.delete(agent.session.id);
|
|
84
|
+
}, { global: true });
|
|
73
85
|
ctx.effect(() => () => {
|
|
74
86
|
for (const close of this.closeFollowers)
|
|
75
87
|
close();
|
|
@@ -122,7 +134,7 @@ export class SessionHistoryController {
|
|
|
122
134
|
* Follow events appended after an initial cursor on one durable address.
|
|
123
135
|
* @param request - durable address and last committed sequence already held by the caller.
|
|
124
136
|
* @param signal - stream cancellation owned by the Remote carrier.
|
|
125
|
-
* @returns a complete opening snapshot followed by gap-free
|
|
137
|
+
* @returns a complete opening snapshot followed by gap-free durable events and opted-in assistant frames.
|
|
126
138
|
*/
|
|
127
139
|
async *follow(request, signal) {
|
|
128
140
|
validateFollowRequest(request);
|
|
@@ -130,6 +142,7 @@ export class SessionHistoryController {
|
|
|
130
142
|
const target = addressId(address);
|
|
131
143
|
const buffered = new Deque();
|
|
132
144
|
let snapshotCursor;
|
|
145
|
+
let assistantStreamOrdinal = 0;
|
|
133
146
|
let wake;
|
|
134
147
|
const notify = () => {
|
|
135
148
|
const resume = wake;
|
|
@@ -145,7 +158,7 @@ export class SessionHistoryController {
|
|
|
145
158
|
const disposeEvent = this.ctx.on('session/event', (session, event) => {
|
|
146
159
|
if (session.id !== target)
|
|
147
160
|
return;
|
|
148
|
-
buffered.pushBack(event);
|
|
161
|
+
buffered.pushBack({ type: 'event', event });
|
|
149
162
|
notify();
|
|
150
163
|
}, { global: true });
|
|
151
164
|
const disposeCreated = this.ctx.on('session/created', (session) => {
|
|
@@ -158,10 +171,22 @@ export class SessionHistoryController {
|
|
|
158
171
|
? session.firstLiveSeq
|
|
159
172
|
: SessionLogOffset(snapshotCursor + 1));
|
|
160
173
|
for (let index = suffix.length - 1; index >= 0; index -= 1) {
|
|
161
|
-
buffered.pushFront(suffix[index]);
|
|
174
|
+
buffered.pushFront({ type: 'event', event: suffix[index] });
|
|
162
175
|
}
|
|
163
176
|
notify();
|
|
164
177
|
}, { global: true });
|
|
178
|
+
const disposeAssistantStream = request.assistantStream !== true
|
|
179
|
+
? undefined
|
|
180
|
+
: this.ctx.on('agent/assistant-stream', ({ agent, frame }) => {
|
|
181
|
+
if (agent.session.id !== target)
|
|
182
|
+
return;
|
|
183
|
+
buffered.pushBack({
|
|
184
|
+
type: 'assistant-stream',
|
|
185
|
+
frame: wireAssistantStreamFrame(frame, cursorBeforeNext(agent.session.seq)),
|
|
186
|
+
ordinal: ++assistantStreamOrdinal,
|
|
187
|
+
});
|
|
188
|
+
notify();
|
|
189
|
+
}, { global: true });
|
|
165
190
|
const onAbort = () => { notify(); };
|
|
166
191
|
signal.addEventListener('abort', onAbort, { once: true });
|
|
167
192
|
try {
|
|
@@ -173,15 +198,24 @@ export class SessionHistoryController {
|
|
|
173
198
|
const cursor = source.cursor;
|
|
174
199
|
snapshotCursor = cursor;
|
|
175
200
|
const page = paginate(events, undefined, request.maxMessages ?? DEFAULT_MAX_MESSAGES);
|
|
201
|
+
const assistantStream = request.assistantStream === true
|
|
202
|
+
? this.assistantStreams.get(target)?.snapshot() ?? { revision: 0 }
|
|
203
|
+
: undefined;
|
|
204
|
+
// The accumulator snapshot and this watermark are synchronous. Frames
|
|
205
|
+
// through the cut are represented or superseded by that baseline,
|
|
206
|
+
// including larger revisions from a retired Agent; later revision
|
|
207
|
+
// resets reach Client continuity validation.
|
|
208
|
+
const assistantStreamOrdinalCut = assistantStreamOrdinal;
|
|
176
209
|
yield {
|
|
177
210
|
type: 'snapshot',
|
|
178
|
-
header: wireHeader(source.header
|
|
211
|
+
header: wireHeader(source.header),
|
|
179
212
|
cursor,
|
|
180
213
|
records: pageRecords(page.events),
|
|
181
214
|
hasMore: page.hasMore,
|
|
182
215
|
projections: source.projections === undefined
|
|
183
216
|
? { asOfSeq: cursor, values: {} }
|
|
184
217
|
: projectionBlock(source.projections),
|
|
218
|
+
...assistantStream === undefined ? {} : { assistantStream },
|
|
185
219
|
};
|
|
186
220
|
if (address.kind === 'session' && source.source === 'prepared') {
|
|
187
221
|
const promotion = source.retain();
|
|
@@ -200,14 +234,20 @@ export class SessionHistoryController {
|
|
|
200
234
|
await new Promise((resolve) => { wake = resolve; });
|
|
201
235
|
continue;
|
|
202
236
|
}
|
|
237
|
+
if (item.type === 'assistant-stream') {
|
|
238
|
+
if (item.ordinal > assistantStreamOrdinalCut) {
|
|
239
|
+
yield { type: 'assistant-stream', frame: item.frame };
|
|
240
|
+
}
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
203
243
|
const expectedSeq = SessionSeq(nextOffset);
|
|
204
|
-
if (item.seq < expectedSeq)
|
|
244
|
+
if (item.event.seq < expectedSeq)
|
|
205
245
|
continue;
|
|
206
|
-
if (item.seq !== expectedSeq) {
|
|
246
|
+
if (item.event.seq !== expectedSeq) {
|
|
207
247
|
throw new RemoteError('gateway/internal', `session event stream skipped seq ${String(expectedSeq)}`, {});
|
|
208
248
|
}
|
|
209
249
|
nextOffset = SessionLogOffset(nextOffset + 1);
|
|
210
|
-
yield entryFor(item);
|
|
250
|
+
yield entryFor(item.event);
|
|
211
251
|
}
|
|
212
252
|
}
|
|
213
253
|
catch (e_2) {
|
|
@@ -223,6 +263,7 @@ export class SessionHistoryController {
|
|
|
223
263
|
signal.removeEventListener('abort', onAbort);
|
|
224
264
|
disposeCreated();
|
|
225
265
|
disposeEvent();
|
|
266
|
+
disposeAssistantStream?.();
|
|
226
267
|
}
|
|
227
268
|
}
|
|
228
269
|
async sourceFor(address, signal, withProjections) {
|
|
@@ -253,6 +294,19 @@ export class SessionHistoryController {
|
|
|
253
294
|
}
|
|
254
295
|
}
|
|
255
296
|
}
|
|
297
|
+
function cursorBeforeNext(nextSeq) {
|
|
298
|
+
return nextSeq === 0 ? -1 : SessionSeq(nextSeq - 1);
|
|
299
|
+
}
|
|
300
|
+
function wireAssistantStreamFrame(frame, durableCursor) {
|
|
301
|
+
if (frame.type === 'start')
|
|
302
|
+
return { ...frame, startedAfterSeq: durableCursor };
|
|
303
|
+
if (frame.type === 'end')
|
|
304
|
+
return frame;
|
|
305
|
+
return {
|
|
306
|
+
...frame,
|
|
307
|
+
chunk: frame.chunk,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
256
310
|
function projectionBlock(snapshot) {
|
|
257
311
|
return {
|
|
258
312
|
asOfSeq: snapshot.asOfSeq,
|
|
@@ -354,13 +408,9 @@ function paginate(events, beforeSeq, maxMessages, throughSeq = events.at(-1)?.se
|
|
|
354
408
|
}
|
|
355
409
|
return { events: events.slice(cut, end), hasMore: cut > 0 };
|
|
356
410
|
}
|
|
357
|
-
/** Translate logical Session metadata to the
|
|
358
|
-
function wireHeader(header
|
|
359
|
-
|
|
360
|
-
return {
|
|
361
|
-
...wire,
|
|
362
|
-
...isSeeded ? { seedLength: inheritedEventCount } : {},
|
|
363
|
-
};
|
|
411
|
+
/** Translate current logical Session metadata to the browser wire. */
|
|
412
|
+
function wireHeader(header) {
|
|
413
|
+
return { ...header };
|
|
364
414
|
}
|
|
365
415
|
function entryFor(event) {
|
|
366
416
|
return {
|
|
@@ -369,29 +419,8 @@ function entryFor(event) {
|
|
|
369
419
|
event: event,
|
|
370
420
|
};
|
|
371
421
|
}
|
|
372
|
-
function chunkEntryFor(row) {
|
|
373
|
-
switch (row.type) {
|
|
374
|
-
case 'text-chunks':
|
|
375
|
-
return {
|
|
376
|
-
type: 'chunks',
|
|
377
|
-
event: { type: 'chunkrow/text-chunks', seq: row.seq0, time: row.time0, data: row.data },
|
|
378
|
-
};
|
|
379
|
-
case 'reasoning-chunks':
|
|
380
|
-
return {
|
|
381
|
-
type: 'chunks',
|
|
382
|
-
event: { type: 'chunkrow/reasoning-chunks', seq: row.seq0, time: row.time0, data: row.data },
|
|
383
|
-
};
|
|
384
|
-
case 'tool-call-chunks':
|
|
385
|
-
return {
|
|
386
|
-
type: 'chunks',
|
|
387
|
-
event: { type: 'chunkrow/tool-call-chunks', seq: row.seq0, time: row.time0, data: row.data },
|
|
388
|
-
};
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
422
|
/** Encode one bounded logical page without changing its pagination cut. */
|
|
392
423
|
function pageRecords(events) {
|
|
393
|
-
return
|
|
394
|
-
? chunkEntryFor(record)
|
|
395
|
-
: entryFor(record));
|
|
424
|
+
return events.map(entryFor);
|
|
396
425
|
}
|
|
397
426
|
//# sourceMappingURL=history.js.map
|