@harapter/adapter-dsh 0.1.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.
- package/LICENSE +201 -0
- package/README.md +262 -0
- package/dist/adapter.d.ts +36 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +1065 -0
- package/dist/adapter.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol.d.ts +93 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +708 -0
- package/dist/protocol.js.map +1 -0
- package/package.json +53 -0
package/dist/protocol.js
ADDED
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { HarnessError, providerId, } from '@harapter/core';
|
|
3
|
+
/** Stable Provider identity owned by the DeepSeek Harness Provider Adapter. */
|
|
4
|
+
export const DSH_PROVIDER_ID = providerId('deepseek.harness');
|
|
5
|
+
/** Provider-owned extension for bounded, redacted notification observation. */
|
|
6
|
+
export const DSH_NOTIFICATION_EXTENSION = 'deepseek.harness.notifications';
|
|
7
|
+
/** Structural protocol family used for new Session references. */
|
|
8
|
+
export const DSH_SESSION_COMPATIBILITY_REF = `${DSH_PROVIDER_ID};sdk-jsonrpc-stdio=current`;
|
|
9
|
+
const knownPassthroughEvents = new Set([
|
|
10
|
+
'agent/inbox/spliced',
|
|
11
|
+
'request/context',
|
|
12
|
+
'request/header',
|
|
13
|
+
'session/end-seed',
|
|
14
|
+
'session/title',
|
|
15
|
+
'step/end',
|
|
16
|
+
'step/start',
|
|
17
|
+
'turn/start',
|
|
18
|
+
'user/message',
|
|
19
|
+
]);
|
|
20
|
+
const knownSessionEvents = new Set([
|
|
21
|
+
...knownPassthroughEvents,
|
|
22
|
+
'assistant/chunk',
|
|
23
|
+
'assistant/message',
|
|
24
|
+
'tool/call',
|
|
25
|
+
'tool/result',
|
|
26
|
+
'turn/end',
|
|
27
|
+
]);
|
|
28
|
+
const knownMethods = new Set([
|
|
29
|
+
'initialize',
|
|
30
|
+
'session.event',
|
|
31
|
+
'session.status',
|
|
32
|
+
'session/prompt',
|
|
33
|
+
'shutdown',
|
|
34
|
+
'subagent.finished',
|
|
35
|
+
'subagent.started',
|
|
36
|
+
]);
|
|
37
|
+
const knownStructuralValues = new Set([
|
|
38
|
+
...knownSessionEvents,
|
|
39
|
+
...knownMethods,
|
|
40
|
+
'aborted',
|
|
41
|
+
'assistant',
|
|
42
|
+
'block-end',
|
|
43
|
+
'block-start',
|
|
44
|
+
'blocked',
|
|
45
|
+
'canceled',
|
|
46
|
+
'completed',
|
|
47
|
+
'disposed',
|
|
48
|
+
'error',
|
|
49
|
+
'finish',
|
|
50
|
+
'hook',
|
|
51
|
+
'idle',
|
|
52
|
+
'image',
|
|
53
|
+
'interrupted',
|
|
54
|
+
'legacy',
|
|
55
|
+
'max-tokens',
|
|
56
|
+
'model',
|
|
57
|
+
'next-step',
|
|
58
|
+
'next-turn',
|
|
59
|
+
'ok',
|
|
60
|
+
'parent',
|
|
61
|
+
'plugin',
|
|
62
|
+
'reasoning',
|
|
63
|
+
'reasoning-delta',
|
|
64
|
+
'running',
|
|
65
|
+
'system',
|
|
66
|
+
'text',
|
|
67
|
+
'text-delta',
|
|
68
|
+
'tool',
|
|
69
|
+
'tool-call',
|
|
70
|
+
'tool-call-delta',
|
|
71
|
+
'tool-result',
|
|
72
|
+
'usage',
|
|
73
|
+
'user',
|
|
74
|
+
]);
|
|
75
|
+
const structuralStringKeys = new Set([
|
|
76
|
+
'blockType',
|
|
77
|
+
'kind',
|
|
78
|
+
'method',
|
|
79
|
+
'outcome',
|
|
80
|
+
'status',
|
|
81
|
+
'target',
|
|
82
|
+
'type',
|
|
83
|
+
]);
|
|
84
|
+
const safeRawKeys = new Set([
|
|
85
|
+
'block',
|
|
86
|
+
'blockType',
|
|
87
|
+
'cacheReadTokens',
|
|
88
|
+
'cacheWriteTokens',
|
|
89
|
+
'childSessionId',
|
|
90
|
+
'code',
|
|
91
|
+
'data',
|
|
92
|
+
'error',
|
|
93
|
+
'event',
|
|
94
|
+
'id',
|
|
95
|
+
'ignorable',
|
|
96
|
+
'index',
|
|
97
|
+
'inputTokens',
|
|
98
|
+
'inserted',
|
|
99
|
+
'kind',
|
|
100
|
+
'message',
|
|
101
|
+
'messageId',
|
|
102
|
+
'method',
|
|
103
|
+
'name',
|
|
104
|
+
'outcome',
|
|
105
|
+
'outputTokens',
|
|
106
|
+
'params',
|
|
107
|
+
'parentSessionId',
|
|
108
|
+
'reason',
|
|
109
|
+
'reasoningTokens',
|
|
110
|
+
'removedCount',
|
|
111
|
+
'seq',
|
|
112
|
+
'serverInfo',
|
|
113
|
+
'sessionId',
|
|
114
|
+
'status',
|
|
115
|
+
'step',
|
|
116
|
+
'target',
|
|
117
|
+
'time',
|
|
118
|
+
'totalTokens',
|
|
119
|
+
'turn',
|
|
120
|
+
'type',
|
|
121
|
+
'usage',
|
|
122
|
+
'version',
|
|
123
|
+
]);
|
|
124
|
+
const identifierRawKeys = new Set([
|
|
125
|
+
'childSessionId',
|
|
126
|
+
'id',
|
|
127
|
+
'messageId',
|
|
128
|
+
'parentSessionId',
|
|
129
|
+
'sessionId',
|
|
130
|
+
]);
|
|
131
|
+
const numericRawKeys = new Set([
|
|
132
|
+
'cacheReadTokens',
|
|
133
|
+
'cacheWriteTokens',
|
|
134
|
+
'index',
|
|
135
|
+
'inputTokens',
|
|
136
|
+
'outputTokens',
|
|
137
|
+
'reasoningTokens',
|
|
138
|
+
'removedCount',
|
|
139
|
+
'seq',
|
|
140
|
+
'step',
|
|
141
|
+
'totalTokens',
|
|
142
|
+
'turn',
|
|
143
|
+
]);
|
|
144
|
+
/** Build a non-sensitive identity for the observed protocol runtime. */
|
|
145
|
+
export function dshCompatibilityIdentity(runtimeVersion) {
|
|
146
|
+
return `${DSH_SESSION_COMPATIBILITY_REF};runtime=${runtimeDiagnostic(runtimeVersion)}`;
|
|
147
|
+
}
|
|
148
|
+
/** Validate the official initialize response without accepting lookalikes. */
|
|
149
|
+
export function parseDshInitializeResponse(value) {
|
|
150
|
+
const serverInfo = record(record(value)?.['serverInfo']);
|
|
151
|
+
if (serverInfo?.['name'] !== 'deepseek-harness-sdk-runtime' ||
|
|
152
|
+
typeof serverInfo['version'] !== 'string' ||
|
|
153
|
+
serverInfo['version'].length === 0) {
|
|
154
|
+
throw incompatible('initialize response');
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
name: 'deepseek-harness-sdk-runtime',
|
|
158
|
+
version: runtimeDiagnostic(serverInfo['version']),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
/** Validate the durable enqueue receipt returned by `session/prompt`. */
|
|
162
|
+
export function parseDshPromptResponse(value) {
|
|
163
|
+
const messageId = record(value)?.['messageId'];
|
|
164
|
+
if (typeof messageId !== 'string' || messageId.length === 0) {
|
|
165
|
+
throw incompatible('session/prompt response');
|
|
166
|
+
}
|
|
167
|
+
return messageId;
|
|
168
|
+
}
|
|
169
|
+
/** Convert portable input to the initial verified DSH prompt subset. */
|
|
170
|
+
export function prepareDshPrompt(input, options = {}) {
|
|
171
|
+
if (input.parts.length === 0) {
|
|
172
|
+
throw invalidRequest('A DeepSeek Harness Run requires text input.');
|
|
173
|
+
}
|
|
174
|
+
if (options.providerOptions !== undefined ||
|
|
175
|
+
options.metadata !== undefined ||
|
|
176
|
+
input.metadata !== undefined) {
|
|
177
|
+
throw invalidRequest('DeepSeek Harness Run metadata and Provider options are not mapped.');
|
|
178
|
+
}
|
|
179
|
+
return input.parts.map((part) => {
|
|
180
|
+
if (part.type !== 'text' || part.text.length === 0) {
|
|
181
|
+
throw unsupported(`input.${part.type}`, 'The initial DeepSeek Harness Provider Adapter supports non-empty text parts only.');
|
|
182
|
+
}
|
|
183
|
+
return { type: 'text', text: part.text };
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
/** Reject Session settings the process-wide DSH handshake cannot represent. */
|
|
187
|
+
export function validateDshSessionInput(input, initializedWorkspaceUri) {
|
|
188
|
+
if (input.systemContext !== undefined ||
|
|
189
|
+
input.model !== undefined ||
|
|
190
|
+
input.providerOptions !== undefined ||
|
|
191
|
+
input.metadata !== undefined) {
|
|
192
|
+
throw unsupported('session.options', 'DeepSeek Harness Session settings are fixed by the process-wide Profile.');
|
|
193
|
+
}
|
|
194
|
+
if (input.workspace !== undefined &&
|
|
195
|
+
input.workspace.uri !== initializedWorkspaceUri) {
|
|
196
|
+
throw unsupported('session.workspace', 'DeepSeek Harness cannot change workspace after process initialization.');
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/** Parse one `session.event` notification envelope. */
|
|
200
|
+
export function parseDshSessionEventNotification(value) {
|
|
201
|
+
const params = record(value);
|
|
202
|
+
const sessionId = params?.['sessionId'];
|
|
203
|
+
const event = record(params?.['event']);
|
|
204
|
+
const type = event?.['type'];
|
|
205
|
+
const seq = event?.['seq'];
|
|
206
|
+
const time = event?.['time'];
|
|
207
|
+
const data = record(event?.['data']);
|
|
208
|
+
if (typeof sessionId !== 'string' ||
|
|
209
|
+
sessionId.length === 0 ||
|
|
210
|
+
typeof type !== 'string' ||
|
|
211
|
+
type.length === 0 ||
|
|
212
|
+
typeof seq !== 'number' ||
|
|
213
|
+
!Number.isSafeInteger(seq) ||
|
|
214
|
+
seq < 0 ||
|
|
215
|
+
typeof time !== 'number' ||
|
|
216
|
+
!Number.isFinite(time) ||
|
|
217
|
+
data === undefined ||
|
|
218
|
+
(event?.['ignorable'] !== undefined && event['ignorable'] !== true)) {
|
|
219
|
+
throw incompatible('session.event notification');
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
sessionId,
|
|
223
|
+
event: {
|
|
224
|
+
type,
|
|
225
|
+
seq,
|
|
226
|
+
time,
|
|
227
|
+
data,
|
|
228
|
+
...(event?.['ignorable'] === true ? { ignorable: true } : {}),
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
/** Parse one whole-agent status notification. */
|
|
233
|
+
export function parseDshStatusNotification(value) {
|
|
234
|
+
const params = record(value);
|
|
235
|
+
const sessionId = params?.['sessionId'];
|
|
236
|
+
const status = params?.['status'];
|
|
237
|
+
if (typeof sessionId !== 'string' ||
|
|
238
|
+
sessionId.length === 0 ||
|
|
239
|
+
(status !== 'idle' && status !== 'running')) {
|
|
240
|
+
throw incompatible('session.status notification');
|
|
241
|
+
}
|
|
242
|
+
return { sessionId, status };
|
|
243
|
+
}
|
|
244
|
+
/** Extract and validate one parent/child relationship notification. */
|
|
245
|
+
export function parseDshSubagentStarted(value) {
|
|
246
|
+
const params = record(value);
|
|
247
|
+
const parentSessionId = params?.['parentSessionId'];
|
|
248
|
+
const childSessionId = params?.['childSessionId'];
|
|
249
|
+
if (typeof parentSessionId !== 'string' ||
|
|
250
|
+
parentSessionId.length === 0 ||
|
|
251
|
+
typeof childSessionId !== 'string' ||
|
|
252
|
+
childSessionId.length === 0) {
|
|
253
|
+
throw incompatible('subagent.started notification');
|
|
254
|
+
}
|
|
255
|
+
return { parentSessionId, childSessionId };
|
|
256
|
+
}
|
|
257
|
+
/** Validate the ownership fields of one completed in-process subagent. */
|
|
258
|
+
export function parseDshSubagentFinished(value) {
|
|
259
|
+
const params = record(value);
|
|
260
|
+
const provider = params?.['provider'];
|
|
261
|
+
const agentId = params?.['agentId'];
|
|
262
|
+
const parentSessionId = params?.['parentSessionId'];
|
|
263
|
+
const childSessionId = params?.['childSessionId'];
|
|
264
|
+
const status = params?.['status'];
|
|
265
|
+
const stopReason = record(params?.['stopReason']);
|
|
266
|
+
if (!nonEmptyString(provider) ||
|
|
267
|
+
!nonEmptyString(agentId) ||
|
|
268
|
+
!nonEmptyString(parentSessionId) ||
|
|
269
|
+
!nonEmptyString(childSessionId) ||
|
|
270
|
+
(status !== 'ok' && status !== 'error') ||
|
|
271
|
+
!nonEmptyString(stopReason?.['kind']) ||
|
|
272
|
+
(params?.['lastAssistantMessage'] !== undefined &&
|
|
273
|
+
!contentBlocks(params['lastAssistantMessage']))) {
|
|
274
|
+
throw incompatible('subagent.finished notification');
|
|
275
|
+
}
|
|
276
|
+
return { parentSessionId, childSessionId };
|
|
277
|
+
}
|
|
278
|
+
/** Map one validated DSH session-log event to portable observations. */
|
|
279
|
+
export function mapDshSessionEvent(event) {
|
|
280
|
+
const raw = redactDshEvent('session.event', { event });
|
|
281
|
+
const provider = () => {
|
|
282
|
+
const eventType = publicEventType(event.type);
|
|
283
|
+
return {
|
|
284
|
+
type: 'provider',
|
|
285
|
+
data: { eventType },
|
|
286
|
+
providerEventType: eventType,
|
|
287
|
+
raw,
|
|
288
|
+
};
|
|
289
|
+
};
|
|
290
|
+
if (event.type === 'agent/inbox/spliced') {
|
|
291
|
+
const target = event.data['target'];
|
|
292
|
+
const start = event.data['start'];
|
|
293
|
+
const removedCount = event.data['removedCount'];
|
|
294
|
+
const outcome = event.data['outcome'];
|
|
295
|
+
const inserted = event.data['inserted'];
|
|
296
|
+
if ((target !== 'next-turn' && target !== 'next-step') ||
|
|
297
|
+
!nonNegativeInteger(start) ||
|
|
298
|
+
(removedCount !== undefined && !nonNegativeInteger(removedCount)) ||
|
|
299
|
+
(outcome !== undefined && outcome !== 'canceled') ||
|
|
300
|
+
!Array.isArray(inserted)) {
|
|
301
|
+
throw incompatible('agent/inbox/spliced event');
|
|
302
|
+
}
|
|
303
|
+
const messages = inserted.map(parseUserMessage);
|
|
304
|
+
const insertedMessageIds = target === 'next-turn' &&
|
|
305
|
+
(removedCount ?? 0) === 0 &&
|
|
306
|
+
outcome === undefined
|
|
307
|
+
? messages
|
|
308
|
+
.filter(({ content, sourceKind }) => sourceKind === 'user' && content.length > 0)
|
|
309
|
+
.map(({ id }) => id)
|
|
310
|
+
: [];
|
|
311
|
+
return {
|
|
312
|
+
events: [provider()],
|
|
313
|
+
insertedMessageCount: messages.length,
|
|
314
|
+
insertedMessageIds,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
if (event.type === 'assistant/chunk') {
|
|
318
|
+
requireTurnStep(event.data, event.type);
|
|
319
|
+
const chunk = record(event.data['chunk']);
|
|
320
|
+
const type = chunk?.['type'];
|
|
321
|
+
if (type === 'text-delta' || type === 'reasoning-delta') {
|
|
322
|
+
const text = chunk?.['text'];
|
|
323
|
+
if (!nonNegativeInteger(chunk?.['index']) || typeof text !== 'string') {
|
|
324
|
+
throw incompatible(event.type);
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
events: [
|
|
328
|
+
{
|
|
329
|
+
type: type === 'text-delta' ? 'message.delta' : 'reasoning.delta',
|
|
330
|
+
data: { delta: text },
|
|
331
|
+
},
|
|
332
|
+
],
|
|
333
|
+
insertedMessageCount: 0,
|
|
334
|
+
insertedMessageIds: [],
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
if (type === 'usage') {
|
|
338
|
+
const usage = parseUsage(chunk?.['usage']);
|
|
339
|
+
return {
|
|
340
|
+
events: [{ type: 'usage.updated', data: usage, usage }],
|
|
341
|
+
insertedMessageCount: 0,
|
|
342
|
+
insertedMessageIds: [],
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
if (typeof type !== 'string')
|
|
346
|
+
throw incompatible(event.type);
|
|
347
|
+
return {
|
|
348
|
+
events: [provider()],
|
|
349
|
+
insertedMessageCount: 0,
|
|
350
|
+
insertedMessageIds: [],
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
if (event.type === 'assistant/message') {
|
|
354
|
+
requireTurnStep(event.data, event.type);
|
|
355
|
+
const { content } = parseAssistantMessage(event.data['message']);
|
|
356
|
+
const text = [];
|
|
357
|
+
for (const blockValue of content) {
|
|
358
|
+
const block = record(blockValue);
|
|
359
|
+
if (typeof block?.['type'] !== 'string')
|
|
360
|
+
throw incompatible(event.type);
|
|
361
|
+
if (block['type'] === 'text') {
|
|
362
|
+
if (typeof block['text'] !== 'string')
|
|
363
|
+
throw incompatible(event.type);
|
|
364
|
+
text.push(block['text']);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
const finalMessage = text.join('');
|
|
368
|
+
const usage = event.data['usage'] === undefined
|
|
369
|
+
? undefined
|
|
370
|
+
: parseUsage(event.data['usage']);
|
|
371
|
+
return {
|
|
372
|
+
events: [
|
|
373
|
+
{
|
|
374
|
+
type: 'message.completed',
|
|
375
|
+
data: { message: finalMessage },
|
|
376
|
+
finalMessage,
|
|
377
|
+
},
|
|
378
|
+
...(usage === undefined
|
|
379
|
+
? []
|
|
380
|
+
: [{ type: 'usage.updated', data: usage, usage }]),
|
|
381
|
+
],
|
|
382
|
+
insertedMessageCount: 0,
|
|
383
|
+
insertedMessageIds: [],
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
if (event.type === 'tool/call') {
|
|
387
|
+
requireTurnStep(event.data, event.type);
|
|
388
|
+
const callId = event.data['callId'];
|
|
389
|
+
const name = event.data['name'];
|
|
390
|
+
const argumentsValue = event.data['arguments'];
|
|
391
|
+
if (!nonEmptyString(callId) ||
|
|
392
|
+
!nonEmptyString(name) ||
|
|
393
|
+
typeof argumentsValue !== 'string') {
|
|
394
|
+
throw incompatible(event.type);
|
|
395
|
+
}
|
|
396
|
+
return {
|
|
397
|
+
events: [{ type: 'tool.started', data: { callId, name } }],
|
|
398
|
+
insertedMessageCount: 0,
|
|
399
|
+
insertedMessageIds: [],
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
if (event.type === 'tool/result') {
|
|
403
|
+
requireTurnStep(event.data, event.type);
|
|
404
|
+
const callId = parseToolResultMessage(event.data['message']);
|
|
405
|
+
const error = event.data['error'];
|
|
406
|
+
if (error !== undefined) {
|
|
407
|
+
const parsedError = record(error);
|
|
408
|
+
const errorName = parsedError?.['name'];
|
|
409
|
+
const errorCode = parsedError?.['code'];
|
|
410
|
+
if (!nonEmptyString(errorName) || !nonEmptyString(errorCode)) {
|
|
411
|
+
throw incompatible(event.type);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return {
|
|
415
|
+
events: [
|
|
416
|
+
{
|
|
417
|
+
type: 'tool.completed',
|
|
418
|
+
data: {
|
|
419
|
+
callId,
|
|
420
|
+
failed: event.data['error'] !== undefined,
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
],
|
|
424
|
+
insertedMessageCount: 0,
|
|
425
|
+
insertedMessageIds: [],
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
if (event.type === 'turn/end') {
|
|
429
|
+
if (!positiveInteger(event.data['turn']))
|
|
430
|
+
throw incompatible(event.type);
|
|
431
|
+
return {
|
|
432
|
+
events: [],
|
|
433
|
+
insertedMessageCount: 0,
|
|
434
|
+
insertedMessageIds: [],
|
|
435
|
+
terminal: terminalObservation(event.data['reason']),
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
if (event.type === 'session/title') {
|
|
439
|
+
validateSessionTitle(event.data);
|
|
440
|
+
}
|
|
441
|
+
if (knownPassthroughEvents.has(event.type) || event.ignorable === true) {
|
|
442
|
+
return {
|
|
443
|
+
events: [provider()],
|
|
444
|
+
insertedMessageCount: 0,
|
|
445
|
+
insertedMessageIds: [],
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
throw incompatible('required session event');
|
|
449
|
+
}
|
|
450
|
+
function validateSessionTitle(data) {
|
|
451
|
+
const source = record(data['source']);
|
|
452
|
+
const sourceKind = source?.['kind'];
|
|
453
|
+
const messageSeqs = data['messageSeqs'];
|
|
454
|
+
if (!nonEmptyString(data['title']) ||
|
|
455
|
+
!Array.isArray(messageSeqs) ||
|
|
456
|
+
!messageSeqs.every(nonNegativeInteger) ||
|
|
457
|
+
(sourceKind !== 'fallback' &&
|
|
458
|
+
sourceKind !== 'provider' &&
|
|
459
|
+
sourceKind !== 'user')) {
|
|
460
|
+
throw incompatible('session/title event');
|
|
461
|
+
}
|
|
462
|
+
if (sourceKind !== 'provider' || source === undefined)
|
|
463
|
+
return;
|
|
464
|
+
const model = source['model'];
|
|
465
|
+
const parsedModel = model === undefined ? undefined : record(model);
|
|
466
|
+
if (!nonEmptyString(source['provider']) ||
|
|
467
|
+
(model !== undefined &&
|
|
468
|
+
(!nonEmptyString(parsedModel?.['provider']) ||
|
|
469
|
+
!nonEmptyString(parsedModel['model'])))) {
|
|
470
|
+
throw incompatible('session/title event');
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
/** Produce a bounded structural event without prompt, content, path, or secret values. */
|
|
474
|
+
export function redactDshEvent(method, params) {
|
|
475
|
+
return {
|
|
476
|
+
method: publicMethod(method),
|
|
477
|
+
params: redact(params, 0, { nodes: 0 }),
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
function terminalObservation(reasonValue) {
|
|
481
|
+
const reason = record(reasonValue);
|
|
482
|
+
const kind = reason?.['kind'];
|
|
483
|
+
if (kind === 'completed') {
|
|
484
|
+
return {
|
|
485
|
+
eventType: 'run.completed',
|
|
486
|
+
result: { status: 'completed', providerResult: { reason: kind } },
|
|
487
|
+
valid: true,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
if (kind === 'aborted') {
|
|
491
|
+
const cause = record(reason?.['reason']);
|
|
492
|
+
const causeKind = cause?.['kind'];
|
|
493
|
+
const known = ['disposed', 'legacy', 'parent', 'user'].includes(String(causeKind));
|
|
494
|
+
const hook = causeKind === 'hook' && typeof cause?.['reason'] === 'string';
|
|
495
|
+
if (!known && !hook)
|
|
496
|
+
return invalidTerminal('malformed_aborted_reason');
|
|
497
|
+
return {
|
|
498
|
+
eventType: 'run.cancelled',
|
|
499
|
+
result: {
|
|
500
|
+
status: 'cancelled',
|
|
501
|
+
providerResult: { reason: kind, cause: causeKind },
|
|
502
|
+
},
|
|
503
|
+
valid: true,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
if (kind === 'blocked' || kind === 'max-tokens' || kind === 'interrupted') {
|
|
507
|
+
return {
|
|
508
|
+
eventType: 'run.failed',
|
|
509
|
+
result: { status: 'failed', providerResult: { reason: kind } },
|
|
510
|
+
valid: true,
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
if (kind === 'error') {
|
|
514
|
+
const error = record(reason?.['error']);
|
|
515
|
+
const code = error?.['code'];
|
|
516
|
+
if (typeof code !== 'string')
|
|
517
|
+
return invalidTerminal('malformed_error');
|
|
518
|
+
return {
|
|
519
|
+
eventType: 'run.failed',
|
|
520
|
+
result: {
|
|
521
|
+
status: 'failed',
|
|
522
|
+
providerResult: {
|
|
523
|
+
reason: kind,
|
|
524
|
+
providerCode: safeToken(code) ? code : 'UNKNOWN',
|
|
525
|
+
},
|
|
526
|
+
},
|
|
527
|
+
valid: true,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
return invalidTerminal(typeof kind === 'string' ? 'unknown_terminal_reason' : 'malformed_terminal');
|
|
531
|
+
}
|
|
532
|
+
function parseUserMessage(value) {
|
|
533
|
+
const message = record(value);
|
|
534
|
+
const id = message?.['id'];
|
|
535
|
+
const source = record(message?.['source']);
|
|
536
|
+
const sourceKind = source?.['kind'];
|
|
537
|
+
const content = message?.['content'];
|
|
538
|
+
if (!nonEmptyString(id) ||
|
|
539
|
+
message?.['role'] !== 'user' ||
|
|
540
|
+
!contentBlocks(content) ||
|
|
541
|
+
!nonEmptyString(sourceKind)) {
|
|
542
|
+
throw incompatible('agent/inbox/spliced event');
|
|
543
|
+
}
|
|
544
|
+
return { content, id, sourceKind };
|
|
545
|
+
}
|
|
546
|
+
function parseAssistantMessage(value) {
|
|
547
|
+
const message = record(value);
|
|
548
|
+
const source = record(message?.['source']);
|
|
549
|
+
const content = message?.['content'];
|
|
550
|
+
const role = message?.['role'];
|
|
551
|
+
if (!nonEmptyString(message?.['id']) ||
|
|
552
|
+
role !== 'assistant' ||
|
|
553
|
+
!contentBlocks(content) ||
|
|
554
|
+
source?.['kind'] !== 'model' ||
|
|
555
|
+
!nonEmptyString(source['provider']) ||
|
|
556
|
+
!nonEmptyString(source['model'])) {
|
|
557
|
+
throw incompatible('assistant/message event');
|
|
558
|
+
}
|
|
559
|
+
return { content };
|
|
560
|
+
}
|
|
561
|
+
function parseToolResultMessage(value) {
|
|
562
|
+
const message = record(value);
|
|
563
|
+
const source = record(message?.['source']);
|
|
564
|
+
const callId = source?.['callId'];
|
|
565
|
+
const content = message?.['content'];
|
|
566
|
+
const role = message?.['role'];
|
|
567
|
+
const block = Array.isArray(content) && content.length === 1
|
|
568
|
+
? record(content[0])
|
|
569
|
+
: undefined;
|
|
570
|
+
if (!nonEmptyString(message?.['id']) ||
|
|
571
|
+
role !== 'user' ||
|
|
572
|
+
source?.['kind'] !== 'tool' ||
|
|
573
|
+
!nonEmptyString(callId) ||
|
|
574
|
+
block?.['type'] !== 'tool-result' ||
|
|
575
|
+
block['toolCallId'] !== callId ||
|
|
576
|
+
!contentBlocks(block['content'])) {
|
|
577
|
+
throw incompatible('tool/result event');
|
|
578
|
+
}
|
|
579
|
+
return callId;
|
|
580
|
+
}
|
|
581
|
+
function contentBlocks(value) {
|
|
582
|
+
return (Array.isArray(value) &&
|
|
583
|
+
value.every((item) => nonEmptyString(record(item)?.['type'])));
|
|
584
|
+
}
|
|
585
|
+
function requireTurnStep(data, eventType) {
|
|
586
|
+
if (!positiveInteger(data['turn']) || !positiveInteger(data['step'])) {
|
|
587
|
+
throw incompatible(eventType);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
function invalidTerminal(reason) {
|
|
591
|
+
return {
|
|
592
|
+
eventType: 'run.failed',
|
|
593
|
+
result: { status: 'failed', providerResult: { reason } },
|
|
594
|
+
valid: false,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
function parseUsage(value) {
|
|
598
|
+
const usage = record(value);
|
|
599
|
+
const inputTokens = usage?.['inputTokens'];
|
|
600
|
+
const outputTokens = usage?.['outputTokens'];
|
|
601
|
+
const totalTokens = usage?.['totalTokens'];
|
|
602
|
+
if (!nonNegativeInteger(inputTokens) ||
|
|
603
|
+
!nonNegativeInteger(outputTokens) ||
|
|
604
|
+
(totalTokens !== undefined && !nonNegativeInteger(totalTokens))) {
|
|
605
|
+
throw incompatible('usage');
|
|
606
|
+
}
|
|
607
|
+
return {
|
|
608
|
+
inputTokens,
|
|
609
|
+
outputTokens,
|
|
610
|
+
...(totalTokens === undefined ? {} : { totalTokens }),
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
function redact(value, depth, state, key) {
|
|
614
|
+
state.nodes += 1;
|
|
615
|
+
if (state.nodes > 64 || depth >= 4)
|
|
616
|
+
return '[truncated]';
|
|
617
|
+
if (value === null || typeof value === 'boolean')
|
|
618
|
+
return value;
|
|
619
|
+
if (typeof value === 'number') {
|
|
620
|
+
return key !== undefined && numericRawKeys.has(key) ? value : '[redacted]';
|
|
621
|
+
}
|
|
622
|
+
if (typeof value === 'string') {
|
|
623
|
+
return key !== undefined && structuralStringKeys.has(key)
|
|
624
|
+
? publicStructuralValue(value)
|
|
625
|
+
: '[redacted]';
|
|
626
|
+
}
|
|
627
|
+
if (Array.isArray(value)) {
|
|
628
|
+
const result = value
|
|
629
|
+
.slice(0, 16)
|
|
630
|
+
.map((item) => redact(item, depth + 1, state));
|
|
631
|
+
if (value.length > 16)
|
|
632
|
+
result.push('[truncated]');
|
|
633
|
+
return result;
|
|
634
|
+
}
|
|
635
|
+
const object = record(value);
|
|
636
|
+
if (object === undefined)
|
|
637
|
+
return '[redacted]';
|
|
638
|
+
const result = {};
|
|
639
|
+
const entries = Object.entries(object);
|
|
640
|
+
for (const [index, [entryKey, item]] of entries.slice(0, 16).entries()) {
|
|
641
|
+
const safeKey = entryKey.length <= 64 && safeRawKeys.has(entryKey)
|
|
642
|
+
? entryKey
|
|
643
|
+
: `[redacted-key-${String(index)}]`;
|
|
644
|
+
result[safeKey] = identifierRawKeys.has(entryKey)
|
|
645
|
+
? '[redacted]'
|
|
646
|
+
: redact(item, depth + 1, state, entryKey);
|
|
647
|
+
}
|
|
648
|
+
if (entries.length > 16 || state.nodes > 64) {
|
|
649
|
+
result['__truncated__'] = '[truncated]';
|
|
650
|
+
}
|
|
651
|
+
return result;
|
|
652
|
+
}
|
|
653
|
+
function safeToken(value) {
|
|
654
|
+
return /^[A-Za-z0-9][0-9A-Za-z._+-]{0,127}$/u.test(value);
|
|
655
|
+
}
|
|
656
|
+
function publicMethod(value) {
|
|
657
|
+
return knownMethods.has(value) ? value : stableDiagnostic('method', value);
|
|
658
|
+
}
|
|
659
|
+
function publicEventType(value) {
|
|
660
|
+
return knownSessionEvents.has(value)
|
|
661
|
+
? value
|
|
662
|
+
: stableDiagnostic('unknown', value);
|
|
663
|
+
}
|
|
664
|
+
function publicStructuralValue(value) {
|
|
665
|
+
return knownStructuralValues.has(value)
|
|
666
|
+
? value
|
|
667
|
+
: stableDiagnostic('value', value);
|
|
668
|
+
}
|
|
669
|
+
function runtimeDiagnostic(value) {
|
|
670
|
+
return /^version-[0-9a-f]{16}$/u.test(value)
|
|
671
|
+
? value
|
|
672
|
+
: stableDiagnostic('version', value);
|
|
673
|
+
}
|
|
674
|
+
function stableDiagnostic(prefix, value) {
|
|
675
|
+
const digest = createHash('sha256').update(value).digest('hex').slice(0, 16);
|
|
676
|
+
return `${prefix}-${digest}`;
|
|
677
|
+
}
|
|
678
|
+
function nonEmptyString(value) {
|
|
679
|
+
return typeof value === 'string' && value.length > 0;
|
|
680
|
+
}
|
|
681
|
+
function nonNegativeInteger(value) {
|
|
682
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
|
|
683
|
+
}
|
|
684
|
+
function positiveInteger(value) {
|
|
685
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value > 0;
|
|
686
|
+
}
|
|
687
|
+
function record(value) {
|
|
688
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
689
|
+
? value
|
|
690
|
+
: undefined;
|
|
691
|
+
}
|
|
692
|
+
function incompatible(surface) {
|
|
693
|
+
return new HarnessError('provider_api_incompatible', `DeepSeek Harness ${surface} is not compatible with the verified SDK JSON-RPC protocol.`, { retryable: false, providerId: DSH_PROVIDER_ID });
|
|
694
|
+
}
|
|
695
|
+
function invalidRequest(message) {
|
|
696
|
+
return new HarnessError('invalid_request', message, {
|
|
697
|
+
retryable: false,
|
|
698
|
+
providerId: DSH_PROVIDER_ID,
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
function unsupported(capability, message) {
|
|
702
|
+
return new HarnessError('unsupported_capability', message, {
|
|
703
|
+
retryable: false,
|
|
704
|
+
providerId: DSH_PROVIDER_ID,
|
|
705
|
+
details: { capability },
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
//# sourceMappingURL=protocol.js.map
|