@addozhang/dsh-discord 0.4.0 → 0.5.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -14
- package/README.zh.md +12 -13
- package/cordis.patch.yml +1 -1
- package/lib/client/card-controller.d.ts +1 -1
- package/lib/client/card-form.d.ts +2 -1
- package/lib/client/index.d.ts +2 -2
- package/lib/client/slot-contract.d.ts +7 -0
- package/lib/client/snapshot-store.d.ts +1 -1
- package/lib/compose.d.ts +2 -2
- package/lib/compose.js +1 -1
- package/lib/dsh/host-asks.d.ts +96 -0
- package/lib/dsh/host-asks.js +151 -0
- package/lib/dsh/host-events.d.ts +42 -0
- package/lib/dsh/host-events.js +279 -0
- package/lib/dsh/host-face.d.ts +418 -0
- package/lib/dsh/host-face.js +598 -0
- package/lib/features/adapter-status.d.ts +15 -2
- package/lib/features/adapter-status.js +100 -4
- package/lib/features/approval-store.d.ts +1 -1
- package/lib/features/interaction-router.d.ts +2 -2
- package/lib/features/model-control.d.ts +1 -1
- package/lib/features/project-list.d.ts +1 -1
- package/lib/features/project-list.js +1 -1
- package/lib/features/session-resume.d.ts +1 -1
- package/lib/i18n.d.ts +5 -0
- package/lib/i18n.js +14 -0
- package/lib/index.js +121 -67
- package/lib/settings.js +5 -2
- package/lib/startup.d.ts +1 -1
- package/lib/startup.js +14 -7
- package/lib/stream/live.d.ts +13 -36
- package/lib/stream/live.js +157 -86
- package/lib/stream/tool-view.d.ts +12 -2
- package/lib/stream/tool-view.js +32 -2
- package/package.json +10 -12
- package/lib/dsh/api-proxy-face.d.ts +0 -400
- package/lib/dsh/api-proxy-face.js +0 -515
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The 0.1.6 host event bridge: fans the per-session `session/follow`
|
|
3
|
+
* journals plus the host-wide `session/control` stream into the single
|
|
4
|
+
* frame stream the live renderer consumes (the rc.2 `apiProxy.events.mux`
|
|
5
|
+
* global stream no longer exists).
|
|
6
|
+
*
|
|
7
|
+
* Frame vocabulary is preserved verbatim (`session/event`,
|
|
8
|
+
* `session/subscribed`, `session/queue`), so `src/stream/live.ts` and its
|
|
9
|
+
* tests are untouched by the host-side rebase. Two deliberate exclusions:
|
|
10
|
+
*
|
|
11
|
+
* - `snapshot` opening windows ARE translated through the same seq
|
|
12
|
+
* watermark as live events: the turn often completes between prompt
|
|
13
|
+
* admission and our follow subscription, so the opening window is the
|
|
14
|
+
* ONLY carrier of those records. The watermark (last delivered seq per
|
|
15
|
+
* session) keeps re-subscription replays idempotent.
|
|
16
|
+
* - approval/question request frames are NOT synthesized here: the 0.1.6
|
|
17
|
+
* host routes asks through the composed-approval model, which the
|
|
18
|
+
* ask-wiring migration wires separately.
|
|
19
|
+
*/
|
|
20
|
+
/** Build the push-side of the single consumer queue. */
|
|
21
|
+
function createFrameQueue() {
|
|
22
|
+
const buffered = [];
|
|
23
|
+
let notify;
|
|
24
|
+
let closed = false;
|
|
25
|
+
const queue = {
|
|
26
|
+
push(frame) {
|
|
27
|
+
if (closed)
|
|
28
|
+
return;
|
|
29
|
+
buffered.push(frame);
|
|
30
|
+
const wake = notify;
|
|
31
|
+
notify = undefined;
|
|
32
|
+
wake?.();
|
|
33
|
+
},
|
|
34
|
+
close() {
|
|
35
|
+
closed = true;
|
|
36
|
+
const wake = notify;
|
|
37
|
+
notify = undefined;
|
|
38
|
+
wake?.();
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
return {
|
|
42
|
+
...queue,
|
|
43
|
+
iterate(signal) {
|
|
44
|
+
return {
|
|
45
|
+
async *[Symbol.asyncIterator]() {
|
|
46
|
+
try {
|
|
47
|
+
for (;;) {
|
|
48
|
+
if (signal.aborted)
|
|
49
|
+
return;
|
|
50
|
+
while (buffered.length > 0) {
|
|
51
|
+
// shift() cannot miss: the length guard ran first.
|
|
52
|
+
yield buffered.shift();
|
|
53
|
+
}
|
|
54
|
+
if (closed)
|
|
55
|
+
return;
|
|
56
|
+
await new Promise(resolve => {
|
|
57
|
+
notify = resolve;
|
|
58
|
+
const abort = () => {
|
|
59
|
+
notify = undefined;
|
|
60
|
+
resolve();
|
|
61
|
+
};
|
|
62
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
queue.close();
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** Defensive record probe: the wire is untrusted regardless of declared types. */
|
|
75
|
+
function isRecord(value) {
|
|
76
|
+
return typeof value === 'object' && value !== null;
|
|
77
|
+
}
|
|
78
|
+
/** Defensive text extraction from a queued message's JSON content parts. */
|
|
79
|
+
function queueItemSummary(content) {
|
|
80
|
+
if (!Array.isArray(content))
|
|
81
|
+
return '';
|
|
82
|
+
const texts = [];
|
|
83
|
+
for (const part of content) {
|
|
84
|
+
if (typeof part !== 'object' || part === null)
|
|
85
|
+
continue;
|
|
86
|
+
const typed = part;
|
|
87
|
+
if (typed.type === 'text' && typeof typed.text === 'string')
|
|
88
|
+
texts.push(typed.text);
|
|
89
|
+
}
|
|
90
|
+
return texts.join(' ').slice(0, 200);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The fan-in router. `track(sessionId)` is idempotent and safe to call from
|
|
94
|
+
* every session-acquisition site (create, adopt, resume, prompt); each
|
|
95
|
+
* tracked session owns one follow loop whose frames join the shared queue.
|
|
96
|
+
*/
|
|
97
|
+
export function createHostEventRouter(services, options = {}) {
|
|
98
|
+
const log = options.log;
|
|
99
|
+
const tracked = new Map();
|
|
100
|
+
/** Last delivered durable seq per session: the replay-dedupe watermark. */
|
|
101
|
+
const watermark = new Map();
|
|
102
|
+
let consumer;
|
|
103
|
+
let rootSignal;
|
|
104
|
+
const startLoop = (sessionId, attempt = 0) => {
|
|
105
|
+
const per = new AbortController();
|
|
106
|
+
tracked.set(sessionId, per);
|
|
107
|
+
void (async () => {
|
|
108
|
+
if (process.env['DSH_DISCORD_TRACE'] === '1')
|
|
109
|
+
console.error(`[dsh-discord:trace] follow-start session=${sessionId.slice(0, 8)} attempt=${String(attempt)}`);
|
|
110
|
+
try {
|
|
111
|
+
consumer?.push({ type: 'session/subscribed', sessionId });
|
|
112
|
+
// The follow request MUST ask for assistant streaming: without the
|
|
113
|
+
// flag the host (alpha.2) delivers only the opening snapshot and
|
|
114
|
+
// never pushes later journal records — the live tail stays dead
|
|
115
|
+
// (diagnosis.md §D, run 2 vs run 3).
|
|
116
|
+
for await (const raw of services.follow({ address: { kind: 'session', sessionId }, assistantStream: true }, per.signal)) {
|
|
117
|
+
if (process.env['DSH_DISCORD_TRACE'] === '1') {
|
|
118
|
+
const dumped = JSON.stringify(raw);
|
|
119
|
+
console.error(`[dsh-discord:trace] raw-follow-frame type=${String(isRecord(raw) ? raw['type'] : typeof raw)} frame=${dumped.length > 300 ? `${dumped.slice(0, 300)}…` : dumped}`);
|
|
120
|
+
}
|
|
121
|
+
if (!isRecord(raw))
|
|
122
|
+
continue;
|
|
123
|
+
const frame = raw;
|
|
124
|
+
// Both the opening snapshot and live journal entries carry the
|
|
125
|
+
// same {type, data, seq} record envelope the rc.2 mux delivered;
|
|
126
|
+
// the seq watermark keeps replayed windows idempotent.
|
|
127
|
+
if (frame['type'] === 'event' || frame['type'] === 'snapshot') {
|
|
128
|
+
// Two carriers, both real-machine verified (alpha.2): snapshot
|
|
129
|
+
// windows batch their records under `records`, while live records
|
|
130
|
+
// arrive as single-record frames — the record rides the `event`
|
|
131
|
+
// key of the frame itself, no array. Snapshots without a records
|
|
132
|
+
// array stay drops (malformed), live frames become their own
|
|
133
|
+
// one-element batch.
|
|
134
|
+
const records = raw.records;
|
|
135
|
+
const batch = Array.isArray(records)
|
|
136
|
+
? records
|
|
137
|
+
: frame['type'] === 'event'
|
|
138
|
+
? [raw]
|
|
139
|
+
: undefined;
|
|
140
|
+
if (batch === undefined)
|
|
141
|
+
continue;
|
|
142
|
+
const through = watermark.get(sessionId) ?? 0;
|
|
143
|
+
let delivered = through;
|
|
144
|
+
for (const record of batch) {
|
|
145
|
+
// Journal records arrive double-wrapped: {type:'event',
|
|
146
|
+
// event:{type, seq, time, data}} — the wire event rides the
|
|
147
|
+
// `event` key. Accept the flat shape defensively too.
|
|
148
|
+
const candidate = record;
|
|
149
|
+
if (candidate === null || typeof candidate !== 'object')
|
|
150
|
+
continue;
|
|
151
|
+
const inner = typeof candidate.event === 'object' && candidate.event !== null
|
|
152
|
+
? candidate.event
|
|
153
|
+
: candidate;
|
|
154
|
+
if (typeof inner.type !== 'string')
|
|
155
|
+
continue;
|
|
156
|
+
if (typeof inner.seq === 'number') {
|
|
157
|
+
if (inner.seq <= through)
|
|
158
|
+
continue;
|
|
159
|
+
if (inner.seq > delivered)
|
|
160
|
+
delivered = inner.seq;
|
|
161
|
+
}
|
|
162
|
+
if (process.env['DSH_DISCORD_TRACE'] === '1') {
|
|
163
|
+
// Full data dump (truncated): diagnosis needs the field
|
|
164
|
+
// shapes, not just the key names — keys hide nesting.
|
|
165
|
+
const dumped = JSON.stringify(inner.data);
|
|
166
|
+
console.error(`[dsh-discord:trace] record type=${inner.type} seq=${String(inner.seq)} data=${dumped.length > 800 ? `${dumped.slice(0, 800)}…` : dumped}`);
|
|
167
|
+
}
|
|
168
|
+
consumer?.push({
|
|
169
|
+
type: 'session/event',
|
|
170
|
+
sessionId,
|
|
171
|
+
event: { type: inner.type, data: (typeof inner.data === 'object' && inner.data !== null ? inner.data : {}) },
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
if (delivered > through)
|
|
175
|
+
watermark.set(sessionId, delivered);
|
|
176
|
+
}
|
|
177
|
+
else if (frame['type'] === 'assistant-stream') {
|
|
178
|
+
// Live assistant deltas ride the dedicated stream frames; the
|
|
179
|
+
// renderer's durable events already carry the message texts.
|
|
180
|
+
if (process.env['DSH_DISCORD_TRACE'] === '1') {
|
|
181
|
+
const dumped = JSON.stringify(frame);
|
|
182
|
+
console.error(`[dsh-discord:trace] assistant-stream frame=${dumped.length > 300 ? `${dumped.slice(0, 300)}…` : dumped}`);
|
|
183
|
+
}
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
else if (process.env['DSH_DISCORD_TRACE'] === '1') {
|
|
187
|
+
const dumped = JSON.stringify(frame);
|
|
188
|
+
console.error(`[dsh-discord:trace] follow-frame UNHANDLED type=${String(frame['type'])} frame=${dumped.length > 400 ? `${dumped.slice(0, 400)}…` : dumped}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch (cause) {
|
|
193
|
+
if (!per.signal.aborted) {
|
|
194
|
+
if (process.env['DSH_DISCORD_TRACE'] === '1')
|
|
195
|
+
console.error(`[dsh-discord:trace] follow-threw session=${sessionId.slice(0, 8)} cause=${String(cause).slice(0, 300)}`);
|
|
196
|
+
log?.('discord_host_follow_threw', { sessionId, cause: String(cause) });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
if (process.env['DSH_DISCORD_TRACE'] === '1')
|
|
201
|
+
console.error(`[dsh-discord:trace] follow-end session=${sessionId.slice(0, 8)} aborted=${String(per.signal.aborted)} tracked=${String(tracked.get(sessionId) === per)}`);
|
|
202
|
+
if (tracked.get(sessionId) === per)
|
|
203
|
+
tracked.delete(sessionId);
|
|
204
|
+
// A follow stream may END normally once its snapshot is delivered
|
|
205
|
+
// (a cold session has no live agent to follow): without a
|
|
206
|
+
// re-subscribe, every later turn is invisible until the next
|
|
207
|
+
// process restart replays the journal. Re-arm with backoff.
|
|
208
|
+
if (!per.signal.aborted) {
|
|
209
|
+
const delayMs = Math.min(30_000, 1_000 * 2 ** attempt);
|
|
210
|
+
setTimeout(() => {
|
|
211
|
+
if (!per.signal.aborted && !tracked.has(sessionId)) {
|
|
212
|
+
log?.('discord_host_follow_resubscribed', { sessionId, delayMs });
|
|
213
|
+
startLoop(sessionId, attempt + 1);
|
|
214
|
+
}
|
|
215
|
+
}, delayMs);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
})();
|
|
219
|
+
};
|
|
220
|
+
const startControlLoop = (signal) => {
|
|
221
|
+
void (async () => {
|
|
222
|
+
try {
|
|
223
|
+
for await (const raw of services.control(signal)) {
|
|
224
|
+
if (!isRecord(raw))
|
|
225
|
+
continue;
|
|
226
|
+
const frame = raw;
|
|
227
|
+
if (process.env['DSH_DISCORD_TRACE'] === '1') {
|
|
228
|
+
const dumped = JSON.stringify(frame);
|
|
229
|
+
console.error(`[dsh-discord:trace] control type=${String(frame['type'])} frame=${dumped.length > 400 ? `${dumped.slice(0, 400)}…` : dumped}`);
|
|
230
|
+
}
|
|
231
|
+
if (frame['type'] === 'queue' && typeof frame['sessionId'] === 'string') {
|
|
232
|
+
const rawItems = Array.isArray(frame['items']) ? frame['items'] : [];
|
|
233
|
+
const items = rawItems
|
|
234
|
+
.filter((item) => isRecord(item) && typeof item['id'] === 'string')
|
|
235
|
+
.map(item => ({
|
|
236
|
+
id: item['id'],
|
|
237
|
+
summary: queueItemSummary(isRecord(item['message']) ? item['message']['content'] : undefined),
|
|
238
|
+
}));
|
|
239
|
+
consumer?.push({
|
|
240
|
+
type: 'session/queue',
|
|
241
|
+
sessionId: frame['sessionId'],
|
|
242
|
+
items,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
// baseline / jobs / projection frames carry no live-render state.
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
catch (cause) {
|
|
249
|
+
if (!signal.aborted) {
|
|
250
|
+
log?.('discord_host_control_threw', { cause: String(cause) });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
})();
|
|
254
|
+
};
|
|
255
|
+
return {
|
|
256
|
+
track(sessionId) {
|
|
257
|
+
if (sessionId === '' || tracked.has(sessionId))
|
|
258
|
+
return;
|
|
259
|
+
if (rootSignal?.aborted)
|
|
260
|
+
return;
|
|
261
|
+
startLoop(sessionId);
|
|
262
|
+
},
|
|
263
|
+
stream(signal) {
|
|
264
|
+
if (process.env['DSH_DISCORD_TRACE'] === '1')
|
|
265
|
+
console.error(`[dsh-discord:trace] router-stream-open tracked=${String(tracked.size)} rearm=${String(tracked.size)}`);
|
|
266
|
+
rootSignal = signal;
|
|
267
|
+
const queue = createFrameQueue();
|
|
268
|
+
consumer = queue;
|
|
269
|
+
// Re-arm every already-tracked session against the new consumer.
|
|
270
|
+
for (const [sessionId, per] of [...tracked.entries()]) {
|
|
271
|
+
per.abort();
|
|
272
|
+
tracked.delete(sessionId);
|
|
273
|
+
startLoop(sessionId);
|
|
274
|
+
}
|
|
275
|
+
startControlLoop(signal);
|
|
276
|
+
return queue.iterate(signal);
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
}
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The typed防腐层 over the Host's 0.1.6 controller services — the cordis
|
|
3
|
+
* services `sessionController`, `workspaceController`, and `sessionQuery`
|
|
4
|
+
* that replaced the 0.1.1-rc.2 `apiProxy` gateway. Domain methods speak the
|
|
5
|
+
* typert faces directly: plain request objects in, plain values out, and
|
|
6
|
+
* business rejections carried as thrown `RemoteError`s (`{code, message}`).
|
|
7
|
+
* Two guarantees are preserved from the rc.2 seam:
|
|
8
|
+
*
|
|
9
|
+
* 1. Boundedness — a Host that never answers must not wedge an interaction
|
|
10
|
+
* handler (or a Discord ephemeral) forever; every call races a timeout and
|
|
11
|
+
* resolves to an unobservable outcome instead.
|
|
12
|
+
* 2. Observability — every terminal outcome is reported through the injected
|
|
13
|
+
* log sink, so a silent-void call can never again be misread as a hang.
|
|
14
|
+
*
|
|
15
|
+
* The exported port-level API (function names, outcome unions) is unchanged
|
|
16
|
+
* from the rc.2 face: the Discord features and their tests keep their
|
|
17
|
+
* vocabulary while this module alone absorbs the host-side rebase.
|
|
18
|
+
*/
|
|
19
|
+
import type { ProjectListPort } from '../features/project-list.js';
|
|
20
|
+
import type { WorkspaceResolver } from '../features/project-bind.js';
|
|
21
|
+
import type { DshModelPort } from '../features/model-control.js';
|
|
22
|
+
/** The workspace rows the catalog port needs (subset of WorkspaceView). */
|
|
23
|
+
export interface WorkspaceCatalogEntry {
|
|
24
|
+
workspaceId: string;
|
|
25
|
+
title: string;
|
|
26
|
+
/** Canonical directory; present in Host responses, rendered only to proven administrators. */
|
|
27
|
+
path?: string | undefined;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Narrow slice of the 0.1.6 `sessionController` cordis service. Method
|
|
31
|
+
* shapes mirror the typert descriptors in @deepseek-ai/dsh-api-session-controller.
|
|
32
|
+
*/
|
|
33
|
+
export interface DshSessionControllerFace {
|
|
34
|
+
prompt(request: {
|
|
35
|
+
requestId: string;
|
|
36
|
+
sessionId: string;
|
|
37
|
+
mode: 'queue' | 'steer';
|
|
38
|
+
content: Array<{
|
|
39
|
+
type: 'text';
|
|
40
|
+
text: string;
|
|
41
|
+
} | {
|
|
42
|
+
type: 'image';
|
|
43
|
+
mediaType: string;
|
|
44
|
+
data: string;
|
|
45
|
+
}>;
|
|
46
|
+
clientTimeZone?: string;
|
|
47
|
+
}, signal: AbortSignal): Promise<{
|
|
48
|
+
accepted: true;
|
|
49
|
+
}>;
|
|
50
|
+
create(request: {
|
|
51
|
+
workspaceId?: string;
|
|
52
|
+
cwd?: string;
|
|
53
|
+
sessionId?: string;
|
|
54
|
+
agentPreset?: string;
|
|
55
|
+
}): Promise<{
|
|
56
|
+
sessionId: string;
|
|
57
|
+
agentPreset?: string;
|
|
58
|
+
}>;
|
|
59
|
+
/** The durable Session list; the host takes ONLY an optional cancellation signal. */
|
|
60
|
+
list(signal: AbortSignal | undefined): Promise<{
|
|
61
|
+
items: unknown[];
|
|
62
|
+
}>;
|
|
63
|
+
cancel(request: {
|
|
64
|
+
sessionId: string;
|
|
65
|
+
}): Promise<{
|
|
66
|
+
accepted: true;
|
|
67
|
+
}>;
|
|
68
|
+
updateQueue(request: {
|
|
69
|
+
sessionId: string;
|
|
70
|
+
itemId: string;
|
|
71
|
+
action: {
|
|
72
|
+
kind: 'remove';
|
|
73
|
+
};
|
|
74
|
+
}): Promise<{
|
|
75
|
+
accepted: true;
|
|
76
|
+
}>;
|
|
77
|
+
selectModel(request: {
|
|
78
|
+
sessionId: string;
|
|
79
|
+
provider: string;
|
|
80
|
+
model: string;
|
|
81
|
+
reasoningEffort?: string;
|
|
82
|
+
}): Promise<{
|
|
83
|
+
selected: ModelSelectionShape;
|
|
84
|
+
}>;
|
|
85
|
+
modelCatalog(): Promise<ModelCatalogWireShape>;
|
|
86
|
+
/** Per-session durable journal stream (live frames after one opening snapshot). */
|
|
87
|
+
follow(request: {
|
|
88
|
+
address: {
|
|
89
|
+
kind: 'session';
|
|
90
|
+
sessionId: string;
|
|
91
|
+
};
|
|
92
|
+
assistantStream?: true;
|
|
93
|
+
}, signal: AbortSignal): AsyncIterable<unknown>;
|
|
94
|
+
/** Host-wide live state stream (queue/jobs/projection frames over one baseline). */
|
|
95
|
+
control(signal: AbortSignal): AsyncIterable<unknown>;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Narrow slice of the 0.1.6 `workspaceController` cordis service. The
|
|
99
|
+
* registry baseline (`baseline()`) is the synchronous successor of the rc.2
|
|
100
|
+
* unary `workspace.list` RPC.
|
|
101
|
+
*/
|
|
102
|
+
export interface DshWorkspaceControllerFace {
|
|
103
|
+
/** Workspace state stream; every generation starts with exactly one baseline frame. */
|
|
104
|
+
follow(signal: AbortSignal): AsyncIterable<unknown>;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Narrow slice of `sessionQuery` — the per-session projection read the /model
|
|
108
|
+
* surface needs for the session's live selection (the rc.2 `sessions.models`
|
|
109
|
+
* RPC split into the global `modelCatalog` plus this projection).
|
|
110
|
+
*/
|
|
111
|
+
export interface DshSessionQueryFace {
|
|
112
|
+
observeSession(sessionId: string): Promise<{
|
|
113
|
+
header?: {
|
|
114
|
+
cwd?: string;
|
|
115
|
+
};
|
|
116
|
+
projections?: {
|
|
117
|
+
values?: Record<string, unknown>;
|
|
118
|
+
};
|
|
119
|
+
} & Partial<AsyncDisposable>>;
|
|
120
|
+
}
|
|
121
|
+
/** The host-generation model catalog (`session/modelCatalog`, host-wide). */
|
|
122
|
+
export interface ModelCatalogWireShape {
|
|
123
|
+
default: ModelSelectionShape;
|
|
124
|
+
routableProviders: string[];
|
|
125
|
+
groups: ModelProviderGroupShape[];
|
|
126
|
+
failures: Array<{
|
|
127
|
+
id: string;
|
|
128
|
+
name: string;
|
|
129
|
+
message: string;
|
|
130
|
+
}>;
|
|
131
|
+
}
|
|
132
|
+
/** The composite host face every port factory in this module consumes. */
|
|
133
|
+
export interface DshHostFace {
|
|
134
|
+
session: DshSessionControllerFace;
|
|
135
|
+
workspace: DshWorkspaceControllerFace;
|
|
136
|
+
sessionQuery: DshSessionQueryFace;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Resolve the 0.1.6 controller services off the Cordis context. Throws one
|
|
140
|
+
* actionable TypeError naming every absent service — the composition root
|
|
141
|
+
* treats that as a fail-loud startup boundary, never a silent half-mount.
|
|
142
|
+
*/
|
|
143
|
+
export declare function resolveHostFace(ctx: {
|
|
144
|
+
get(name: string): unknown;
|
|
145
|
+
}): DshHostFace;
|
|
146
|
+
/** Defensive read of the title projection in a list row's values. */
|
|
147
|
+
export interface SessionProjectionsShape {
|
|
148
|
+
values?: {
|
|
149
|
+
title?: unknown;
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/** The per-session summary `session.list` returns (rich rows, untrusted wire). */
|
|
153
|
+
export interface SessionSummaryShape {
|
|
154
|
+
sessionId: string;
|
|
155
|
+
updatedAt: number;
|
|
156
|
+
running: boolean;
|
|
157
|
+
blank: boolean;
|
|
158
|
+
cwd?: string;
|
|
159
|
+
agentPreset?: string;
|
|
160
|
+
origin?: 'subagent';
|
|
161
|
+
projections?: SessionProjectionsShape;
|
|
162
|
+
}
|
|
163
|
+
/** The complete provider/model/reasoning selection (dsh-agent ModelSelection). */
|
|
164
|
+
export interface ModelSelectionShape {
|
|
165
|
+
provider: string;
|
|
166
|
+
model: string;
|
|
167
|
+
reasoningEffort?: string;
|
|
168
|
+
}
|
|
169
|
+
/** One reasoning effort a model's adapter advertises (sessions.d.ts). */
|
|
170
|
+
export interface ModelReasoningEffortShape {
|
|
171
|
+
id: string;
|
|
172
|
+
name: string;
|
|
173
|
+
description?: string;
|
|
174
|
+
}
|
|
175
|
+
/** Exact-route reasoning metadata for one catalog model. */
|
|
176
|
+
export interface ModelReasoningShape {
|
|
177
|
+
efforts: ModelReasoningEffortShape[];
|
|
178
|
+
defaultEffort?: string;
|
|
179
|
+
}
|
|
180
|
+
/** One model inside a provider group (sessions.d.ts ModelCatalogModel). */
|
|
181
|
+
export interface ModelCatalogModelShape {
|
|
182
|
+
id: string;
|
|
183
|
+
name: string;
|
|
184
|
+
description?: string;
|
|
185
|
+
reasoning?: ModelReasoningShape;
|
|
186
|
+
}
|
|
187
|
+
/** One provider group and its models (sessions.d.ts ModelProviderGroup). */
|
|
188
|
+
export interface ModelProviderGroupShape {
|
|
189
|
+
id: string;
|
|
190
|
+
name: string;
|
|
191
|
+
models: ModelCatalogModelShape[];
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* The detached model directory the /model cascade browses (port shape,
|
|
195
|
+
* unchanged from rc.2): the session's live selection, whether its route
|
|
196
|
+
* still serves, and the per-provider catalog groups.
|
|
197
|
+
*/
|
|
198
|
+
export interface SessionModelsShape {
|
|
199
|
+
current: ModelSelectionShape;
|
|
200
|
+
routable: boolean;
|
|
201
|
+
groups: ModelProviderGroupShape[];
|
|
202
|
+
failures: Array<{
|
|
203
|
+
id: string;
|
|
204
|
+
name: string;
|
|
205
|
+
message: string;
|
|
206
|
+
}>;
|
|
207
|
+
}
|
|
208
|
+
/** Raised when the Host did not answer within the bounded window. */
|
|
209
|
+
export declare class RpcTimeoutError extends Error {
|
|
210
|
+
constructor(timeoutMs: number);
|
|
211
|
+
}
|
|
212
|
+
/** Race one host promise against a bounded window. */
|
|
213
|
+
export declare function withRpcTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T>;
|
|
214
|
+
/** Diagnostic sink shared by every face function. */
|
|
215
|
+
export type ApiProxyLog = (event: string, detail?: unknown) => void;
|
|
216
|
+
export interface ApiProxyFaceOptions {
|
|
217
|
+
timeoutMs?: number;
|
|
218
|
+
log?: ApiProxyLog;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* The bind flow's catalog verifier: resolves an opaque `ws:` reference
|
|
222
|
+
* against the live workspace baseline. A well-formed reference the registry
|
|
223
|
+
* no longer knows — and any malformed one — resolve `stale` (fail-closed, no
|
|
224
|
+
* write can follow); a Host error is `failed`; a timeout is `unknown`.
|
|
225
|
+
*/
|
|
226
|
+
export declare function createWorkspaceResolver(dsh: DshHostFace, options?: ApiProxyFaceOptions): WorkspaceResolver;
|
|
227
|
+
/**
|
|
228
|
+
* The `/project list` catalog port over the workspace registry baseline.
|
|
229
|
+
* Outcomes follow the port contract: a definitive Host error is `failed`
|
|
230
|
+
* (sanitized before Discord), while a timeout or unreadable body is
|
|
231
|
+
* `unknown` — delivery was not observed, so no retry is implied.
|
|
232
|
+
*/
|
|
233
|
+
export declare function createWorkspaceCatalogPort(dsh: DshHostFace, options?: ApiProxyFaceOptions): ProjectListPort;
|
|
234
|
+
export type WorkspaceDetailOutcome = {
|
|
235
|
+
outcome: 'found';
|
|
236
|
+
workspace: {
|
|
237
|
+
id: string;
|
|
238
|
+
title: string;
|
|
239
|
+
path: string | undefined;
|
|
240
|
+
};
|
|
241
|
+
} | {
|
|
242
|
+
outcome: 'stale';
|
|
243
|
+
} | {
|
|
244
|
+
outcome: 'failed';
|
|
245
|
+
} | {
|
|
246
|
+
outcome: 'unknown';
|
|
247
|
+
};
|
|
248
|
+
/**
|
|
249
|
+
* Read one Workspace's full view (title plus canonical path). The path is
|
|
250
|
+
* for the administrator-only ephemeral info response — the disclosure
|
|
251
|
+
* policy owns whether it ever renders; this face only carries it in memory.
|
|
252
|
+
*/
|
|
253
|
+
export declare function readWorkspaceDetail(dsh: DshHostFace, reference: string, options?: ApiProxyFaceOptions): Promise<WorkspaceDetailOutcome>;
|
|
254
|
+
export type PromptOutcome = {
|
|
255
|
+
outcome: 'accepted';
|
|
256
|
+
} | {
|
|
257
|
+
outcome: 'rejected';
|
|
258
|
+
reason: string;
|
|
259
|
+
} | {
|
|
260
|
+
outcome: 'unknown';
|
|
261
|
+
};
|
|
262
|
+
/**
|
|
263
|
+
* Submit one prompt turn through the session controller. A definitive Host
|
|
264
|
+
* rejection (thrown RemoteError) is a rejection carrying the sanitized
|
|
265
|
+
* reason; a timeout or host fault is `unknown` — the turn may or may not
|
|
266
|
+
* have been admitted, so callers must not resubmit. `options.rpcId` pins
|
|
267
|
+
* the adapter-owned stable request id, which the Host records on the
|
|
268
|
+
* durable user message (`source.rpcId`) and de-duplicates on — the 0.1.6
|
|
269
|
+
* admission layer replays `{accepted: true}` for a repeated id, keeping the
|
|
270
|
+
* at-most-once discipline observable. Images (16.50) encode as ordered
|
|
271
|
+
* `image` parts after the text part.
|
|
272
|
+
*/
|
|
273
|
+
export declare function promptSession(dsh: DshHostFace, request: {
|
|
274
|
+
sessionId: string;
|
|
275
|
+
prompt: string;
|
|
276
|
+
images?: ReadonlyArray<{
|
|
277
|
+
mediaType: string;
|
|
278
|
+
base64: string;
|
|
279
|
+
}>;
|
|
280
|
+
}, options?: ApiProxyFaceOptions & {
|
|
281
|
+
rpcId?: string;
|
|
282
|
+
}): Promise<PromptOutcome>;
|
|
283
|
+
/**
|
|
284
|
+
* Steer the session's active turn: `session.prompt` with `mode: 'steer'`,
|
|
285
|
+
* carrying the same stable request-id discipline as the queue path.
|
|
286
|
+
*/
|
|
287
|
+
export declare function steerSession(dsh: DshHostFace, request: {
|
|
288
|
+
sessionId: string;
|
|
289
|
+
prompt: string;
|
|
290
|
+
}, options?: ApiProxyFaceOptions & {
|
|
291
|
+
rpcId?: string;
|
|
292
|
+
}): Promise<PromptOutcome>;
|
|
293
|
+
export type CreateSessionOutcome = {
|
|
294
|
+
outcome: 'completed';
|
|
295
|
+
sessionId: string;
|
|
296
|
+
} | {
|
|
297
|
+
outcome: 'rejected';
|
|
298
|
+
reason: string;
|
|
299
|
+
} | {
|
|
300
|
+
outcome: 'unknown';
|
|
301
|
+
};
|
|
302
|
+
/**
|
|
303
|
+
* Create one DSH Session against a preallocated id (design.md §10): the 0.1.6
|
|
304
|
+
* controller adopts the same session id idempotently, so an uncertain
|
|
305
|
+
* response never forks a second Session. Same outcome discipline as the
|
|
306
|
+
* prompt path.
|
|
307
|
+
*/
|
|
308
|
+
export declare function createSessionViaProxy(dsh: DshHostFace, request: {
|
|
309
|
+
sessionId: string;
|
|
310
|
+
workspaceId: string;
|
|
311
|
+
}, options?: ApiProxyFaceOptions): Promise<CreateSessionOutcome>;
|
|
312
|
+
/** The durable Session-id baseline reconciliation reconciles against. */
|
|
313
|
+
export type SessionIdListOutcome = {
|
|
314
|
+
outcome: 'completed';
|
|
315
|
+
ids: string[];
|
|
316
|
+
} | {
|
|
317
|
+
outcome: 'failed';
|
|
318
|
+
} | {
|
|
319
|
+
outcome: 'unknown';
|
|
320
|
+
};
|
|
321
|
+
/** List durable Session ids (`session.list` returns everything in one page). */
|
|
322
|
+
export declare function listSessionIds(dsh: DshHostFace, options?: ApiProxyFaceOptions): Promise<SessionIdListOutcome>;
|
|
323
|
+
/** A list row narrowed to what the /session resume surface renders. */
|
|
324
|
+
export interface SessionResumeRow {
|
|
325
|
+
sessionId: string;
|
|
326
|
+
title: string | undefined;
|
|
327
|
+
updatedAt: number;
|
|
328
|
+
running: boolean;
|
|
329
|
+
blank: boolean;
|
|
330
|
+
cwd: string | undefined;
|
|
331
|
+
origin: 'subagent' | undefined;
|
|
332
|
+
}
|
|
333
|
+
export type SessionSummariesOutcome = {
|
|
334
|
+
outcome: 'completed';
|
|
335
|
+
sessions: SessionResumeRow[];
|
|
336
|
+
} | {
|
|
337
|
+
outcome: 'failed';
|
|
338
|
+
} | {
|
|
339
|
+
outcome: 'unknown';
|
|
340
|
+
};
|
|
341
|
+
/**
|
|
342
|
+
* The rich `session.list` for the /session resume surface: titles ride each
|
|
343
|
+
* row's projection values (absence = the session has no title yet), blank
|
|
344
|
+
* sessions are flagged, and rows arrive updatedAt-descending. Defensive
|
|
345
|
+
* narrowing: the wire is untrusted, extra/missing fields never throw.
|
|
346
|
+
*/
|
|
347
|
+
export declare function listSessionSummaries(dsh: DshHostFace, options?: ApiProxyFaceOptions): Promise<SessionSummariesOutcome>;
|
|
348
|
+
export type CancelOutcome = {
|
|
349
|
+
outcome: 'accepted';
|
|
350
|
+
} | {
|
|
351
|
+
outcome: 'rejected';
|
|
352
|
+
reason: string;
|
|
353
|
+
} | {
|
|
354
|
+
outcome: 'unknown';
|
|
355
|
+
};
|
|
356
|
+
/** Cancel the session's active turn (`session.cancel`); DSH preserves the pending inbox. */
|
|
357
|
+
export declare function cancelSessionViaProxy(dsh: DshHostFace, request: {
|
|
358
|
+
sessionId: string;
|
|
359
|
+
}, options?: ApiProxyFaceOptions): Promise<CancelOutcome>;
|
|
360
|
+
export type QueueRemoveOutcome = {
|
|
361
|
+
outcome: 'accepted';
|
|
362
|
+
} | {
|
|
363
|
+
outcome: 'rejected';
|
|
364
|
+
reason: string;
|
|
365
|
+
} | {
|
|
366
|
+
outcome: 'unknown';
|
|
367
|
+
};
|
|
368
|
+
/** Remove one pending inbox item (`session.updateQueue`, action remove). */
|
|
369
|
+
export declare function removeQueueItemViaProxy(dsh: DshHostFace, request: {
|
|
370
|
+
sessionId: string;
|
|
371
|
+
itemId: string;
|
|
372
|
+
}, options?: ApiProxyFaceOptions): Promise<QueueRemoveOutcome>;
|
|
373
|
+
export type SessionModelsOutcome = {
|
|
374
|
+
outcome: 'completed';
|
|
375
|
+
models: SessionModelsShape;
|
|
376
|
+
} | {
|
|
377
|
+
outcome: 'failed';
|
|
378
|
+
} | {
|
|
379
|
+
outcome: 'unknown';
|
|
380
|
+
};
|
|
381
|
+
/**
|
|
382
|
+
* The session's detached model directory: the live selection, whether its
|
|
383
|
+
* route still serves, and the per-provider catalog groups the /model
|
|
384
|
+
* cascade browses. Composed from the 0.1.6 global `modelCatalog` plus the
|
|
385
|
+
* session's `modelSelection` projection — the rc.2 per-session
|
|
386
|
+
* `sessions.models` RPC no longer exists.
|
|
387
|
+
*/
|
|
388
|
+
export declare function sessionModels(dsh: DshHostFace, request: {
|
|
389
|
+
sessionId: string;
|
|
390
|
+
}, options?: ApiProxyFaceOptions): Promise<SessionModelsOutcome>;
|
|
391
|
+
export type SelectModelOutcome = {
|
|
392
|
+
outcome: 'completed';
|
|
393
|
+
selected: ModelSelectionShape;
|
|
394
|
+
} | {
|
|
395
|
+
outcome: 'rejected';
|
|
396
|
+
reason: string;
|
|
397
|
+
} | {
|
|
398
|
+
outcome: 'unknown';
|
|
399
|
+
};
|
|
400
|
+
/**
|
|
401
|
+
* Select the complete model selection for one session (session.selectModel):
|
|
402
|
+
* the session switches immediately and the Host records the choice as the
|
|
403
|
+
* default for sessions that have not logged their own — the response only
|
|
404
|
+
* proves the session switch, so callers must not claim the persistence
|
|
405
|
+
* outcome (design.md §7).
|
|
406
|
+
*/
|
|
407
|
+
export declare function selectSessionModel(dsh: DshHostFace, request: {
|
|
408
|
+
sessionId: string;
|
|
409
|
+
provider: string;
|
|
410
|
+
model: string;
|
|
411
|
+
reasoningEffort?: string;
|
|
412
|
+
}, options?: ApiProxyFaceOptions): Promise<SelectModelOutcome>;
|
|
413
|
+
/**
|
|
414
|
+
* The /model surface over the session controller: the composed per-session
|
|
415
|
+
* directory and the guarded selection mutation — the shapes model-control
|
|
416
|
+
* reasons about.
|
|
417
|
+
*/
|
|
418
|
+
export declare function createModelPort(dsh: DshHostFace, options?: ApiProxyFaceOptions): DshModelPort;
|