@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.
Files changed (42) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +16 -6
  3. package/README.zh.md +16 -6
  4. package/lib/client.js +838 -58
  5. package/lib/index.js +376 -243
  6. package/lib/typert.host.js +236 -178
  7. package/lib/typert.remote-client.js +97 -115
  8. package/lib/types/agent.js +6 -8
  9. package/lib/types/assistant-stream.d.ts +26 -0
  10. package/lib/types/assistant-stream.js +83 -0
  11. package/lib/types/client/contract/events.d.ts +38 -7
  12. package/lib/types/client/contract/events.js +21 -0
  13. package/lib/types/client/contract/session.d.ts +7 -7
  14. package/lib/types/client/contract/snapshot.d.ts +15 -2
  15. package/lib/types/client/index.d.ts +2 -2
  16. package/lib/types/client/index.js +2 -0
  17. package/lib/types/client/session-wire-event.d.ts +11 -0
  18. package/lib/types/client/session-wire-event.js +43 -0
  19. package/lib/types/client/sessions/assistant-stream.d.ts +51 -0
  20. package/lib/types/client/sessions/assistant-stream.js +168 -0
  21. package/lib/types/client/sessions/history-records.d.ts +2 -2
  22. package/lib/types/client/sessions/history-records.js +3 -8
  23. package/lib/types/client/sessions/queue-mirror.js +3 -3
  24. package/lib/types/client/sessions/remotes.d.ts +2 -2
  25. package/lib/types/client/sessions/session.d.ts +3 -1
  26. package/lib/types/client/sessions/session.js +63 -15
  27. package/lib/types/client/transport.d.ts +7 -3
  28. package/lib/types/client/transport.js +24 -3
  29. package/lib/types/commands.d.ts +1 -1
  30. package/lib/types/commands.js +112 -32
  31. package/lib/types/control.d.ts +0 -1
  32. package/lib/types/control.js +19 -22
  33. package/lib/types/history.d.ts +2 -1
  34. package/lib/types/history.js +66 -37
  35. package/lib/types/index.d.ts +4 -4
  36. package/lib/types/index.js +15 -5
  37. package/lib/types/list.d.ts +2 -9
  38. package/lib/types/list.js +10 -124
  39. package/lib/types/media-references.d.ts +16 -0
  40. package/lib/types/media-references.js +77 -0
  41. package/lib/types/types.d.ts +85 -35
  42. 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 */
@@ -38,7 +38,7 @@ export declare class SessionCommandController {
38
38
  */
39
39
  fork(request: SessionForkRequest): Promise<SessionForkValue>;
40
40
  /**
41
- * Admit one browser prompt after explicit Agent resume and image validation.
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
  */
@@ -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, admitPromptContent } from '@deepseek-ai/dsh-attachment';
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
- * Admit one browser prompt after explicit Agent resume and image validation.
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
- if (hasImage) {
298
- const current = this.agents.selectionFor(agent).current;
299
- const model = await this.ctx.llm.resolveModelInfo(current.provider, current.model);
300
- if (model.inputModalities !== undefined && !model.inputModalities.includes('image')) {
301
- throw new RemoteError('session/attachment-invalid', `Model "${current.model}" does not support image input.`, { reason: 'MODEL_DOES_NOT_SUPPORT_IMAGES' });
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
- && request.action.content.some(block => block.type !== 'text')) {
365
- throw new RemoteError('session/attachment-invalid', 'queue edits accept text content only', { reason: 'QUEUE_EDIT_NON_TEXT' });
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
- if (request.action.kind === 'edit') {
387
- agent.inbox.replace(request.itemId, freezeMessage({
388
- ...message,
389
- content: [...request.action.content],
390
- }));
391
- }
392
- else {
393
- agent.inbox.remove(request.itemId);
394
- if (request.action.kind === 'steer')
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
- return event.type === 'assistant/chunk' && data.chunk?.type === 'block-end'
499
- ? imageBlockIn([data.chunk.block], match)
500
- : undefined;
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) {
@@ -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;
@@ -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, splice) {
155
- const project = (target) => {
156
- const messages = target === 'next-turn' ? agent.inbox.nextTurn : agent.inbox.nextStep;
157
- return splice?.target === target
158
- ? messages.toSpliced(splice.start, splice.removedCount ?? 0, ...splice.inserted)
159
- : messages;
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
- ...project('next-turn').map(message => ({
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
- ...project('next-step').map(message => ({
165
+ ...inbox['next-step'].map(message => ({
169
166
  id: message.id,
170
167
  placement: message.source.kind === 'user' ? 'steering' : 'context',
171
168
  ...promptRpcId(message),
@@ -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 event frames.
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;
@@ -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 event frames.
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, source.inheritedEventCount),
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 unchanged v0 browser wire. */
358
- function wireHeader(header, inheritedEventCount) {
359
- const { isSeeded, ...wire } = header;
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 packChunkRuns(events).map(record => isChunkRow(record)
394
- ? chunkEntryFor(record)
395
- : entryFor(record));
424
+ return events.map(entryFor);
396
425
  }
397
426
  //# sourceMappingURL=history.js.map