@crazx/dsh-api-session-controller 0.1.2-rc.1.zw.2 → 0.1.5-alpha.1.zw.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 +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 */
@@ -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;
@@ -306,11 +310,14 @@ export class SessionCommandController {
306
310
  }
307
311
  }
308
312
  /**
309
- * Admit one browser prompt after explicit Agent resume and image validation.
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
- if (hasImage) {
334
- const current = this.agents.selectionFor(agent).current;
335
- const model = await this.ctx.llm.resolveModelInfo(current.provider, current.model);
336
- if (model.inputModalities !== undefined && !model.inputModalities.includes('image')) {
337
- throw new RemoteError('session/attachment-invalid', `Model "${current.model}" does not support image input.`, { reason: 'MODEL_DOES_NOT_SUPPORT_IMAGES' });
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
- && request.action.content.some(block => block.type !== 'text')) {
401
- throw new RemoteError('session/attachment-invalid', 'queue edits accept text content only', { reason: 'QUEUE_EDIT_NON_TEXT' });
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
- if (request.action.kind === 'edit') {
423
- agent.inbox.replace(request.itemId, freezeMessage({
424
- ...message,
425
- content: [...request.action.content],
426
- }));
427
- }
428
- else {
429
- agent.inbox.remove(request.itemId);
430
- if (request.action.kind === 'steer')
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
- return event.type === 'assistant/chunk' && data.chunk?.type === 'block-end'
535
- ? imageBlockIn([data.chunk.block], match)
536
- : undefined;
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) {
@@ -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