@goodandready/dsh-agent-orchestrator 0.1.6
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/LICENSE +22 -0
- package/README.md +184 -0
- package/README.ru.md +86 -0
- package/README.zh.md +64 -0
- package/cordis.patch.yml +5 -0
- package/lib/client.js +1374 -0
- package/lib/http-guard.js +166 -0
- package/lib/index.js +950 -0
- package/lib/integrations/kanban-bridge.js +102 -0
- package/lib/pipeline/cache-prefixer.js +226 -0
- package/lib/pipeline/concurrency-gate.js +110 -0
- package/lib/pipeline/dag-engine.js +290 -0
- package/lib/pipeline/decision-trace.js +124 -0
- package/lib/pipeline/decomposer.js +102 -0
- package/lib/pipeline/delegation.js +625 -0
- package/lib/pipeline/guidance.js +94 -0
- package/lib/pipeline/intent.js +98 -0
- package/lib/pipeline/intersection.js +235 -0
- package/lib/pipeline/model-selection.js +742 -0
- package/lib/pipeline/preset-sync.js +84 -0
- package/lib/pipeline/scenarios.js +468 -0
- package/lib/pipeline/session-lifecycle.js +520 -0
- package/lib/pipeline/snapshots.js +240 -0
- package/lib/pipeline/token-watchdog.js +101 -0
- package/lib/pipeline/worker-pool.js +143 -0
- package/lib/routes.js +277 -0
- package/lib/store.js +176 -0
- package/package.json +94 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,950 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepSeek Harness Multi-Agent Orchestrator Plugin (Host Half)
|
|
3
|
+
*
|
|
4
|
+
* Scoped Name: @goodandready/dsh-agent-orchestrator
|
|
5
|
+
* Short ID: dsh-agent-orchestrator
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { randomUUID } from 'crypto'
|
|
9
|
+
// Safe peerDependency resolution with standalone fallback
|
|
10
|
+
function createSchemaStub(target = {}) {
|
|
11
|
+
return new Proxy(target, {
|
|
12
|
+
get(t, prop) {
|
|
13
|
+
if (prop === 'shape') return t.shape || {}
|
|
14
|
+
return (...args) => createSchemaStub({ ...t, [prop]: args[0] })
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let z
|
|
20
|
+
try {
|
|
21
|
+
const mod = await import('@deepseek-ai/schemastery')
|
|
22
|
+
z = mod.default || mod
|
|
23
|
+
} catch {
|
|
24
|
+
z = {
|
|
25
|
+
object: (shape) => createSchemaStub({ shape }),
|
|
26
|
+
boolean: () => createSchemaStub({ type: 'boolean' }),
|
|
27
|
+
string: () => createSchemaStub({ type: 'string' }),
|
|
28
|
+
number: () => createSchemaStub({ type: 'number' }),
|
|
29
|
+
union: () => createSchemaStub({ type: 'union' }),
|
|
30
|
+
array: () => createSchemaStub({ type: 'array' }),
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let defineTool
|
|
35
|
+
try {
|
|
36
|
+
const mod = await import('@deepseek-ai/dsh-tools')
|
|
37
|
+
defineTool = mod.defineTool || ((def) => def)
|
|
38
|
+
} catch {
|
|
39
|
+
defineTool = (def) => def
|
|
40
|
+
}
|
|
41
|
+
import { getDefaultRoles, getDefaultScenarios } from './pipeline/scenarios.js'
|
|
42
|
+
import { decomposeTask } from './pipeline/decomposer.js'
|
|
43
|
+
import { executeDAG } from './pipeline/dag-engine.js'
|
|
44
|
+
import { executeStageWorker } from './pipeline/worker-pool.js'
|
|
45
|
+
import { executeDirectDelegation, toRolesArray, computeDelegationDepth } from './pipeline/delegation.js'
|
|
46
|
+
import { SerializationGate } from './pipeline/concurrency-gate.js'
|
|
47
|
+
import { SessionLifecycleManager } from './pipeline/session-lifecycle.js'
|
|
48
|
+
import { detectOrchestratorIntent } from './pipeline/intent.js'
|
|
49
|
+
import { readModelSelection, fetchModelCatalog } from './pipeline/model-selection.js'
|
|
50
|
+
import { applyGuidance } from './pipeline/guidance.js'
|
|
51
|
+
import { OrchestratorStore } from './store.js'
|
|
52
|
+
import { KanbanBridge } from './integrations/kanban-bridge.js'
|
|
53
|
+
import { registerOrchestratorRoutes } from './routes.js'
|
|
54
|
+
import { SnapshotManager } from './pipeline/snapshots.js'
|
|
55
|
+
import { syncDefaultPresets } from './pipeline/preset-sync.js'
|
|
56
|
+
|
|
57
|
+
export const name = '@goodandready/dsh-agent-orchestrator'
|
|
58
|
+
export const inject = ['settings', 'webServer', 'llm', 'tools', 'commands', 'systemPrompt', 'subagents']
|
|
59
|
+
|
|
60
|
+
export const Config = z.object({
|
|
61
|
+
enabled: z.boolean().default(true).description('Enable Multi-Agent Orchestrator pipeline dispatcher'),
|
|
62
|
+
defaultScenario: z.string().default('auto').description('Default complexity scenario (auto, hotfix, simple, medium, complex, enterprise)'),
|
|
63
|
+
disableNestedDelegation: z.boolean().default(false).description('Anti-Matryoshka Guard: Disallow subagents from spawning nested subagents (Issue #19)'),
|
|
64
|
+
maxConcurrentSubagents: z.number().default(3).description('Maximum concurrent subagents per parent session (Issue #93)'),
|
|
65
|
+
subagentGracePeriodMs: z.number().default(180000).description('Grace period before archiving completed one-shot subagents (Issue #56)'),
|
|
66
|
+
smartModelRouting: z.boolean().default(false).description('Smart Model Routing: dynamically select model based on task complexity (Issue #48)'),
|
|
67
|
+
allowedProviders: z.array(z.string()).default([]).description('Whitelist of allowed LLM provider IDs (Issue #48)'),
|
|
68
|
+
failFastModelValidation: z.boolean().default(false).description('Fail-fast when requested model is not found in provider registry (Issue #82)'),
|
|
69
|
+
maxStoredSessions: z.number().default(400).description('Capacity recycling threshold for subagent sessions (Issue #67)'),
|
|
70
|
+
sessionRetentionMs: z.number().default(86400000).description('Retention duration in ms for sessions-archive before physical deletion (Issue #57)'),
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
export function apply(ctx, config = {}) {
|
|
74
|
+
const NS = 'dsh-agent-orchestrator'
|
|
75
|
+
const logger = ctx?.logger || {
|
|
76
|
+
debug: () => {},
|
|
77
|
+
info: () => {},
|
|
78
|
+
warn: () => {},
|
|
79
|
+
error: () => {},
|
|
80
|
+
}
|
|
81
|
+
let currentSettings = {
|
|
82
|
+
...config,
|
|
83
|
+
disableNestedDelegation: Boolean(config.disableNestedDelegation),
|
|
84
|
+
smartModelRouting: Boolean(config.smartModelRouting),
|
|
85
|
+
allowedProviders: Array.isArray(config.allowedProviders) ? config.allowedProviders : [],
|
|
86
|
+
failFastModelValidation: Boolean(config.failFastModelValidation),
|
|
87
|
+
maxStoredSessions: config.maxStoredSessions || 400,
|
|
88
|
+
sessionRetentionMs: config.sessionRetentionMs || 86400000,
|
|
89
|
+
roles: getDefaultRoles(),
|
|
90
|
+
scenarios: getDefaultScenarios(),
|
|
91
|
+
kanbanSync: {
|
|
92
|
+
enabled: false,
|
|
93
|
+
targetColumn: 'review',
|
|
94
|
+
createChecklist: true,
|
|
95
|
+
...(config.kanbanSync || {}),
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let settingsScope = null
|
|
100
|
+
|
|
101
|
+
// Register settings scope
|
|
102
|
+
ctx.inject(['settings'], (sctx) => {
|
|
103
|
+
try {
|
|
104
|
+
const scope = sctx.settings.register(NS, Config, { base: config })
|
|
105
|
+
if (scope) {
|
|
106
|
+
settingsScope = scope
|
|
107
|
+
const saved = scope.get()
|
|
108
|
+
if (saved) {
|
|
109
|
+
currentSettings = {
|
|
110
|
+
...currentSettings,
|
|
111
|
+
...saved,
|
|
112
|
+
disableNestedDelegation: saved.disableNestedDelegation ?? currentSettings.disableNestedDelegation,
|
|
113
|
+
roles: saved.roles || getDefaultRoles(),
|
|
114
|
+
scenarios: saved.scenarios || getDefaultScenarios(),
|
|
115
|
+
kanbanSync: { ...currentSettings.kanbanSync, ...(saved.kanbanSync || {}) },
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} catch (e) {
|
|
120
|
+
logger.warn('[dsh-agent-orchestrator] Settings register warning:', e?.message || e)
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
const getConfig = () => currentSettings
|
|
125
|
+
const updateConfig = async (partial) => {
|
|
126
|
+
currentSettings = { ...currentSettings, ...partial }
|
|
127
|
+
if (settingsScope) {
|
|
128
|
+
try {
|
|
129
|
+
for (const [k, v] of Object.entries(partial)) {
|
|
130
|
+
if (k === 'enabled' || k === 'defaultScenario' || k === 'disableNestedDelegation') {
|
|
131
|
+
await settingsScope.set(k, v)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
} catch (err) {
|
|
135
|
+
logger.debug('[dsh-agent-orchestrator] Settings update warning:', err?.message || err)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Self-Syncing Presets & Roles (Issue #107)
|
|
141
|
+
const syncResult = syncDefaultPresets({
|
|
142
|
+
currentRoles: currentSettings.roles,
|
|
143
|
+
currentScenarios: currentSettings.scenarios,
|
|
144
|
+
logger,
|
|
145
|
+
})
|
|
146
|
+
currentSettings.roles = syncResult.roles
|
|
147
|
+
currentSettings.scenarios = syncResult.scenarios
|
|
148
|
+
|
|
149
|
+
const snapshotManager = new SnapshotManager({ logger })
|
|
150
|
+
const store = new OrchestratorStore({ logger })
|
|
151
|
+
const kanbanBridge = new KanbanBridge({
|
|
152
|
+
port: ctx.webServer?.port || 3080,
|
|
153
|
+
fetchImpl: globalThis.fetch,
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
const serializationGate = new SerializationGate(currentSettings.maxConcurrentSubagents || 3)
|
|
157
|
+
const sessionLifecycle = new SessionLifecycleManager({
|
|
158
|
+
gracePeriodMs: currentSettings.subagentGracePeriodMs,
|
|
159
|
+
maxCapacity: currentSettings.maxStoredSessions || 400,
|
|
160
|
+
retentionMs: currentSettings.sessionRetentionMs || 86400000,
|
|
161
|
+
subagentsService: ctx.subagents,
|
|
162
|
+
sessionsService: (typeof ctx.get === 'function' ? ctx.get('sessions', false) : null) || null,
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
// Track active execution controllers for clean lifecycle teardown
|
|
166
|
+
const activeControllers = new Set()
|
|
167
|
+
|
|
168
|
+
ctx.effect(() => () => {
|
|
169
|
+
// Dispose / cleanup all active child runs
|
|
170
|
+
for (const ctrl of activeControllers) {
|
|
171
|
+
try {
|
|
172
|
+
ctrl.abort(new Error('Plugin unmounted or restarted'))
|
|
173
|
+
} catch (err) {
|
|
174
|
+
logger.debug('[dsh-agent-orchestrator] Controller abort error:', err?.message || err)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
activeControllers.clear()
|
|
178
|
+
sessionLifecycle.dispose()
|
|
179
|
+
}, 'dsh-agent-orchestrator: lifecycle resource cleanup')
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Unified LLM caller wrapping ctx.llm.prepareCall().stream()
|
|
183
|
+
*/
|
|
184
|
+
const callLlm = async ({ provider, model, messages, temperature = 0.3, maxTokens = 4096, reasoningEffort, onStreamDelta }) => {
|
|
185
|
+
if (!ctx.llm) {
|
|
186
|
+
throw new Error('ctx.llm is not available in cordis context')
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Resolve provider aliases (e.g. 'deepseek' -> 'deepseek-official')
|
|
190
|
+
let resolvedProvider = provider || 'deepseek-official'
|
|
191
|
+
try {
|
|
192
|
+
const providers = typeof ctx.llm.listProviders === 'function' ? ctx.llm.listProviders() : []
|
|
193
|
+
if (providers.length > 0) {
|
|
194
|
+
if (provider === 'deepseek' && providers.some((p) => p.id === 'deepseek-official')) {
|
|
195
|
+
resolvedProvider = 'deepseek-official'
|
|
196
|
+
} else if (!providers.some((p) => p.id === provider)) {
|
|
197
|
+
const ds = providers.find((p) => p.id === 'deepseek-official' || p.id === 'deepseek')
|
|
198
|
+
if (ds) resolvedProvider = ds.id
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
} catch (_) {
|
|
202
|
+
/* safe ignore */
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const callConfig = {
|
|
206
|
+
provider: resolvedProvider,
|
|
207
|
+
model,
|
|
208
|
+
temperature,
|
|
209
|
+
maxTokens,
|
|
210
|
+
...(reasoningEffort && reasoningEffort !== 'off' ? { reasoningEffort, reasoning_effort: reasoningEffort } : {}),
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let prep
|
|
214
|
+
try {
|
|
215
|
+
prep = await ctx.llm.prepareCall(callConfig)
|
|
216
|
+
} catch (err) {
|
|
217
|
+
throw new Error(`Failed to prepareCall for ${resolvedProvider}:${model} (${err?.message || err})`)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (!prep || typeof prep.stream !== 'function') {
|
|
221
|
+
throw new Error(`LLM provider/model ${resolvedProvider}:${model} does not support streaming`)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const abortCtrl = new AbortController()
|
|
225
|
+
activeControllers.add(abortCtrl)
|
|
226
|
+
const timeoutId = setTimeout(() => abortCtrl.abort(new Error('LLM call timeout (120s)')), 120000)
|
|
227
|
+
timeoutId.unref?.()
|
|
228
|
+
|
|
229
|
+
const normalizedMessages = (messages || []).map((m) => {
|
|
230
|
+
if (typeof m.content === 'string') {
|
|
231
|
+
return {
|
|
232
|
+
id: m.id || randomUUID(),
|
|
233
|
+
role: m.role,
|
|
234
|
+
content: [{ type: 'text', text: m.content }],
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return m
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
const stream = prep.stream({
|
|
242
|
+
...prep.config,
|
|
243
|
+
messages: normalizedMessages,
|
|
244
|
+
signal: abortCtrl.signal,
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
let fullText = ''
|
|
248
|
+
let usageInfo = null
|
|
249
|
+
|
|
250
|
+
for await (const chunk of stream) {
|
|
251
|
+
if (chunk.type === 'finish' && chunk.reason?.kind === 'error') {
|
|
252
|
+
const failMsg =
|
|
253
|
+
chunk.reason.failure?.message ||
|
|
254
|
+
chunk.reason.failure?.detail ||
|
|
255
|
+
JSON.stringify(chunk.reason.failure || chunk.reason)
|
|
256
|
+
throw new Error(`LLM stream finished with error: ${failMsg}`)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (chunk.type === 'text-delta' && typeof chunk.text === 'string') {
|
|
260
|
+
fullText += chunk.text
|
|
261
|
+
if (typeof onStreamDelta === 'function') {
|
|
262
|
+
onStreamDelta(chunk.text)
|
|
263
|
+
}
|
|
264
|
+
} else if (chunk.type === 'block-end' && chunk.block?.text) {
|
|
265
|
+
if (!fullText) fullText = chunk.block.text
|
|
266
|
+
} else if (typeof chunk.delta?.text === 'string') {
|
|
267
|
+
fullText += chunk.delta.text
|
|
268
|
+
if (typeof onStreamDelta === 'function') {
|
|
269
|
+
onStreamDelta(chunk.delta.text)
|
|
270
|
+
}
|
|
271
|
+
} else if (typeof chunk.text === 'string' && !chunk.type) {
|
|
272
|
+
fullText += chunk.text
|
|
273
|
+
} else if (chunk.type === 'usage' && chunk.usage) {
|
|
274
|
+
usageInfo = chunk.usage
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
clearTimeout(timeoutId)
|
|
279
|
+
activeControllers.delete(abortCtrl)
|
|
280
|
+
return {
|
|
281
|
+
text: fullText,
|
|
282
|
+
content: fullText,
|
|
283
|
+
usage: usageInfo || {
|
|
284
|
+
prompt_tokens: Math.round(JSON.stringify(messages).length / 4),
|
|
285
|
+
completion_tokens: Math.round(fullText.length / 4),
|
|
286
|
+
},
|
|
287
|
+
}
|
|
288
|
+
} catch (err) {
|
|
289
|
+
clearTimeout(timeoutId)
|
|
290
|
+
activeControllers.delete(abortCtrl)
|
|
291
|
+
throw err
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Runner object to start, execute, and monitor pipelines
|
|
297
|
+
*/
|
|
298
|
+
const runner = {
|
|
299
|
+
async startPipeline({ taskTitle, taskDescription, scenarioId, taskId }) {
|
|
300
|
+
const cfg = getConfig()
|
|
301
|
+
const plan = decomposeTask({
|
|
302
|
+
taskTitle,
|
|
303
|
+
taskDescription,
|
|
304
|
+
scenarioId: scenarioId || cfg.defaultScenario || 'auto',
|
|
305
|
+
customScenarios: cfg.scenarios,
|
|
306
|
+
customRoles: cfg.roles,
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
const pipelineData = {
|
|
310
|
+
...plan,
|
|
311
|
+
taskId: taskId || null,
|
|
312
|
+
status: 'running',
|
|
313
|
+
startedAt: Date.now(),
|
|
314
|
+
completedAt: null,
|
|
315
|
+
durationMs: 0,
|
|
316
|
+
stages: plan.stages.map((s) => ({ ...s, status: 'pending' })),
|
|
317
|
+
stateMap: {},
|
|
318
|
+
artifacts: {},
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
store.recordPipeline(pipelineData)
|
|
322
|
+
|
|
323
|
+
// Execute DAG asynchronously
|
|
324
|
+
const executor = async (stage, context) => {
|
|
325
|
+
store.updateStage(plan.pipelineId, stage.id, {
|
|
326
|
+
status: 'running',
|
|
327
|
+
startedAt: Date.now(),
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
const rolesList = toRolesArray(cfg.roles)
|
|
331
|
+
const role = rolesList.find((r) => r.id === stage.roleId) || {
|
|
332
|
+
id: stage.roleId,
|
|
333
|
+
displayName: stage.roleName,
|
|
334
|
+
defaultModel: { provider: 'deepseek-official', model: 'deepseek-chat' },
|
|
335
|
+
systemPrompt: 'You are a specialized autonomous engineer. Produce clear, production-ready deliverables.',
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const modelSelection = readModelSelection(ctx)
|
|
339
|
+
|
|
340
|
+
try {
|
|
341
|
+
const result = await executeStageWorker(stage, {
|
|
342
|
+
pipeline: pipelineData,
|
|
343
|
+
agentRole: role,
|
|
344
|
+
config: cfg,
|
|
345
|
+
callLlm,
|
|
346
|
+
upstreamOutputs: context.upstreamOutputs,
|
|
347
|
+
logger,
|
|
348
|
+
allowedRoutes: modelSelection.allowedRoutes,
|
|
349
|
+
onStreamDelta: (delta) => {
|
|
350
|
+
store.appendStageLog(plan.pipelineId, stage.id, delta)
|
|
351
|
+
},
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
store.updateStage(plan.pipelineId, stage.id, {
|
|
355
|
+
status: 'completed',
|
|
356
|
+
completedAt: Date.now(),
|
|
357
|
+
output: result.output,
|
|
358
|
+
metrics: result.metrics,
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
return result.output
|
|
362
|
+
} catch (stageErr) {
|
|
363
|
+
store.updateStage(plan.pipelineId, stage.id, {
|
|
364
|
+
status: 'failed',
|
|
365
|
+
completedAt: Date.now(),
|
|
366
|
+
error: stageErr?.message || String(stageErr),
|
|
367
|
+
})
|
|
368
|
+
throw stageErr
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
executeDAG({
|
|
373
|
+
stages: plan.stages,
|
|
374
|
+
executor,
|
|
375
|
+
concurrency: 3,
|
|
376
|
+
})
|
|
377
|
+
.then(async (dagResult) => {
|
|
378
|
+
store.updatePipeline(plan.pipelineId, {
|
|
379
|
+
status: 'completed',
|
|
380
|
+
completedAt: Date.now(),
|
|
381
|
+
durationMs: Date.now() - pipelineData.startedAt,
|
|
382
|
+
artifacts: dagResult.artifacts,
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
// Sync to Kanban if enabled
|
|
386
|
+
const syncConfig = cfg.kanbanSync
|
|
387
|
+
if (syncConfig?.enabled) {
|
|
388
|
+
try {
|
|
389
|
+
let targetTaskId = taskId
|
|
390
|
+
if (!targetTaskId) {
|
|
391
|
+
const createdTask = await kanbanBridge.createTask({
|
|
392
|
+
title: `[Orchestrated] ${plan.taskTitle}`,
|
|
393
|
+
description: `Pipeline ID: \`${plan.pipelineId}\`\nScenario: \`${plan.scenarioTitle}\`\nObjective: ${plan.taskDescription}`,
|
|
394
|
+
})
|
|
395
|
+
targetTaskId = createdTask?.id
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (targetTaskId) {
|
|
399
|
+
if (syncConfig.createChecklist) {
|
|
400
|
+
const checklistItems = plan.stages.map((s) => ({
|
|
401
|
+
title: `${s.name} (${s.roleName})`,
|
|
402
|
+
completed: dagResult.statusMap[s.id] === 'completed',
|
|
403
|
+
}))
|
|
404
|
+
await kanbanBridge.syncStageChecklist(targetTaskId, checklistItems)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (syncConfig.targetColumn) {
|
|
408
|
+
await kanbanBridge.moveTask(targetTaskId, syncConfig.targetColumn)
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
} catch (kErr) {
|
|
412
|
+
logger.warn('[dsh-agent-orchestrator] Kanban sync error:', kErr?.message || kErr)
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Record Dispatch Snapshot (Issue #109)
|
|
417
|
+
try {
|
|
418
|
+
snapshotManager.createSnapshot({
|
|
419
|
+
id: plan.pipelineId,
|
|
420
|
+
title: plan.taskTitle,
|
|
421
|
+
scenarioId: plan.scenarioId,
|
|
422
|
+
status: dagResult.allSucceeded ? 'completed' : 'failed',
|
|
423
|
+
stages: plan.stages,
|
|
424
|
+
durationMs: Date.now() - pipelineData.startedAt,
|
|
425
|
+
})
|
|
426
|
+
} catch (snapErr) {
|
|
427
|
+
logger.debug('[dsh-agent-orchestrator] Snapshot record skipped:', snapErr?.message || snapErr)
|
|
428
|
+
}
|
|
429
|
+
})
|
|
430
|
+
.catch((dagErr) => {
|
|
431
|
+
store.updatePipeline(plan.pipelineId, {
|
|
432
|
+
status: 'failed',
|
|
433
|
+
completedAt: Date.now(),
|
|
434
|
+
durationMs: Date.now() - pipelineData.startedAt,
|
|
435
|
+
error: dagErr?.message || String(dagErr),
|
|
436
|
+
})
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
return pipelineData
|
|
440
|
+
},
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Register Web REST API Routes
|
|
444
|
+
registerOrchestratorRoutes(ctx, {
|
|
445
|
+
store,
|
|
446
|
+
runner,
|
|
447
|
+
getConfig,
|
|
448
|
+
updateConfig,
|
|
449
|
+
callLlm,
|
|
450
|
+
snapshotManager,
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
// Inject systemPrompt specialist roster (so the main chat agent knows about all 12 roles)
|
|
454
|
+
applyGuidance(ctx, () => getConfig().roles, 'orchestrator_delegate_specialist', { logger })
|
|
455
|
+
|
|
456
|
+
// Register model-facing tools via tools service
|
|
457
|
+
const registerToolsOnContext = (targetCtx) => {
|
|
458
|
+
if (!targetCtx.tools || typeof targetCtx.tools.register !== 'function') return
|
|
459
|
+
const render = (_a, v) => [{ type: 'text', text: typeof v === 'string' ? v : JSON.stringify(v, null, 2) }]
|
|
460
|
+
|
|
461
|
+
targetCtx.effect(() => {
|
|
462
|
+
const disposers = []
|
|
463
|
+
|
|
464
|
+
// 1. Full Multi-Agent Pipeline Dispatcher
|
|
465
|
+
try {
|
|
466
|
+
disposers.push(
|
|
467
|
+
targetCtx.tools.register(
|
|
468
|
+
defineTool({
|
|
469
|
+
name: 'orchestrator_dispatch',
|
|
470
|
+
description:
|
|
471
|
+
'Dispatch a multi-agent orchestrated pipeline to decompose and execute a complex task across specialized roles (Architecture, Spec, UI Design, Code, QA, Docs).',
|
|
472
|
+
parameters: {
|
|
473
|
+
taskTitle: { type: 'string', required: true, description: 'Concise title of the objective' },
|
|
474
|
+
taskDescription: { type: 'string', required: true, description: 'Detailed functional requirements and scope' },
|
|
475
|
+
scenarioId: {
|
|
476
|
+
type: 'string',
|
|
477
|
+
description: 'Complexity scenario (auto, hotfix, simple, medium, complex, enterprise)',
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
output: { schema: { type: 'string' }, render },
|
|
481
|
+
async execute(args) {
|
|
482
|
+
const res = await runner.startPipeline(args)
|
|
483
|
+
return JSON.stringify(
|
|
484
|
+
{
|
|
485
|
+
status: 'started',
|
|
486
|
+
pipelineId: res.pipelineId,
|
|
487
|
+
scenario: res.scenarioTitle,
|
|
488
|
+
stages: res.stages.map((s) => ({ id: s.id, name: s.name, role: s.roleName })),
|
|
489
|
+
},
|
|
490
|
+
null,
|
|
491
|
+
2
|
|
492
|
+
)
|
|
493
|
+
},
|
|
494
|
+
})
|
|
495
|
+
)
|
|
496
|
+
)
|
|
497
|
+
logger.debug('[dsh-agent-orchestrator] Registered tool: orchestrator_dispatch')
|
|
498
|
+
} catch (e) {
|
|
499
|
+
logger.error('[dsh-agent-orchestrator] Failed to register orchestrator_dispatch:', e)
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// 2. Direct Single Specialist Delegation Tool (In-Chat Subagent)
|
|
503
|
+
try {
|
|
504
|
+
disposers.push(
|
|
505
|
+
targetCtx.tools.register(
|
|
506
|
+
defineTool({
|
|
507
|
+
name: 'orchestrator_delegate_specialist',
|
|
508
|
+
description:
|
|
509
|
+
'Delegate a focused task directly to a specialized autonomous subagent (e.g. ui_design, architecture, spec, frontend, backend, qa_tests, code_review, security_audit, devops, docs, data_engineer, dba) without running a full multi-stage DAG pipeline. Returns the specialist deliverable directly into this turn.',
|
|
510
|
+
parameters: {
|
|
511
|
+
roleId: {
|
|
512
|
+
type: 'string',
|
|
513
|
+
required: true,
|
|
514
|
+
description:
|
|
515
|
+
'Target role identifier: architecture, spec, ui_design, frontend, backend, qa_tests, code_review, security_audit, devops, docs, data_engineer, dba',
|
|
516
|
+
},
|
|
517
|
+
task: {
|
|
518
|
+
type: 'string',
|
|
519
|
+
required: true,
|
|
520
|
+
description: 'The complete, self-contained task and instructions for the specialist subagent.',
|
|
521
|
+
},
|
|
522
|
+
context: {
|
|
523
|
+
type: 'string',
|
|
524
|
+
description: 'Optional existing code, guidelines, or conversation context relevant to the task.',
|
|
525
|
+
},
|
|
526
|
+
iteration: {
|
|
527
|
+
type: 'integer',
|
|
528
|
+
description: 'Optional iteration number for rework/refinement cycles (e.g. 2, 3).',
|
|
529
|
+
},
|
|
530
|
+
feedback: {
|
|
531
|
+
type: 'string',
|
|
532
|
+
description: 'Optional review feedback from previous iteration to send to subagent.',
|
|
533
|
+
},
|
|
534
|
+
status: {
|
|
535
|
+
type: 'string',
|
|
536
|
+
description: 'Optional review status: "accepted", "rework", or "in_progress". Default: "accepted"',
|
|
537
|
+
},
|
|
538
|
+
capability: {
|
|
539
|
+
type: 'string',
|
|
540
|
+
description: 'Optional semantic capability requirement: coding, reasoning, fast, general',
|
|
541
|
+
},
|
|
542
|
+
reasoningEffort: {
|
|
543
|
+
type: 'string',
|
|
544
|
+
description: 'Optional reasoning effort level: off, low, medium, high, max',
|
|
545
|
+
},
|
|
546
|
+
maxTokens: {
|
|
547
|
+
type: 'integer',
|
|
548
|
+
description: 'Optional custom ceiling for output tokens',
|
|
549
|
+
},
|
|
550
|
+
},
|
|
551
|
+
output: { schema: { type: 'string' }, render },
|
|
552
|
+
async execute(args, execCtx) {
|
|
553
|
+
const cfg = {
|
|
554
|
+
...getConfig(),
|
|
555
|
+
registryProviders: typeof ctx.llm?.listProviders === 'function' ? ctx.llm.listProviders() : [],
|
|
556
|
+
}
|
|
557
|
+
const modelSelection = readModelSelection(ctx)
|
|
558
|
+
const currentDepth = computeDelegationDepth(execCtx)
|
|
559
|
+
const isChildSession = Boolean(execCtx?.session?.parentId || execCtx?.session?.parent || currentDepth > 0)
|
|
560
|
+
const result = await executeDirectDelegation({
|
|
561
|
+
roleId: args.roleId,
|
|
562
|
+
task: args.task,
|
|
563
|
+
context: args.context || '',
|
|
564
|
+
roles: cfg.roles,
|
|
565
|
+
config: cfg,
|
|
566
|
+
callLlm,
|
|
567
|
+
allowedRoutes: modelSelection.allowedRoutes,
|
|
568
|
+
serializationGate,
|
|
569
|
+
sessionLifecycle,
|
|
570
|
+
currentDepth,
|
|
571
|
+
isChildSession,
|
|
572
|
+
capability: args.capability,
|
|
573
|
+
reasoningEffort: args.reasoningEffort,
|
|
574
|
+
maxTokens: args.maxTokens,
|
|
575
|
+
})
|
|
576
|
+
|
|
577
|
+
const hitPct = ((result.metrics?.hitRatio ?? 0) * 100).toFixed(1)
|
|
578
|
+
const savingsPct = result.metrics?.estimatedSavingsPct ?? 0
|
|
579
|
+
|
|
580
|
+
const isRework = Boolean(
|
|
581
|
+
args.feedback || (args.iteration && args.iteration > 1) || args.status === 'rework'
|
|
582
|
+
)
|
|
583
|
+
const iterationNum = args.iteration || (isRework ? 2 : 1)
|
|
584
|
+
const statusBadge = isRework
|
|
585
|
+
? `> 🟡 **Result received from subagent [${result.roleName}] and sent for rework (iteration ${iterationNum})**`
|
|
586
|
+
: `> 🟢 **Subagent result accepted by orchestrator [${result.roleName}]**`
|
|
587
|
+
|
|
588
|
+
return (
|
|
589
|
+
`${statusBadge}\n` +
|
|
590
|
+
`> *Specialist: \`${result.roleId}\` | Model: \`${result.model}\` | Prompt Cache Hit: ${hitPct}% (Savings: ~${savingsPct}%)*\n\n` +
|
|
591
|
+
`${result.output}`
|
|
592
|
+
)
|
|
593
|
+
},
|
|
594
|
+
})
|
|
595
|
+
)
|
|
596
|
+
)
|
|
597
|
+
logger.debug('[dsh-agent-orchestrator] Registered tool: orchestrator_delegate_specialist')
|
|
598
|
+
} catch (e) {
|
|
599
|
+
logger.error('[dsh-agent-orchestrator] Failed to register orchestrator_delegate_specialist:', e)
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// 3. Named Agent Pool Runner Tool (agent_run by name - Issue #84 & #110 & #2 & #3)
|
|
603
|
+
try {
|
|
604
|
+
disposers.push(
|
|
605
|
+
targetCtx.tools.register(
|
|
606
|
+
defineTool({
|
|
607
|
+
name: 'agent_run',
|
|
608
|
+
description:
|
|
609
|
+
'Run a specialized subagent from the agent pool by name (e.g. architecture, spec, ui_design, frontend, backend, qa_tests, code_review, security_audit, devops, docs, data_engineer, dba). Supports one-shot deliverables and continuable interactive subagents with automatic tool intersection security and pinned non-interactive approvals.',
|
|
610
|
+
parameters: {
|
|
611
|
+
name: {
|
|
612
|
+
type: 'string',
|
|
613
|
+
required: true,
|
|
614
|
+
description:
|
|
615
|
+
'Specialist agent name or role ID from {{subagent_pool}} (e.g. architecture, code, qa_tests, ui_design, docs, devops, security, spec, backend, frontend)',
|
|
616
|
+
},
|
|
617
|
+
task: {
|
|
618
|
+
type: 'string',
|
|
619
|
+
required: true,
|
|
620
|
+
description: 'The specific, actionable task description for the specialist subagent.',
|
|
621
|
+
},
|
|
622
|
+
mode: {
|
|
623
|
+
type: 'string',
|
|
624
|
+
description: 'Execution mode: "one-shot" (default standalone task completion) or "continuable" (interactive multi-turn subagent).',
|
|
625
|
+
},
|
|
626
|
+
cwd: {
|
|
627
|
+
type: 'string',
|
|
628
|
+
description: 'Optional working directory path relative to project root (e.g. packages/core, src/ui).',
|
|
629
|
+
},
|
|
630
|
+
context: {
|
|
631
|
+
type: 'string',
|
|
632
|
+
description: 'Optional additional context, prior deliverable, or code excerpts.',
|
|
633
|
+
},
|
|
634
|
+
},
|
|
635
|
+
output: {
|
|
636
|
+
schema: {
|
|
637
|
+
type: 'object',
|
|
638
|
+
additionalProperties: true,
|
|
639
|
+
properties: {
|
|
640
|
+
success: { type: 'boolean' },
|
|
641
|
+
name: { type: 'string' },
|
|
642
|
+
roleName: { type: 'string' },
|
|
643
|
+
output: { type: 'string' },
|
|
644
|
+
childSessionId: { type: 'string' },
|
|
645
|
+
},
|
|
646
|
+
},
|
|
647
|
+
render: (_a, v) => [
|
|
648
|
+
{
|
|
649
|
+
type: 'text',
|
|
650
|
+
text:
|
|
651
|
+
typeof v === 'string'
|
|
652
|
+
? v
|
|
653
|
+
: v?.output || JSON.stringify(v, null, 2),
|
|
654
|
+
},
|
|
655
|
+
],
|
|
656
|
+
},
|
|
657
|
+
async execute(args, execCtx) {
|
|
658
|
+
const parentTools = targetCtx.tools?.schemas ? targetCtx.tools.schemas() : []
|
|
659
|
+
const currentDepth = computeDelegationDepth(execCtx)
|
|
660
|
+
const isChildSession = Boolean(execCtx?.session?.parentId || execCtx?.session?.parent || currentDepth > 0)
|
|
661
|
+
const runCfg = {
|
|
662
|
+
...getConfig(),
|
|
663
|
+
registryProviders: typeof ctx.llm?.listProviders === 'function' ? ctx.llm.listProviders() : [],
|
|
664
|
+
}
|
|
665
|
+
const res = await executeDirectDelegation({
|
|
666
|
+
roleId: args.name,
|
|
667
|
+
task: args.task,
|
|
668
|
+
mode: args.mode || 'one-shot',
|
|
669
|
+
cwd: args.cwd,
|
|
670
|
+
context: args.context || '',
|
|
671
|
+
roles: runCfg.roles,
|
|
672
|
+
config: runCfg,
|
|
673
|
+
callLlm,
|
|
674
|
+
subagents: ctx.subagents,
|
|
675
|
+
parentTools,
|
|
676
|
+
parentSessionId: execCtx?.session?.id || execCtx?.sessionId,
|
|
677
|
+
allowedRoutes: readModelSelection(ctx).allowedRoutes,
|
|
678
|
+
serializationGate,
|
|
679
|
+
sessionLifecycle,
|
|
680
|
+
currentDepth,
|
|
681
|
+
isChildSession,
|
|
682
|
+
signal: execCtx?.signal,
|
|
683
|
+
})
|
|
684
|
+
return res
|
|
685
|
+
},
|
|
686
|
+
})
|
|
687
|
+
)
|
|
688
|
+
)
|
|
689
|
+
logger.debug('[dsh-agent-orchestrator] Registered tool: agent_run')
|
|
690
|
+
} catch (e) {
|
|
691
|
+
logger.error('[dsh-agent-orchestrator] Failed to register agent_run:', e)
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// 4. Dynamic Model & Subagent Catalog Tool (Issue #76)
|
|
695
|
+
try {
|
|
696
|
+
disposers.push(
|
|
697
|
+
targetCtx.tools.register(
|
|
698
|
+
defineTool({
|
|
699
|
+
name: 'model_subagent_catalog',
|
|
700
|
+
description:
|
|
701
|
+
'Query dynamic live catalog of available LLM providers, models, capabilities (coding, reasoning, fast, general), max output tokens, and reasoning effort support.',
|
|
702
|
+
parameters: {
|
|
703
|
+
provider: {
|
|
704
|
+
type: 'string',
|
|
705
|
+
description: 'Optional provider filter (e.g. "deepseek-official", "deepseek", "openrouter", "ollama", "anthropic")',
|
|
706
|
+
},
|
|
707
|
+
capability: {
|
|
708
|
+
type: 'string',
|
|
709
|
+
description: 'Optional semantic capability filter ("coding", "reasoning", "fast", "general")',
|
|
710
|
+
},
|
|
711
|
+
},
|
|
712
|
+
output: { schema: { type: 'string' }, render },
|
|
713
|
+
async execute(args) {
|
|
714
|
+
const catalog = await fetchModelCatalog(ctx, {
|
|
715
|
+
provider: args.provider,
|
|
716
|
+
capability: args.capability,
|
|
717
|
+
})
|
|
718
|
+
return JSON.stringify(
|
|
719
|
+
{
|
|
720
|
+
total: catalog.length,
|
|
721
|
+
catalog,
|
|
722
|
+
},
|
|
723
|
+
null,
|
|
724
|
+
2
|
|
725
|
+
)
|
|
726
|
+
},
|
|
727
|
+
})
|
|
728
|
+
)
|
|
729
|
+
)
|
|
730
|
+
logger.debug('[dsh-agent-orchestrator] Registered tool: model_subagent_catalog')
|
|
731
|
+
} catch (e) {
|
|
732
|
+
logger.error('[dsh-agent-orchestrator] Failed to register model_subagent_catalog:', e)
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
return () => {
|
|
736
|
+
for (const d of disposers) {
|
|
737
|
+
try { if (typeof d === 'function') d() } catch (_) {
|
|
738
|
+
/* safe ignore */
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}, 'dsh-agent-orchestrator: registered model tools')
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
if (ctx.tools?.register) {
|
|
746
|
+
registerToolsOnContext(ctx)
|
|
747
|
+
} else {
|
|
748
|
+
ctx.inject(['tools'], (tctx) => registerToolsOnContext(tctx))
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// Helper to handle pipeline dispatch triggered from chat
|
|
752
|
+
const handlePipelineDispatch = async ({ taskTitle, scenarioId, targetAgent, targetSession }) => {
|
|
753
|
+
try {
|
|
754
|
+
const pipeline = await runner.startPipeline({
|
|
755
|
+
taskTitle,
|
|
756
|
+
taskDescription: taskTitle,
|
|
757
|
+
scenarioId: scenarioId || 'auto',
|
|
758
|
+
})
|
|
759
|
+
|
|
760
|
+
const stagesFormatted = pipeline.stages
|
|
761
|
+
.map((s, i) => ` ${i + 1}. **${s.name}** — *${s.roleName}* (${s.assignedModel?.model || 'deepseek-chat'})`)
|
|
762
|
+
.join('\n')
|
|
763
|
+
|
|
764
|
+
const responseText =
|
|
765
|
+
`🚀 **Multi-Agent Orchestrator Pipeline Dispatched**\n\n` +
|
|
766
|
+
`- **Scenario**: \`${pipeline.scenarioTitle}\`\n` +
|
|
767
|
+
`- **Pipeline ID**: \`${pipeline.pipelineId}\`\n` +
|
|
768
|
+
`- **Objective**: ${pipeline.taskTitle}\n\n` +
|
|
769
|
+
`**Execution Graph (DAG):**\n${stagesFormatted}\n\n` +
|
|
770
|
+
`⚡ *Prompt Caching Active*: Prefix canonicalization enabled (>1024 token L1 static anchor + L2 task context). Downstream stages will stream deliverables with shared KV-cache.`
|
|
771
|
+
|
|
772
|
+
if (targetAgent && typeof targetAgent.followup === 'function') {
|
|
773
|
+
try {
|
|
774
|
+
targetAgent.followup({
|
|
775
|
+
id: randomUUID(),
|
|
776
|
+
role: 'user',
|
|
777
|
+
content: [{ type: 'text', text: responseText }],
|
|
778
|
+
source: { kind: 'user' },
|
|
779
|
+
})
|
|
780
|
+
} catch (_) {
|
|
781
|
+
/* safe ignore */
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
if (targetSession) {
|
|
786
|
+
if (typeof targetSession.reply === 'function') {
|
|
787
|
+
targetSession.reply(responseText)
|
|
788
|
+
} else if (typeof targetSession.append === 'function') {
|
|
789
|
+
targetSession.append('message', {
|
|
790
|
+
id: randomUUID(),
|
|
791
|
+
role: 'assistant',
|
|
792
|
+
content: [{ type: 'text', text: responseText }],
|
|
793
|
+
})
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
return { kind: 'success', text: responseText }
|
|
798
|
+
} catch (err) {
|
|
799
|
+
const errMsg = `❌ Failed to dispatch pipeline: ${err?.message || err}`
|
|
800
|
+
if (targetAgent && typeof targetAgent.followup === 'function') {
|
|
801
|
+
try {
|
|
802
|
+
targetAgent.followup({
|
|
803
|
+
id: randomUUID(),
|
|
804
|
+
role: 'user',
|
|
805
|
+
content: [{ type: 'text', text: errMsg }],
|
|
806
|
+
source: { kind: 'user' },
|
|
807
|
+
})
|
|
808
|
+
} catch (_) {
|
|
809
|
+
/* safe ignore */
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
return { kind: 'error', text: errMsg }
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// Chat slash commands: /orchestrate and /orc via commands service
|
|
817
|
+
ctx.inject(['commands'], (cctx) => {
|
|
818
|
+
try {
|
|
819
|
+
if (typeof cctx.commands?.register !== 'function') return
|
|
820
|
+
|
|
821
|
+
try {
|
|
822
|
+
const globalLayer = cctx.commands?.layers?.global
|
|
823
|
+
if (globalLayer?.commands?.data instanceof Map) {
|
|
824
|
+
if (globalLayer.commands.data.has('orchestrate')) {
|
|
825
|
+
globalLayer.commands.data.delete('orchestrate')
|
|
826
|
+
}
|
|
827
|
+
if (globalLayer.commands.data.has('orc')) {
|
|
828
|
+
globalLayer.commands.data.delete('orc')
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
} catch (_) {
|
|
832
|
+
/* safe ignore */
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
const commandHandler = async (invocation) => {
|
|
836
|
+
const raw =
|
|
837
|
+
invocation?.rawInput ??
|
|
838
|
+
invocation?.line ??
|
|
839
|
+
invocation?.input ??
|
|
840
|
+
(typeof invocation === 'string' ? invocation : '')
|
|
841
|
+
const text = String(raw || '').trim()
|
|
842
|
+
const intent = detectOrchestratorIntent(`/orchestrate ${text}`)
|
|
843
|
+
return handlePipelineDispatch({
|
|
844
|
+
taskTitle: intent.taskTitle || text || 'Interactive Orchestrated Task',
|
|
845
|
+
scenarioId: intent.scenarioId || 'auto',
|
|
846
|
+
targetAgent: invocation?.agent,
|
|
847
|
+
})
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
const unregister1 = cctx.commands.register({
|
|
851
|
+
name: 'orchestrate',
|
|
852
|
+
description: 'Multi-Agent Orchestrator: decompose task and dispatch across specialized roles with prompt caching',
|
|
853
|
+
input: { hint: '[hotfix|simple|medium|complex|enterprise] <task objective>' },
|
|
854
|
+
handler: commandHandler,
|
|
855
|
+
})
|
|
856
|
+
|
|
857
|
+
const unregister2 = cctx.commands.register({
|
|
858
|
+
name: 'orc',
|
|
859
|
+
description: 'Alias for /orchestrate',
|
|
860
|
+
input: { hint: '[hotfix|simple|medium|complex|enterprise] <task objective>' },
|
|
861
|
+
handler: commandHandler,
|
|
862
|
+
})
|
|
863
|
+
|
|
864
|
+
// Roster Viewer Slash Command (Issue #105)
|
|
865
|
+
const rosterHandler = async (invocation) => {
|
|
866
|
+
const rolesList = toRolesArray(getConfig().roles || getDefaultRoles())
|
|
867
|
+
const rows = [
|
|
868
|
+
'| Name | Role ID | Model | Status | maxTokens | Skills/Tools |',
|
|
869
|
+
'| :--- | :--- | :--- | :---: | :---: | :--- |',
|
|
870
|
+
]
|
|
871
|
+
for (const r of rolesList) {
|
|
872
|
+
const modelStr = r.defaultModel ? `${r.defaultModel.provider}:${r.defaultModel.model}` : 'default'
|
|
873
|
+
const statusStr = r.enabled !== false ? 'Active' : 'Disabled'
|
|
874
|
+
const maxTok = r.maxTokens || 4096
|
|
875
|
+
const skillsList = (r.skills || []).concat(r.tools || []).slice(0, 4).join(', ')
|
|
876
|
+
rows.push(`| ${r.name || r.displayName || r.id} | \`${r.id}\` | ${modelStr} | ${statusStr} | ${maxTok} | ${skillsList} |`)
|
|
877
|
+
}
|
|
878
|
+
const output = `### Multi-Agent Orchestrator: Agent Roster (${rolesList.length} specialists)\n\n${rows.join('\n')}`
|
|
879
|
+
if (invocation?.agent && typeof invocation.agent.followup === 'function') {
|
|
880
|
+
try {
|
|
881
|
+
invocation.agent.followup({
|
|
882
|
+
id: randomUUID(),
|
|
883
|
+
role: 'assistant',
|
|
884
|
+
content: [{ type: 'text', text: output }],
|
|
885
|
+
})
|
|
886
|
+
} catch (_) {
|
|
887
|
+
/* safe ignore */
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
return { kind: 'success', text: output }
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
const unregister3 = cctx.commands.register({
|
|
894
|
+
name: 'subagents',
|
|
895
|
+
description: 'View orchestrator agent roster, capabilities, and active statuses (Issue #105)',
|
|
896
|
+
handler: rosterHandler,
|
|
897
|
+
})
|
|
898
|
+
|
|
899
|
+
const unregister4 = cctx.commands.register({
|
|
900
|
+
name: 'roster',
|
|
901
|
+
description: 'Alias for /subagents roster viewer (Issue #105)',
|
|
902
|
+
handler: rosterHandler,
|
|
903
|
+
})
|
|
904
|
+
|
|
905
|
+
if (typeof cctx.effect === 'function') {
|
|
906
|
+
cctx.effect(() => () => {
|
|
907
|
+
try {
|
|
908
|
+
if (typeof unregister1 === 'function') unregister1()
|
|
909
|
+
if (typeof unregister2 === 'function') unregister2()
|
|
910
|
+
if (typeof unregister3 === 'function') unregister3()
|
|
911
|
+
if (typeof unregister4 === 'function') unregister4()
|
|
912
|
+
} catch (_) {
|
|
913
|
+
/* safe ignore */
|
|
914
|
+
}
|
|
915
|
+
}, 'dsh-agent-orchestrator: commands unregister')
|
|
916
|
+
}
|
|
917
|
+
} catch (err) {
|
|
918
|
+
logger.warn('[dsh-agent-orchestrator] Commands register warning:', err?.message || err)
|
|
919
|
+
}
|
|
920
|
+
})
|
|
921
|
+
|
|
922
|
+
// Auto-archive one-shot subagents on completion (Issue #56)
|
|
923
|
+
ctx.effect(() => {
|
|
924
|
+
return ctx.on('subagent/end', (session) => {
|
|
925
|
+
const id = session?.id || session?.sessionId
|
|
926
|
+
if (id) {
|
|
927
|
+
sessionLifecycle.markCompleted(id, { event: 'subagent/end' })
|
|
928
|
+
}
|
|
929
|
+
})
|
|
930
|
+
}, 'dsh-agent-orchestrator: subagent/end lifecycle listener')
|
|
931
|
+
|
|
932
|
+
// Natural Language Chat Listener (session/event)
|
|
933
|
+
// Detects orchestrator triggers: "/orchestrate ...", "orchestrate: ...", etc.
|
|
934
|
+
ctx.effect(() => {
|
|
935
|
+
return ctx.on('session/event', async (session, event) => {
|
|
936
|
+
if (!session || event?.type !== 'input/user') return
|
|
937
|
+
|
|
938
|
+
const text = String(event.text || event.content || '').trim()
|
|
939
|
+
const intent = detectOrchestratorIntent(text)
|
|
940
|
+
|
|
941
|
+
if (intent.isTrigger && intent.action === 'on') {
|
|
942
|
+
await handlePipelineDispatch({
|
|
943
|
+
taskTitle: intent.taskTitle || 'Interactive Orchestrated Task',
|
|
944
|
+
scenarioId: intent.scenarioId || 'auto',
|
|
945
|
+
targetSession: session,
|
|
946
|
+
})
|
|
947
|
+
}
|
|
948
|
+
})
|
|
949
|
+
}, 'dsh-agent-orchestrator: natural language chat listener')
|
|
950
|
+
}
|