@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
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Direct Specialist Delegation Handler.
|
|
3
|
+
*
|
|
4
|
+
* Implements:
|
|
5
|
+
* 1. MVO 1-to-1 Specialist Delegation (`orchestrator_delegate_specialist` and `agent_run`)
|
|
6
|
+
* 2. KV-Cache Prompt Alignment fallback when native subagents service is absent.
|
|
7
|
+
* 3. Bidirectional synonym bridge & safe tool intersection (Issue #2).
|
|
8
|
+
* 4. Pinned non-interactive approvals (`approval: 'never'`) (Issue #110).
|
|
9
|
+
* 5. Anti-Redelegation Shield (Issue #103): child subagents cannot receive delegation tools.
|
|
10
|
+
* 6. Leaf Experts Guard (Issue #102): leaf specialized experts run with maxDepth: 1.
|
|
11
|
+
* 7. Anti-Matryoshka Guard (Issue #19): hard limit on nesting depth (HARD_MAX_DEPTH = 3),
|
|
12
|
+
* and optional disableNestedDelegation configuration toggle.
|
|
13
|
+
* 8. Adaptive tool sanitization diagnostics (`droppedTools`) (Issue #111).
|
|
14
|
+
* 9. Per-Parent Serialization Gate & Concurrency Cap (Issue #93).
|
|
15
|
+
* 10. Per-Call `cwd` Workspace Scoping & Sandbox containment check (Issue #87).
|
|
16
|
+
* 11. Multi-layered Decision Trace Ledger in `presentationMeta` (≤4KB limit) (Issue #108).
|
|
17
|
+
* 12. Session Lifecycle Auto-Archiving for one-shot subagents (Issue #56).
|
|
18
|
+
* 13. Fail-Fast Model Validation with Candidate Shortlist (Issue #82).
|
|
19
|
+
* 14. Smart Model Routing by task complexity tier (Issue #48).
|
|
20
|
+
* 15. Deterministic Max-Tokens Watchdog with continue-once guarantee (Issue #75).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { randomUUID } from 'crypto'
|
|
24
|
+
import { resolveToolIntersection } from './intersection.js'
|
|
25
|
+
import { resolveSpecialistModel } from './model-selection.js'
|
|
26
|
+
import { resolveScopedCwd } from './concurrency-gate.js'
|
|
27
|
+
import { createDecisionTrace } from './decision-trace.js'
|
|
28
|
+
import { isMaxTokensTruncated, runWithMaxTokensWatchdog } from './token-watchdog.js'
|
|
29
|
+
import {
|
|
30
|
+
buildStaticBaseAnchor,
|
|
31
|
+
buildSharedTaskAnchor,
|
|
32
|
+
formatCumulativeArtifacts,
|
|
33
|
+
assembleAgentMessages,
|
|
34
|
+
extractCacheMetrics,
|
|
35
|
+
} from './cache-prefixer.js'
|
|
36
|
+
import { callWithTransientRetry } from './worker-pool.js'
|
|
37
|
+
import { getDefaultRoles } from './scenarios.js'
|
|
38
|
+
|
|
39
|
+
export const HARD_MAX_DEPTH = 3
|
|
40
|
+
export const NESTED_DELEGATION_GUARD_MESSAGE =
|
|
41
|
+
'Nested delegation is disabled by plugin policy (disableNestedDelegation=true).'
|
|
42
|
+
export const DELEGATION_DEPTH_LIMIT_MESSAGE =
|
|
43
|
+
`Delegation depth limit reached: maximum allowed depth is ${HARD_MAX_DEPTH} (Anti-Matryoshka Guard).`
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Normalizes roles collection to a flat array.
|
|
47
|
+
*/
|
|
48
|
+
export function toRolesArray(roles) {
|
|
49
|
+
if (!roles) return []
|
|
50
|
+
if (Array.isArray(roles)) return roles
|
|
51
|
+
if (typeof roles === 'object') {
|
|
52
|
+
return Object.entries(roles).map(([k, v]) => ({
|
|
53
|
+
id: v?.id || k,
|
|
54
|
+
displayName: v?.displayName || v?.name || k,
|
|
55
|
+
...v,
|
|
56
|
+
}))
|
|
57
|
+
}
|
|
58
|
+
return []
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Canonical mapping of natural language role aliases (English and Russian)
|
|
63
|
+
* for chat prompt and slash command parsing (Issue #133).
|
|
64
|
+
*/
|
|
65
|
+
export const ROLE_INPUT_ALIASES = {
|
|
66
|
+
coder: 'code',
|
|
67
|
+
developer: 'code',
|
|
68
|
+
dev: 'code',
|
|
69
|
+
разработчик: 'code',
|
|
70
|
+
программист: 'code',
|
|
71
|
+
код: 'code',
|
|
72
|
+
|
|
73
|
+
reviewer: 'qa_tests',
|
|
74
|
+
review: 'qa_tests',
|
|
75
|
+
ревью: 'qa_tests',
|
|
76
|
+
ревьюер: 'qa_tests',
|
|
77
|
+
tester: 'qa_tests',
|
|
78
|
+
qa: 'qa_tests',
|
|
79
|
+
тестировщик: 'qa_tests',
|
|
80
|
+
тесты: 'qa_tests',
|
|
81
|
+
|
|
82
|
+
architect: 'architecture',
|
|
83
|
+
архитектор: 'architecture',
|
|
84
|
+
архитектура: 'architecture',
|
|
85
|
+
design_system: 'architecture',
|
|
86
|
+
|
|
87
|
+
writer: 'docs',
|
|
88
|
+
technical_writer: 'docs',
|
|
89
|
+
документация: 'docs',
|
|
90
|
+
документатор: 'docs',
|
|
91
|
+
доки: 'docs',
|
|
92
|
+
|
|
93
|
+
spec: 'spec',
|
|
94
|
+
тз: 'spec',
|
|
95
|
+
спецификация: 'spec',
|
|
96
|
+
analyst: 'spec',
|
|
97
|
+
|
|
98
|
+
// Frontend
|
|
99
|
+
frontend: 'frontend',
|
|
100
|
+
front: 'frontend',
|
|
101
|
+
фронтенд: 'frontend',
|
|
102
|
+
фронт: 'frontend',
|
|
103
|
+
|
|
104
|
+
// Backend
|
|
105
|
+
backend: 'backend',
|
|
106
|
+
back: 'backend',
|
|
107
|
+
бэкенд: 'backend',
|
|
108
|
+
бэк: 'backend',
|
|
109
|
+
|
|
110
|
+
designer: 'ui_design',
|
|
111
|
+
design: 'ui_design',
|
|
112
|
+
дизайнер: 'ui_design',
|
|
113
|
+
дизайн: 'ui_design',
|
|
114
|
+
ux: 'ui_design',
|
|
115
|
+
ui: 'ui_design',
|
|
116
|
+
|
|
117
|
+
bugfix: 'bugfix',
|
|
118
|
+
баг: 'bugfix',
|
|
119
|
+
исправление: 'bugfix',
|
|
120
|
+
фикс: 'bugfix',
|
|
121
|
+
|
|
122
|
+
refactoring: 'refactoring',
|
|
123
|
+
рефакторинг: 'refactoring',
|
|
124
|
+
чистка: 'refactoring',
|
|
125
|
+
|
|
126
|
+
research: 'research',
|
|
127
|
+
исследование: 'research',
|
|
128
|
+
анализ: 'research',
|
|
129
|
+
|
|
130
|
+
devops: 'devops',
|
|
131
|
+
девопс: 'devops',
|
|
132
|
+
деплой: 'devops',
|
|
133
|
+
ci: 'devops',
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Resolves alias (e.g. 'coder', 'разработчик', 'code', 'тестировщик') to canonical role ID.
|
|
138
|
+
*/
|
|
139
|
+
export function normalizeRoleId(input, availableRoles = []) {
|
|
140
|
+
if (!input || typeof input !== 'string') return 'architecture'
|
|
141
|
+
const lower = input.trim().toLowerCase()
|
|
142
|
+
|
|
143
|
+
const rolesList = toRolesArray(
|
|
144
|
+
availableRoles && (Array.isArray(availableRoles) ? availableRoles.length > 0 : Object.keys(availableRoles).length > 0)
|
|
145
|
+
? availableRoles
|
|
146
|
+
: getDefaultRoles()
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
for (const role of rolesList) {
|
|
150
|
+
if (role.id.toLowerCase() === lower) return role.id
|
|
151
|
+
if (role.name && role.name.toLowerCase() === lower) return role.id
|
|
152
|
+
if (role.displayName && role.displayName.toLowerCase() === lower) return role.id
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
for (const [alias, id] of Object.entries(ROLE_INPUT_ALIASES)) {
|
|
156
|
+
if (lower === alias || lower.includes(alias)) {
|
|
157
|
+
const found = rolesList.find((r) => r.id === id)
|
|
158
|
+
if (found) return found.id
|
|
159
|
+
if (id === 'code') {
|
|
160
|
+
const fallbackCoding = rolesList.find((r) => r.id === 'fullstack' || r.id === 'backend')
|
|
161
|
+
if (fallbackCoding) return fallbackCoding.id
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return 'architecture' // Safe fallback role
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export const resolveTargetRole = normalizeRoleId
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Extracts plain text from canonical DSH subagent output block array.
|
|
173
|
+
*/
|
|
174
|
+
function extractOutputText(output) {
|
|
175
|
+
if (!output) return ''
|
|
176
|
+
if (typeof output === 'string') return output
|
|
177
|
+
if (Array.isArray(output)) {
|
|
178
|
+
return output
|
|
179
|
+
.map((block) => {
|
|
180
|
+
if (typeof block === 'string') return block
|
|
181
|
+
if (block && typeof block === 'object' && block.type === 'text' && typeof block.text === 'string') {
|
|
182
|
+
return block.text
|
|
183
|
+
}
|
|
184
|
+
return ''
|
|
185
|
+
})
|
|
186
|
+
.join('')
|
|
187
|
+
}
|
|
188
|
+
if (typeof output === 'object') {
|
|
189
|
+
return output.text || output.content || JSON.stringify(output)
|
|
190
|
+
}
|
|
191
|
+
return String(output)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Computes the delegation nesting depth based on parent context or session metadata.
|
|
196
|
+
*
|
|
197
|
+
* @param {object} [execCtx]
|
|
198
|
+
* @param {number} [explicitDepth]
|
|
199
|
+
* @returns {number} Current delegation depth (root = 0, child = 1)
|
|
200
|
+
*/
|
|
201
|
+
export function computeDelegationDepth(execCtx, explicitDepth) {
|
|
202
|
+
if (typeof explicitDepth === 'number') return explicitDepth
|
|
203
|
+
if (typeof execCtx?.depth === 'number') return execCtx.depth
|
|
204
|
+
if (typeof execCtx?.session?.metadata?.delegationDepth === 'number') {
|
|
205
|
+
return execCtx.session.metadata.delegationDepth
|
|
206
|
+
}
|
|
207
|
+
if (execCtx?.session?.parentId || execCtx?.session?.parent) {
|
|
208
|
+
return 1
|
|
209
|
+
}
|
|
210
|
+
return 0
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Executes delegation to a specialist worker with security guardrails.
|
|
215
|
+
*
|
|
216
|
+
* @param {object} params
|
|
217
|
+
* @param {string} params.roleId Target specialist role ID or alias
|
|
218
|
+
* @param {string} params.task Detailed task description for the specialist
|
|
219
|
+
* @param {string} [params.mode='one-shot'] Execution mode: 'one-shot' or 'continuable'
|
|
220
|
+
* @param {string} [params.cwd] Working directory scoping for the specialist
|
|
221
|
+
* @param {string} [params.context] Existing files, code, or background context
|
|
222
|
+
* @param {Array<object>|object} [params.roles] Available roles list or dict
|
|
223
|
+
* @param {object} [params.config] Plugin config
|
|
224
|
+
* @param {function} [params.callLlm] Direct LLM caller
|
|
225
|
+
* @param {object} [params.subagents] Native DSH subagents service (ctx.subagents)
|
|
226
|
+
* @param {Array<string|object>} [params.parentTools] Host/parent tools for security intersection
|
|
227
|
+
* @param {string} [params.parentSessionId] Current DSH session ID for child session linking
|
|
228
|
+
* @param {number} [params.currentDepth=0] Current recursion depth of caller
|
|
229
|
+
* @param {boolean} [params.isChildSession=false] Whether caller is already a delegated child
|
|
230
|
+
* @param {Array<object>} [params.allowedRoutes] Authorized models from subagent-model-selection
|
|
231
|
+
* @param {object} [params.serializationGate] Concurrency and serialization gate
|
|
232
|
+
* @param {object} [params.sessionLifecycle] Lifecycle manager for subagent sessions
|
|
233
|
+
* @param {function} [params.onStreamDelta] Streaming callback
|
|
234
|
+
* @param {AbortSignal} [params.signal] Cancellation abort signal
|
|
235
|
+
* @returns {Promise<object>} Result deliverable
|
|
236
|
+
*/
|
|
237
|
+
export async function executeDirectDelegation({
|
|
238
|
+
roleId,
|
|
239
|
+
task,
|
|
240
|
+
mode = 'one-shot',
|
|
241
|
+
cwd,
|
|
242
|
+
context = '',
|
|
243
|
+
roles = [],
|
|
244
|
+
config = {},
|
|
245
|
+
callLlm,
|
|
246
|
+
subagents,
|
|
247
|
+
parentTools,
|
|
248
|
+
parentSessionId = 'root',
|
|
249
|
+
currentDepth = 0,
|
|
250
|
+
isChildSession = false,
|
|
251
|
+
allowedRoutes,
|
|
252
|
+
capability,
|
|
253
|
+
reasoningEffort: requestedReasoningEffort,
|
|
254
|
+
maxTokens: explicitMaxTokens,
|
|
255
|
+
serializationGate,
|
|
256
|
+
sessionLifecycle,
|
|
257
|
+
onStreamDelta,
|
|
258
|
+
signal,
|
|
259
|
+
logger = null,
|
|
260
|
+
}) {
|
|
261
|
+
const startTime = Date.now()
|
|
262
|
+
|
|
263
|
+
// 0. Anti-Matryoshka & Anti-Redelegation Depth Guardrails (Issues #19, #102, #103)
|
|
264
|
+
const isNested = isChildSession || currentDepth > 0
|
|
265
|
+
|
|
266
|
+
// Check 1: Nested delegation disabled by policy
|
|
267
|
+
if (config.disableNestedDelegation && isNested) {
|
|
268
|
+
throw new Error(`[DelegationGuard] ${NESTED_DELEGATION_GUARD_MESSAGE}`)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Check 2: Hard ceiling on delegation depth (HARD_MAX_DEPTH = 3)
|
|
272
|
+
if (currentDepth >= HARD_MAX_DEPTH) {
|
|
273
|
+
throw new Error(`[DelegationGuard] ${DELEGATION_DEPTH_LIMIT_MESSAGE}`)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// 1. Workspace Scoping & Sandbox Containment (Issue #87)
|
|
277
|
+
const scopedCwd = cwd ? resolveScopedCwd(cwd, config.baseDir || process.cwd()) : undefined
|
|
278
|
+
|
|
279
|
+
const allRoles = toRolesArray(
|
|
280
|
+
roles && (Array.isArray(roles) ? roles.length > 0 : Object.keys(roles).length > 0)
|
|
281
|
+
? roles
|
|
282
|
+
: getDefaultRoles()
|
|
283
|
+
)
|
|
284
|
+
const canonicalId = normalizeRoleId(roleId, allRoles)
|
|
285
|
+
const role = allRoles.find((r) => r.id === canonicalId) || allRoles[0]
|
|
286
|
+
const roleName = role.displayName || role.name || role.id
|
|
287
|
+
|
|
288
|
+
const executionId = `delegation-${randomUUID().slice(0, 8)}`
|
|
289
|
+
|
|
290
|
+
// 2. Concurrency & Serialization Gate Acquisition (Issue #93)
|
|
291
|
+
let releaseGateSlot = () => {}
|
|
292
|
+
if (serializationGate && typeof serializationGate.acquire === 'function') {
|
|
293
|
+
releaseGateSlot = await serializationGate.acquire(parentSessionId || 'root', executionId)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
// 3. Calculate Tool Intersection & Adaptive Sanitization (Issues #2, #103, #111)
|
|
298
|
+
const hasContext = Boolean(context && String(context).trim() !== '')
|
|
299
|
+
const intersection = resolveToolIntersection({
|
|
300
|
+
parentTools,
|
|
301
|
+
roleTools: role.tools,
|
|
302
|
+
denyList: config.deniedTools || [],
|
|
303
|
+
failLoud: parentTools !== undefined && !hasContext,
|
|
304
|
+
roleId: role.id,
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
// 4. Model resolution: Smart Model Routing (#48), Fail-Fast Candidate Shortlist (#82),
|
|
308
|
+
// Semantic Capability Routing (#74), maxTokens ceiling (#79), Reasoning Effort (#86)
|
|
309
|
+
const {
|
|
310
|
+
provider,
|
|
311
|
+
model,
|
|
312
|
+
warning,
|
|
313
|
+
candidateShortlist,
|
|
314
|
+
selectionReason,
|
|
315
|
+
maxTokens: resolvedMaxTokens,
|
|
316
|
+
reasoningEffort,
|
|
317
|
+
} = resolveSpecialistModel({
|
|
318
|
+
roleModel: role.defaultModel,
|
|
319
|
+
fallbackModel: { provider: 'deepseek', model: 'deepseek-chat' },
|
|
320
|
+
allowedRoutes,
|
|
321
|
+
roleId: role.id,
|
|
322
|
+
task,
|
|
323
|
+
capability: capability || role.capability,
|
|
324
|
+
reasoningEffort: requestedReasoningEffort || role.reasoningEffort,
|
|
325
|
+
maxTokens: explicitMaxTokens || role.maxTokens,
|
|
326
|
+
smartRoutingEnabled: Boolean(config.smartModelRouting),
|
|
327
|
+
allowedProviders: config.allowedProviders || [],
|
|
328
|
+
registryProviders: config.registryProviders || [],
|
|
329
|
+
catalog: config.catalog || [],
|
|
330
|
+
failFast: Boolean(config.failFastModelValidation),
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
const log = logger || config?.logger || null
|
|
334
|
+
if (warning && log?.warn) {
|
|
335
|
+
log.warn(`[dsh-agent-orchestrator] ${warning}`)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const temperature = role.temperature ?? 0.3
|
|
339
|
+
const maxTokens = resolvedMaxTokens ?? role.maxTokens ?? 4096
|
|
340
|
+
|
|
341
|
+
// Helper to produce standard result deliverable with Decision Trace Ledger (Issue #108)
|
|
342
|
+
const produceDeliverable = (data) => {
|
|
343
|
+
const durationMs = Date.now() - startTime
|
|
344
|
+
const decisionTrace = createDecisionTrace({
|
|
345
|
+
executionId,
|
|
346
|
+
roleId: role.id,
|
|
347
|
+
roleName,
|
|
348
|
+
requestedModel: role.defaultModel?.model,
|
|
349
|
+
assignedModel: `${provider}:${model}`,
|
|
350
|
+
selectionReason: selectionReason || (warning ? 'fallback_allowed' : 'preset_match'),
|
|
351
|
+
toolSummary: {
|
|
352
|
+
allowed: intersection.tools,
|
|
353
|
+
strippedSecurity: intersection.droppedTools,
|
|
354
|
+
},
|
|
355
|
+
droppedTools: intersection.droppedTools,
|
|
356
|
+
depth: currentDepth + 1,
|
|
357
|
+
durationMs,
|
|
358
|
+
status: data.status || 'completed',
|
|
359
|
+
rationale: `Delegated to [${roleName}] with ${intersection.tools.length} allowed tools`,
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
...data,
|
|
364
|
+
maxTokens,
|
|
365
|
+
reasoningEffort,
|
|
366
|
+
cwd: scopedCwd,
|
|
367
|
+
candidateShortlist: candidateShortlist?.length ? candidateShortlist : undefined,
|
|
368
|
+
decisionTrace,
|
|
369
|
+
presentationMeta: {
|
|
370
|
+
decisionTrace,
|
|
371
|
+
maxTokens,
|
|
372
|
+
reasoningEffort,
|
|
373
|
+
},
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// 5. Check for Native DSH subagent service (Issues #3, #110, #56, #75)
|
|
378
|
+
if (subagents && typeof subagents.getProvider === 'function') {
|
|
379
|
+
const subagentProviderName = config.subagentProvider || 'spawn'
|
|
380
|
+
const transport = subagents.getProvider(subagentProviderName)
|
|
381
|
+
|
|
382
|
+
if (transport) {
|
|
383
|
+
const isContinuable = mode === 'continuable' && typeof subagents.startContinuable === 'function'
|
|
384
|
+
|
|
385
|
+
// Leaf Experts Guard: maxDepth is capped at 1 for leaf subagents (Issue #102)
|
|
386
|
+
const subagentRequest = {
|
|
387
|
+
label: `[${roleName}] ${task.slice(0, 48)}`,
|
|
388
|
+
prompt: [{ type: 'text', text: task }],
|
|
389
|
+
parent: parentSessionId,
|
|
390
|
+
persona: role.systemPrompt,
|
|
391
|
+
toolFilter: { allow: intersection.tools },
|
|
392
|
+
approval: 'never', // Pinned Non-Interactive Approvals (Issue #110)
|
|
393
|
+
maxDepth: 1, // Leaf expert bound: cannot spawn nested agents (Issue #102)
|
|
394
|
+
metadata: {
|
|
395
|
+
delegationDepth: currentDepth + 1,
|
|
396
|
+
roleId: role.id,
|
|
397
|
+
isLeaf: true,
|
|
398
|
+
},
|
|
399
|
+
...(scopedCwd ? { cwd: scopedCwd } : {}),
|
|
400
|
+
agentOptions: {
|
|
401
|
+
provider,
|
|
402
|
+
model,
|
|
403
|
+
maxTokens,
|
|
404
|
+
temperature,
|
|
405
|
+
...(reasoningEffort ? { reasoningEffort } : {}),
|
|
406
|
+
},
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (isContinuable) {
|
|
410
|
+
const continuableResult = await subagents.startContinuable({
|
|
411
|
+
provider: subagentProviderName,
|
|
412
|
+
label: subagentRequest.label,
|
|
413
|
+
request: subagentRequest,
|
|
414
|
+
signal,
|
|
415
|
+
})
|
|
416
|
+
const childId = continuableResult.childId || continuableResult.id
|
|
417
|
+
if (sessionLifecycle && childId) {
|
|
418
|
+
sessionLifecycle.register(childId, {
|
|
419
|
+
roleId: role.id,
|
|
420
|
+
executionId,
|
|
421
|
+
mode: 'continuable',
|
|
422
|
+
parentSessionId,
|
|
423
|
+
})
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return produceDeliverable({
|
|
427
|
+
kind: 'continuable',
|
|
428
|
+
childSessionId: childId,
|
|
429
|
+
executionId,
|
|
430
|
+
roleId: role.id,
|
|
431
|
+
roleName,
|
|
432
|
+
model: `${provider}:${model}`,
|
|
433
|
+
toolDiff: intersection.diff,
|
|
434
|
+
droppedTools: intersection.droppedTools,
|
|
435
|
+
depth: currentDepth + 1,
|
|
436
|
+
status: 'running',
|
|
437
|
+
output: `Continuable child session started: ${childId}`,
|
|
438
|
+
})
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Foreground / one-shot subagent execution with Deterministic max-tokens Watchdog (#75)
|
|
442
|
+
let outputText = ''
|
|
443
|
+
let runResult = null
|
|
444
|
+
let finalStopReason = 'completed'
|
|
445
|
+
let capturedChildSessionId = null
|
|
446
|
+
|
|
447
|
+
const executeForegroundTurn = async (count, contPrompt) => {
|
|
448
|
+
const activePrompt = count === 0 ? task : contPrompt
|
|
449
|
+
const run = await subagents.start(subagentProviderName, {
|
|
450
|
+
...subagentRequest,
|
|
451
|
+
prompt: [{ type: 'text', text: activePrompt }],
|
|
452
|
+
signal,
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
if (run?.id && !capturedChildSessionId) {
|
|
456
|
+
capturedChildSessionId = run.id
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (sessionLifecycle && run?.id) {
|
|
460
|
+
sessionLifecycle.register(run.id, {
|
|
461
|
+
roleId: role.id,
|
|
462
|
+
executionId,
|
|
463
|
+
mode: 'one-shot',
|
|
464
|
+
parentSessionId,
|
|
465
|
+
})
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
try {
|
|
469
|
+
runResult = await run.result
|
|
470
|
+
} finally {
|
|
471
|
+
if (run && typeof run.dispose === 'function') {
|
|
472
|
+
await run.dispose().catch((err) => {
|
|
473
|
+
if (log?.debug) {
|
|
474
|
+
log.debug('[dsh-agent-orchestrator] run.dispose skipped:', err?.message || err)
|
|
475
|
+
}
|
|
476
|
+
})
|
|
477
|
+
}
|
|
478
|
+
if (sessionLifecycle && run?.id) {
|
|
479
|
+
sessionLifecycle.markCompleted(run.id, {
|
|
480
|
+
roleId: role.id,
|
|
481
|
+
stopReason: runResult?.stopReason,
|
|
482
|
+
})
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
finalStopReason = runResult?.stopReason || 'completed'
|
|
487
|
+
return {
|
|
488
|
+
output: extractOutputText(runResult?.output),
|
|
489
|
+
stopReason: finalStopReason,
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const watchdogResult = await runWithMaxTokensWatchdog({
|
|
494
|
+
executionId,
|
|
495
|
+
executeTurn: executeForegroundTurn,
|
|
496
|
+
})
|
|
497
|
+
outputText = watchdogResult.output
|
|
498
|
+
|
|
499
|
+
return produceDeliverable({
|
|
500
|
+
kind: 'foreground',
|
|
501
|
+
childSessionId: capturedChildSessionId || executionId,
|
|
502
|
+
executionId,
|
|
503
|
+
roleId: role.id,
|
|
504
|
+
roleName,
|
|
505
|
+
model: `${provider}:${model}`,
|
|
506
|
+
toolDiff: intersection.diff,
|
|
507
|
+
droppedTools: intersection.droppedTools,
|
|
508
|
+
depth: currentDepth + 1,
|
|
509
|
+
status: watchdogResult.stopReason,
|
|
510
|
+
output: outputText,
|
|
511
|
+
metrics: {
|
|
512
|
+
model: `${provider}:${model}`,
|
|
513
|
+
roleId: role.id,
|
|
514
|
+
executionId,
|
|
515
|
+
depth: currentDepth + 1,
|
|
516
|
+
stopReason: watchdogResult.stopReason,
|
|
517
|
+
continuations: watchdogResult.continuationCount,
|
|
518
|
+
},
|
|
519
|
+
})
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// 6. Fallback: Direct LLM Turn with Prompt Cache Alignment & Watchdog (Issue #75)
|
|
524
|
+
if (typeof callLlm !== 'function') {
|
|
525
|
+
throw new Error('Neither subagents transport nor callLlm function is available for delegation')
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const baseAnchor = buildStaticBaseAnchor({
|
|
529
|
+
projectType: config.projectType || 'dsh-plugin',
|
|
530
|
+
customRules: config.customRules || '',
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
const taskAnchor = buildSharedTaskAnchor({
|
|
534
|
+
id: executionId,
|
|
535
|
+
title: `Direct Delegation: ${roleName}`,
|
|
536
|
+
description: task,
|
|
537
|
+
repo: config.repo || 'active-workspace',
|
|
538
|
+
scenario: 'direct-delegation',
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
const upstreamArtifacts = context
|
|
542
|
+
? formatCumulativeArtifacts([
|
|
543
|
+
{
|
|
544
|
+
stageId: 'user-context',
|
|
545
|
+
roleId: 'parent-agent',
|
|
546
|
+
output: context,
|
|
547
|
+
},
|
|
548
|
+
])
|
|
549
|
+
: ''
|
|
550
|
+
|
|
551
|
+
const stage = {
|
|
552
|
+
id: executionId,
|
|
553
|
+
name: `Direct Delegation to ${roleName}`,
|
|
554
|
+
roleId: role.id,
|
|
555
|
+
prompt: task,
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const messages = assembleAgentMessages({
|
|
559
|
+
baseAnchor,
|
|
560
|
+
taskAnchor,
|
|
561
|
+
cumulativeArtifacts: upstreamArtifacts,
|
|
562
|
+
agentRole: role,
|
|
563
|
+
currentStage: stage,
|
|
564
|
+
})
|
|
565
|
+
|
|
566
|
+
let lastUsage = {}
|
|
567
|
+
const executeLlmTurn = async (count, contPrompt) => {
|
|
568
|
+
const turnMessages = count === 0
|
|
569
|
+
? messages
|
|
570
|
+
: [
|
|
571
|
+
...messages,
|
|
572
|
+
{ role: 'user', content: contPrompt }
|
|
573
|
+
]
|
|
574
|
+
|
|
575
|
+
const res = await callWithTransientRetry(
|
|
576
|
+
callLlm,
|
|
577
|
+
{
|
|
578
|
+
provider,
|
|
579
|
+
model,
|
|
580
|
+
messages: turnMessages,
|
|
581
|
+
temperature,
|
|
582
|
+
maxTokens,
|
|
583
|
+
reasoningEffort,
|
|
584
|
+
onStreamDelta,
|
|
585
|
+
},
|
|
586
|
+
2,
|
|
587
|
+
1200
|
|
588
|
+
)
|
|
589
|
+
lastUsage = res.usage || {}
|
|
590
|
+
return {
|
|
591
|
+
output: res.text || res.content || '',
|
|
592
|
+
stopReason: res.stopReason || res.finishReason || 'completed',
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const watchdogResult = await runWithMaxTokensWatchdog({
|
|
597
|
+
executionId,
|
|
598
|
+
executeTurn: executeLlmTurn,
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
const cacheMetrics = extractCacheMetrics(lastUsage)
|
|
602
|
+
|
|
603
|
+
return produceDeliverable({
|
|
604
|
+
kind: 'inline',
|
|
605
|
+
output: watchdogResult.output,
|
|
606
|
+
roleId: role.id,
|
|
607
|
+
roleName,
|
|
608
|
+
model: `${provider}:${model}`,
|
|
609
|
+
toolDiff: intersection.diff,
|
|
610
|
+
droppedTools: intersection.droppedTools,
|
|
611
|
+
depth: currentDepth + 1,
|
|
612
|
+
status: watchdogResult.stopReason,
|
|
613
|
+
metrics: {
|
|
614
|
+
...cacheMetrics,
|
|
615
|
+
model: `${provider}:${model}`,
|
|
616
|
+
roleId: role.id,
|
|
617
|
+
executionId,
|
|
618
|
+
depth: currentDepth + 1,
|
|
619
|
+
continuations: watchdogResult.continuationCount,
|
|
620
|
+
},
|
|
621
|
+
})
|
|
622
|
+
} finally {
|
|
623
|
+
releaseGateSlot()
|
|
624
|
+
}
|
|
625
|
+
}
|