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.
- package/README.md +520 -0
- package/client/index.js +492 -0
- package/cordis.patch.yml +14 -0
- package/dsh.plugin.json +7 -0
- package/lib/agora-client.d.ts +73 -0
- package/lib/agora-client.d.ts.map +1 -0
- package/lib/agora-client.js +207 -0
- package/lib/agora-client.js.map +1 -0
- package/lib/client.js +492 -0
- package/lib/command-adapter.d.ts +44 -0
- package/lib/command-adapter.d.ts.map +1 -0
- package/lib/command-adapter.js +70 -0
- package/lib/command-adapter.js.map +1 -0
- package/lib/command.d.ts +53 -0
- package/lib/command.d.ts.map +1 -0
- package/lib/command.js +375 -0
- package/lib/command.js.map +1 -0
- package/lib/context-types.d.ts +51 -0
- package/lib/context-types.d.ts.map +1 -0
- package/lib/context-types.js +2 -0
- package/lib/context-types.js.map +1 -0
- package/lib/contracts.d.ts +402 -0
- package/lib/contracts.d.ts.map +1 -0
- package/lib/contracts.js +2 -0
- package/lib/contracts.js.map +1 -0
- package/lib/extension-sdk.d.ts +74 -0
- package/lib/extension-sdk.d.ts.map +1 -0
- package/lib/extension-sdk.js +160 -0
- package/lib/extension-sdk.js.map +1 -0
- package/lib/harness-runtime.d.ts +29 -0
- package/lib/harness-runtime.d.ts.map +1 -0
- package/lib/harness-runtime.js +422 -0
- package/lib/harness-runtime.js.map +1 -0
- package/lib/http-api.d.ts +10 -0
- package/lib/http-api.d.ts.map +1 -0
- package/lib/http-api.js +282 -0
- package/lib/http-api.js.map +1 -0
- package/lib/im-bridge-v1.d.ts +37 -0
- package/lib/im-bridge-v1.d.ts.map +1 -0
- package/lib/im-bridge-v1.js +43 -0
- package/lib/im-bridge-v1.js.map +1 -0
- package/lib/im-gateway.d.ts +24 -0
- package/lib/im-gateway.d.ts.map +1 -0
- package/lib/im-gateway.js +53 -0
- package/lib/im-gateway.js.map +1 -0
- package/lib/index.d.ts +46 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +210 -0
- package/lib/index.js.map +1 -0
- package/lib/node-worker.d.ts +52 -0
- package/lib/node-worker.d.ts.map +1 -0
- package/lib/node-worker.js +372 -0
- package/lib/node-worker.js.map +1 -0
- package/lib/service.d.ts +51 -0
- package/lib/service.d.ts.map +1 -0
- package/lib/service.js +196 -0
- package/lib/service.js.map +1 -0
- package/lib/tool.d.ts +72 -0
- package/lib/tool.d.ts.map +1 -0
- package/lib/tool.js +206 -0
- package/lib/tool.js.map +1 -0
- package/package.json +74 -0
- package/patches/dsh-im/@xmanrui__dsh-im@2.1.0.patch +920 -0
- package/patches/dsh-im/@xmanrui__dsh-im@2.3.0.patch +984 -0
- package/patches/dsh-im/README.md +82 -0
- package/src/agora-client.ts +333 -0
- package/src/command-adapter.ts +106 -0
- package/src/command.ts +424 -0
- package/src/context-types.ts +54 -0
- package/src/contracts.ts +413 -0
- package/src/extension-sdk.ts +225 -0
- package/src/harness-runtime.ts +518 -0
- package/src/http-api.ts +269 -0
- package/src/im-bridge-v1.ts +73 -0
- package/src/im-gateway.ts +82 -0
- package/src/index.ts +260 -0
- package/src/node-worker.ts +442 -0
- package/src/service.ts +262 -0
- 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
|
+
}
|