@sparkelf/dsh-group-chat 0.0.0-stage → 0.2.0

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/lib/index.d.ts ADDED
@@ -0,0 +1,357 @@
1
+ import { CredentialRef } from "@deepseek-ai/dsh-credentials";
2
+ import z from "@deepseek-ai/schemastery";
3
+ import { Agent } from "@deepseek-ai/dsh-agent";
4
+ import { Session, SessionEvent } from "@deepseek-ai/dsh-session";
5
+ import { Context } from "@deepseek-ai/cordis";
6
+ //#region src/context.d.ts
7
+ /**
8
+ * Host services this plugin needs.
9
+ *
10
+ * `agents` and `sessions` carry the room subscription, `webServer` exposes the
11
+ * browser-facing room API, and `credentials` resolves the token this workspace
12
+ * presents — the secret is never read from configuration or cached in the plugin.
13
+ */
14
+ interface GroupChatHostServices {
15
+ readonly agents: {
16
+ get(id: string): Agent | undefined;
17
+ list(): readonly Agent[];
18
+ };
19
+ readonly sessions: {
20
+ get(id: string): Session | undefined;
21
+ list(): readonly Session[];
22
+ create(id: string, options?: {
23
+ meta?: {
24
+ cwd?: string;
25
+ };
26
+ }): Session;
27
+ };
28
+ readonly credentials: {
29
+ resolve(ref: CredentialRef): Promise<{
30
+ value: string;
31
+ source: string;
32
+ } | undefined>;
33
+ };
34
+ readonly webServer: {
35
+ register(route: {
36
+ kind: 'exact' | 'prefix';
37
+ path: string;
38
+ handler(req: unknown, res: unknown): void | Promise<void>;
39
+ }): () => void;
40
+ };
41
+ readonly logger?: {
42
+ error(...args: unknown[]): void;
43
+ warn?(...args: unknown[]): void;
44
+ info?(...args: unknown[]): void;
45
+ };
46
+ }
47
+ type GroupChatContext = Context & GroupChatHostServices;
48
+ //#endregion
49
+ //#region src/identity.d.ts
50
+ /**
51
+ * How this plugin learns where its server is and who it is.
52
+ *
53
+ * The **protocol**, not DataOps, is the contract: any server implementing the
54
+ * documented room endpoints works, so nothing here is specific to DataOps. A
55
+ * deployment supplies values through the plugin's own settings, and DataOps
56
+ * additionally injects environment defaults into its workspace containers so the
57
+ * first-run experience needs no typing.
58
+ *
59
+ * Resolution order is settings first, environment second:
60
+ *
61
+ * 1. An explicit setting always wins — that is what an operator edits, and it is
62
+ * how a workspace points at any conforming server.
63
+ * 2. The DataOps environment variables fill whatever the settings leave blank.
64
+ */
65
+ /** Environment variables DataOps injects into a managed workspace container. */
66
+ declare const MANAGED_ENV: {
67
+ /** Base URL of the server, e.g. `http://host.docker.internal:3101`. */
68
+ readonly baseUrl: "DATAOPS_AI_WORKSPACE_BACKEND_BASE_URL";
69
+ /** Workspace root, used as a room session's `cwd`. */
70
+ readonly mountPath: "DATAOPS_AI_WORKSPACE_MOUNT_PATH";
71
+ };
72
+ /**
73
+ * The default credential reference.
74
+ *
75
+ * It names the DataOps access token the workspace already holds in the DSH
76
+ * credential store — the same reference `dataops-managed` and `knowledge-base`
77
+ * use. That is what makes this work in a managed workspace: the companion
78
+ * container has no credential in its environment by design, but the credential
79
+ * store does carry this entry, and a deployment can point the setting at any other
80
+ * reference for a different server.
81
+ */
82
+ declare const DEFAULT_CREDENTIAL_REF = "DATAOPS_ACCESS_TOKEN";
83
+ /** What the plugin needs in hand before it can talk to a server. */
84
+ interface ServerTarget {
85
+ /** Origin with any trailing slash removed. */
86
+ readonly baseUrl: string;
87
+ /** Credential reference to resolve per request. */
88
+ readonly credentialRef: string;
89
+ }
90
+ interface ResolveTargetInput {
91
+ /** Value from the settings panel, if the operator set one. */
92
+ readonly baseUrl?: string;
93
+ readonly credentialRef?: string;
94
+ /** Environment bag; defaults to `process.env`. */
95
+ readonly env?: Readonly<Record<string, string | undefined>>;
96
+ }
97
+ /**
98
+ * Resolves the server target, or `undefined` when this workspace is not configured.
99
+ *
100
+ * `undefined` means "do not activate": no route, no listener, no timer. A
101
+ * half-configured workspace is treated as unconfigured rather than failing loudly,
102
+ * because a plugin that cannot reach a server has nothing useful to do at startup
103
+ * and must not disturb ordinary sessions.
104
+ *
105
+ * The actor is deliberately absent: a bearer credential identifies the account
106
+ * server-side, so the plugin never states who it is and cannot disagree with the
107
+ * server about it.
108
+ */
109
+ declare const resolveServerTarget: (input?: ResolveTargetInput) => ServerTarget | undefined;
110
+ /** The workspace root a room session uses as its `cwd`. */
111
+ declare const resolveWorkspaceRoot: (env?: Readonly<Record<string, string | undefined>>) => string;
112
+ //#endregion
113
+ //#region src/backend.d.ts
114
+ /** One durable room event as the backend stores it. */
115
+ interface RoomEventRecord {
116
+ /** Global order across every container, as a decimal string. */
117
+ readonly id: string;
118
+ /** The complete DSH event envelope, stored without translation. */
119
+ readonly event: RoomEvent;
120
+ readonly createdAt?: string;
121
+ }
122
+ /**
123
+ * The envelope half of a DSH event. `data` is `unknown` on purpose: the backend
124
+ * stores it verbatim and this plugin re-appends it verbatim, so nothing here needs
125
+ * to understand content blocks.
126
+ */
127
+ interface RoomEvent {
128
+ readonly type: string;
129
+ readonly seq?: number;
130
+ readonly time?: number;
131
+ readonly data: unknown;
132
+ readonly surfaceOp?: string;
133
+ }
134
+ interface RoomSummary {
135
+ readonly id: string;
136
+ readonly name: string;
137
+ readonly ownerUserId: string;
138
+ readonly invitePolicy: 'anyone' | 'admin';
139
+ readonly discoverability: 'discoverable' | 'invite_only';
140
+ readonly memberCount: number;
141
+ readonly joined: boolean;
142
+ readonly createdAt: string;
143
+ readonly updatedAt: string;
144
+ }
145
+ interface RoomMember {
146
+ readonly userId: string;
147
+ readonly displayName: string;
148
+ readonly owner: boolean;
149
+ readonly joinedAt: string;
150
+ }
151
+ interface EventPage {
152
+ readonly events: readonly RoomEventRecord[];
153
+ /** Cursor to resume from; `null` when the room has no events yet. */
154
+ readonly nextCursor: string | null;
155
+ readonly hasMore: boolean;
156
+ }
157
+ /** A backend refusal that carries the HTTP status so callers can distinguish them. */
158
+ declare class BackendError extends Error {
159
+ readonly status: number;
160
+ readonly body: unknown;
161
+ constructor(message: string, status: number, body: unknown);
162
+ }
163
+ /** True when the request never reached the backend, so retrying cannot duplicate state. */
164
+ declare const isTransportFailure: (error: unknown) => boolean;
165
+ interface BackendClientOptions {
166
+ /** Per-request timeout; a stalled backend must not hold a session append path. */
167
+ readonly timeoutMs?: number;
168
+ /** Injected for tests. */
169
+ readonly fetchImpl?: typeof fetch;
170
+ }
171
+ /**
172
+ * Resolves the credential for one request.
173
+ *
174
+ * The secret is fetched per request rather than captured at activation: a
175
+ * credential can be rotated or added while the plugin is loaded, and caching it
176
+ * would keep sending a stale value. Returning `undefined` means "no credential
177
+ * available", which fails the request instead of sending an anonymous one.
178
+ */
179
+ type CredentialResolver = () => Promise<string | undefined>;
180
+ /** Thin typed wrapper over the group-chat server protocol. */
181
+ declare class GroupChatBackend {
182
+ private readonly target;
183
+ private readonly resolveCredential;
184
+ private readonly timeoutMs;
185
+ private readonly fetchImpl;
186
+ constructor(target: ServerTarget, resolveCredential: CredentialResolver, options?: BackendClientOptions);
187
+ /**
188
+ * Builds the headers for one call.
189
+ *
190
+ * The credential is a bearer token, so the request carries the deployment's own
191
+ * identity rather than a plugin-specific one. The room routes are the same ones
192
+ * a browser uses, which is why the plugin needs no parallel server surface.
193
+ */
194
+ private headers;
195
+ /** Rooms this workspace's member has joined. */
196
+ listRooms(signal?: AbortSignal): Promise<readonly RoomSummary[]>;
197
+ /**
198
+ * The account this workspace acts as, asked of the server rather than configured.
199
+ *
200
+ * The bearer credential already identifies the caller, so the server is the only
201
+ * correct source for "who am I": a configured value could disagree with the
202
+ * credential and silently change which messages trigger this agent.
203
+ */
204
+ whoami(signal?: AbortSignal): Promise<{
205
+ readonly id: string;
206
+ } | undefined>;
207
+ listMembers(roomId: string, signal?: AbortSignal): Promise<readonly RoomMember[]>;
208
+ /**
209
+ * Pull events after `cursor`. Omitting the cursor asks for the room from the start,
210
+ * which is what a fresh container does on its first sync.
211
+ */
212
+ listEvents(roomId: string, cursor?: string, limit?: number, signal?: AbortSignal): Promise<EventPage>;
213
+ /**
214
+ * Send one human message. `rpcId` is the idempotency key: a retry that reuses it
215
+ * returns the original row instead of creating a second one, so a timed-out
216
+ * request is safe to repeat.
217
+ */
218
+ sendMessage(roomId: string, input: {
219
+ readonly content: string;
220
+ readonly rpcId: string;
221
+ readonly mentions?: readonly string[];
222
+ }, signal?: AbortSignal): Promise<{
223
+ readonly id: string;
224
+ readonly event: RoomEvent;
225
+ readonly deduplicated: boolean;
226
+ }>;
227
+ /**
228
+ * Report one agent reply. Only the turn's final `assistant/message` is sent —
229
+ * intermediate steps stay in this container by construction.
230
+ */
231
+ reportAgentEvent(roomId: string, input: {
232
+ readonly rpcId: string;
233
+ readonly event: RoomEvent;
234
+ }, signal?: AbortSignal): Promise<{
235
+ readonly id: string;
236
+ readonly event: RoomEvent;
237
+ readonly deduplicated: boolean;
238
+ }>;
239
+ private request;
240
+ }
241
+ //#endregion
242
+ //#region src/routing.d.ts
243
+ /** Identifies a member mentioned by an event. */
244
+ interface MentionTarget {
245
+ readonly memberId: string;
246
+ /** `all` is reserved for `@everyone`; it never equals a member id, so it never triggers. */
247
+ readonly kind: 'member' | 'all';
248
+ }
249
+ /** The DataOps user a message came from, when the event identifies one. */
250
+ declare const senderId: (event: RoomEvent) => string | undefined;
251
+ /** True when the event is a member's agent reply rather than a human message. */
252
+ declare const isAgentReply: (event: RoomEvent) => boolean;
253
+ /** The global backend id carried on the event, used for local deduplication. */
254
+ declare const messageId: (event: RoomEvent) => string | undefined;
255
+ /**
256
+ * Reads structured mentions off an event.
257
+ *
258
+ * Mentions are declared by the sender's client as member ids — never parsed out of
259
+ * message text. Parsing text would misread a code sample containing `@`, and it
260
+ * would let any message smuggle itself into an execution path.
261
+ */
262
+ declare const mentionsOf: (event: RoomEvent) => readonly MentionTarget[];
263
+ /**
264
+ * The decision the whole design turns on.
265
+ *
266
+ * `selfMention` requires the sender to be this workspace's member *and* to have
267
+ * mentioned that same member, which is the only combination that may start a turn.
268
+ */
269
+ declare const shouldTriggerAgent: (event: RoomEvent, workspaceUserId: string, roomMembers: readonly RoomMember[]) => boolean;
270
+ /**
271
+ * Renders the name prefix for a message this workspace did not author.
272
+ *
273
+ * DSH's compaction summarizes raw `role` + `content` and carries no speaker field,
274
+ * so identity must live inside `content` to survive summarization. This mirrors the
275
+ * shipped `agent-team` plugin, which prefixes `Team message from <name>:`.
276
+ */
277
+ declare const speakerPrefix: (event: RoomEvent, roomMembers: readonly RoomMember[]) => string;
278
+ /** Prepends the speaker to each text block so the model can attribute the message. */
279
+ declare const withSpeakerPrefix: (event: RoomEvent, prefix: string) => RoomEvent;
280
+ //#endregion
281
+ //#region src/index.d.ts
282
+ declare const name = "group-chat";
283
+ /** Host services this plugin needs beyond the room runtime. */
284
+ declare const inject: string[];
285
+ /**
286
+ * Plugin configuration, rendered as editable fields in the DSH settings panel.
287
+ *
288
+ * Every field is optional: leaving them blank uses the environment defaults a
289
+ * managed workspace already carries, so the common case needs no editing while a
290
+ * workspace pointed at any other conforming server can be configured from the UI.
291
+ */
292
+ interface Config {
293
+ /** Origin of the room server, e.g. `http://host.docker.internal:3101`. */
294
+ baseUrl?: string;
295
+ /** Credential reference resolved per request; never a secret value. */
296
+ credentialRef?: string;
297
+ }
298
+ declare const Config: z<Config>;
299
+ /** Browser-facing route the sidebar calls to list and read rooms. */
300
+ declare const GROUP_CHAT_ROUTE = "/dsh-group-chat/api";
301
+ /**
302
+ * Milliseconds between cursor pulls.
303
+ *
304
+ * Pulling is the fallback and currently the only downlink: it needs no second
305
+ * transport, and a cursor is the whole synchronization state, so it survives a
306
+ * restart without re-appending history. The interval trades a bounded delay for a
307
+ * request the backend already serves.
308
+ */
309
+ declare const POLL_INTERVAL_MS = 3000;
310
+ /**
311
+ * Every room session id starts with this, so a room's session is recognizable
312
+ * after a restart and an ordinary session never matches.
313
+ *
314
+ * The binding lives in the id rather than in `SessionHeader` because the header
315
+ * carries no free-form metadata field.
316
+ */
317
+ declare const ROOM_SESSION_PREFIX = "group-room-";
318
+ /** Deterministic session id for a room. */
319
+ declare const roomSessionId: (roomId: string) => string;
320
+ /** Reads the room a session is bound to, or `undefined` for an ordinary session. */
321
+ declare const roomIdOfSession: (session: unknown) => string | undefined;
322
+ /** The most recently appended event belonging to a room. */
323
+ declare const lastRoomEvent: (session: Session, roomId: string) => SessionEvent | undefined;
324
+ /** Narrows the payload `followup` accepts; the check is structural because JSON crossed a boundary. */
325
+ declare const isUserMessageData: (data: unknown) => data is Parameters<Agent["followup"]>[0];
326
+ /**
327
+ * Tracks which room events this workspace already appended.
328
+ *
329
+ * The key is the backend's global id, never the local sequence: the backend owns
330
+ * cross-container order, and the same event can arrive twice (a broadcast and a
331
+ * catch-up pull). Without this the model would read one message twice.
332
+ */
333
+ declare class RoomEventTracker {
334
+ private readonly seen;
335
+ /** Records an event and reports whether it was new. */
336
+ accept(roomId: string, globalId: string): boolean;
337
+ has(roomId: string, globalId: string): boolean;
338
+ /** Highest global id seen for a room, as the cursor to resume from. */
339
+ cursorOf(roomId: string): string | undefined;
340
+ /**
341
+ * Rebuilds the tracker from a room's own session log, so a restart resumes from
342
+ * what this workspace already holds instead of re-appending the whole room.
343
+ */
344
+ seedFromSession(roomId: string, events: readonly SessionEvent[]): void;
345
+ }
346
+ /**
347
+ * Wire the room runtime.
348
+ *
349
+ * Returns `undefined` on an unmanaged DSH: no route, no listener, no timer. That is
350
+ * the mechanism behind "ordinary sessions are byte-identical with and without this
351
+ * plugin".
352
+ */
353
+ declare const apply: (ctx: GroupChatContext, config?: Config) => (() => void) | undefined;
354
+ /** The activated runtime; separated so target resolution stays independently testable. */
355
+ declare const activate: (ctx: GroupChatContext, target: ServerTarget) => (() => void);
356
+ //#endregion
357
+ export { BackendError, Config, DEFAULT_CREDENTIAL_REF, type EventPage, GROUP_CHAT_ROUTE, GroupChatBackend, MANAGED_ENV, POLL_INTERVAL_MS, ROOM_SESSION_PREFIX, type RoomEvent, type RoomEventRecord, RoomEventTracker, type RoomMember, type RoomSummary, type ServerTarget, activate, apply, inject, isAgentReply, isTransportFailure, isUserMessageData, lastRoomEvent, mentionsOf, messageId, name, resolveServerTarget, resolveWorkspaceRoot, roomIdOfSession, roomSessionId, senderId, shouldTriggerAgent, speakerPrefix, withSpeakerPrefix };