dsh-plugin-subagent-bridge 0.14.0 → 0.14.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +84 -4
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -309,6 +309,57 @@ const TOOLS = [
|
|
|
309
309
|
},
|
|
310
310
|
]
|
|
311
311
|
|
|
312
|
+
/**
|
|
313
|
+
* Take over the questions a worker asks, so the calling agent can answer them.
|
|
314
|
+
*
|
|
315
|
+
* `ask_user_question` pauses the tool call until something answers on the
|
|
316
|
+
* `user-questions/request` waterfall. The Web UI answers from the browser; for a worker
|
|
317
|
+
* nobody is watching, the bridge answers instead and forwards the question to its caller.
|
|
318
|
+
* @param state - bridge state.
|
|
319
|
+
* @param agent - live agent of the worker session.
|
|
320
|
+
*/
|
|
321
|
+
function attachQuestionAnswerer(state, agent) {
|
|
322
|
+
if (agent?.ctx === undefined || typeof agent.ctx.on !== 'function') return
|
|
323
|
+
state.openQuestions ??= new Map()
|
|
324
|
+
state.questionAnswerers ??= new Map()
|
|
325
|
+
const sessionId = String(agent.id)
|
|
326
|
+
if (state.questionAnswerers.has(sessionId)) return
|
|
327
|
+
const handler = (request, next) => {
|
|
328
|
+
const questions = Array.isArray(request?.questions) ? request.questions : []
|
|
329
|
+
if (questions.length === 0) return next()
|
|
330
|
+
return new Promise((resolve, reject) => {
|
|
331
|
+
const waiter = {
|
|
332
|
+
questions,
|
|
333
|
+
settle(answer) {
|
|
334
|
+
state.openQuestions.delete(sessionId)
|
|
335
|
+
waiter.cleanup()
|
|
336
|
+
resolve(answer)
|
|
337
|
+
},
|
|
338
|
+
fail(error) {
|
|
339
|
+
state.openQuestions.delete(sessionId)
|
|
340
|
+
waiter.cleanup()
|
|
341
|
+
reject(error)
|
|
342
|
+
},
|
|
343
|
+
}
|
|
344
|
+
const signal = request?.signal
|
|
345
|
+
const onAbort = () => waiter.fail(new Error('the question was withdrawn'))
|
|
346
|
+
waiter.cleanup = () => signal?.removeEventListener?.('abort', onAbort)
|
|
347
|
+
if (signal?.aborted === true) {
|
|
348
|
+
waiter.fail(new Error('the question was withdrawn'))
|
|
349
|
+
return
|
|
350
|
+
}
|
|
351
|
+
signal?.addEventListener?.('abort', onAbort)
|
|
352
|
+
state.openQuestions.set(sessionId, waiter)
|
|
353
|
+
})
|
|
354
|
+
}
|
|
355
|
+
try {
|
|
356
|
+
const off = agent.ctx.on('user-questions/request', handler)
|
|
357
|
+
state.questionAnswerers.set(sessionId, typeof off === 'function' ? off : () => {})
|
|
358
|
+
} catch {
|
|
359
|
+
/* An older Host without the waterfall: the continued-state path still works. */
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
312
363
|
/**
|
|
313
364
|
* The question a worker is currently blocked on, if any.
|
|
314
365
|
* `ask_user_question` records a tool call and waits for an answer; while it waits the call
|
|
@@ -321,13 +372,33 @@ function askable(state, ctx, targets) {
|
|
|
321
372
|
for (const target of targets) {
|
|
322
373
|
const resolved = resolveWorker(state, target)
|
|
323
374
|
const sessionId = resolved === undefined ? target : resolved.sessionId
|
|
324
|
-
const asked = pendingQuestion(ctx, sessionId)
|
|
375
|
+
const asked = pendingQuestion(ctx, sessionId, state)
|
|
325
376
|
if (asked !== undefined) return { worker: target, ...asked }
|
|
326
377
|
}
|
|
327
378
|
return undefined
|
|
328
379
|
}
|
|
329
380
|
|
|
330
|
-
export function pendingQuestion(ctx, sessionId) {
|
|
381
|
+
export function pendingQuestion(ctx, sessionId, state) {
|
|
382
|
+
const open = state?.openQuestions?.get(sessionId)
|
|
383
|
+
if (open !== undefined) {
|
|
384
|
+
return {
|
|
385
|
+
callId: `open:${sessionId}`,
|
|
386
|
+
questions: open.questions.map((question) => ({
|
|
387
|
+
id: String(question?.id ?? ''),
|
|
388
|
+
question: String(question?.question ?? ''),
|
|
389
|
+
...(typeof question?.header === 'string' ? { header: question.header } : {}),
|
|
390
|
+
...(question?.multiSelect === true ? { multiSelect: true } : {}),
|
|
391
|
+
...(Array.isArray(question?.options)
|
|
392
|
+
? {
|
|
393
|
+
options: question.options.map((option) => ({
|
|
394
|
+
label: String(option?.label ?? ''),
|
|
395
|
+
...(typeof option?.description === 'string' ? { description: option.description } : {}),
|
|
396
|
+
})),
|
|
397
|
+
}
|
|
398
|
+
: {}),
|
|
399
|
+
})),
|
|
400
|
+
}
|
|
401
|
+
}
|
|
331
402
|
const session = ctx.get('sessions')?.get?.(sessionId)
|
|
332
403
|
if (session === undefined || session === null) return undefined
|
|
333
404
|
const settled = new Set()
|
|
@@ -393,7 +464,7 @@ export function apply(ctx, config = {}) {
|
|
|
393
464
|
const settings = resolveSettings(config)
|
|
394
465
|
if (settings.enabled !== true) return
|
|
395
466
|
|
|
396
|
-
const state = { dispatched: new Set(), children: new Set() }
|
|
467
|
+
const state = { dispatched: new Set(), children: new Set(), openQuestions: new Map(), questionAnswerers: new Map() }
|
|
397
468
|
const api = createApi(ctx, state, settings)
|
|
398
469
|
let bridge
|
|
399
470
|
|
|
@@ -890,6 +961,7 @@ export function createApi(ctx, state, settings) {
|
|
|
890
961
|
}
|
|
891
962
|
|
|
892
963
|
const appliedModel = await applyModelSpec(ctx, sessionId, model, reasoningEffort)
|
|
964
|
+
attachQuestionAnswerer(state, ctx.get('agents')?.get?.(sessionId))
|
|
893
965
|
const before = Number(sessions()?.get?.(sessionId)?.seq ?? 0)
|
|
894
966
|
const requestId = randomUUID()
|
|
895
967
|
const accepted = await service.prompt(
|
|
@@ -998,6 +1070,7 @@ export function createApi(ctx, state, settings) {
|
|
|
998
1070
|
const appliedModel = await applyModelSpec(ctx, targetId, model, reasoningEffort)
|
|
999
1071
|
/* DSH delivers a prompt either queued behind the running turn or steered into it. */
|
|
1000
1072
|
const deliveryMode = mode === 'steer' ? 'steer' : 'queue'
|
|
1073
|
+
attachQuestionAnswerer(state, ctx.get('agents')?.get?.(targetId))
|
|
1001
1074
|
const turnBefore = lastTurn(ctx, targetId)
|
|
1002
1075
|
/* A steered message is absorbed by the turn that is already running, so waiting for
|
|
1003
1076
|
* a strictly newer turn would hang until the timeout even after the answer landed. */
|
|
@@ -1068,6 +1141,7 @@ export function createApi(ctx, state, settings) {
|
|
|
1068
1141
|
let settled = false
|
|
1069
1142
|
for (const target of targets) {
|
|
1070
1143
|
const resolved = resolveWorker(state, target)
|
|
1144
|
+
if (resolved !== undefined) attachQuestionAnswerer(state, ctx.get('agents')?.get?.(resolved.sessionId))
|
|
1071
1145
|
const sessionId = resolved === undefined ? target : resolved.sessionId
|
|
1072
1146
|
const value = resolved === undefined ? 'not_found' : workerStatus(ctx, sessionId, state, settings)
|
|
1073
1147
|
if (resolved !== undefined) await rememberOutcome(settings, state, sessionId, value)
|
|
@@ -1104,7 +1178,13 @@ export function createApi(ctx, state, settings) {
|
|
|
1104
1178
|
async answer({ worker, sessionId, answers }) {
|
|
1105
1179
|
const target = worker === undefined ? sessionId : (resolveWorker(state, worker)?.sessionId ?? worker)
|
|
1106
1180
|
if (typeof target !== 'string' || target === '') throw new Error('name a worker or a sessionId')
|
|
1107
|
-
const
|
|
1181
|
+
const open = state.openQuestions?.get(target)
|
|
1182
|
+
if (open !== undefined) {
|
|
1183
|
+
/* The tool call is still blocked, so hand the batch straight to its waiter. */
|
|
1184
|
+
open.settle({ answers })
|
|
1185
|
+
return { answered: true, callId: `open:${target}`, sessionId: target }
|
|
1186
|
+
}
|
|
1187
|
+
const asked = pendingQuestion(ctx, target, state)
|
|
1108
1188
|
if (asked === undefined) throw new Error('that worker is not waiting on a question')
|
|
1109
1189
|
const service = ctx.get('userQuestions')
|
|
1110
1190
|
const agent = ctx.get('agents')?.get?.(target)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-subagent-bridge",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Subagent bridge: lets Codex delegate a task to DeepSeek Harness the way it would spawn a subagent (one call, waits for the answer), plus a codex_dispatch tool for the reverse direction.",
|
|
6
6
|
"keywords": [
|