@avernet-plugin/deepseek-harness-channel-bcn 0.1.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 (54) hide show
  1. package/LEGAL.md +9 -0
  2. package/LICENSE +176 -0
  3. package/README.md +242 -0
  4. package/cordis.patch.yml +5 -0
  5. package/dist/bridge.d.ts +51 -0
  6. package/dist/bridge.d.ts.map +1 -0
  7. package/dist/bridge.js +695 -0
  8. package/dist/bridge.js.map +1 -0
  9. package/dist/config.d.ts +23 -0
  10. package/dist/config.d.ts.map +1 -0
  11. package/dist/config.js +60 -0
  12. package/dist/config.js.map +1 -0
  13. package/dist/configure.d.ts +18 -0
  14. package/dist/configure.d.ts.map +1 -0
  15. package/dist/configure.js +207 -0
  16. package/dist/configure.js.map +1 -0
  17. package/dist/credentials.d.ts +15 -0
  18. package/dist/credentials.d.ts.map +1 -0
  19. package/dist/credentials.js +44 -0
  20. package/dist/credentials.js.map +1 -0
  21. package/dist/endpoint.d.ts +21 -0
  22. package/dist/endpoint.d.ts.map +1 -0
  23. package/dist/endpoint.js +156 -0
  24. package/dist/endpoint.js.map +1 -0
  25. package/dist/http-client.d.ts +28 -0
  26. package/dist/http-client.d.ts.map +1 -0
  27. package/dist/http-client.js +106 -0
  28. package/dist/http-client.js.map +1 -0
  29. package/dist/index.d.ts +8 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +42 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/protocol.d.ts +107 -0
  34. package/dist/protocol.d.ts.map +1 -0
  35. package/dist/protocol.js +157 -0
  36. package/dist/protocol.js.map +1 -0
  37. package/dist/route-tool.d.ts +5 -0
  38. package/dist/route-tool.d.ts.map +1 -0
  39. package/dist/route-tool.js +38 -0
  40. package/dist/route-tool.js.map +1 -0
  41. package/dist/session-identity.d.ts +22 -0
  42. package/dist/session-identity.d.ts.map +1 -0
  43. package/dist/session-identity.js +35 -0
  44. package/dist/session-identity.js.map +1 -0
  45. package/dist/task-tools.d.ts +9 -0
  46. package/dist/task-tools.d.ts.map +1 -0
  47. package/dist/task-tools.js +81 -0
  48. package/dist/task-tools.js.map +1 -0
  49. package/dist/ws-client.d.ts +62 -0
  50. package/dist/ws-client.d.ts.map +1 -0
  51. package/dist/ws-client.js +433 -0
  52. package/dist/ws-client.js.map +1 -0
  53. package/install-dsh.sh +95 -0
  54. package/package.json +80 -0
package/dist/bridge.js ADDED
@@ -0,0 +1,695 @@
1
+ import { installModelSelection, } from '@deepseek-ai/dsh-agent';
2
+ import { createUserMessage } from '@deepseek-ai/dsh-llm';
3
+ import { asNonEmptyString, asRecord, extractMessageText, parseChatInjectParams, parseChatSendParams, } from './protocol.js';
4
+ import { createBcsRouteTool } from './route-tool.js';
5
+ import { dshSessionIdForV2, resolveBcnSessionIdentity } from './session-identity.js';
6
+ import { createBcsAssignTaskTool, createBcsSendTaskMessageTool, createBcsTaskCompleteTool, } from './task-tools.js';
7
+ export class BcnBridge {
8
+ ctx;
9
+ client;
10
+ config;
11
+ log;
12
+ managedAgents = new Map();
13
+ ownedAgents = new Set();
14
+ sessionIdentityBySessionId = new Map();
15
+ runIdByMessageId = new Map();
16
+ runIdByTurn = new Map();
17
+ activeRunBySessionId = new Map();
18
+ runs = new Map();
19
+ completedRunIds = [];
20
+ disposers = [];
21
+ disposed = false;
22
+ constructor(ctx, client, config, log) {
23
+ this.ctx = ctx;
24
+ this.client = client;
25
+ this.config = config;
26
+ this.log = log;
27
+ }
28
+ start() {
29
+ this.disposers.push(this.client.onRequest('chat.send', frame => this.handleChatSend(frame)), this.client.onRequest('chat.inject', frame => this.handleChatInject(frame)), this.ctx.on('agent/inbox/claimed', payload => this.handleInboxClaimed(payload)), this.ctx.on('agent/inbox/discarded', payload => this.handleInboxDiscarded(payload)), this.ctx.on('session/event', (session, event) => this.handleSessionEvent(session, event)));
30
+ }
31
+ get busy() {
32
+ for (const run of this.runs.values())
33
+ if (!run.terminal)
34
+ return true;
35
+ return false;
36
+ }
37
+ async dispose() {
38
+ if (this.disposed)
39
+ return;
40
+ this.disposed = true;
41
+ for (const dispose of this.disposers.splice(0).reverse())
42
+ await dispose();
43
+ for (const run of this.runs.values()) {
44
+ if (run.terminal)
45
+ continue;
46
+ const agent = this.ctx.agents.get(run.sessionId);
47
+ agent?.cancel({ kind: 'disposed' });
48
+ }
49
+ await Promise.allSettled([...this.ownedAgents].map(handle => handle.dispose()));
50
+ this.ownedAgents.clear();
51
+ this.managedAgents.clear();
52
+ this.sessionIdentityBySessionId.clear();
53
+ this.runIdByMessageId.clear();
54
+ this.runIdByTurn.clear();
55
+ this.activeRunBySessionId.clear();
56
+ this.runs.clear();
57
+ }
58
+ async captureRoute(agent, selectors, reason, signal) {
59
+ if (!agent)
60
+ return { ok: false, error: 'NO_AGENT', message: 'No active DSH agent owns this call.' };
61
+ const runId = this.activeRunBySessionId.get(String(agent.id));
62
+ const run = runId ? this.runs.get(runId) : undefined;
63
+ if (!run || run.terminal) {
64
+ return { ok: false, error: 'NO_BCN_RUN', message: 'No active BCN run owns this call.' };
65
+ }
66
+ if (run.toolProfile !== 'route') {
67
+ return { ok: false, error: 'TOOL_NOT_ALLOWED', message: 'bcs_route is not available in this BCN session.' };
68
+ }
69
+ if (!Array.isArray(selectors) || selectors.length === 0 || selectors.length > 20) {
70
+ return { ok: false, error: 'INVALID_PARAMS', message: 'to must contain 1-20 selectors.' };
71
+ }
72
+ const normalized = [];
73
+ for (const selector of selectors) {
74
+ const value = selector.value.trim();
75
+ if ((selector.type !== 'name' && selector.type !== 'bot') || !value) {
76
+ return { ok: false, error: 'INVALID_PARAMS', message: 'Each selector needs type name|bot and a value.' };
77
+ }
78
+ normalized.push({ type: selector.type, value });
79
+ }
80
+ const normalizedReason = reason.trim();
81
+ if (!normalizedReason)
82
+ return { ok: false, error: 'INVALID_PARAMS', message: 'reason must not be empty.' };
83
+ const params = { group_id: run.groupId, selectors: normalized };
84
+ if (run.bcsSessionId)
85
+ params.session_id = run.bcsSessionId;
86
+ const response = await this.client.sendRequest('route.resolve', params, 10_000, signal);
87
+ if (!response.ok) {
88
+ return {
89
+ ok: false,
90
+ error: response.error?.code ?? 'ROUTE_RESOLVE_FAILED',
91
+ message: response.error?.message ?? 'BCN could not resolve route targets.',
92
+ };
93
+ }
94
+ const payload = asRecord(response.payload);
95
+ const resolved = normalizeResolvedRouteTargets(payload?.resolved);
96
+ if (payload?.ok !== true || resolved.length === 0) {
97
+ return {
98
+ ok: false,
99
+ error: asNonEmptyString(payload?.error) ?? 'ROUTE_TARGET_INVALID',
100
+ message: asNonEmptyString(payload?.message) ?? 'BCN could not resolve route targets.',
101
+ ...normalizeRouteCandidates(payload?.candidates),
102
+ };
103
+ }
104
+ const incoming = {
105
+ responders: resolved,
106
+ mode: 'required',
107
+ reason: normalizedReason.slice(0, 500),
108
+ includeSelf: false,
109
+ };
110
+ run.route = mergeRouteIntent(run.route, incoming);
111
+ return { ok: true, captured: true, display_to_user: false, resolved };
112
+ }
113
+ async assignTask(agent, targetBot, message, responseMode, signal) {
114
+ const run = this.activeRunForTaskTool(agent, 'manager');
115
+ if (!run)
116
+ return taskToolUnavailable();
117
+ const normalizedTarget = targetBot.trim();
118
+ const normalizedMessage = message.trim();
119
+ if (!normalizedTarget || !normalizedMessage) {
120
+ return taskToolInvalid("'target_bot' and 'message' must be non-empty strings.");
121
+ }
122
+ if (responseMode !== undefined && responseMode !== 'after-last-tool-call' && responseMode !== 'full') {
123
+ return taskToolInvalid("'response_mode' must be 'after-last-tool-call' or 'full'.");
124
+ }
125
+ const params = {
126
+ group_id: run.groupId,
127
+ target_bot: normalizedTarget,
128
+ message: normalizedMessage,
129
+ ...(responseMode ? { response_mode: responseMode } : {}),
130
+ };
131
+ const response = await this.sendTaskRequest('task.dispatch', params, signal);
132
+ if (!response.ok)
133
+ return taskRequestFailure(response, 'TASK_DISPATCH_FAILED');
134
+ const payload = asRecord(response.payload);
135
+ const taskId = asNonEmptyString(payload?.task_id);
136
+ if (!taskId)
137
+ return taskToolInvalidResponse('task.dispatch');
138
+ return {
139
+ ok: true,
140
+ task_id: taskId,
141
+ status: asNonEmptyString(payload?.status) ?? 'dispatched',
142
+ };
143
+ }
144
+ async sendTaskMessage(agent, message, signal) {
145
+ const run = this.activeRunForTaskTool(agent, 'worker');
146
+ if (!run)
147
+ return taskToolUnavailable();
148
+ const normalizedMessage = message.trim();
149
+ if (!normalizedMessage)
150
+ return taskToolInvalid("'message' must be a non-empty string.");
151
+ const response = await this.sendTaskRequest('task.message', {
152
+ group_id: run.groupId,
153
+ message: normalizedMessage,
154
+ }, signal);
155
+ if (!response.ok)
156
+ return taskRequestFailure(response, 'TASK_MESSAGE_FAILED');
157
+ const payload = asRecord(response.payload);
158
+ return { ok: true, status: asNonEmptyString(payload?.status) ?? 'sent' };
159
+ }
160
+ async completeTask(agent, summary, signal) {
161
+ const run = this.activeRunForTaskTool(agent, 'manager');
162
+ if (!run)
163
+ return taskToolUnavailable();
164
+ const normalizedSummary = summary.trim();
165
+ if (!normalizedSummary)
166
+ return taskToolInvalid("'summary' must be a non-empty string.");
167
+ const response = await this.sendTaskRequest('task.complete', {
168
+ group_id: run.groupId,
169
+ summary: normalizedSummary,
170
+ }, signal);
171
+ if (!response.ok)
172
+ return taskRequestFailure(response, 'TASK_COMPLETE_FAILED');
173
+ return { ok: true };
174
+ }
175
+ async handleChatSend(frame) {
176
+ let params;
177
+ try {
178
+ params = parseChatSendParams(frame.params);
179
+ }
180
+ catch (error) {
181
+ this.client.sendResponse(frame.id, false, undefined, invalidRequest(error));
182
+ return;
183
+ }
184
+ const runId = params.idempotency_key ?? frame.id;
185
+ const existing = this.runs.get(runId);
186
+ if (existing) {
187
+ this.client.sendResponse(frame.id, true, { run_id: existing.runId });
188
+ return;
189
+ }
190
+ const sessionIdentity = resolveBcnSessionIdentity({
191
+ sessionKey: params.session_key,
192
+ groupId: params.bcs_group_id,
193
+ ...(params.bcs_session_id ? { bcsSessionId: params.bcs_session_id } : {}),
194
+ });
195
+ const sessionId = dshSessionIdForV2(sessionIdentity);
196
+ const toolProfile = resolveToolProfile(params.session_context);
197
+ const run = {
198
+ runId,
199
+ sessionId,
200
+ groupId: params.bcs_group_id,
201
+ ...(params.bcs_session_id ? { bcsSessionId: params.bcs_session_id } : {}),
202
+ text: '',
203
+ stepText: new Map(),
204
+ terminal: false,
205
+ usage: { input: 0, output: 0 },
206
+ toolNames: new Map(),
207
+ toolStarts: new Set(),
208
+ toolResults: new Set(),
209
+ pendingToolResults: new Map(),
210
+ toolProfile,
211
+ };
212
+ this.runs.set(runId, run);
213
+ this.client.sendResponse(frame.id, true, { run_id: runId });
214
+ try {
215
+ const managed = await this.getOrCreateAgent(sessionIdentity, toolProfile);
216
+ const message = createUserMessage({
217
+ content: [{ type: 'text', text: formatInboundMessage(params) }],
218
+ source: { kind: 'plugin', plugin: 'deepseek-harness-channel-bcn', form: 'relay' },
219
+ });
220
+ this.runIdByMessageId.set(String(message.id), runId);
221
+ managed.agent.followup(message);
222
+ }
223
+ catch {
224
+ this.sendTerminal(run, 'error', 'DELIVERY_FAILED');
225
+ }
226
+ }
227
+ async handleChatInject(frame) {
228
+ let params;
229
+ try {
230
+ params = parseChatInjectParams(frame.params);
231
+ }
232
+ catch (error) {
233
+ this.client.sendResponse(frame.id, false, undefined, invalidRequest(error));
234
+ return;
235
+ }
236
+ try {
237
+ const sessionIdentity = resolveBcnSessionIdentity({
238
+ sessionKey: params.session_key,
239
+ groupId: params.bcs_group_id,
240
+ ...(params.bcs_session_id ? { bcsSessionId: params.bcs_session_id } : {}),
241
+ });
242
+ const managed = await this.getOrCreateAgent(sessionIdentity, resolveToolProfile(params.session_context));
243
+ managed.agent.inject(createUserMessage({
244
+ content: [{ type: 'text', text: formatInboundMessage(params) }],
245
+ source: { kind: 'plugin', plugin: 'deepseek-harness-channel-bcn', form: 'relay' },
246
+ }));
247
+ this.client.sendResponse(frame.id, true, {});
248
+ }
249
+ catch {
250
+ this.client.sendResponse(frame.id, false, undefined, {
251
+ code: 'DELIVERY_FAILED',
252
+ message: 'DeepSeek Harness could not inject the BCN message',
253
+ retryable: true,
254
+ });
255
+ }
256
+ }
257
+ handleInboxClaimed(payload) {
258
+ const runId = this.runIdByMessageId.get(String(payload.message.id));
259
+ if (!runId)
260
+ return;
261
+ const run = this.runs.get(runId);
262
+ if (!run || run.sessionId !== payload.agent.id)
263
+ return;
264
+ this.runIdByMessageId.delete(String(payload.message.id));
265
+ run.turn = payload.turn;
266
+ this.runIdByTurn.set(turnKey(payload.agent.id, payload.turn), runId);
267
+ this.activeRunBySessionId.set(String(payload.agent.id), runId);
268
+ }
269
+ handleInboxDiscarded(payload) {
270
+ const messageId = String(payload.message.id);
271
+ const runId = this.runIdByMessageId.get(messageId);
272
+ if (!runId)
273
+ return;
274
+ this.runIdByMessageId.delete(messageId);
275
+ const run = this.runs.get(runId);
276
+ if (run)
277
+ this.sendTerminal(run, 'aborted');
278
+ }
279
+ handleSessionEvent(session, event) {
280
+ if (this.disposed || !this.sessionIdentityBySessionId.has(String(session.id)))
281
+ return;
282
+ const eventData = asRecord(event.data);
283
+ const turn = typeof eventData?.turn === 'number' ? eventData.turn : undefined;
284
+ if (turn === undefined)
285
+ return;
286
+ const runId = this.runIdByTurn.get(turnKey(session.id, turn));
287
+ const run = runId ? this.runs.get(runId) : undefined;
288
+ if (!run || run.terminal)
289
+ return;
290
+ if (event.type === 'assistant/chunk') {
291
+ const chunk = event.data.chunk;
292
+ if (chunk.type !== 'text-delta' || !chunk.text)
293
+ return;
294
+ this.appendDelta(run, event.data.step, chunk.text);
295
+ return;
296
+ }
297
+ if (event.type === 'assistant/message') {
298
+ this.captureAssistantMessage(run, event.data.step, event.data.message.content, event.data.usage);
299
+ return;
300
+ }
301
+ if (event.type === 'tool/call') {
302
+ this.captureToolCall(run, String(event.data.callId), event.data.name, event.data.arguments);
303
+ return;
304
+ }
305
+ if (event.type === 'tool/result') {
306
+ const block = event.data.message.content[0];
307
+ this.captureToolResult(run, {
308
+ callId: String(block.toolCallId),
309
+ content: block.content,
310
+ isError: block.isError === true || event.data.error !== undefined,
311
+ });
312
+ return;
313
+ }
314
+ if (event.type === 'turn/end') {
315
+ if (event.data.reason.kind === 'aborted')
316
+ this.sendTerminal(run, 'aborted');
317
+ else if (event.data.reason.kind === 'error' || event.data.reason.kind === 'interrupted') {
318
+ const code = event.data.reason.kind === 'error'
319
+ ? safeErrorCode(event.data.reason.error.code)
320
+ : 'INTERRUPTED';
321
+ this.sendTerminal(run, 'error', code);
322
+ }
323
+ else {
324
+ this.sendTerminal(run, 'final', undefined, event.data.reason.kind);
325
+ }
326
+ }
327
+ }
328
+ appendDelta(run, step, text) {
329
+ run.text += text;
330
+ run.stepText.set(step, `${run.stepText.get(step) ?? ''}${text}`);
331
+ this.client.sendEvent('chat.event', {
332
+ run_id: run.runId,
333
+ bcs_group_id: run.groupId,
334
+ state: 'delta',
335
+ delta_text: text,
336
+ });
337
+ }
338
+ captureAssistantMessage(run, step, content, usage) {
339
+ if (usage) {
340
+ run.usage.input += usage.inputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0);
341
+ run.usage.output += usage.outputTokens;
342
+ }
343
+ const assembled = visibleText(content);
344
+ const streamed = run.stepText.get(step) ?? '';
345
+ if (!assembled || assembled === streamed)
346
+ return;
347
+ if (assembled.startsWith(streamed)) {
348
+ this.appendDelta(run, step, assembled.slice(streamed.length));
349
+ }
350
+ else if (!streamed) {
351
+ this.appendDelta(run, step, assembled);
352
+ }
353
+ }
354
+ captureToolCall(run, callId, name, rawArguments) {
355
+ if (run.toolStarts.has(callId))
356
+ return;
357
+ run.toolNames.set(callId, name);
358
+ let args = rawArguments;
359
+ try {
360
+ args = JSON.parse(rawArguments);
361
+ }
362
+ catch {
363
+ // Preserve the provider's raw argument string when it is not complete JSON.
364
+ }
365
+ this.client.sendEvent('agent', {
366
+ run_id: run.runId,
367
+ bcs_group_id: run.groupId,
368
+ stream: 'tool',
369
+ ts: Date.now(),
370
+ data: { phase: 'start', toolCallId: callId, name, args },
371
+ });
372
+ run.toolStarts.add(callId);
373
+ const pending = run.pendingToolResults.get(callId);
374
+ if (pending) {
375
+ this.sendToolResult(run, pending);
376
+ run.pendingToolResults.delete(callId);
377
+ }
378
+ }
379
+ captureToolResult(run, result) {
380
+ if (run.toolResults.has(result.callId))
381
+ return;
382
+ if (!run.toolNames.has(result.callId)) {
383
+ run.pendingToolResults.set(result.callId, result);
384
+ return;
385
+ }
386
+ this.sendToolResult(run, result);
387
+ }
388
+ sendToolResult(run, result) {
389
+ if (run.toolResults.has(result.callId))
390
+ return;
391
+ this.client.sendEvent('agent', {
392
+ run_id: run.runId,
393
+ bcs_group_id: run.groupId,
394
+ stream: 'tool',
395
+ ts: Date.now(),
396
+ data: {
397
+ phase: 'result',
398
+ toolCallId: result.callId,
399
+ name: run.toolNames.get(result.callId) ?? 'unknown',
400
+ result: { content: result.content },
401
+ isError: result.isError,
402
+ },
403
+ });
404
+ run.toolResults.add(result.callId);
405
+ }
406
+ sendTerminal(run, state, errorCode, stopReason) {
407
+ if (run.terminal)
408
+ return;
409
+ for (const pending of run.pendingToolResults.values())
410
+ this.sendToolResult(run, pending);
411
+ run.pendingToolResults.clear();
412
+ const payload = {
413
+ run_id: run.runId,
414
+ bcs_group_id: run.groupId,
415
+ state,
416
+ };
417
+ if (state === 'final') {
418
+ if (run.text)
419
+ payload.message = assistantMessage(run.text);
420
+ if (run.usage.input || run.usage.output)
421
+ payload.usage = run.usage;
422
+ if (stopReason)
423
+ payload.stop_reason = stopReason;
424
+ if (run.route)
425
+ payload.routing = routeWire(run.route);
426
+ }
427
+ else if (state === 'error') {
428
+ payload.errorMessage = 'DeepSeek Harness run failed';
429
+ payload.errorKind = 'dsh_turn';
430
+ payload.errorCode = errorCode ?? 'UNKNOWN';
431
+ }
432
+ else {
433
+ payload.stop_reason = 'aborted';
434
+ }
435
+ this.client.sendEvent('chat.event', payload);
436
+ run.terminal = true;
437
+ this.activeRunBySessionId.delete(String(run.sessionId));
438
+ if (run.turn !== undefined)
439
+ this.runIdByTurn.delete(turnKey(run.sessionId, run.turn));
440
+ this.completedRunIds.push(run.runId);
441
+ while (this.completedRunIds.length > 2_000) {
442
+ const oldest = this.completedRunIds.shift();
443
+ if (oldest)
444
+ this.runs.delete(oldest);
445
+ }
446
+ }
447
+ async getOrCreateAgent(sessionIdentity, toolProfile) {
448
+ const existing = this.managedAgents.get(sessionIdentity);
449
+ if (existing) {
450
+ const managed = await existing;
451
+ if (managed.toolProfile !== toolProfile) {
452
+ throw new Error('BCN session coordination role changed after its DSH agent was created');
453
+ }
454
+ return managed;
455
+ }
456
+ const pending = this.createAgent(sessionIdentity, toolProfile).catch(error => {
457
+ this.managedAgents.delete(sessionIdentity);
458
+ throw error;
459
+ });
460
+ this.managedAgents.set(sessionIdentity, pending);
461
+ return pending;
462
+ }
463
+ async createAgent(sessionIdentity, toolProfile) {
464
+ const sessionId = dshSessionIdForV2(sessionIdentity);
465
+ if (this.ctx.agents.get(sessionId)) {
466
+ throw new Error('A non-BCN DSH agent already owns the derived BCN session id');
467
+ }
468
+ const persistedHeader = (await this.ctx.sessionPersistence.list()).find(header => header.id === sessionId);
469
+ const cwd = process.cwd();
470
+ const bcnCtx = this.ctx;
471
+ const defaultSelection = bcnCtx.agentDefaultModel.currentSelection();
472
+ const recordedPreset = persistedHeader
473
+ ? presetForInspection(await this.ctx.sessionPersistence.inspect(sessionId))
474
+ : undefined;
475
+ const presetId = (await bcnCtx.agentPresets.resolve(recordedPreset)).id;
476
+ const setup = async (agentCtx) => {
477
+ installModelSelection(agentCtx, {
478
+ current: selectionForSession(agentCtx.agent, defaultSelection),
479
+ assembled: undefined,
480
+ });
481
+ agentCtx.systemPrompt.variable('cwd', () => agentCtx.agent?.session.header.cwd ?? cwd);
482
+ await bcnCtx.agentPresets.mount(agentCtx, presetId);
483
+ if (toolProfile === 'route') {
484
+ agentCtx.tools.register(createBcsRouteTool(this));
485
+ }
486
+ else if (toolProfile === 'manager') {
487
+ agentCtx.tools.register(createBcsAssignTaskTool(this));
488
+ agentCtx.tools.register(createBcsTaskCompleteTool(this));
489
+ }
490
+ else if (toolProfile === 'worker') {
491
+ agentCtx.tools.register(createBcsSendTaskMessageTool(this));
492
+ }
493
+ };
494
+ const agentOptions = {
495
+ provider: defaultSelection.provider,
496
+ model: defaultSelection.model,
497
+ };
498
+ const handle = persistedHeader
499
+ ? await this.ctx.agents.resume({ resumeSessionId: sessionId, agentOptions, setup })
500
+ : await this.ctx.agents.create({
501
+ sessionId,
502
+ meta: { cwd, agentPreset: presetId },
503
+ agentOptions,
504
+ setup,
505
+ });
506
+ const managed = { sessionIdentity, sessionId, agent: handle.agent, handle, toolProfile };
507
+ this.ownedAgents.add(handle);
508
+ this.sessionIdentityBySessionId.set(String(sessionId), sessionIdentity);
509
+ return managed;
510
+ }
511
+ activeRunForTaskTool(agent, requiredProfile) {
512
+ if (!agent)
513
+ return undefined;
514
+ const runId = this.activeRunBySessionId.get(String(agent.id));
515
+ const run = runId ? this.runs.get(runId) : undefined;
516
+ return run && !run.terminal && run.toolProfile === requiredProfile ? run : undefined;
517
+ }
518
+ async sendTaskRequest(method, params, signal) {
519
+ if (!this.client.connected) {
520
+ return {
521
+ type: 'res',
522
+ id: 'local-not-connected',
523
+ ok: false,
524
+ error: {
525
+ code: 'BCN_NOT_CONNECTED',
526
+ message: 'BCN WebSocket is not connected.',
527
+ retryable: true,
528
+ },
529
+ };
530
+ }
531
+ try {
532
+ return await this.client.sendRequest(method, params, 30_000, signal);
533
+ }
534
+ catch {
535
+ return {
536
+ type: 'res',
537
+ id: 'local-request-failed',
538
+ ok: false,
539
+ error: {
540
+ code: 'BCN_REQUEST_FAILED',
541
+ message: 'BCN task request failed.',
542
+ retryable: true,
543
+ },
544
+ };
545
+ }
546
+ }
547
+ }
548
+ function presetForInspection(inspection) {
549
+ for (let index = inspection.events.length - 1; index >= 0; index -= 1) {
550
+ const event = asRecord(inspection.events[index]);
551
+ if (event?.type !== 'agent-preset/selected')
552
+ continue;
553
+ const selected = asNonEmptyString(asRecord(event.data)?.agentPreset);
554
+ if (selected)
555
+ return selected;
556
+ }
557
+ return inspection.meta.agentPreset;
558
+ }
559
+ function selectionForSession(agent, fallback) {
560
+ const recorded = agent?.session.requestHeader()?.config;
561
+ if (!recorded)
562
+ return fallback;
563
+ return {
564
+ provider: recorded.provider,
565
+ model: recorded.model,
566
+ ...(recorded.reasoningEffort ? { reasoningEffort: recorded.reasoningEffort } : {}),
567
+ };
568
+ }
569
+ function resolveToolProfile(sessionContext) {
570
+ const groupType = asNonEmptyString(sessionContext.group_type);
571
+ if (groupType === 'manager_worker') {
572
+ // COSEC: task-tool authority comes only from the authenticated BCN
573
+ // downlink context. Missing or unknown roles fail closed.
574
+ const recipientRole = asNonEmptyString(sessionContext.recipient_role);
575
+ if (recipientRole === 'manager')
576
+ return 'manager';
577
+ if (recipientRole === 'worker')
578
+ return 'worker';
579
+ return 'none';
580
+ }
581
+ return asNonEmptyString(sessionContext.routing_mode) === 'mention' ? 'none' : 'route';
582
+ }
583
+ function formatInboundMessage(params) {
584
+ const text = extractMessageText(params.message);
585
+ const metadata = {
586
+ protocol_version: 2,
587
+ bcs_group_id: params.bcs_group_id,
588
+ ...(params.bcs_session_id ? { bcs_session_id: params.bcs_session_id } : {}),
589
+ channel: params.channel,
590
+ group_context: params.session_context,
591
+ ...(params.tags?.length ? { tags: params.tags } : {}),
592
+ ...(params.attachments?.length ? { attachments: params.attachments } : {}),
593
+ };
594
+ return `<bcn_context>\n${JSON.stringify(metadata)}\n</bcn_context>\n\n${text}`;
595
+ }
596
+ function assistantMessage(text) {
597
+ return { role: 'assistant', content: [{ type: 'text', text }], timestamp: Date.now() };
598
+ }
599
+ function visibleText(content) {
600
+ return content.flatMap(block => block.type === 'text' ? [block.text] : []).join('');
601
+ }
602
+ function turnKey(sessionId, turn) {
603
+ return `${String(sessionId)}:${turn}`;
604
+ }
605
+ function invalidRequest(error) {
606
+ return {
607
+ code: 'INVALID_REQUEST',
608
+ message: error instanceof Error ? error.message : 'Invalid BCN request',
609
+ retryable: false,
610
+ };
611
+ }
612
+ function safeErrorCode(value) {
613
+ const code = asNonEmptyString(value);
614
+ return code && /^[A-Za-z0-9_.-]{1,80}$/.test(code) ? code : 'UNKNOWN';
615
+ }
616
+ function taskToolUnavailable() {
617
+ return {
618
+ ok: false,
619
+ error: 'TOOL_NOT_ALLOWED',
620
+ message: 'This BCN task tool is not available for the active session role.',
621
+ };
622
+ }
623
+ function taskToolInvalid(message) {
624
+ return { ok: false, error: 'INVALID_PARAMS', message };
625
+ }
626
+ function taskToolInvalidResponse(method) {
627
+ return {
628
+ ok: false,
629
+ error: 'INVALID_RESPONSE',
630
+ message: `BCN returned an invalid ${method} response.`,
631
+ };
632
+ }
633
+ function taskRequestFailure(response, fallbackCode) {
634
+ return {
635
+ ok: false,
636
+ error: response.error?.code ?? fallbackCode,
637
+ message: response.error?.message ?? 'BCN rejected the task request.',
638
+ };
639
+ }
640
+ function normalizeResolvedRouteTargets(value) {
641
+ if (!Array.isArray(value))
642
+ return [];
643
+ const targets = [];
644
+ for (const item of value) {
645
+ const record = asRecord(item);
646
+ const target = asNonEmptyString(record?.value);
647
+ if (record?.type === 'bot' && target)
648
+ targets.push({ type: 'bot', value: target });
649
+ }
650
+ return targets;
651
+ }
652
+ function normalizeRouteCandidates(value) {
653
+ if (!Array.isArray(value))
654
+ return {};
655
+ const candidates = [];
656
+ for (const item of value) {
657
+ const record = asRecord(item);
658
+ const botUuid = asNonEmptyString(record?.bot_uuid);
659
+ if (!botUuid)
660
+ continue;
661
+ const botName = asNonEmptyString(record?.bot_name);
662
+ const role = asNonEmptyString(record?.role);
663
+ candidates.push({
664
+ bot_uuid: botUuid,
665
+ ...(botName ? { bot_name: botName } : {}),
666
+ ...(role ? { role } : {}),
667
+ });
668
+ }
669
+ return candidates.length ? { candidates } : {};
670
+ }
671
+ function mergeRouteIntent(existing, incoming) {
672
+ if (!existing)
673
+ return incoming;
674
+ const keys = new Set(existing.responders.map(item => `${item.type}:${item.value ?? ''}`));
675
+ for (const responder of incoming.responders) {
676
+ if (existing.responders.length >= 20)
677
+ break;
678
+ const key = `${responder.type}:${responder.value ?? ''}`;
679
+ if (!keys.has(key))
680
+ existing.responders.push(responder);
681
+ keys.add(key);
682
+ }
683
+ existing.reason = `${existing.reason}; ${incoming.reason}`.slice(0, 500);
684
+ return existing;
685
+ }
686
+ function routeWire(intent) {
687
+ return {
688
+ responders: intent.responders,
689
+ mode: intent.mode,
690
+ reason: intent.reason,
691
+ include_self: intent.includeSelf,
692
+ ...(intent.dedupeKey ? { dedupe_key: intent.dedupeKey } : {}),
693
+ };
694
+ }
695
+ //# sourceMappingURL=bridge.js.map