@crazx/dsh-api-session-controller 0.1.2-rc.1.zw.2 → 0.1.5-alpha.1.zw.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 +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 +70 -57
|
@@ -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;
|
|
@@ -306,11 +310,14 @@ export class SessionCommandController {
|
|
|
306
310
|
}
|
|
307
311
|
}
|
|
308
312
|
/**
|
|
309
|
-
*
|
|
313
|
+
* Reject empty content, then admit one prompt after Agent and attachment validation.
|
|
310
314
|
* @param request - Session identity, prompt content, source metadata, and delivery mode.
|
|
311
315
|
* @returns acknowledgement that the Agent accepted the prompt.
|
|
312
316
|
*/
|
|
313
317
|
async prompt(request) {
|
|
318
|
+
if (!hasPromptContent(request.content)) {
|
|
319
|
+
throw new RemoteError('gateway/bad-request', 'prompt content must include non-whitespace text or an attachment', {});
|
|
320
|
+
}
|
|
314
321
|
const clientTimeZone = request.clientTimeZone === undefined
|
|
315
322
|
? undefined
|
|
316
323
|
: canonicalClientTimeZone(request.clientTimeZone);
|
|
@@ -318,6 +325,8 @@ export class SessionCommandController {
|
|
|
318
325
|
throw new RemoteError('session/invalid-time-zone', 'clientTimeZone must be UTC or a valid IANA Area/Location name', { value: request.clientTimeZone });
|
|
319
326
|
}
|
|
320
327
|
const agent = await this.resolveAgent(request.sessionId);
|
|
328
|
+
if (hasPromptRequest(agent, request.requestId))
|
|
329
|
+
return { accepted: true };
|
|
321
330
|
const selection = this.agents.selectionFor(agent).current;
|
|
322
331
|
if (!routeServed(this.ctx, selection.provider)) {
|
|
323
332
|
throw new RemoteError('session/model-unavailable', `no adapter serves provider "${selection.provider}"; select a model for this session`, { provider: selection.provider, model: selection.model });
|
|
@@ -330,19 +339,35 @@ export class SessionCommandController {
|
|
|
330
339
|
const hasImage = request.content.some(part => part.type === 'image');
|
|
331
340
|
const admit = async () => {
|
|
332
341
|
try {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
342
|
+
const env_2 = { stack: [], error: void 0, hasError: false };
|
|
343
|
+
try {
|
|
344
|
+
if (hasImage) {
|
|
345
|
+
const current = this.agents.selectionFor(agent).current;
|
|
346
|
+
const model = await this.ctx.llm.resolveModelInfo(current.provider, current.model);
|
|
347
|
+
if (model.inputModalities !== undefined && !model.inputModalities.includes('image')) {
|
|
348
|
+
throw new RemoteError('session/attachment-invalid', `Model "${current.model}" does not support image input.`, { reason: 'MODEL_DOES_NOT_SUPPORT_IMAGES' });
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const admission = resolvePromptFileReceipts(request.content, receiptId => this.ctx.fileUploads.resolve(agent, receiptId));
|
|
352
|
+
const content = await this.ctx.attachments.admitPromptContent(admission.content);
|
|
353
|
+
const message = createUserMessage({ content, source });
|
|
354
|
+
if (this.ctx.agents.get(agent.id) !== agent) {
|
|
355
|
+
throw new RemoteError('session/not-found', `session "${agent.id}" was disposed during prompt admission`, { sessionId: agent.id });
|
|
338
356
|
}
|
|
357
|
+
const binding = __addDisposableResource(env_2, this.ctx.fileUploads.bindPrompt(agent, admission.receiptIds, request.requestId), false);
|
|
358
|
+
if (request.mode === 'steer')
|
|
359
|
+
agent.steer(message);
|
|
360
|
+
else
|
|
361
|
+
agent.followup(message);
|
|
362
|
+
binding.commit();
|
|
363
|
+
}
|
|
364
|
+
catch (e_2) {
|
|
365
|
+
env_2.error = e_2;
|
|
366
|
+
env_2.hasError = true;
|
|
367
|
+
}
|
|
368
|
+
finally {
|
|
369
|
+
__disposeResources(env_2);
|
|
339
370
|
}
|
|
340
|
-
const content = await admitPromptContent(this.ctx.attachments, request.content);
|
|
341
|
-
const message = createUserMessage({ content, source });
|
|
342
|
-
if (request.mode === 'steer')
|
|
343
|
-
agent.steer(message);
|
|
344
|
-
else
|
|
345
|
-
agent.followup(message);
|
|
346
371
|
}
|
|
347
372
|
catch (error) {
|
|
348
373
|
if (remoteErrorOf(error) !== undefined)
|
|
@@ -396,17 +421,27 @@ export class SessionCommandController {
|
|
|
396
421
|
* @returns acknowledgement that the queue mutation was applied.
|
|
397
422
|
*/
|
|
398
423
|
updateQueue(request) {
|
|
399
|
-
if (request.action.kind === 'edit'
|
|
400
|
-
|
|
401
|
-
|
|
424
|
+
if (request.action.kind === 'edit') {
|
|
425
|
+
if (request.action.content.some(block => block.type !== 'text')) {
|
|
426
|
+
throw new RemoteError('session/attachment-invalid', 'queue edits accept text content only', { reason: 'QUEUE_EDIT_NON_TEXT' });
|
|
427
|
+
}
|
|
428
|
+
if (!hasPromptContent(request.action.content)) {
|
|
429
|
+
throw new RemoteError('gateway/bad-request', 'queue edit content must include non-whitespace text', {});
|
|
430
|
+
}
|
|
402
431
|
}
|
|
403
432
|
const agent = this.ctx.agents.get(request.sessionId);
|
|
404
|
-
if (agent !== undefined && hasApiSessionSubagentOwner(this.ctx, agent.session, agent)) {
|
|
405
|
-
throw apiSessionSubagentOwnershipError(request.sessionId);
|
|
406
|
-
}
|
|
407
433
|
if (agent === undefined) {
|
|
408
434
|
throw new RemoteError('session/queue-item-not-found', 'queued item is no longer pending', { itemId: request.itemId });
|
|
409
435
|
}
|
|
436
|
+
if (hasApiSessionSubagentOwner(this.ctx, agent.session, agent)) {
|
|
437
|
+
const identity = this.ctx.sessionProjections
|
|
438
|
+
.snapshot(agent.session, ['subagent'])
|
|
439
|
+
.values.subagent;
|
|
440
|
+
if (identity?.mode !== 'continuable'
|
|
441
|
+
|| !agent.session.isOwnSeq(identity.seq)) {
|
|
442
|
+
throw apiSessionSubagentOwnershipError(request.sessionId);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
410
445
|
const nextTurn = agent.inbox.nextTurn.find(message => message.id === request.itemId);
|
|
411
446
|
const nextStep = agent.inbox.nextStep.find(message => message.id === request.itemId);
|
|
412
447
|
const located = nextTurn === undefined
|
|
@@ -419,16 +454,28 @@ export class SessionCommandController {
|
|
|
419
454
|
if (request.action.kind === 'steer' && (target !== 'next-turn' || agent.status !== 'running')) {
|
|
420
455
|
throw new RemoteError('session/steer-unavailable', 'current turn no longer accepts steering', { itemId: request.itemId });
|
|
421
456
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
457
|
+
switch (request.action.kind) {
|
|
458
|
+
case 'edit':
|
|
459
|
+
agent.inbox.replace(request.itemId, freezeMessage({
|
|
460
|
+
...message,
|
|
461
|
+
content: [...request.action.content],
|
|
462
|
+
}));
|
|
463
|
+
break;
|
|
464
|
+
case 'remove': {
|
|
465
|
+
agent.inbox.remove(request.itemId);
|
|
466
|
+
const source = message.source;
|
|
467
|
+
if (source.kind === 'user' && 'rpcId' in source) {
|
|
468
|
+
this.ctx.fileUploads.retirePrompt(agent, source.rpcId);
|
|
469
|
+
}
|
|
470
|
+
break;
|
|
471
|
+
}
|
|
472
|
+
case 'steer':
|
|
473
|
+
agent.inbox.remove(request.itemId);
|
|
431
474
|
agent.steer(message);
|
|
475
|
+
break;
|
|
476
|
+
/* v8 ignore next 2 -- closed-union exhaustiveness guard */
|
|
477
|
+
default:
|
|
478
|
+
assertNever(request.action, 'queue action');
|
|
432
479
|
}
|
|
433
480
|
return { accepted: true };
|
|
434
481
|
}
|
|
@@ -498,6 +545,34 @@ export class SessionCommandController {
|
|
|
498
545
|
return undefined;
|
|
499
546
|
}
|
|
500
547
|
}
|
|
548
|
+
function resolvePromptFileReceipts(content, stagedFile) {
|
|
549
|
+
const receiptIds = new Set();
|
|
550
|
+
const resolved = content.map((part) => {
|
|
551
|
+
if (part.type !== 'file')
|
|
552
|
+
return part;
|
|
553
|
+
const attachment = stagedFile(part.receiptId);
|
|
554
|
+
if (attachment === undefined) {
|
|
555
|
+
throw new RemoteError('session/attachment-invalid', 'File was not uploaded for this session.', { reason: 'FILE_NOT_STAGED' });
|
|
556
|
+
}
|
|
557
|
+
receiptIds.add(part.receiptId);
|
|
558
|
+
return { type: 'file', attachment };
|
|
559
|
+
});
|
|
560
|
+
return { content: resolved, receiptIds: [...receiptIds] };
|
|
561
|
+
}
|
|
562
|
+
function hasPromptRequest(agent, requestId) {
|
|
563
|
+
const matches = (message) => {
|
|
564
|
+
const source = message.source;
|
|
565
|
+
return source.kind === 'user' && 'rpcId' in source && source.rpcId === requestId;
|
|
566
|
+
};
|
|
567
|
+
if (agent.inbox.nextTurn.some(matches) || agent.inbox.nextStep.some(matches))
|
|
568
|
+
return true;
|
|
569
|
+
return agent.session.snapshotEvents().some((event) => {
|
|
570
|
+
if (event.type !== 'user/message')
|
|
571
|
+
return false;
|
|
572
|
+
const source = event.data.source;
|
|
573
|
+
return source.kind === 'user' && 'rpcId' in source && source.rpcId === requestId;
|
|
574
|
+
});
|
|
575
|
+
}
|
|
501
576
|
function imageBlockIn(content, match) {
|
|
502
577
|
if (!Array.isArray(content))
|
|
503
578
|
return undefined;
|
|
@@ -531,9 +606,14 @@ function imageInEvent(event, match) {
|
|
|
531
606
|
if (found !== undefined)
|
|
532
607
|
return found;
|
|
533
608
|
}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
609
|
+
if (event.type === 'assistant/message' || event.type === 'assistant/attempt') {
|
|
610
|
+
for (const chunk of assistantStreamChunks(event.data.stream, 'block-end')) {
|
|
611
|
+
const found = imageBlockIn([chunk.block], match);
|
|
612
|
+
if (found !== undefined)
|
|
613
|
+
return found;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return undefined;
|
|
537
617
|
}
|
|
538
618
|
function referencedImage(events, attachmentId) {
|
|
539
619
|
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
|