dsh-my-go 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.
package/lib/index.js ADDED
@@ -0,0 +1,707 @@
1
+ /**
2
+ * dsh-my-go — Sisyphus agent orchestration (HOST half, npm bundle).
3
+ *
4
+ * The host plugin of the `dsh-my-go` npm package. Registered through the
5
+ * package's own `cordis.patch.yml` (dsh.bundle.patch), so `dsh plugin add
6
+ * dsh-my-go` activates it automatically as a profile layer.
7
+ *
8
+ * Provides:
9
+ * - orchestration tools: go_work / continue / need_help / forward /
10
+ * orchestration_status
11
+ * - per-agent model/effort binding at the `agent/request` waterfall
12
+ * - conclusion injection + queue advancement on `subagent/end`
13
+ * - settings namespace `dsh-my-go` (provider/model/reasoningEffort/
14
+ * dsv4p0813 per agent type) when a settings service is mounted
15
+ */
16
+
17
+ export const name = 'dsh-my-go'
18
+
19
+ export const inject = ['tools', 'subagents', 'systemPrompt']
20
+
21
+ const AGENT_TYPES = ['hermes', 'explore', 'librarian', 'looker', 'hephaestus', 'prometheus', 'oracle']
22
+
23
+ const AGENT_TYPE_PREFIX = 'dsh-my-go:'
24
+
25
+ function agentLabel(type, summary) {
26
+ return `${AGENT_TYPE_PREFIX}${type}${summary ? `: ${summary}` : ''}`
27
+ }
28
+
29
+ function parseAgentType(label) {
30
+ if (typeof label !== 'string') return undefined
31
+ const match = /^dsh-my-go:([a-z-]+)/.exec(label)
32
+ if (!match) return undefined
33
+ return AGENT_TYPES.includes(match[1]) ? match[1] : undefined
34
+ }
35
+
36
+ /** Default bindings per AGENTS.md. Provider stays unset for light agents so
37
+ * they inherit Sisyphus's route; heavy agents pin the octopus gateway.
38
+ * reasoningEffort is only ever applied when the exact model supports that
39
+ * level (checked against the DSH model catalog at request time); light agents
40
+ * leave it unset so the model's own default applies. */
41
+ function defaultBindings() {
42
+ return {
43
+ sisyphus: { dsv4p0813: false },
44
+ hermes: { model: 'mimo-v2.5', dsv4p0813: false },
45
+ explore: { model: 'mimo-v2.5', dsv4p0813: false },
46
+ librarian: { model: 'mimo-v2.5', dsv4p0813: false },
47
+ looker: { model: 'mimo-v2.5', dsv4p0813: false },
48
+ hephaestus: { provider: 'octopus', model: 'deepseek-v4-flash', reasoningEffort: 'high', dsv4p0813: false },
49
+ prometheus: { provider: 'octopus', model: 'deepseek-v4-pro', reasoningEffort: 'max', dsv4p0813: false },
50
+ oracle: { provider: 'octopus', model: 'deepseek-v4-pro', reasoningEffort: 'max', dsv4p0813: false },
51
+ }
52
+ }
53
+
54
+ function describeAgent(type) {
55
+ switch (type) {
56
+ case 'hermes': return 'fast execution: batch replace, formatting, imports, copy-paste'
57
+ case 'explore': return 'fast search: grep, read files, locate symbols, scan structure'
58
+ case 'librarian': return 'document lookup: README, API reference, comments'
59
+ case 'looker': return 'multimodal recognition: UI screenshots, designs, PDF charts'
60
+ case 'hephaestus': return 'code writing: single-file refactor, module implementation, unit tests'
61
+ case 'prometheus': return 'requirement planning: break vague requirements into executable steps (call once at flow start)'
62
+ case 'oracle': return 'architecture debugging + final acceptance: cross-module analysis, deep bugs, review'
63
+ }
64
+ }
65
+
66
+ let seq = 0
67
+ function nextId(prefix) {
68
+ seq += 1
69
+ return `${prefix}-${Date.now().toString(36)}-${seq.toString(36)}`
70
+ }
71
+
72
+ /** Minimal single-line-blocking orchestration state. */
73
+ class Orchestration {
74
+ constructor() {
75
+ this.currentMap = new Map()
76
+ this.queue = []
77
+ this.helpRequests = new Map()
78
+ this.history = []
79
+ this.listeners = new Set()
80
+ }
81
+
82
+ onChange(listener) {
83
+ this.listeners.add(listener)
84
+ return () => this.listeners.delete(listener)
85
+ }
86
+
87
+ snapshot() {
88
+ return {
89
+ current: this.currentMap.size > 0 ? [...this.currentMap.values()][0] ?? null : null,
90
+ queue: [...this.queue],
91
+ helpRequests: [...this.helpRequests.values()],
92
+ history: [...this.history],
93
+ }
94
+ }
95
+
96
+ emit() {
97
+ const snapshot = this.snapshot()
98
+ for (const listener of [...this.listeners]) {
99
+ try { listener(snapshot) } catch { /* noop */ }
100
+ }
101
+ }
102
+
103
+ isBusy() { return this.currentMap.size > 0 }
104
+
105
+ enqueue(agentType, prompt, parentId) {
106
+ const id = nextId('work')
107
+ this.queue.push({ id, agentType, prompt, parentId, createdAt: Date.now() })
108
+ this.emit()
109
+ return id
110
+ }
111
+
112
+ beginSpawning(agentType, prompt) {
113
+ const record = {
114
+ childId: nextId('child'),
115
+ agentType,
116
+ prompt,
117
+ status: 'spawning',
118
+ createdAt: Date.now(),
119
+ updatedAt: Date.now(),
120
+ }
121
+ this.currentMap.set(record.childId, record)
122
+ this.emit()
123
+ return record
124
+ }
125
+
126
+ bindChild(placeholderId, childId) {
127
+ const record = this.currentMap.get(placeholderId)
128
+ if (!record) return undefined
129
+ this.currentMap.delete(placeholderId)
130
+ const next = { ...record, childId, status: 'running', updatedAt: Date.now() }
131
+ this.currentMap.set(childId, next)
132
+ this.emit()
133
+ return next
134
+ }
135
+
136
+ dequeue() {
137
+ const work = this.queue.shift()
138
+ if (work) this.emit()
139
+ return work
140
+ }
141
+
142
+ suspend(childId, help) {
143
+ const record = this.currentMap.get(childId)
144
+ if (!record) return undefined
145
+ this.helpRequests.set(help.id, help)
146
+ const next = { ...record, status: 'waiting', updatedAt: Date.now() }
147
+ this.currentMap.set(childId, next)
148
+ this.emit()
149
+ return next
150
+ }
151
+
152
+ resolveHelp(id) {
153
+ const help = this.helpRequests.get(id)
154
+ if (help) { this.helpRequests.delete(id); this.emit() }
155
+ return help
156
+ }
157
+
158
+ resume(childId) {
159
+ const record = this.currentMap.get(childId)
160
+ if (!record || record.status !== 'waiting') return record
161
+ const next = { ...record, status: 'running', updatedAt: Date.now() }
162
+ this.currentMap.set(childId, next)
163
+ this.emit()
164
+ return next
165
+ }
166
+
167
+ finish(childId, conclusion, failed = false) {
168
+ const record = this.currentMap.get(childId)
169
+ if (!record) return undefined
170
+ const conclusionId = nextId('conclusion')
171
+ const done = {
172
+ ...record,
173
+ status: failed ? 'failed' : 'done',
174
+ conclusion,
175
+ conclusionId,
176
+ updatedAt: Date.now(),
177
+ }
178
+ this.currentMap.delete(childId)
179
+ this.history = [...this.history, done]
180
+ if (this.history.length > 200) this.history = this.history.slice(-200)
181
+ this.emit()
182
+ return done
183
+ }
184
+
185
+ abort(childId) {
186
+ this.currentMap.delete(childId)
187
+ this.emit()
188
+ }
189
+
190
+ record(childId) {
191
+ return this.currentMap.get(childId) ?? this.history.find((r) => r.childId === childId)
192
+ }
193
+
194
+ /** Record the latest prompt Sisyphus sent to one child (go_work or continue). */
195
+ followupPrompt(childId, prompt) {
196
+ const rec = this.currentMap.get(childId)
197
+ if (rec) {
198
+ this.currentMap.set(childId, { ...rec, prompt, updatedAt: Date.now() })
199
+ this.emit()
200
+ return this.currentMap.get(childId)
201
+ }
202
+ const idx = this.history.findIndex((r) => r.childId === childId)
203
+ if (idx >= 0) {
204
+ const next = { ...this.history[idx], prompt, updatedAt: Date.now() }
205
+ this.history = [...this.history.slice(0, idx), next, ...this.history.slice(idx + 1)]
206
+ this.emit()
207
+ return next
208
+ }
209
+ return undefined
210
+ }
211
+
212
+ help(id) { return this.helpRequests.get(id) }
213
+ }
214
+
215
+ export async function apply(ctx, config = {}) {
216
+ const orchestration = new Orchestration()
217
+ const sessionTypes = new Map()
218
+ let bindings = { ...defaultBindings(), ...(config.bindings ?? {}) }
219
+ const bindSisyphus = config.bindSisyphus === true
220
+
221
+ // Track the Sisyphus root session id: the first agent that calls a broker
222
+ // tool is treated as the root. Children cannot delegate (guards below).
223
+ let rootSessionId = null
224
+ const isSisyphus = (agent) => {
225
+ if (!agent || typeof agent.id !== 'string') return false
226
+ if (rootSessionId === null) rootSessionId = agent.id
227
+ return agent.id === rootSessionId
228
+ }
229
+ // ── settings-backed bindings (WebUI configurable) ───────────────────────
230
+ const settings = ctx.get('settings')
231
+ let settingsScope
232
+ if (settings !== undefined) {
233
+ try {
234
+ // Dynamic import so a loader without npm-package resolution for local
235
+ // mjs files degrades to defaults instead of failing the preset mount.
236
+ const mod = await import('@deepseek-ai/schemastery')
237
+ const z = mod.default ?? mod
238
+ const agentSchema = z.object({
239
+ provider: z.string(),
240
+ model: z.string(),
241
+ reasoningEffort: z.string(),
242
+ dsv4p0813: z.boolean(),
243
+ })
244
+ settingsScope = settings.register(
245
+ 'dsh-my-go',
246
+ z.object({
247
+ sisyphus: agentSchema,
248
+ hermes: agentSchema,
249
+ explore: agentSchema,
250
+ librarian: agentSchema,
251
+ looker: agentSchema,
252
+ hephaestus: agentSchema,
253
+ prometheus: agentSchema,
254
+ oracle: agentSchema,
255
+ }),
256
+ {},
257
+ )
258
+ const stored = settings.get('dsh-my-go')
259
+ if (stored && typeof stored === 'object') {
260
+ const merged = { ...bindings }
261
+ for (const key of ['sisyphus', ...AGENT_TYPES]) {
262
+ const row = stored[key]
263
+ if (row && typeof row === 'object') {
264
+ merged[key] = {
265
+ provider: row.provider ?? merged[key]?.provider,
266
+ model: row.model ?? merged[key]?.model,
267
+ reasoningEffort: row.reasoningEffort ?? merged[key]?.reasoningEffort,
268
+ dsv4p0813: row.dsv4p0813 ?? merged[key]?.dsv4p0813 ?? false,
269
+ }
270
+ }
271
+ }
272
+ bindings = merged
273
+ }
274
+ ctx.on('settings/updated', (ns) => {
275
+ if (ns !== 'dsh-my-go') return
276
+ const next = settings.get('dsh-my-go')
277
+ if (next && typeof next === 'object') {
278
+ const merged = { ...bindings }
279
+ for (const key of ['sisyphus', ...AGENT_TYPES]) {
280
+ const row = next[key]
281
+ if (row && typeof row === 'object') {
282
+ merged[key] = {
283
+ provider: row.provider ?? merged[key]?.provider,
284
+ model: row.model ?? merged[key]?.model,
285
+ reasoningEffort: row.reasoningEffort ?? merged[key]?.reasoningEffort,
286
+ dsv4p0813: row.dsv4p0813 ?? merged[key]?.dsv4p0813 ?? false,
287
+ }
288
+ }
289
+ }
290
+ bindings = merged
291
+ }
292
+ })
293
+ } catch {
294
+ // Settings optional — defaults apply.
295
+ }
296
+ }
297
+
298
+ // ── notify a client bridge (optional harness handle) ────────────────────
299
+ let latestSnapshot = null
300
+ let snapshotSeq = 0
301
+ const bump = () => {
302
+ snapshotSeq += 1
303
+ latestSnapshot = { seq: snapshotSeq, ...orchestration.snapshot() }
304
+ }
305
+ orchestration.onChange(() => bump())
306
+ const harness = globalThis.harness
307
+ const disposers = []
308
+ if (harness && typeof harness.handle === 'function') {
309
+ disposers.push(harness.handle('dsh-my-go/snapshot', async () => latestSnapshot ?? { seq: 0, current: null, queue: [], helpRequests: [], history: [] }))
310
+ disposers.push(harness.handle('dsh-my-go/notify', async () => { bump(); return { seq: snapshotSeq } }))
311
+ }
312
+
313
+ // ── internal go_work implementation (shared by the tool, forward, queue) ─
314
+ async function dispatchWork(agentType, prompt, parent, signal) {
315
+ if (!AGENT_TYPES.includes(agentType)) throw new Error(`unknown agent type: ${String(agentType)}`)
316
+ const binding = bindings[agentType] ?? {}
317
+ // Parent may be absent during queue advancement (agent object not retained);
318
+ // fall back to the live Sisyphus root session so delegation still works.
319
+ if (!parent) {
320
+ const agents = ctx.get('agents')
321
+ const root = agents?.roots?.()?.find((agent) => agent && typeof agent.id === 'string')
322
+ if (root) parent = root
323
+ }
324
+ if (!parent) throw new Error('go_work requires a live parent agent to delegate from')
325
+ if (orchestration.isBusy()) {
326
+ const workId = orchestration.enqueue(agentType, prompt, parent?.id)
327
+ bump()
328
+ return { childId: workId, status: 'queued', label: agentLabel(agentType, prompt.slice(0, 60)), queued: true }
329
+ }
330
+ const placeholder = orchestration.beginSpawning(agentType, prompt)
331
+ try {
332
+ const request = {
333
+ label: agentLabel(agentType, prompt.slice(0, 60)),
334
+ prompt: [{ type: 'text', text: prompt }],
335
+ parent,
336
+ ...(binding.provider !== undefined || binding.model !== undefined
337
+ ? { agentOptions: { ...(binding.provider !== undefined ? { provider: binding.provider } : {}), ...(binding.model !== undefined ? { model: binding.model } : {}) } }
338
+ : {}),
339
+ signal,
340
+ }
341
+ const { childId } = await ctx.subagents.startContinuable({
342
+ provider: 'spawn',
343
+ label: request.label,
344
+ request,
345
+ signal,
346
+ })
347
+ sessionTypes.set(childId, agentType)
348
+ orchestration.bindChild(placeholder.childId, childId)
349
+ bump()
350
+ return { childId, status: 'running', label: request.label, queued: false }
351
+ } catch (error) {
352
+ orchestration.abort(placeholder.childId)
353
+ bump()
354
+ throw new Error(`go_work failed: ${String(error)}`)
355
+ }
356
+ }
357
+
358
+ // ── tools ───────────────────────────────────────────────────────────────
359
+ ctx.tools.register({
360
+ name: 'go_work',
361
+ description: [
362
+ 'Dispatch a new sub-agent to work on a task. The sub-agent starts with an empty context and only the tools of its type.',
363
+ 'Available agent types:',
364
+ ...AGENT_TYPES.map((t) => `- ${t}: ${describeAgent(t)}`),
365
+ 'Single-line blocking: if a sub-agent is already running, this task is queued and starts when the current one finishes.',
366
+ 'The result contains a childId you keep for later continue/forward operations.',
367
+ ].join('\n'),
368
+ parameters: {
369
+ type: 'object',
370
+ properties: {
371
+ agent: { type: 'string', enum: AGENT_TYPES, description: 'Which sub-agent type to dispatch.' },
372
+ prompt: { type: 'string', description: 'The complete, self-contained task prompt for the sub-agent.' },
373
+ },
374
+ required: ['agent', 'prompt'],
375
+ additionalProperties: false,
376
+ },
377
+ output: {
378
+ schema: {
379
+ type: 'object',
380
+ additionalProperties: false,
381
+ properties: {
382
+ childId: { type: 'string' },
383
+ status: { type: 'string' },
384
+ label: { type: 'string' },
385
+ queued: { type: 'boolean' },
386
+ },
387
+ required: ['childId', 'status'],
388
+ },
389
+ render: (_args, value) => [{ type: 'text', text: `go_work → ${value.status}: ${value.childId}${value.queued ? ' (queued)' : ''}` }],
390
+ },
391
+ isConcurrencySafe: () => true,
392
+ async execute(args, exec) {
393
+ const parent = exec?.agent
394
+ if (!parent) throw new Error('go_work requires a calling agent (exec.agent was undefined)')
395
+ if (!isSisyphus(parent)) throw new Error('go_work is reserved for Sisyphus (the root session)')
396
+ return dispatchWork(args.agent, args.prompt, parent, exec?.signal)
397
+ },
398
+ })
399
+
400
+ ctx.tools.register({
401
+ name: 'continue',
402
+ description: 'Resume a sub-agent by its childId with a new prompt. Use to reject its conclusion (state reason + correction) or relay a follow-up. The sub-agent keeps its current turn context.',
403
+ parameters: {
404
+ type: 'object',
405
+ properties: {
406
+ id: { type: 'string', description: 'The childId of the sub-agent to resume.' },
407
+ prompt: { type: 'string', description: 'The new prompt: rejection reason + correction, or a follow-up task.' },
408
+ },
409
+ required: ['id', 'prompt'],
410
+ additionalProperties: false,
411
+ },
412
+ output: {
413
+ schema: {
414
+ type: 'object',
415
+ additionalProperties: false,
416
+ properties: { accepted: { type: 'boolean' }, messageId: { type: 'string' } },
417
+ required: ['accepted'],
418
+ },
419
+ render: (_args, value) => [{ type: 'text', text: `continue → ${value.accepted ? `delivered ${value.messageId}` : 'rejected'}` }],
420
+ },
421
+ isConcurrencySafe: () => true,
422
+ async execute(args, exec) {
423
+ const parent = exec?.agent
424
+ if (!parent) throw new Error('continue requires a calling agent (exec.agent was undefined)')
425
+ if (!isSisyphus(parent)) throw new Error('continue is reserved for Sisyphus (the root session)')
426
+ const record = orchestration.record(args.id)
427
+ if (!record) throw new Error(`unknown sub-agent id: ${String(args.id)}`)
428
+ if (record.status === 'waiting') {
429
+ for (const help of orchestration.snapshot().helpRequests) {
430
+ if (help.childId === record.childId) orchestration.resolveHelp(help.id)
431
+ }
432
+ orchestration.resume(record.childId)
433
+ }
434
+ const messageId = await ctx.subagents.followup(parent, record.childId, [{ type: 'text', text: args.prompt }], {
435
+ source: { kind: 'coordinator', form: 'relay', senderSessionId: parent.id },
436
+ signal: exec?.signal,
437
+ })
438
+ orchestration.followupPrompt(record.childId, args.prompt)
439
+ bump()
440
+ return { accepted: true, messageId }
441
+ },
442
+ })
443
+
444
+ ctx.tools.register({
445
+ name: 'need_help',
446
+ description: [
447
+ 'Request assistance from Sisyphus. Use when you need another sub-agent\'s capability (explore/read_doc/look_image) or the task is beyond your ability (replan).',
448
+ 'Calling this suspends you: Sisyphus will review the request and either forward it or continue you with a new prompt.',
449
+ ].join('\n'),
450
+ parameters: {
451
+ type: 'object',
452
+ properties: {
453
+ intent: {
454
+ type: 'string',
455
+ enum: ['explore', 'read_doc', 'look_image', 'replan'],
456
+ description: 'explore: need Explore to read files/search code. read_doc: need Librarian for docs. look_image: need Multimodal Looker for an image. replan: task exceeds your ability, request reassignment.',
457
+ },
458
+ content: { type: 'string', description: 'The concrete situation, reason, and details of what you need.' },
459
+ },
460
+ required: ['intent', 'content'],
461
+ additionalProperties: false,
462
+ },
463
+ output: {
464
+ schema: {
465
+ type: 'object',
466
+ additionalProperties: false,
467
+ properties: { suspended: { type: 'boolean' }, helpRequestId: { type: 'string' } },
468
+ required: ['suspended', 'helpRequestId'],
469
+ },
470
+ render: (_args, value) => [{ type: 'text', text: `need_help → suspended, request ${value.helpRequestId}` }],
471
+ },
472
+ isConcurrencySafe: () => true,
473
+ async execute(args, exec) {
474
+ const child = exec?.agent
475
+ if (!child) throw new Error('need_help requires a calling agent (exec.agent was undefined)')
476
+ const id = `help-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`
477
+ const help = {
478
+ id,
479
+ childId: child.id,
480
+ agentType: sessionTypes.get(child.id),
481
+ intent: args.intent,
482
+ content: args.content,
483
+ createdAt: Date.now(),
484
+ }
485
+ const suspended = orchestration.suspend(child.id, help)
486
+ if (suspended === undefined) {
487
+ // The caller is not a tracked sub-agent (e.g. Sisyphus itself).
488
+ throw new Error('need_help is only available to tracked sub-agents (this session is not one)')
489
+ }
490
+ bump()
491
+ try {
492
+ await ctx.subagents.reportFrom(child, [{
493
+ type: 'text',
494
+ text: `<need_help id="${id}" intent="${args.intent}" child="${child.id}">\n${args.content}\n</need_help>`,
495
+ }], { delivery: 'next-step', signal: exec?.signal })
496
+ } catch {
497
+ // Report failure must not break the suspension bookkeeping.
498
+ }
499
+ return { suspended: true, helpRequestId: id }
500
+ },
501
+ })
502
+
503
+ ctx.tools.register({
504
+ name: 'forward',
505
+ description: [
506
+ 'Forward a pending need_help request to a target sub-agent.',
507
+ '- target = childId: equivalent to continue with the help content as prompt (same sub-agent resumes).',
508
+ '- target = agent type: dispatch a NEW sub-agent of that type with the help content as prompt (go_work).',
509
+ 'The forwarded help request is resolved; the requesting child stays suspended until you continue it explicitly.',
510
+ ].join('\n'),
511
+ parameters: {
512
+ type: 'object',
513
+ properties: {
514
+ from: { type: 'string', description: 'The helpRequestId to forward.' },
515
+ target: { type: 'string', description: 'Target childId (resume) or agent type name (dispatch new).' },
516
+ },
517
+ required: ['from', 'target'],
518
+ additionalProperties: false,
519
+ },
520
+ output: {
521
+ schema: {
522
+ type: 'object',
523
+ additionalProperties: false,
524
+ properties: { kind: { type: 'string' }, targetId: { type: 'string' }, resolved: { type: 'boolean' } },
525
+ required: ['kind', 'targetId'],
526
+ },
527
+ render: (_args, value) => [{ type: 'text', text: `forward → ${value.kind}: ${value.targetId}` }],
528
+ },
529
+ isConcurrencySafe: () => true,
530
+ async execute(args, exec) {
531
+ const parent = exec?.agent
532
+ if (!parent) throw new Error('forward requires a calling agent (exec.agent was undefined)')
533
+ if (!isSisyphus(parent)) throw new Error('forward is reserved for Sisyphus (the root session)')
534
+ const help = orchestration.help(args.from)
535
+ if (!help) throw new Error(`unknown help request id: ${String(args.from)}`)
536
+ const prompt = help.content
537
+ const target = String(args.target)
538
+ orchestration.resolveHelp(help.id)
539
+ if (AGENT_TYPES.includes(target)) {
540
+ // Dispatch a new sub-agent of that type.
541
+ const result = await dispatchWork(target, prompt, parent, exec?.signal)
542
+ bump()
543
+ return { kind: 'go_work', targetId: String(result?.childId ?? ''), resolved: true }
544
+ }
545
+ const messageId = await ctx.subagents.followup(parent, target, [{ type: 'text', text: prompt }], {
546
+ source: { kind: 'coordinator', form: 'relay', senderSessionId: parent.id },
547
+ signal: exec?.signal,
548
+ })
549
+ orchestration.followupPrompt(target, prompt)
550
+ orchestration.resume(target)
551
+ bump()
552
+ return { kind: 'continue', targetId: messageId, resolved: true }
553
+ },
554
+ })
555
+
556
+ ctx.tools.register({
557
+ name: 'orchestration_status',
558
+ description: 'Read the current orchestration state: running sub-agent, queue, pending help requests, and run history with conclusions.',
559
+ parameters: { type: 'object', properties: {}, additionalProperties: false },
560
+ output: {
561
+ schema: {
562
+ type: 'object',
563
+ additionalProperties: false,
564
+ properties: { text: { type: 'string' } },
565
+ required: ['text'],
566
+ },
567
+ render: (_args, value) => [{ type: 'text', text: value.text }],
568
+ },
569
+ isConcurrencySafe: () => true,
570
+ async execute() {
571
+ const s = orchestration.snapshot()
572
+ const lines = []
573
+ if (s.current) {
574
+ lines.push(`● running: ${s.current.agentType} (${s.current.childId}) — ${s.current.status}`)
575
+ } else {
576
+ lines.push('○ idle')
577
+ }
578
+ if (s.queue.length > 0) lines.push(`⏳ queue: ${s.queue.map((w) => `${w.agentType}#${w.id}`).join(', ')}`)
579
+ for (const help of s.helpRequests) lines.push(`❓ help ${help.id}: [${help.intent}] ${help.content.slice(0, 120)}`)
580
+ for (const r of s.history.slice(-5)) {
581
+ const summary = (r.conclusion ?? '').replace(/\s+/g, ' ').slice(0, 80)
582
+ lines.push(`✓ ${r.agentType} (${r.childId}) ${r.status}: ${summary}`)
583
+ }
584
+ return { text: lines.join('\n') }
585
+ },
586
+ })
587
+
588
+ ctx.tools.register({
589
+ name: 'list_subagents',
590
+ description: [
591
+ 'List every sub-agent this orchestration has spawned: its agent type, childId, current status, and the LAST prompt Sisyphus sent it (go_work or continue).',
592
+ 'Use this to decide whether to continue an existing sub-agent (same task, keep context) or dispatch a new one — especially when reusing an idle/done worker for a follow-up step instead of paying for a fresh context.',
593
+ ].join('\n'),
594
+ parameters: { type: 'object', properties: {}, additionalProperties: false },
595
+ output: {
596
+ schema: {
597
+ type: 'object',
598
+ additionalProperties: false,
599
+ properties: { text: { type: 'string' } },
600
+ required: ['text'],
601
+ },
602
+ render: (_args, value) => [{ type: 'text', text: value.text }],
603
+ },
604
+ isConcurrencySafe: () => true,
605
+ async execute() {
606
+ const s = orchestration.snapshot()
607
+ const lines = ['# 当前 sub-agents']
608
+ const all = [...(s.current ? [s.current] : []), ...s.history.slice(-50)]
609
+ const seen = new Set()
610
+ for (const r of all) {
611
+ if (seen.has(r.childId)) continue
612
+ seen.add(r.childId)
613
+ const prompt = (r.prompt ?? '').replace(/\s+/g, ' ').slice(0, 140)
614
+ lines.push(`- ${r.agentType} (${r.childId}) [${r.status}] 最后 prompt: ${prompt}`)
615
+ }
616
+ if (s.queue.length > 0) {
617
+ lines.push('# 队列(等待中)')
618
+ for (const w of s.queue) lines.push(`- ${w.agentType} (${w.id}) 排队中 prompt: ${w.prompt.replace(/\s+/g, ' ').slice(0, 140)}`)
619
+ }
620
+ if (lines.length === 1) lines.push('(还没有任何 sub-agent)')
621
+ return { text: lines.join('\n') }
622
+ },
623
+ })
624
+
625
+ // ── model/effort binding at the request waterfall ───────────────────────
626
+ // reasoningEffort follows the DSH model catalog: some models have no
627
+ // thinking levels, others expose a different set (off/high/max, low, etc.).
628
+ // We only ever set an effort the exact model actually supports; when the
629
+ // configured effort is unsupported (or the model exposes none), we leave
630
+ // the field unset so the adapter's default behavior applies — never hard-map
631
+ // or clamp, which would reject or silently alter the request.
632
+ const llm = ctx.get('llm')
633
+ const effortCache = new Map() // `${provider}/${model}` -> Set<effortId> | null
634
+ async function supportedEfforts(provider, model) {
635
+ const key = `${provider}/${model}`
636
+ const cached = effortCache.get(key)
637
+ if (cached !== undefined) return cached
638
+ let result = null // null = unknown (leave effort unset)
639
+ try {
640
+ if (llm && typeof llm.resolveModelInfo === 'function') {
641
+ const info = await llm.resolveModelInfo(provider, model)
642
+ const efforts = info?.reasoning?.efforts
643
+ if (Array.isArray(efforts) && efforts.length > 0) {
644
+ result = new Set(efforts.map((e) => String(e?.id)))
645
+ }
646
+ }
647
+ } catch {
648
+ // Capability lookup must never break the request; unknown → leave unset.
649
+ }
650
+ effortCache.set(key, result)
651
+ return result
652
+ }
653
+
654
+ ctx.on('agent/request', async (payload, next) => {
655
+ const seed = await next()
656
+ const agent = payload?.agent
657
+ if (!agent) return seed
658
+ const type = sessionTypes.get(agent.id)
659
+ if (type === undefined && !bindSisyphus) return seed
660
+ const binding = bindings[type ?? 'sisyphus'] ?? {}
661
+ const nextConfig = { ...seed }
662
+ if (binding.provider !== undefined) nextConfig.provider = binding.provider
663
+ if (binding.model !== undefined) nextConfig.model = binding.model
664
+ const desiredEffort = binding.reasoningEffort
665
+ if (desiredEffort !== undefined && desiredEffort !== null) {
666
+ const provider = String(nextConfig.provider ?? binding.provider ?? '')
667
+ const model = String(nextConfig.model ?? binding.model ?? '')
668
+ const efforts = await supportedEfforts(provider, model)
669
+ if (efforts !== null && efforts.has(String(desiredEffort))) {
670
+ nextConfig.reasoningEffort = desiredEffort
671
+ }
672
+ // Unsupported or unknown → leave reasoningEffort unset (adapter default).
673
+ }
674
+ return nextConfig
675
+ })
676
+
677
+ // ── conclusion injection + queue advancement on subagent/end ────────────
678
+ ctx.on('subagent/end', (info) => {
679
+ const childId = info?.id
680
+ const type = sessionTypes.get(childId)
681
+ if (type === undefined) return
682
+ const blocks = info?.lastAssistantMessage ?? []
683
+ const text = blocks
684
+ .filter((block) => block?.type === 'text' && typeof block.text === 'string')
685
+ .map((block) => block.text)
686
+ .join('\n')
687
+ const failed = info?.stopReason !== 'completed'
688
+ orchestration.finish(childId, text || `(${String(info?.stopReason)})`, failed)
689
+ sessionTypes.delete(childId)
690
+ bump()
691
+ // Advance queue.
692
+ if (orchestration.snapshot().queue.length > 0 && !orchestration.isBusy()) {
693
+ const work = orchestration.dequeue()
694
+ if (work) {
695
+ // Resolve the recorded parent agent (the Sisyphus session) and
696
+ // dispatch the next queued work through the same internal path.
697
+ const agents = ctx.get('agents')
698
+ const parentAgent = work.parentId && agents ? agents.get(work.parentId) : undefined
699
+ void dispatchWork(work.agentType, work.prompt, parentAgent, undefined).catch(() => undefined)
700
+ }
701
+ }
702
+ })
703
+
704
+ return () => {
705
+ for (const dispose of disposers) dispose()
706
+ }
707
+ }