@khorsheed/dsh-room 0.2.3 → 0.2.5
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/CHANGELOG.md +18 -0
- package/lib/client.js +2309 -1127
- package/lib/index.js +572 -36
- package/lib/typert.host.js +249 -65
- package/lib/typert.remote-client.d.ts +4 -1
- package/lib/typert.remote-client.js +208 -53
- package/lib/types/client/InviteDialog.js +11 -4
- package/lib/types/client/RoomAttachments.d.ts +20 -0
- package/lib/types/client/RoomAttachments.js +36 -0
- package/lib/types/client/RoomComposer.d.ts +6 -12
- package/lib/types/client/RoomComposer.js +116 -12
- package/lib/types/client/RoomQuestionView.d.ts +10 -2
- package/lib/types/client/RoomQuestionView.js +18 -5
- package/lib/types/client/RoomRunView.d.ts +1 -1
- package/lib/types/client/RoomRunView.js +15 -5
- package/lib/types/client/RoomSpeechView.d.ts +24 -1
- package/lib/types/client/RoomSpeechView.js +39 -3
- package/lib/types/client/RoomStatsLine.d.ts +3 -1
- package/lib/types/client/RoomStatsLine.js +3 -1
- package/lib/types/client/RoomTaskCapsule.d.ts +6 -1
- package/lib/types/client/RoomTaskCapsule.js +2 -2
- package/lib/types/client/TasksView.js +1 -1
- package/lib/types/client/TeamView.js +1 -1
- package/lib/types/client/icons.d.ts +2 -1
- package/lib/types/client/icons.js +1 -0
- package/lib/types/client/index.js +94 -15
- package/lib/types/client/locales.d.ts +44 -0
- package/lib/types/client/locales.js +44 -0
- package/lib/types/client/nodes.d.ts +22 -3
- package/lib/types/client/nodes.js +28 -5
- package/lib/types/client/path-mentions.d.ts +34 -0
- package/lib/types/client/path-mentions.js +39 -0
- package/lib/types/client/slots.d.ts +51 -1
- package/lib/types/dispatch.d.ts +60 -0
- package/lib/types/dispatch.js +136 -1
- package/lib/types/index.d.ts +69 -1
- package/lib/types/index.js +368 -20
- package/lib/types/jobs.js +7 -3
- package/lib/types/journal.d.ts +1 -1
- package/lib/types/journal.js +18 -2
- package/lib/types/types.d.ts +77 -0
- package/package.json +10 -6
- package/lib/journal-B4FOGO6S.js +0 -482
- package/lib/journal-BWL3-i0s.js +0 -462
- package/lib/journal-Rid9AAnD.js +0 -485
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* RoomService.postMessage) and renders through the official user node.
|
|
12
12
|
* @module @khorsheed/dsh-room/client/nodes
|
|
13
13
|
*/
|
|
14
|
-
import type { RoomDecisionEvent, RoomHelp, RoomQuestionAnswer, RoomQuestionItem } from '../types.ts';
|
|
14
|
+
import type { RoomAsk, RoomDecisionEvent, RoomHelp, RoomQuestionAnswer, RoomQuestionItem } from '../types.ts';
|
|
15
15
|
import type { SessionId } from '@deepseek-ai/dsh-session/types';
|
|
16
16
|
import type { ConversationNodeDefinition } from '@deepseek-ai/dsh-client-ui-conversation/client';
|
|
17
17
|
/** Chat node data of one member speech row. */
|
|
@@ -26,6 +26,11 @@ export interface RoomSpeechData {
|
|
|
26
26
|
readonly time: number;
|
|
27
27
|
readonly member: string;
|
|
28
28
|
readonly text: string;
|
|
29
|
+
/** Questions the round asked, at their offsets in `text` (see RoomSpeechEvent.asks). */
|
|
30
|
+
readonly asks?: readonly {
|
|
31
|
+
readonly id: string;
|
|
32
|
+
readonly at: number;
|
|
33
|
+
}[];
|
|
29
34
|
/** The member's child session (jump target), when recorded. */
|
|
30
35
|
readonly childSessionId?: SessionId;
|
|
31
36
|
/** Dispatch→settle milliseconds, when measured. */
|
|
@@ -37,7 +42,7 @@ export interface RoomSpeechData {
|
|
|
37
42
|
}
|
|
38
43
|
/** Chat node data of one member run row. */
|
|
39
44
|
export interface RoomRunData {
|
|
40
|
-
/** Seq
|
|
45
|
+
/** Seq the card anchors at: the running edge's event, or its latest move (`room/run-moved`). */
|
|
41
46
|
readonly seq: number;
|
|
42
47
|
/** Unix epoch ms from the running edge's event. */
|
|
43
48
|
readonly time: number;
|
|
@@ -112,7 +117,20 @@ export interface RoomQuestionData {
|
|
|
112
117
|
readonly questions: readonly RoomQuestionItem[];
|
|
113
118
|
readonly state: 'pending' | 'answered' | 'cancelled';
|
|
114
119
|
readonly answers?: readonly RoomQuestionAnswer[];
|
|
120
|
+
/** A consent gate: never answerable once it expired. */
|
|
121
|
+
readonly consent?: true;
|
|
122
|
+
/** Answered after it expired (the answers went to the member as a message). */
|
|
123
|
+
readonly late?: true;
|
|
124
|
+
/** The asker's round: the question renders inside that round (its card, then its reply), not as a row of its own. */
|
|
125
|
+
readonly runId?: string;
|
|
115
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* The card data of a question from the room's ask log, for the inline
|
|
129
|
+
* renderings (a round's live card and its reply).
|
|
130
|
+
* @param ask - the logged question.
|
|
131
|
+
* @returns the card data.
|
|
132
|
+
*/
|
|
133
|
+
export declare function askCardData(ask: RoomAsk): RoomQuestionData;
|
|
116
134
|
export interface RoomRelayData {
|
|
117
135
|
/** Seq of the relay's latest journal event. */
|
|
118
136
|
readonly seq: number;
|
|
@@ -170,7 +188,8 @@ export declare const roomSpeechDefinition: ConversationNodeDefinition<RoomSpeech
|
|
|
170
188
|
/**
|
|
171
189
|
* The member-run Definition: one Context per run (matched on
|
|
172
190
|
* member+startedAt), started by the running edge and updated by its terminal
|
|
173
|
-
* edge. A
|
|
191
|
+
* edge. A `room/run-moved` edge re-anchors a running card (its seq, which the
|
|
192
|
+
* view orders by) below a reply or human message that landed after it. A running run renders the live row; a failed run stays as a dim
|
|
174
193
|
* error row; done/cancelled hide the row IN PLACE — the assembler's live
|
|
175
194
|
* incremental flush forbids withdrawing a materialized node with null (the
|
|
176
195
|
* withdrawal contract is the same key with `visibility: 'hidden'`; a null
|
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
import { isAppendSurfaceEvent } from '@deepseek-ai/dsh-session/surface';
|
|
2
|
+
/**
|
|
3
|
+
* The card data of a question from the room's ask log, for the inline
|
|
4
|
+
* renderings (a round's live card and its reply).
|
|
5
|
+
* @param ask - the logged question.
|
|
6
|
+
* @returns the card data.
|
|
7
|
+
*/
|
|
8
|
+
export function askCardData(ask) {
|
|
9
|
+
return {
|
|
10
|
+
seq: 0, time: ask.time, questionId: ask.id, member: ask.member, questions: ask.questions, state: ask.state,
|
|
11
|
+
...ask.answers === undefined ? {} : { answers: ask.answers },
|
|
12
|
+
...ask.consent === true ? { consent: true } : {},
|
|
13
|
+
...ask.late === true ? { late: true } : {},
|
|
14
|
+
...ask.runId === undefined ? {} : { runId: ask.runId },
|
|
15
|
+
};
|
|
16
|
+
}
|
|
2
17
|
/** The shared final-node materialization (single shape across the three kinds). */
|
|
3
18
|
function viewNode(context, kind, visibility = 'visible') {
|
|
4
19
|
if (context.state === undefined)
|
|
@@ -33,6 +48,7 @@ export const roomSpeechDefinition = {
|
|
|
33
48
|
time: event.time,
|
|
34
49
|
member: event.data.member,
|
|
35
50
|
text: event.data.text,
|
|
51
|
+
...event.data.asks === undefined ? {} : { asks: event.data.asks },
|
|
36
52
|
...event.data.interrupted === undefined ? {} : { interrupted: event.data.interrupted },
|
|
37
53
|
...event.data.reportedTo === undefined ? {} : { reportedTo: event.data.reportedTo },
|
|
38
54
|
...event.data.childSessionId === undefined ? {} : { childSessionId: event.data.childSessionId },
|
|
@@ -47,7 +63,8 @@ export const roomSpeechDefinition = {
|
|
|
47
63
|
/**
|
|
48
64
|
* The member-run Definition: one Context per run (matched on
|
|
49
65
|
* member+startedAt), started by the running edge and updated by its terminal
|
|
50
|
-
* edge. A
|
|
66
|
+
* edge. A `room/run-moved` edge re-anchors a running card (its seq, which the
|
|
67
|
+
* view orders by) below a reply or human message that landed after it. A running run renders the live row; a failed run stays as a dim
|
|
51
68
|
* error row; done/cancelled hide the row IN PLACE — the assembler's live
|
|
52
69
|
* incremental flush forbids withdrawing a materialized node with null (the
|
|
53
70
|
* withdrawal contract is the same key with `visibility: 'hidden'`; a null
|
|
@@ -62,7 +79,9 @@ export const roomRunDefinition = {
|
|
|
62
79
|
id: `${event.data.member}:${event.data.startedAt}`,
|
|
63
80
|
role: event.data.state === 'running' ? 'start' : 'update',
|
|
64
81
|
}
|
|
65
|
-
:
|
|
82
|
+
: event.type === 'room/run-moved'
|
|
83
|
+
? { id: `${event.data.member}:${event.data.startedAt}`, role: 'update' }
|
|
84
|
+
: null,
|
|
66
85
|
start: (_context, match) => {
|
|
67
86
|
const event = match.event;
|
|
68
87
|
if (event.type !== 'room/run-state')
|
|
@@ -77,6 +96,9 @@ export const roomRunDefinition = {
|
|
|
77
96
|
},
|
|
78
97
|
update: (context, match) => {
|
|
79
98
|
const event = match.event;
|
|
99
|
+
// A move re-anchors a running card below what landed after it (the view orders by seq).
|
|
100
|
+
if (event.type === 'room/run-moved')
|
|
101
|
+
return context.state.state === 'running' ? { ...context.state, seq: event.seq } : context.state;
|
|
80
102
|
if (event.type !== 'room/run-state')
|
|
81
103
|
return context.state;
|
|
82
104
|
const { error: _error, elapsedMs: _elapsed, ...previous } = context.state;
|
|
@@ -201,15 +223,16 @@ export const roomQuestionDefinition = {
|
|
|
201
223
|
const event = match.event;
|
|
202
224
|
if (event.type !== 'room/question')
|
|
203
225
|
throw new Error('room-question start requires a room/question event');
|
|
204
|
-
return { seq: event.seq, time: event.time, questionId: event.data.id, member: event.data.member, questions: event.data.questions, state: 'pending' };
|
|
226
|
+
return { seq: event.seq, time: event.time, questionId: event.data.id, member: event.data.member, questions: event.data.questions, state: 'pending', ...event.data.consent === true ? { consent: true } : {}, ...event.data.runId === undefined ? {} : { runId: event.data.runId } };
|
|
205
227
|
},
|
|
206
228
|
update: (context, match) => {
|
|
207
229
|
const event = match.event;
|
|
208
230
|
if (event.type !== 'room/question-resolved')
|
|
209
231
|
return context.state;
|
|
210
|
-
return { ...context.state, state: event.data.outcome, ...event.data.answers === undefined ? {} : { answers: event.data.answers } };
|
|
232
|
+
return { ...context.state, state: event.data.outcome, ...event.data.answers === undefined ? {} : { answers: event.data.answers }, ...event.data.late === true ? { late: true } : {} };
|
|
211
233
|
},
|
|
212
|
-
|
|
234
|
+
// A question placed in a round renders inside it (RoomRunView, RoomSpeechView): its own row hides.
|
|
235
|
+
buildViewNode: context => viewNode(context, 'room-question', context.state?.runId === undefined ? 'visible' : 'hidden'),
|
|
213
236
|
};
|
|
214
237
|
/**
|
|
215
238
|
* The relay Definition: one Context per relay (matched on the relay id),
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File paths a member writes in inline code (`game-2/design/art.png`) become
|
|
3
|
+
* openers, the way the main conversation links the files a turn produced.
|
|
4
|
+
* The host's own mention vocabulary (ui-deliverables) only knows files a
|
|
5
|
+
* host turn touched; a CLI member's writes happen in its own process, so the
|
|
6
|
+
* room recognizes the path's shape instead: no whitespace, no URL, a common
|
|
7
|
+
* file extension. A token that names no real file opens the viewer's
|
|
8
|
+
* "missing" state — never a broken link.
|
|
9
|
+
* @module @khorsheed/dsh-room/client/path-mentions
|
|
10
|
+
*/
|
|
11
|
+
import type { MarkdownFileMentions } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
12
|
+
/**
|
|
13
|
+
* Whether an inline-code token reads as a file path.
|
|
14
|
+
* @param value - the token as authored.
|
|
15
|
+
* @returns true for a path-shaped token with a known file extension.
|
|
16
|
+
*/
|
|
17
|
+
export declare function looksLikeFilePath(value: string): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* The absolute path a member's token names: as written when absolute, under
|
|
20
|
+
* the member's working directory when it has its own, else left relative (the
|
|
21
|
+
* file address then resolves it against the room session's workspace).
|
|
22
|
+
* @param value - the token.
|
|
23
|
+
* @param cwd - the member's own working directory, when it has one.
|
|
24
|
+
* @returns the path to open.
|
|
25
|
+
*/
|
|
26
|
+
export declare function memberPath(value: string, cwd: string | undefined): string;
|
|
27
|
+
/**
|
|
28
|
+
* The file-mention vocabulary for one member's text.
|
|
29
|
+
* @param open - opens one resolved path in the sidebar.
|
|
30
|
+
* @param cwd - the member's own working directory, when it has one.
|
|
31
|
+
* @returns the MarkdownText fileMentions value.
|
|
32
|
+
*/
|
|
33
|
+
export declare function pathMentions(open: (path: string) => void, cwd: string | undefined): MarkdownFileMentions;
|
|
34
|
+
//# sourceMappingURL=path-mentions.d.ts.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Extensions a member's inline code most often names as a deliverable or source file. */
|
|
2
|
+
const FILE_EXTENSION = /\.(?:png|jpe?g|gif|webp|svg|md|txt|json|ya?ml|toml|csv|html?|css|[cm]?[jt]sx?|py|sh|glb|gltf|blend|fbx|obj|mp3|wav|ogg|mp4|webm|pdf)$/i;
|
|
3
|
+
/**
|
|
4
|
+
* Whether an inline-code token reads as a file path.
|
|
5
|
+
* @param value - the token as authored.
|
|
6
|
+
* @returns true for a path-shaped token with a known file extension.
|
|
7
|
+
*/
|
|
8
|
+
export function looksLikeFilePath(value) {
|
|
9
|
+
return value.length <= 300 && !/\s/.test(value) && !value.includes('://') && !value.startsWith('-') && FILE_EXTENSION.test(value);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The absolute path a member's token names: as written when absolute, under
|
|
13
|
+
* the member's working directory when it has its own, else left relative (the
|
|
14
|
+
* file address then resolves it against the room session's workspace).
|
|
15
|
+
* @param value - the token.
|
|
16
|
+
* @param cwd - the member's own working directory, when it has one.
|
|
17
|
+
* @returns the path to open.
|
|
18
|
+
*/
|
|
19
|
+
export function memberPath(value, cwd) {
|
|
20
|
+
if (value.startsWith('/') || value.startsWith('~') || cwd === undefined)
|
|
21
|
+
return value;
|
|
22
|
+
return `${cwd.replace(/\/+$/, '')}/${value.replace(/^\.\//, '')}`;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The file-mention vocabulary for one member's text.
|
|
26
|
+
* @param open - opens one resolved path in the sidebar.
|
|
27
|
+
* @param cwd - the member's own working directory, when it has one.
|
|
28
|
+
* @returns the MarkdownText fileMentions value.
|
|
29
|
+
*/
|
|
30
|
+
export function pathMentions(open, cwd) {
|
|
31
|
+
return {
|
|
32
|
+
resolve: (value) => {
|
|
33
|
+
if (!looksLikeFilePath(value))
|
|
34
|
+
return undefined;
|
|
35
|
+
const path = memberPath(value, cwd);
|
|
36
|
+
return { open: () => { open(path); }, label: value, title: path };
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -160,7 +160,49 @@ export interface RoomModelDirectory {
|
|
|
160
160
|
* own main agent (bare messages ARE its turns); undefined on a host without
|
|
161
161
|
* ui-model-selection — the picker simply does not render.
|
|
162
162
|
*/
|
|
163
|
+
/** One unsent composer attachment (the official conversation service's draft). */
|
|
164
|
+
export type RoomDraftAttachment = {
|
|
165
|
+
readonly kind: 'image';
|
|
166
|
+
readonly id: string;
|
|
167
|
+
readonly file: File;
|
|
168
|
+
readonly previewUrl: string;
|
|
169
|
+
} | {
|
|
170
|
+
readonly kind: 'file';
|
|
171
|
+
readonly id: string;
|
|
172
|
+
readonly file: File;
|
|
173
|
+
};
|
|
174
|
+
/** A file draft's upload state (files upload on pick; images ride the send). */
|
|
175
|
+
export type RoomDraftUpload = {
|
|
176
|
+
readonly status: 'uploading';
|
|
177
|
+
readonly loaded: number;
|
|
178
|
+
readonly total?: number;
|
|
179
|
+
} | {
|
|
180
|
+
readonly status: 'ready';
|
|
181
|
+
} | {
|
|
182
|
+
readonly status: 'error';
|
|
183
|
+
readonly message: string;
|
|
184
|
+
};
|
|
185
|
+
/**
|
|
186
|
+
* The official conversation service's draft-attachment face, re-used so the
|
|
187
|
+
* room's attachments pick, upload, and serialize exactly like the official
|
|
188
|
+
* composer's. Absent on a host without it: the composer offers no attachments.
|
|
189
|
+
*/
|
|
190
|
+
export interface RoomAttachmentsFace {
|
|
191
|
+
/** Register picked/pasted/dropped files as drafts of this session (files start uploading). */
|
|
192
|
+
readonly create: (sessionId: SessionId, files: readonly File[]) => readonly RoomDraftAttachment[];
|
|
193
|
+
readonly resolve: (ids: readonly string[]) => readonly RoomDraftAttachment[];
|
|
194
|
+
readonly release: (id: string) => void;
|
|
195
|
+
readonly retry: (sessionId: SessionId, id: string) => void;
|
|
196
|
+
readonly uploads: {
|
|
197
|
+
subscribe(listener: () => void): () => void;
|
|
198
|
+
getSnapshot(): Readonly<Record<string, RoomDraftUpload>>;
|
|
199
|
+
};
|
|
200
|
+
/** The wire form postMessage takes (images encoded, files as their upload receipts). */
|
|
201
|
+
readonly serialize: (ids: readonly string[]) => Promise<readonly import('../types.ts').RoomAttachmentInput[]>;
|
|
202
|
+
}
|
|
163
203
|
export interface RoomComposerInjected extends RoomGoalInjected, RoomInviteInjected {
|
|
204
|
+
/** Draft attachments; absent without the official conversation service. */
|
|
205
|
+
readonly attachments?: RoomAttachmentsFace | undefined;
|
|
164
206
|
/** Open the task page (at one task); absent without the right sidebar. */
|
|
165
207
|
readonly openTasks?: ((task?: string) => void) | undefined;
|
|
166
208
|
/** The composer's task tag store (see task-tag.ts). */
|
|
@@ -203,7 +245,7 @@ export interface RoomComposerInjected extends RoomGoalInjected, RoomInviteInject
|
|
|
203
245
|
* `task` is the composer's tag (`new` or a task root id): it files the
|
|
204
246
|
* message under that task instead of letting dsh decide.
|
|
205
247
|
*/
|
|
206
|
-
readonly submit: (sessionId: SessionId, text: string, targets?: readonly string[], task?: string) => Promise<RoomMutationOutcome>;
|
|
248
|
+
readonly submit: (sessionId: SessionId, text: string, targets?: readonly string[], task?: string, attachments?: readonly import('../types.ts').RoomAttachmentInput[]) => Promise<RoomMutationOutcome>;
|
|
207
249
|
/** Interrupt the room's own main-agent turn (the hidden official bar's Stop). */
|
|
208
250
|
readonly stop: () => void;
|
|
209
251
|
/**
|
|
@@ -323,18 +365,26 @@ export interface RoomSpeechInjected {
|
|
|
323
365
|
* absent when the composition has no Context Lens.
|
|
324
366
|
*/
|
|
325
367
|
readonly openDetail?: ((sessionId: SessionId, messageId: string) => void) | undefined;
|
|
368
|
+
/** Open a file path a member named in its text (sidebar file viewer); absent without the sidebar. */
|
|
369
|
+
readonly openPath?: ((sessionId: SessionId, path: string) => void) | undefined;
|
|
370
|
+
/** Answer a question asked in this round (an expired one answers late). */
|
|
371
|
+
readonly answerQuestion: RoomQuestionInjected['answerQuestion'];
|
|
326
372
|
}
|
|
327
373
|
/** Full props of the 'room-speech' chat-node renderer. */
|
|
328
374
|
export type RoomSpeechViewProps = PropsRuntime<'conversation.chat.node', 'room-speech'> & InjectFace<RoomSpeechInjected> & PropsLocale<'room'>;
|
|
329
375
|
/** Injected face of the member-run chat node. */
|
|
330
376
|
export interface RoomRunInjected {
|
|
331
377
|
readonly renderMemberOutput?: LocalAgentUi['renderMemberOutput'] | undefined;
|
|
378
|
+
/** Open a file path the member named while it works (sidebar file viewer). */
|
|
379
|
+
readonly openPath?: ((sessionId: SessionId, path: string) => void) | undefined;
|
|
332
380
|
/** The client-side room state store (the roster carries the jump target). */
|
|
333
381
|
readonly roomStore: RoomStore;
|
|
334
382
|
/** Open a session (the whole-row jump into the member's child session). */
|
|
335
383
|
readonly openSession: (sessionId: SessionId) => void;
|
|
336
384
|
/** Cancel the named member's in-flight run through the Remote. */
|
|
337
385
|
readonly cancelMember: (member: string) => Promise<void>;
|
|
386
|
+
/** Answer the question the round is waiting on, right in its live card. */
|
|
387
|
+
readonly answerQuestion: RoomQuestionInjected['answerQuestion'];
|
|
338
388
|
}
|
|
339
389
|
/** Full props of the 'room-run' chat-node renderer. */
|
|
340
390
|
export type RoomRunViewProps = PropsRuntime<'conversation.chat.node', 'room-run'> & InjectFace<RoomRunInjected> & PropsLocale<'room'>;
|
package/lib/types/dispatch.d.ts
CHANGED
|
@@ -29,6 +29,56 @@ export declare function roomSource(): MessageSource;
|
|
|
29
29
|
* member's early running edge predates the child handle's publication (the
|
|
30
30
|
* terminal edge and the journal backfill cover it).
|
|
31
31
|
*/
|
|
32
|
+
/**
|
|
33
|
+
* Keep the running members' cards at the bottom of the room's conversation:
|
|
34
|
+
* after a reply or a human message lands, each member still working gets a
|
|
35
|
+
* `room/run-moved` edge — its card re-anchors below what just landed (a
|
|
36
|
+
* typing-indicator seat), so a member that started first but is still going
|
|
37
|
+
* never reads as if it answered before the later replies. Several running
|
|
38
|
+
* members keep their start order.
|
|
39
|
+
* @param room - the room session.
|
|
40
|
+
* @param except - the member whose own reply just landed (its card ends there).
|
|
41
|
+
*/
|
|
42
|
+
/** How much of a round's text a question keeps to find its place in the reply later. */
|
|
43
|
+
export declare const BEFORE_TAIL = 160;
|
|
44
|
+
/**
|
|
45
|
+
* The text a member's current round has said so far in its own session: the
|
|
46
|
+
* assistant text blocks since the latest turn start, in order.
|
|
47
|
+
* @param events - the member's child session log.
|
|
48
|
+
* @returns the round's text so far.
|
|
49
|
+
*/
|
|
50
|
+
export declare function roundTextSoFar(events: readonly SessionEvent[]): string;
|
|
51
|
+
/**
|
|
52
|
+
* Where each question a round asked sits in its final reply: just after the
|
|
53
|
+
* text the round had said when it asked (its recorded tail, shortened until
|
|
54
|
+
* found). Asked before saying anything → 0; tail not found → -1 (shown
|
|
55
|
+
* first, marked as asked mid-round). Offsets never go backwards.
|
|
56
|
+
* @param events - the room log.
|
|
57
|
+
* @param runId - the round.
|
|
58
|
+
* @param text - the round's reply.
|
|
59
|
+
* @returns the question ids with their offsets.
|
|
60
|
+
*/
|
|
61
|
+
export declare function askOffsets(events: readonly SessionEvent[], runId: string, text: string): {
|
|
62
|
+
id: string;
|
|
63
|
+
at: number;
|
|
64
|
+
}[];
|
|
65
|
+
/** Why a human message's move-on closes a restart-lost round (index.ts moveOnFromLostRounds). */
|
|
66
|
+
export declare const MOVED_ON_TEXT = "\u4F60\u53D1\u4E86\u65B0\u6D88\u606F\uFF1B\u88AB\u91CD\u542F\u6253\u65AD\u7684\u8FD9\u4E00\u8F6E\u6309\u6CA1\u505A\u5B8C\u5904\u7406";
|
|
67
|
+
/** A message that queued behind a lost round and rode along with the move-on message. */
|
|
68
|
+
export declare const MERGED_BACKLOG_TEXT = "\u5DF2\u5E76\u5165\u4E0B\u4E00\u6761\u6D88\u606F\u4E00\u8D77\u9001\u8FBE";
|
|
69
|
+
/** A message that queued behind a lost round an earlier move-on skipped: closed, not replayed. */
|
|
70
|
+
export declare const SUPERSEDED_BACKLOG_TEXT = "\u88AB\u91CD\u542F\u6253\u65AD\u524D\u6392\u961F\uFF0C\u5DF2\u88AB\u540E\u7EED\u6D88\u606F\u53D6\u4EE3";
|
|
71
|
+
/**
|
|
72
|
+
* Whether a queued delivery sat behind a restart-lost round of the same
|
|
73
|
+
* member that a human message has since moved on from (its dispatch lies
|
|
74
|
+
* between the lost round and the move-on edge).
|
|
75
|
+
* @param events - the room journal.
|
|
76
|
+
* @param targetId - the member id.
|
|
77
|
+
* @param dispatchSeq - the queued delivery's dispatch seq.
|
|
78
|
+
* @returns whether the move-on superseded it.
|
|
79
|
+
*/
|
|
80
|
+
export declare function supersededByMoveOn(events: readonly SessionEvent[], targetId: string, dispatchSeq: number): boolean;
|
|
81
|
+
export declare function floatRunningCards(room: Session, except?: string): void;
|
|
32
82
|
export declare function runChildSessionId(roomId: SessionId, member: RoomMember | undefined): SessionId | undefined;
|
|
33
83
|
/**
|
|
34
84
|
* The child session round a run reached: the last turn/start the child wrote.
|
|
@@ -138,7 +188,17 @@ export declare class DispatchEngine {
|
|
|
138
188
|
* coordinator for plan work, else the dispatch's `replyTo`), at most once
|
|
139
189
|
* per delivery. Public so a removal can settle the waits it breaks.
|
|
140
190
|
*/
|
|
191
|
+
/** Report-gate decisions per recipient, one at a time (see reportOutcome). */
|
|
192
|
+
private readonly gateQueues;
|
|
193
|
+
/**
|
|
194
|
+
* Gate one arrived report for the member waiting on it. Decisions for the
|
|
195
|
+
* same recipient run one at a time: a decision-model judgement takes a
|
|
196
|
+
* second or two, and a later report deciding meanwhile (the 「最后一个结果」
|
|
197
|
+
* rule) would hand over its batch without the one still being judged —
|
|
198
|
+
* which then got held behind a round that had already answered.
|
|
199
|
+
*/
|
|
141
200
|
reportOutcome(room: Session, dispatchSeq: number, deliveryId: string, fromId: string, outcome: string, text: string): Promise<void>;
|
|
201
|
+
private gateReport;
|
|
142
202
|
/**
|
|
143
203
|
* Mark the relays this run delivered as sent. Delivery = the member's own
|
|
144
204
|
* session received the prompt (the followup accepted / the CLI run
|
package/lib/types/dispatch.js
CHANGED
|
@@ -49,6 +49,106 @@ export function roomSource() {
|
|
|
49
49
|
* member's early running edge predates the child handle's publication (the
|
|
50
50
|
* terminal edge and the journal backfill cover it).
|
|
51
51
|
*/
|
|
52
|
+
/**
|
|
53
|
+
* Keep the running members' cards at the bottom of the room's conversation:
|
|
54
|
+
* after a reply or a human message lands, each member still working gets a
|
|
55
|
+
* `room/run-moved` edge — its card re-anchors below what just landed (a
|
|
56
|
+
* typing-indicator seat), so a member that started first but is still going
|
|
57
|
+
* never reads as if it answered before the later replies. Several running
|
|
58
|
+
* members keep their start order.
|
|
59
|
+
* @param room - the room session.
|
|
60
|
+
* @param except - the member whose own reply just landed (its card ends there).
|
|
61
|
+
*/
|
|
62
|
+
/** How much of a round's text a question keeps to find its place in the reply later. */
|
|
63
|
+
export const BEFORE_TAIL = 160;
|
|
64
|
+
/**
|
|
65
|
+
* The text a member's current round has said so far in its own session: the
|
|
66
|
+
* assistant text blocks since the latest turn start, in order.
|
|
67
|
+
* @param events - the member's child session log.
|
|
68
|
+
* @returns the round's text so far.
|
|
69
|
+
*/
|
|
70
|
+
export function roundTextSoFar(events) {
|
|
71
|
+
let start = -1;
|
|
72
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
73
|
+
if (events[index].type === 'turn/start') {
|
|
74
|
+
start = index;
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const parts = [];
|
|
79
|
+
for (const event of events.slice(start + 1)) {
|
|
80
|
+
if (event.type !== 'assistant/message')
|
|
81
|
+
continue;
|
|
82
|
+
const content = event.data.message?.content ?? [];
|
|
83
|
+
for (const block of content)
|
|
84
|
+
if (block.type === 'text' && typeof block.text === 'string')
|
|
85
|
+
parts.push(block.text);
|
|
86
|
+
}
|
|
87
|
+
return parts.join('');
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Where each question a round asked sits in its final reply: just after the
|
|
91
|
+
* text the round had said when it asked (its recorded tail, shortened until
|
|
92
|
+
* found). Asked before saying anything → 0; tail not found → -1 (shown
|
|
93
|
+
* first, marked as asked mid-round). Offsets never go backwards.
|
|
94
|
+
* @param events - the room log.
|
|
95
|
+
* @param runId - the round.
|
|
96
|
+
* @param text - the round's reply.
|
|
97
|
+
* @returns the question ids with their offsets.
|
|
98
|
+
*/
|
|
99
|
+
export function askOffsets(events, runId, text) {
|
|
100
|
+
const out = [];
|
|
101
|
+
let floor = 0;
|
|
102
|
+
for (const event of events) {
|
|
103
|
+
if (event.type !== 'room/question' || event.data.runId !== runId)
|
|
104
|
+
continue;
|
|
105
|
+
const tail = (event.data.before ?? '').trimEnd();
|
|
106
|
+
// Nothing said before asking: the question opens the reply.
|
|
107
|
+
let at = tail === '' ? 0 : -1;
|
|
108
|
+
for (const size of [80, 40, 20]) {
|
|
109
|
+
if (tail === '')
|
|
110
|
+
break;
|
|
111
|
+
const probe = tail.slice(-size);
|
|
112
|
+
if (probe.length < Math.min(size, 8))
|
|
113
|
+
break;
|
|
114
|
+
const found = text.indexOf(probe, floor);
|
|
115
|
+
if (found >= 0) {
|
|
116
|
+
at = found + probe.length;
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (at > 0)
|
|
121
|
+
floor = at;
|
|
122
|
+
out.push({ id: event.data.id, at });
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
/** Why a human message's move-on closes a restart-lost round (index.ts moveOnFromLostRounds). */
|
|
127
|
+
export const MOVED_ON_TEXT = '你发了新消息;被重启打断的这一轮按没做完处理';
|
|
128
|
+
/** A message that queued behind a lost round and rode along with the move-on message. */
|
|
129
|
+
export const MERGED_BACKLOG_TEXT = '已并入下一条消息一起送达';
|
|
130
|
+
/** A message that queued behind a lost round an earlier move-on skipped: closed, not replayed. */
|
|
131
|
+
export const SUPERSEDED_BACKLOG_TEXT = '被重启打断前排队,已被后续消息取代';
|
|
132
|
+
/**
|
|
133
|
+
* Whether a queued delivery sat behind a restart-lost round of the same
|
|
134
|
+
* member that a human message has since moved on from (its dispatch lies
|
|
135
|
+
* between the lost round and the move-on edge).
|
|
136
|
+
* @param events - the room journal.
|
|
137
|
+
* @param targetId - the member id.
|
|
138
|
+
* @param dispatchSeq - the queued delivery's dispatch seq.
|
|
139
|
+
* @returns whether the move-on superseded it.
|
|
140
|
+
*/
|
|
141
|
+
export function supersededByMoveOn(events, targetId, dispatchSeq) {
|
|
142
|
+
return events.some(event => event.type === 'room/delivery-state' && event.data.memberId === targetId && event.data.state === 'cancelled'
|
|
143
|
+
&& event.data.text === MOVED_ON_TEXT && event.data.dispatchSeq < dispatchSeq && dispatchSeq < event.seq);
|
|
144
|
+
}
|
|
145
|
+
export function floatRunningCards(room, except) {
|
|
146
|
+
const running = replay(room.snapshotEvents()).runs
|
|
147
|
+
.filter(run => run.state === 'running' && run.member !== except)
|
|
148
|
+
.sort((left, right) => left.startedAt - right.startedAt);
|
|
149
|
+
for (const run of running)
|
|
150
|
+
room.append('room/run-moved', { member: run.member, startedAt: run.startedAt });
|
|
151
|
+
}
|
|
52
152
|
export function runChildSessionId(roomId, member) {
|
|
53
153
|
if (member?.kind === 'main-agent')
|
|
54
154
|
return roomId;
|
|
@@ -372,6 +472,15 @@ export class DispatchEngine {
|
|
|
372
472
|
}
|
|
373
473
|
continue;
|
|
374
474
|
}
|
|
475
|
+
// Queued behind a restart-lost round the human has since moved on
|
|
476
|
+
// from: that move-on carries its backlog along; a row it left behind
|
|
477
|
+
// (a move-on predating the hand-over) closes as superseded instead of
|
|
478
|
+
// replaying a stale round on this access.
|
|
479
|
+
if (supersededByMoveOn(events, targetId, dispatch.seq)) {
|
|
480
|
+
room.append('room/delivery-state', { id, dispatchSeq: dispatch.seq, memberId: targetId, state: 'cancelled', text: SUPERSEDED_BACKLOG_TEXT });
|
|
481
|
+
await this.ctx.sessions.flush(room);
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
375
484
|
const target = state.members.find(member => memberId(events, member) === targetId);
|
|
376
485
|
if (target !== undefined && !state.deliveries?.some(row => row.memberId === targetId && (row.status === 'running' || row.status === 'uncertain')))
|
|
377
486
|
this.dispatch(room, target.name, dispatch.data.text, { dispatchSeq: dispatch.seq, targetId });
|
|
@@ -451,7 +560,28 @@ export class DispatchEngine {
|
|
|
451
560
|
* coordinator for plan work, else the dispatch's `replyTo`), at most once
|
|
452
561
|
* per delivery. Public so a removal can settle the waits it breaks.
|
|
453
562
|
*/
|
|
563
|
+
/** Report-gate decisions per recipient, one at a time (see reportOutcome). */
|
|
564
|
+
gateQueues = new Map();
|
|
565
|
+
/**
|
|
566
|
+
* Gate one arrived report for the member waiting on it. Decisions for the
|
|
567
|
+
* same recipient run one at a time: a decision-model judgement takes a
|
|
568
|
+
* second or two, and a later report deciding meanwhile (the 「最后一个结果」
|
|
569
|
+
* rule) would hand over its batch without the one still being judged —
|
|
570
|
+
* which then got held behind a round that had already answered.
|
|
571
|
+
*/
|
|
454
572
|
async reportOutcome(room, dispatchSeq, deliveryId, fromId, outcome, text) {
|
|
573
|
+
const source = room.snapshotEvents().find(event => event.seq === dispatchSeq);
|
|
574
|
+
const key = `${room.id} ${source?.type === 'room/dispatch' ? source.data.replyTo ?? '' : ''}`;
|
|
575
|
+
const next = (this.gateQueues.get(key) ?? Promise.resolve())
|
|
576
|
+
.catch(() => { })
|
|
577
|
+
.then(() => this.gateReport(room, dispatchSeq, deliveryId, fromId, outcome, text));
|
|
578
|
+
this.gateQueues.set(key, next);
|
|
579
|
+
void next.then(() => { if (this.gateQueues.get(key) === next)
|
|
580
|
+
this.gateQueues.delete(key); }, () => { if (this.gateQueues.get(key) === next)
|
|
581
|
+
this.gateQueues.delete(key); });
|
|
582
|
+
return next;
|
|
583
|
+
}
|
|
584
|
+
async gateReport(room, dispatchSeq, deliveryId, fromId, outcome, text) {
|
|
455
585
|
const events = room.snapshotEvents();
|
|
456
586
|
const source = events.find(event => event.seq === dispatchSeq);
|
|
457
587
|
if (source?.type !== 'room/dispatch' || (source.data.origin !== 'coordinator' && source.data.origin !== 'member') || source.data.replyTo === undefined)
|
|
@@ -705,6 +835,7 @@ export class DispatchEngine {
|
|
|
705
835
|
...lastAssistantMessageId(agent.session, before) === undefined ? {} : { messageId: lastAssistantMessageId(agent.session, before) },
|
|
706
836
|
...reportedTo === undefined ? {} : { reportedTo },
|
|
707
837
|
});
|
|
838
|
+
floatRunningCards(room, replay(room.snapshotEvents()).members.find(entry => entry.id === member.id)?.name ?? member.name);
|
|
708
839
|
await this.ctx.sessions.flush(room);
|
|
709
840
|
return { state: 'done', ...output === '' ? {} : { text: output } };
|
|
710
841
|
}
|
|
@@ -813,9 +944,11 @@ export class DispatchEngine {
|
|
|
813
944
|
const reportedTo = dispatchSeq === undefined ? undefined : reportRecipient(room.snapshotEvents(), dispatchSeq)?.name;
|
|
814
945
|
// The member's own closing message: per-message tools address it in its session.
|
|
815
946
|
const closingMessageId = lastAssistantMessageId(this.ctx.sessions.get(run.id));
|
|
947
|
+
const asks = askOffsets(room.snapshotEvents(), runId, output);
|
|
816
948
|
const speech = output !== '' || interrupted === undefined ? room.append('room/speech', {
|
|
817
949
|
member: replay(room.snapshotEvents()).members.find(entry => entry.id === member.id)?.name ?? member.name,
|
|
818
950
|
text: output,
|
|
951
|
+
...asks.length === 0 ? {} : { asks },
|
|
819
952
|
childSessionId: run.id,
|
|
820
953
|
runId,
|
|
821
954
|
...closingMessageId === undefined ? {} : { messageId: closingMessageId },
|
|
@@ -823,8 +956,10 @@ export class DispatchEngine {
|
|
|
823
956
|
...interrupted === undefined ? {} : { interrupted },
|
|
824
957
|
...reportedTo === undefined ? {} : { reportedTo },
|
|
825
958
|
}) : undefined;
|
|
826
|
-
if (speech !== undefined)
|
|
959
|
+
if (speech !== undefined) {
|
|
960
|
+
floatRunningCards(room, speech.data.member);
|
|
827
961
|
await this.ctx.sessions.flush(room);
|
|
962
|
+
}
|
|
828
963
|
if (result.stopReason === 'completed') {
|
|
829
964
|
await this.ctx.sessions.flush(room);
|
|
830
965
|
return { state: 'done', text: runOutputText(result.output) };
|