dsh-agora-plugin 0.6.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.
Files changed (79) hide show
  1. package/README.md +520 -0
  2. package/client/index.js +492 -0
  3. package/cordis.patch.yml +14 -0
  4. package/dsh.plugin.json +7 -0
  5. package/lib/agora-client.d.ts +73 -0
  6. package/lib/agora-client.d.ts.map +1 -0
  7. package/lib/agora-client.js +207 -0
  8. package/lib/agora-client.js.map +1 -0
  9. package/lib/client.js +492 -0
  10. package/lib/command-adapter.d.ts +44 -0
  11. package/lib/command-adapter.d.ts.map +1 -0
  12. package/lib/command-adapter.js +70 -0
  13. package/lib/command-adapter.js.map +1 -0
  14. package/lib/command.d.ts +53 -0
  15. package/lib/command.d.ts.map +1 -0
  16. package/lib/command.js +375 -0
  17. package/lib/command.js.map +1 -0
  18. package/lib/context-types.d.ts +51 -0
  19. package/lib/context-types.d.ts.map +1 -0
  20. package/lib/context-types.js +2 -0
  21. package/lib/context-types.js.map +1 -0
  22. package/lib/contracts.d.ts +402 -0
  23. package/lib/contracts.d.ts.map +1 -0
  24. package/lib/contracts.js +2 -0
  25. package/lib/contracts.js.map +1 -0
  26. package/lib/extension-sdk.d.ts +74 -0
  27. package/lib/extension-sdk.d.ts.map +1 -0
  28. package/lib/extension-sdk.js +160 -0
  29. package/lib/extension-sdk.js.map +1 -0
  30. package/lib/harness-runtime.d.ts +29 -0
  31. package/lib/harness-runtime.d.ts.map +1 -0
  32. package/lib/harness-runtime.js +422 -0
  33. package/lib/harness-runtime.js.map +1 -0
  34. package/lib/http-api.d.ts +10 -0
  35. package/lib/http-api.d.ts.map +1 -0
  36. package/lib/http-api.js +282 -0
  37. package/lib/http-api.js.map +1 -0
  38. package/lib/im-bridge-v1.d.ts +37 -0
  39. package/lib/im-bridge-v1.d.ts.map +1 -0
  40. package/lib/im-bridge-v1.js +43 -0
  41. package/lib/im-bridge-v1.js.map +1 -0
  42. package/lib/im-gateway.d.ts +24 -0
  43. package/lib/im-gateway.d.ts.map +1 -0
  44. package/lib/im-gateway.js +53 -0
  45. package/lib/im-gateway.js.map +1 -0
  46. package/lib/index.d.ts +46 -0
  47. package/lib/index.d.ts.map +1 -0
  48. package/lib/index.js +210 -0
  49. package/lib/index.js.map +1 -0
  50. package/lib/node-worker.d.ts +52 -0
  51. package/lib/node-worker.d.ts.map +1 -0
  52. package/lib/node-worker.js +372 -0
  53. package/lib/node-worker.js.map +1 -0
  54. package/lib/service.d.ts +51 -0
  55. package/lib/service.d.ts.map +1 -0
  56. package/lib/service.js +196 -0
  57. package/lib/service.js.map +1 -0
  58. package/lib/tool.d.ts +72 -0
  59. package/lib/tool.d.ts.map +1 -0
  60. package/lib/tool.js +206 -0
  61. package/lib/tool.js.map +1 -0
  62. package/package.json +74 -0
  63. package/patches/dsh-im/@xmanrui__dsh-im@2.1.0.patch +920 -0
  64. package/patches/dsh-im/@xmanrui__dsh-im@2.3.0.patch +984 -0
  65. package/patches/dsh-im/README.md +82 -0
  66. package/src/agora-client.ts +333 -0
  67. package/src/command-adapter.ts +106 -0
  68. package/src/command.ts +424 -0
  69. package/src/context-types.ts +54 -0
  70. package/src/contracts.ts +413 -0
  71. package/src/extension-sdk.ts +225 -0
  72. package/src/harness-runtime.ts +518 -0
  73. package/src/http-api.ts +269 -0
  74. package/src/im-bridge-v1.ts +73 -0
  75. package/src/im-gateway.ts +82 -0
  76. package/src/index.ts +260 -0
  77. package/src/node-worker.ts +442 -0
  78. package/src/service.ts +262 -0
  79. package/src/tool.ts +269 -0
@@ -0,0 +1,518 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import type {
3
+ RuntimeDispatch,
4
+ RuntimeNodeAgent,
5
+ RuntimeResultClaim,
6
+ RuntimeResultEnvelope,
7
+ RuntimeResultEvidence,
8
+ } from './contracts.js'
9
+ import {
10
+ DSH_AGORA_RUNTIME_PROTOCOL,
11
+ type DshAgoraRuntimeAdapterV1,
12
+ type RuntimeExecutionResult,
13
+ type RuntimeExecutionContext,
14
+ } from './extension-sdk.js'
15
+
16
+ export interface ConfiguredDshAgent {
17
+ readonly id: string
18
+ readonly displayName?: string
19
+ readonly preset?: string
20
+ readonly model?: string
21
+ readonly workspace?: string
22
+ readonly workspaceAlias?: string
23
+ readonly roles?: readonly string[]
24
+ readonly capabilities?: readonly string[]
25
+ }
26
+
27
+ export interface HarnessRuntimeOptions {
28
+ readonly baseUrl: string
29
+ readonly agents: readonly ConfiguredDshAgent[]
30
+ readonly replyTimeoutMs?: number
31
+ readonly fetch?: typeof globalThis.fetch
32
+ }
33
+
34
+ export class HarnessRuntimeAdapter implements DshAgoraRuntimeAdapterV1 {
35
+ readonly protocol = DSH_AGORA_RUNTIME_PROTOCOL
36
+ private readonly client: HarnessRpcClient
37
+ private readonly agents: readonly NormalizedAgent[]
38
+ private readonly replyTimeoutMs: number
39
+
40
+ constructor(options: HarnessRuntimeOptions) {
41
+ this.client = new HarnessRpcClient(options.baseUrl, options.fetch)
42
+ this.agents = normalizeAgents(options.agents)
43
+ this.replyTimeoutMs = normalizeReplyTimeout(options.replyTimeoutMs)
44
+ }
45
+
46
+ describeAgents(): readonly RuntimeNodeAgent[] {
47
+ return this.agents.map(agent => ({
48
+ agent_ref: agent.id,
49
+ display_name: agent.displayName,
50
+ preset: agent.preset,
51
+ model: agent.model,
52
+ workspace_alias: agent.workspaceAlias,
53
+ roles: agent.roles,
54
+ capabilities: agent.capabilities,
55
+ }))
56
+ }
57
+
58
+ async execute(
59
+ dispatch: RuntimeDispatch,
60
+ signal: AbortSignal,
61
+ context?: RuntimeExecutionContext,
62
+ ): Promise<RuntimeExecutionResult> {
63
+ const startedAt = Date.now()
64
+ const agentRef = dispatch.runtime_target_ref.split(':').at(-1)
65
+ const agent = this.agents.find(item => item.id === agentRef)
66
+ if (!agent) throw new Error(`runtime target ${dispatch.runtime_target_ref} is not configured on this DSH node`)
67
+ const sessionId = dispatch.session_id ?? await this.client.createSession({
68
+ workspace: agent.workspace,
69
+ agentPreset: dispatch.agent_preset ?? agent.preset,
70
+ signal,
71
+ })
72
+ try {
73
+ await context?.reportProgress({
74
+ phase: 'session_ready',
75
+ message: dispatch.session_id ? 'Existing DSH Session resumed' : 'New DSH Session created',
76
+ percent: 10,
77
+ details: { session_id: sessionId },
78
+ })
79
+ const result = await this.client.runPrompt(
80
+ sessionId,
81
+ formatDispatchPrompt(dispatch),
82
+ this.replyTimeoutMs,
83
+ signal,
84
+ `agora-dispatch-${dispatch.id}`,
85
+ {
86
+ onPromptAccepted: () => context?.reportProgress({
87
+ phase: 'prompt_accepted',
88
+ message: 'Prompt accepted by DeepSeek Harness',
89
+ percent: 25,
90
+ }),
91
+ onResponseStarted: () => context?.reportProgress({
92
+ phase: 'response_started',
93
+ message: 'Agent started responding',
94
+ percent: 60,
95
+ }),
96
+ },
97
+ )
98
+ await context?.reportProgress({
99
+ phase: 'response_completed',
100
+ message: 'Agent response completed',
101
+ percent: 90,
102
+ })
103
+ const parsed = parseRuntimeResult(result.answer, agent, dispatch)
104
+ return {
105
+ sessionId,
106
+ answer: parsed.answer,
107
+ reason: result.reason,
108
+ metadata: { agent_ref: agent.id, runtime_target_ref: dispatch.runtime_target_ref },
109
+ resultEnvelope: {
110
+ ...parsed.envelope,
111
+ usage: {
112
+ input_tokens: parsed.envelope.usage?.input_tokens ?? null,
113
+ output_tokens: parsed.envelope.usage?.output_tokens ?? null,
114
+ total_tokens: parsed.envelope.usage?.total_tokens ?? null,
115
+ tool_calls: parsed.envelope.usage?.tool_calls ?? null,
116
+ cost_usd: parsed.envelope.usage?.cost_usd ?? null,
117
+ duration_ms: Date.now() - startedAt,
118
+ },
119
+ },
120
+ }
121
+ } catch (error) {
122
+ if (signal.aborted) {
123
+ try {
124
+ await this.cancel(sessionId, AbortSignal.timeout(30_000))
125
+ } catch {
126
+ // Preserve the original lease-loss or shutdown error. The central
127
+ // fencing token still prevents this abandoned execution from writing.
128
+ }
129
+ }
130
+ throw error
131
+ }
132
+ }
133
+
134
+ async cancel(sessionId: string, signal: AbortSignal): Promise<boolean> {
135
+ await this.client.rpc('session.cancel', { sessionId, keepInbox: true }, 30_000, signal)
136
+ return true
137
+ }
138
+ }
139
+
140
+ interface NormalizedAgent {
141
+ readonly id: string
142
+ readonly displayName: string | null
143
+ readonly preset: string | null
144
+ readonly model: string | null
145
+ readonly workspace: string
146
+ readonly workspaceAlias: string | null
147
+ readonly roles: readonly string[]
148
+ readonly capabilities: readonly string[]
149
+ }
150
+
151
+ class HarnessRpcClient {
152
+ private readonly origin: URL
153
+ private readonly fetchImpl: typeof globalThis.fetch
154
+
155
+ constructor(baseUrl: string, fetchImpl = globalThis.fetch) {
156
+ this.origin = new URL(baseUrl)
157
+ this.fetchImpl = fetchImpl
158
+ }
159
+
160
+ async rpc<T>(method: string, payload: Record<string, unknown>, timeoutMs: number, signal?: AbortSignal, rpcId?: string): Promise<T> {
161
+ const requestId = rpcId ?? `agora-${randomUUID()}`
162
+ const timeout = AbortSignal.timeout(timeoutMs)
163
+ const combined = signal === undefined ? timeout : AbortSignal.any([signal, timeout])
164
+ const response = await this.fetchImpl(new URL(`/api/${method}`, this.origin), {
165
+ method: 'POST',
166
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
167
+ body: JSON.stringify({ type: 'client-request', rpcId: requestId, method, payload }),
168
+ signal: combined,
169
+ })
170
+ if (!response.ok) throw new Error(`DSH ${method} returned HTTP ${response.status}`)
171
+ const body = await response.json() as {
172
+ type?: string
173
+ rpcId?: string
174
+ result?: { ok?: boolean; value?: T; error?: { message?: string; code?: string } }
175
+ }
176
+ if (body.type !== 'server-response' || body.rpcId !== requestId || typeof body.result?.ok !== 'boolean') {
177
+ throw new Error(`DSH ${method} returned an invalid RPC envelope`)
178
+ }
179
+ if (!body.result.ok) {
180
+ const error = new Error(body.result.error?.message ?? `DSH ${method} failed`)
181
+ error.name = body.result.error?.code ?? 'HarnessRpcError'
182
+ throw error
183
+ }
184
+ return body.result.value as T
185
+ }
186
+
187
+ async createSession(options: { workspace: string; agentPreset: string | null; signal: AbortSignal }): Promise<string> {
188
+ const workspaces = await this.rpc<{ items?: Array<{ workspaceId?: string; path?: string }> }>(
189
+ 'workspace.list', {}, 30_000, options.signal,
190
+ )
191
+ let workspaceId = workspaces.items?.find(item => item.path === options.workspace)?.workspaceId
192
+ if (!workspaceId) {
193
+ const created = await this.rpc<{ workspace?: { workspaceId?: string } }>(
194
+ 'workspace.create', { path: options.workspace }, 30_000, options.signal,
195
+ )
196
+ workspaceId = created.workspace?.workspaceId
197
+ }
198
+ if (!workspaceId) throw new Error(`DSH could not resolve workspace ${options.workspace}`)
199
+ const created = await this.rpc<{ sessionId?: string }>(
200
+ 'session.create',
201
+ { workspaceId, ...(options.agentPreset ? { agentPreset: options.agentPreset } : {}) },
202
+ 30_000,
203
+ options.signal,
204
+ )
205
+ if (!created.sessionId) throw new Error('DSH session.create returned no sessionId')
206
+ return created.sessionId
207
+ }
208
+
209
+ async runPrompt(
210
+ sessionId: string,
211
+ prompt: string,
212
+ timeoutMs: number,
213
+ signal: AbortSignal,
214
+ promptRpcId = `agora-dispatch-${randomUUID()}`,
215
+ callbacks: {
216
+ readonly onPromptAccepted?: () => void | Promise<void>
217
+ readonly onResponseStarted?: () => void | Promise<void>
218
+ } = {},
219
+ ): Promise<{ answer: string; reason: string | null }> {
220
+ const timeout = AbortSignal.timeout(timeoutMs)
221
+ const combined = AbortSignal.any([signal, timeout])
222
+ const initial = await this.rpc<HistoryResponse>('session.history', { sessionId, maxMessages: 50 }, 30_000, combined)
223
+ const tracker = new ReplyTracker(maxSeq(initial.events ?? []))
224
+ tracker.promptRpcId = promptRpcId
225
+ await this.rpc(
226
+ 'session.prompt',
227
+ {
228
+ sessionId,
229
+ mode: 'queue',
230
+ content: [{ type: 'text', text: prompt }],
231
+ clientTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
232
+ },
233
+ 30_000,
234
+ combined,
235
+ promptRpcId,
236
+ )
237
+ await callbacks.onPromptAccepted?.()
238
+ let responseStarted = false
239
+ while (!tracker.finished) {
240
+ await sleep(350, combined)
241
+ const history = await this.rpc<HistoryResponse>('session.history', { sessionId, maxMessages: 50 }, 30_000, combined)
242
+ tracker.consume(history.events ?? [])
243
+ if (!responseStarted && tracker.answer !== '') {
244
+ responseStarted = true
245
+ await callbacks.onResponseStarted?.()
246
+ }
247
+ }
248
+ return { answer: tracker.answer, reason: tracker.reason }
249
+ }
250
+ }
251
+
252
+ interface HistoryResponse {
253
+ readonly events?: readonly unknown[]
254
+ }
255
+
256
+ class ReplyTracker {
257
+ promptRpcId = ''
258
+ private lastSeq: number
259
+ private openTurn: unknown = null
260
+ private targetTurn: unknown = null
261
+ private latestText = ''
262
+ private readonly chunks = new Map<string, string>()
263
+ finished = false
264
+ reason: string | null = null
265
+
266
+ constructor(afterSeq: number) {
267
+ this.lastSeq = afterSeq
268
+ }
269
+
270
+ get answer(): string {
271
+ return this.latestText.trim()
272
+ }
273
+
274
+ consume(entries: readonly unknown[]): void {
275
+ const events = entries
276
+ .map(entry => isRecord(entry) && isRecord(entry.event) ? entry.event : entry)
277
+ .filter(isRecord)
278
+ .sort((left, right) => numberValue(left.seq, -1) - numberValue(right.seq, -1))
279
+ for (const event of events) {
280
+ const seq = numberValue(event.seq, -1)
281
+ if (seq <= this.lastSeq) continue
282
+ this.lastSeq = seq
283
+ const data = isRecord(event.data) ? event.data : {}
284
+ if (event.type === 'turn/start') this.openTurn = data.turn ?? null
285
+ if (event.type === 'user/message') {
286
+ const source = isRecord(data.source) ? data.source : {}
287
+ if (source.rpcId === this.promptRpcId) this.targetTurn = this.openTurn
288
+ continue
289
+ }
290
+ if (this.targetTurn === null) continue
291
+ if (event.type === 'turn/end' && data.turn === this.targetTurn) {
292
+ this.finished = true
293
+ this.reason = typeof data.reason === 'string' ? data.reason : null
294
+ continue
295
+ }
296
+ if (data.turn !== this.targetTurn) continue
297
+ if (event.type === 'assistant/message') {
298
+ const message = isRecord(data.message) ? data.message : {}
299
+ const content = Array.isArray(message.content) ? message.content : []
300
+ const text = content.filter(isRecord)
301
+ .filter(part => part.type === 'text' && typeof part.text === 'string')
302
+ .map(part => String(part.text)).join('\n').trim()
303
+ if (text) this.latestText = text
304
+ }
305
+ if (event.type === 'assistant/chunk') {
306
+ const chunk = isRecord(data.chunk) ? data.chunk : {}
307
+ if (chunk.type !== 'text-delta' || typeof chunk.text !== 'string') continue
308
+ const key = `${numberValue(data.step, 0)}:${numberValue(chunk.index, 0)}`
309
+ this.chunks.set(key, (this.chunks.get(key) ?? '') + chunk.text)
310
+ const step = `${numberValue(data.step, 0)}:`
311
+ const text = [...this.chunks.entries()]
312
+ .filter(([part]) => part.startsWith(step))
313
+ .sort(([left], [right]) => Number(left.split(':')[1]) - Number(right.split(':')[1]))
314
+ .map(([, value]) => value).join('\n').trim()
315
+ if (text) this.latestText = text
316
+ }
317
+ }
318
+ }
319
+ }
320
+
321
+ function normalizeAgents(agents: readonly ConfiguredDshAgent[]): readonly NormalizedAgent[] {
322
+ if (agents.length === 0) throw new TypeError('at least one DSH runtime agent must be configured')
323
+ const seen = new Set<string>()
324
+ return agents.map(agent => {
325
+ const id = required(agent.id, 'runtime agent id')
326
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/u.test(id)) throw new TypeError(`invalid runtime agent id "${id}"`)
327
+ if (seen.has(id)) throw new TypeError(`duplicate runtime agent id "${id}"`)
328
+ seen.add(id)
329
+ return Object.freeze({
330
+ id,
331
+ displayName: optional(agent.displayName),
332
+ preset: optional(agent.preset),
333
+ model: optional(agent.model),
334
+ workspace: optional(agent.workspace) ?? process.cwd(),
335
+ workspaceAlias: optional(agent.workspaceAlias),
336
+ roles: unique(agent.roles ?? []),
337
+ capabilities: unique(agent.capabilities ?? ['session.create', 'session.resume', 'session.prompt', 'session.cancel']),
338
+ })
339
+ })
340
+ }
341
+
342
+ function formatDispatchPrompt(dispatch: RuntimeDispatch): string {
343
+ return [
344
+ '[Agora cross-agent dispatch]',
345
+ ...(dispatch.task_id ? [`Task: ${dispatch.task_id}`] : []),
346
+ ...(dispatch.participant_binding_id ? [`Participant binding: ${dispatch.participant_binding_id}`] : []),
347
+ `Dispatch: ${dispatch.id}`,
348
+ '',
349
+ dispatch.prompt,
350
+ '',
351
+ 'Return a concise final result suitable for the requesting agent. Do not approve or reject human governance gates.',
352
+ 'For verifiable claims, append one machine-readable block after the answer:',
353
+ '<agora-evidence>{"claims":[{"id":"claim-1","statement":"...","evidence_ids":["evidence-1"],"confidence":0.9}],"evidence":[{"id":"evidence-1","kind":"file|url|commit|measurement|log|command|other","uri":"...","revision":"..."}],"confidence":0.9,"revision":"workspace commit if known"}</agora-evidence>',
354
+ 'Use only evidence you actually observed. Omit unknown fields and do not put the evidence block inside Markdown fences.',
355
+ ].join('\n')
356
+ }
357
+
358
+ function parseRuntimeResult(
359
+ rawAnswer: string,
360
+ agent: NormalizedAgent,
361
+ dispatch: RuntimeDispatch,
362
+ ): { answer: string; envelope: RuntimeResultEnvelope } {
363
+ const match = /<agora-evidence>([\s\S]*?)<\/agora-evidence>/u.exec(rawAnswer)
364
+ const answer = (match ? rawAnswer.replace(match[0], '') : rawAnswer).trim()
365
+ const payload = match ? parseJsonRecord(match[1] ?? '') : null
366
+ const evidence = parseEvidence(payload?.evidence)
367
+ const evidenceIds = new Set(evidence.map(item => item.id))
368
+ const claims = parseClaims(payload?.claims, evidenceIds)
369
+ const confidence = confidenceValue(payload?.confidence)
370
+ const revision = stringValue(payload?.revision)
371
+ return {
372
+ answer,
373
+ envelope: {
374
+ schema: 'agora.runtime-result/v1',
375
+ answer,
376
+ claims,
377
+ evidence,
378
+ ...(confidence === null ? {} : { confidence }),
379
+ environment: {
380
+ runtime_provider: 'dsh',
381
+ agent_ref: agent.id,
382
+ model: agent.model,
383
+ workspace_alias: dispatch.workspace_alias ?? agent.workspaceAlias,
384
+ ...(revision === null ? {} : { revision }),
385
+ },
386
+ },
387
+ }
388
+ }
389
+
390
+ const evidenceKinds = new Set<RuntimeResultEvidence['kind']>([
391
+ 'file', 'url', 'commit', 'measurement', 'log', 'command', 'other',
392
+ ])
393
+
394
+ function parseEvidence(value: unknown): RuntimeResultEvidence[] {
395
+ if (!Array.isArray(value)) return []
396
+ const seen = new Set<string>()
397
+ const parsed: RuntimeResultEvidence[] = []
398
+ for (const item of value) {
399
+ if (!isRecord(item)) continue
400
+ const id = stringValue(item.id)
401
+ const kind = stringValue(item.kind) as RuntimeResultEvidence['kind'] | null
402
+ if (!id || !kind || !evidenceKinds.has(kind) || seen.has(id)) continue
403
+ seen.add(id)
404
+ const metadata = isRecord(item.metadata) ? item.metadata : null
405
+ parsed.push({
406
+ id,
407
+ kind,
408
+ ...(stringValue(item.label) === null ? {} : { label: stringValue(item.label) }),
409
+ ...(stringValue(item.uri) === null ? {} : { uri: stringValue(item.uri) }),
410
+ ...(stringValue(item.content_hash) === null ? {} : { content_hash: stringValue(item.content_hash) }),
411
+ ...(stringValue(item.revision) === null ? {} : { revision: stringValue(item.revision) }),
412
+ ...(positiveInteger(item.line_start) === null ? {} : { line_start: positiveInteger(item.line_start) }),
413
+ ...(positiveInteger(item.line_end) === null ? {} : { line_end: positiveInteger(item.line_end) }),
414
+ ...(metadata === null ? {} : { metadata }),
415
+ })
416
+ }
417
+ return parsed
418
+ }
419
+
420
+ function parseClaims(value: unknown, evidenceIds: ReadonlySet<string>): RuntimeResultClaim[] {
421
+ if (!Array.isArray(value)) return []
422
+ const seen = new Set<string>()
423
+ const parsed: RuntimeResultClaim[] = []
424
+ for (const item of value) {
425
+ if (!isRecord(item)) continue
426
+ const id = stringValue(item.id)
427
+ const statement = stringValue(item.statement)
428
+ if (!id || !statement || seen.has(id)) continue
429
+ seen.add(id)
430
+ const evidence_ids = Array.isArray(item.evidence_ids)
431
+ ? [...new Set(item.evidence_ids.filter((candidate): candidate is string => (
432
+ typeof candidate === 'string' && evidenceIds.has(candidate)
433
+ )))]
434
+ : []
435
+ const confidence = confidenceValue(item.confidence)
436
+ parsed.push({
437
+ id,
438
+ statement,
439
+ evidence_ids,
440
+ ...(confidence === null ? {} : { confidence }),
441
+ })
442
+ }
443
+ return parsed
444
+ }
445
+
446
+ function parseJsonRecord(value: string): Record<string, unknown> | null {
447
+ try {
448
+ const parsed = JSON.parse(value) as unknown
449
+ return isRecord(parsed) ? parsed : null
450
+ } catch {
451
+ return null
452
+ }
453
+ }
454
+
455
+ function confidenceValue(value: unknown): number | null {
456
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 1 ? value : null
457
+ }
458
+
459
+ function positiveInteger(value: unknown): number | null {
460
+ return typeof value === 'number' && Number.isInteger(value) && value > 0 ? value : null
461
+ }
462
+
463
+ function maxSeq(entries: readonly unknown[]): number {
464
+ return entries.reduce<number>((maximum, entry) => {
465
+ const event = isRecord(entry) && isRecord(entry.event) ? entry.event : entry
466
+ return isRecord(event) ? Math.max(maximum, numberValue(event.seq, -1)) : maximum
467
+ }, -1)
468
+ }
469
+
470
+ function sleep(ms: number, signal: AbortSignal): Promise<void> {
471
+ return new Promise((resolve, reject) => {
472
+ if (signal.aborted) return reject(signal.reason)
473
+ const timer = setTimeout(() => {
474
+ signal.removeEventListener('abort', onAbort)
475
+ resolve()
476
+ }, ms)
477
+ const onAbort = (): void => {
478
+ clearTimeout(timer)
479
+ reject(signal.reason)
480
+ }
481
+ signal.addEventListener('abort', onAbort, { once: true })
482
+ })
483
+ }
484
+
485
+ function normalizeReplyTimeout(value: number | undefined): number {
486
+ const timeout = value ?? 600_000
487
+ if (!Number.isSafeInteger(timeout) || timeout < 10_000 || timeout > 3_600_000) {
488
+ throw new TypeError('runtimeReplyTimeoutMs must be an integer between 10000 and 3600000')
489
+ }
490
+ return timeout
491
+ }
492
+
493
+ function required(value: string, label: string): string {
494
+ const normalized = optional(value)
495
+ if (!normalized) throw new TypeError(`${label} is required`)
496
+ return normalized
497
+ }
498
+
499
+ function optional(value: string | undefined): string | null {
500
+ const normalized = value?.trim()
501
+ return normalized ? normalized : null
502
+ }
503
+
504
+ function unique(values: readonly string[]): readonly string[] {
505
+ return [...new Set(values.map(value => value.trim()).filter(Boolean))].sort()
506
+ }
507
+
508
+ function isRecord(value: unknown): value is Record<string, unknown> {
509
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
510
+ }
511
+
512
+ function numberValue(value: unknown, fallback: number): number {
513
+ return typeof value === 'number' && Number.isFinite(value) ? value : fallback
514
+ }
515
+
516
+ function stringValue(value: unknown): string | null {
517
+ return typeof value === 'string' && value.trim() ? value.trim() : null
518
+ }