@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,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Directed Acyclic Graph (DAG) Execution Engine for DSH Multi-Agent Pipelines.
|
|
3
|
+
*
|
|
4
|
+
* Implements:
|
|
5
|
+
* - Topological dependency resolution and cycle detection
|
|
6
|
+
* - Concurrency control with dependency-aware scheduling (parallel independent nodes)
|
|
7
|
+
* - Blockers propagation: dependent nodes get marked as 'blocked' if a prerequisite fails
|
|
8
|
+
* - Event hooks: onNodeStart, onNodeComplete, onNodeFail, onNodeBlocked
|
|
9
|
+
* - Pure logic: completely testable without DSH runtime or network
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const NODE_STATUS = {
|
|
13
|
+
PENDING: 'pending',
|
|
14
|
+
RUNNING: 'running',
|
|
15
|
+
COMPLETED: 'completed',
|
|
16
|
+
FAILED: 'failed',
|
|
17
|
+
BLOCKED: 'blocked',
|
|
18
|
+
SKIPPED: 'skipped',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Validates stage definitions and checks for circular dependencies.
|
|
23
|
+
* @param {Array<object>} stages List of stages { id, name, dependsOn: string[], roleId, ... }
|
|
24
|
+
* @throws {Error} if stages are invalid or contain cycles
|
|
25
|
+
*/
|
|
26
|
+
export function validateGraph(stages) {
|
|
27
|
+
if (!Array.isArray(stages) || stages.length === 0) {
|
|
28
|
+
throw new Error('Pipeline must have at least one stage')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const ids = new Set()
|
|
32
|
+
for (const s of stages) {
|
|
33
|
+
if (!s.id || typeof s.id !== 'string') {
|
|
34
|
+
throw new Error(`Invalid stage ID: ${JSON.stringify(s)}`)
|
|
35
|
+
}
|
|
36
|
+
if (ids.has(s.id)) {
|
|
37
|
+
throw new Error(`Duplicate stage ID: ${s.id}`)
|
|
38
|
+
}
|
|
39
|
+
ids.add(s.id)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Check that all dependsOn references exist
|
|
43
|
+
for (const s of stages) {
|
|
44
|
+
const deps = s.dependsOn || []
|
|
45
|
+
for (const dep of deps) {
|
|
46
|
+
if (!ids.has(dep)) {
|
|
47
|
+
throw new Error(`Stage "${s.id}" depends on non-existent stage "${dep}"`)
|
|
48
|
+
}
|
|
49
|
+
if (dep === s.id) {
|
|
50
|
+
throw new Error(`Stage "${s.id}" cannot depend on itself`)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Cycle detection via DFS
|
|
56
|
+
const visited = new Map() // id -> 0 (unvisited), 1 (visiting), 2 (visited)
|
|
57
|
+
const adj = new Map()
|
|
58
|
+
for (const s of stages) {
|
|
59
|
+
visited.set(s.id, 0)
|
|
60
|
+
adj.set(s.id, s.dependsOn || [])
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function dfs(nodeId, path = []) {
|
|
64
|
+
visited.set(nodeId, 1)
|
|
65
|
+
const currentPath = [...path, nodeId]
|
|
66
|
+
const neighbors = adj.get(nodeId) || []
|
|
67
|
+
for (const n of neighbors) {
|
|
68
|
+
const state = visited.get(n)
|
|
69
|
+
if (state === 1) {
|
|
70
|
+
const cycle = [...currentPath.slice(currentPath.indexOf(n)), n].join(' -> ')
|
|
71
|
+
throw new Error(`Circular dependency detected: ${cycle}`)
|
|
72
|
+
}
|
|
73
|
+
if (state === 0) {
|
|
74
|
+
dfs(n, currentPath)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
visited.set(nodeId, 2)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (const s of stages) {
|
|
81
|
+
if (visited.get(s.id) === 0) {
|
|
82
|
+
dfs(s.id)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return true
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Returns stages whose dependencies are 100% completed.
|
|
91
|
+
*/
|
|
92
|
+
export function getRunnableStages(stages, stateMap) {
|
|
93
|
+
return stages.filter((s) => {
|
|
94
|
+
const currentState = stateMap.get(s.id)?.status || NODE_STATUS.PENDING
|
|
95
|
+
if (currentState !== NODE_STATUS.PENDING) {
|
|
96
|
+
return false
|
|
97
|
+
}
|
|
98
|
+
const deps = s.dependsOn || []
|
|
99
|
+
return deps.every((d) => stateMap.get(d)?.status === NODE_STATUS.COMPLETED)
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Identifies stages blocked by upstream failures.
|
|
105
|
+
*/
|
|
106
|
+
export function markBlockedStages(stages, stateMap) {
|
|
107
|
+
let changed = false
|
|
108
|
+
for (const s of stages) {
|
|
109
|
+
const state = stateMap.get(s.id)
|
|
110
|
+
if (state.status === NODE_STATUS.PENDING) {
|
|
111
|
+
const deps = s.dependsOn || []
|
|
112
|
+
const failedDep = deps.find((d) => {
|
|
113
|
+
const ds = stateMap.get(d)?.status
|
|
114
|
+
return ds === NODE_STATUS.FAILED || ds === NODE_STATUS.BLOCKED
|
|
115
|
+
})
|
|
116
|
+
if (failedDep) {
|
|
117
|
+
state.status = NODE_STATUS.BLOCKED
|
|
118
|
+
state.blockedBy = failedDep
|
|
119
|
+
state.error = `Blocked due to failure of upstream stage: ${failedDep}`
|
|
120
|
+
changed = true
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return changed
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Executes a pipeline of stages respecting DAG dependencies, concurrency, and blockers.
|
|
129
|
+
*
|
|
130
|
+
* @param {object} options
|
|
131
|
+
* @param {Array<object>} options.stages
|
|
132
|
+
* @param {function} options.executor async (stage, context) => stageResult
|
|
133
|
+
* @param {number} [options.concurrency=4] max parallel tasks
|
|
134
|
+
* @param {object} [options.initialContext={}]
|
|
135
|
+
* @param {function} [options.onNodeStart]
|
|
136
|
+
* @param {function} [options.onNodeComplete]
|
|
137
|
+
* @param {function} [options.onNodeFail]
|
|
138
|
+
* @param {function} [options.onNodeBlocked]
|
|
139
|
+
/**
|
|
140
|
+
* Executes a pipeline of stages respecting DAG dependencies, concurrency, and blockers.
|
|
141
|
+
* Supports both object ({ stages, executor, concurrency }) and positional (stages, executor, options) calls.
|
|
142
|
+
*/
|
|
143
|
+
export async function executeDAG(stagesOrOptions, executorArg, optionsArg = {}) {
|
|
144
|
+
let stages
|
|
145
|
+
let executor
|
|
146
|
+
let concurrency = 4
|
|
147
|
+
let initialContext = {}
|
|
148
|
+
let onNodeStart = () => {}
|
|
149
|
+
let onNodeComplete = () => {}
|
|
150
|
+
let onNodeFail = () => {}
|
|
151
|
+
let onNodeBlocked = () => {}
|
|
152
|
+
|
|
153
|
+
if (Array.isArray(stagesOrOptions)) {
|
|
154
|
+
stages = stagesOrOptions
|
|
155
|
+
executor = executorArg
|
|
156
|
+
concurrency = optionsArg.concurrency || optionsArg.maxConcurrency || 4
|
|
157
|
+
initialContext = optionsArg.initialContext || {}
|
|
158
|
+
if (typeof optionsArg.onNodeStart === 'function') onNodeStart = optionsArg.onNodeStart
|
|
159
|
+
if (typeof optionsArg.onNodeComplete === 'function') onNodeComplete = optionsArg.onNodeComplete
|
|
160
|
+
if (typeof optionsArg.onNodeFail === 'function') onNodeFail = optionsArg.onNodeFail
|
|
161
|
+
if (typeof optionsArg.onNodeBlocked === 'function') onNodeBlocked = optionsArg.onNodeBlocked
|
|
162
|
+
} else if (stagesOrOptions && typeof stagesOrOptions === 'object') {
|
|
163
|
+
stages = stagesOrOptions.stages
|
|
164
|
+
executor = stagesOrOptions.executor
|
|
165
|
+
concurrency = stagesOrOptions.concurrency || stagesOrOptions.maxConcurrency || 4
|
|
166
|
+
initialContext = stagesOrOptions.initialContext || {}
|
|
167
|
+
if (typeof stagesOrOptions.onNodeStart === 'function') onNodeStart = stagesOrOptions.onNodeStart
|
|
168
|
+
if (typeof stagesOrOptions.onNodeComplete === 'function') onNodeComplete = stagesOrOptions.onNodeComplete
|
|
169
|
+
if (typeof stagesOrOptions.onNodeFail === 'function') onNodeFail = stagesOrOptions.onNodeFail
|
|
170
|
+
if (typeof stagesOrOptions.onNodeBlocked === 'function') onNodeBlocked = stagesOrOptions.onNodeBlocked
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
validateGraph(stages)
|
|
174
|
+
|
|
175
|
+
const startTime = Date.now()
|
|
176
|
+
const stateMap = new Map()
|
|
177
|
+
const artifacts = new Map() // stageId -> artifact output
|
|
178
|
+
|
|
179
|
+
for (const s of stages) {
|
|
180
|
+
stateMap.set(s.id, {
|
|
181
|
+
id: s.id,
|
|
182
|
+
name: s.name || s.id,
|
|
183
|
+
roleId: s.roleId,
|
|
184
|
+
status: NODE_STATUS.PENDING,
|
|
185
|
+
dependsOn: s.dependsOn || [],
|
|
186
|
+
startedAt: null,
|
|
187
|
+
completedAt: null,
|
|
188
|
+
durationMs: 0,
|
|
189
|
+
output: null,
|
|
190
|
+
error: null,
|
|
191
|
+
metrics: null,
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let runningCount = 0
|
|
196
|
+
let isDone = false
|
|
197
|
+
|
|
198
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
199
|
+
function checkCompletion() {
|
|
200
|
+
const allDone = stages.every((s) => {
|
|
201
|
+
const st = stateMap.get(s.id).status
|
|
202
|
+
return st === NODE_STATUS.COMPLETED || st === NODE_STATUS.FAILED || st === NODE_STATUS.BLOCKED || st === NODE_STATUS.SKIPPED
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
if (allDone && runningCount === 0 && !isDone) {
|
|
206
|
+
isDone = true
|
|
207
|
+
const hasFailures = stages.some((s) => {
|
|
208
|
+
const st = stateMap.get(s.id).status
|
|
209
|
+
return st === NODE_STATUS.FAILED || st === NODE_STATUS.BLOCKED
|
|
210
|
+
})
|
|
211
|
+
resolvePromise({
|
|
212
|
+
success: !hasFailures,
|
|
213
|
+
durationMs: Date.now() - startTime,
|
|
214
|
+
stateMap: Object.fromEntries(stateMap),
|
|
215
|
+
artifacts: Object.fromEntries(artifacts),
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function triggerNext() {
|
|
221
|
+
if (isDone) return
|
|
222
|
+
|
|
223
|
+
// Propagate blockers first
|
|
224
|
+
if (markBlockedStages(stages, stateMap)) {
|
|
225
|
+
for (const s of stages) {
|
|
226
|
+
const st = stateMap.get(s.id)
|
|
227
|
+
if (st.status === NODE_STATUS.BLOCKED && !st.notifiedBlocked) {
|
|
228
|
+
st.notifiedBlocked = true
|
|
229
|
+
onNodeBlocked(st)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Find runnable candidates
|
|
235
|
+
const runnable = getRunnableStages(stages, stateMap)
|
|
236
|
+
|
|
237
|
+
while (runnable.length > 0 && runningCount < concurrency) {
|
|
238
|
+
const nextStage = runnable.shift()
|
|
239
|
+
const nodeState = stateMap.get(nextStage.id)
|
|
240
|
+
nodeState.status = NODE_STATUS.RUNNING
|
|
241
|
+
nodeState.startedAt = Date.now()
|
|
242
|
+
runningCount++
|
|
243
|
+
|
|
244
|
+
onNodeStart(nodeState)
|
|
245
|
+
|
|
246
|
+
// Run task asynchronously
|
|
247
|
+
const upstreamOutputs = {}
|
|
248
|
+
for (const dep of nextStage.dependsOn || []) {
|
|
249
|
+
upstreamOutputs[dep] = artifacts.get(dep)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const runContext = {
|
|
253
|
+
...initialContext,
|
|
254
|
+
upstreamOutputs,
|
|
255
|
+
allArtifacts: Object.fromEntries(artifacts),
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
Promise.resolve()
|
|
259
|
+
.then(() => executor(nextStage, runContext))
|
|
260
|
+
.then((result) => {
|
|
261
|
+
nodeState.status = NODE_STATUS.COMPLETED
|
|
262
|
+
nodeState.completedAt = Date.now()
|
|
263
|
+
nodeState.durationMs = nodeState.completedAt - nodeState.startedAt
|
|
264
|
+
nodeState.output = result?.output ?? result
|
|
265
|
+
nodeState.metrics = result?.metrics ?? null
|
|
266
|
+
|
|
267
|
+
artifacts.set(nextStage.id, nodeState.output)
|
|
268
|
+
runningCount--
|
|
269
|
+
onNodeComplete(nodeState)
|
|
270
|
+
triggerNext()
|
|
271
|
+
})
|
|
272
|
+
.catch((err) => {
|
|
273
|
+
nodeState.status = NODE_STATUS.FAILED
|
|
274
|
+
nodeState.completedAt = Date.now()
|
|
275
|
+
nodeState.durationMs = nodeState.completedAt - nodeState.startedAt
|
|
276
|
+
nodeState.error = err?.message || String(err)
|
|
277
|
+
|
|
278
|
+
runningCount--
|
|
279
|
+
onNodeFail(nodeState, err)
|
|
280
|
+
triggerNext()
|
|
281
|
+
})
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
checkCompletion()
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Initial trigger
|
|
288
|
+
triggerNext()
|
|
289
|
+
})
|
|
290
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-layered Decision Trace Ledger.
|
|
3
|
+
*
|
|
4
|
+
* Implements Issue #108:
|
|
5
|
+
* - Records reasons for agent selection, model resolution, tool filtering, and execution timings.
|
|
6
|
+
* - Formatted for injection into `presentationMeta` so UI can inspect the trace without polluting LLM context.
|
|
7
|
+
* - Strict size limiter (`assertTraceSize <= 4096B`) with cascade truncation of secondary fields:
|
|
8
|
+
* logs -> checks -> calls -> summary.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const MAX_TRACE_BYTES = 4096
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Validates and measures the JSON-serialized byte size of a decision trace.
|
|
15
|
+
* @param {object} trace
|
|
16
|
+
* @returns {number}
|
|
17
|
+
*/
|
|
18
|
+
export function getTraceByteLength(trace) {
|
|
19
|
+
return Buffer.byteLength(JSON.stringify(trace || {}), 'utf-8')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Truncates trace cascadingly if it exceeds MAX_TRACE_BYTES.
|
|
24
|
+
* Hierarchy of reduction:
|
|
25
|
+
* 1. Truncate long log messages (to max 64 chars)
|
|
26
|
+
* 2. Drop debug logs array entirely
|
|
27
|
+
* 3. Truncate droppedTools array
|
|
28
|
+
* 4. Truncate reasoning/rationale text
|
|
29
|
+
* 5. Minimal fallback summary
|
|
30
|
+
*
|
|
31
|
+
* @param {object} rawTrace
|
|
32
|
+
* @param {number} [maxBytes=MAX_TRACE_BYTES]
|
|
33
|
+
* @returns {object} Guaranteed <= maxBytes
|
|
34
|
+
*/
|
|
35
|
+
export function sanitizeDecisionTrace(rawTrace, maxBytes = MAX_TRACE_BYTES) {
|
|
36
|
+
if (!rawTrace || typeof rawTrace !== 'object') {
|
|
37
|
+
return { error: 'Empty trace' }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let trace = JSON.parse(JSON.stringify(rawTrace))
|
|
41
|
+
if (getTraceByteLength(trace) <= maxBytes) {
|
|
42
|
+
return trace
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Level 1: Truncate logs items
|
|
46
|
+
if (Array.isArray(trace.logs)) {
|
|
47
|
+
trace.logs = trace.logs.map((l) =>
|
|
48
|
+
typeof l === 'string' && l.length > 64 ? `${l.slice(0, 61)}...` : l
|
|
49
|
+
)
|
|
50
|
+
if (getTraceByteLength(trace) <= maxBytes) return trace
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Level 2: Drop logs array completely
|
|
54
|
+
if (trace.logs) {
|
|
55
|
+
delete trace.logs
|
|
56
|
+
if (getTraceByteLength(trace) <= maxBytes) return trace
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Level 3: Compact droppedTools
|
|
60
|
+
if (Array.isArray(trace.droppedTools) && trace.droppedTools.length > 5) {
|
|
61
|
+
const count = trace.droppedTools.length
|
|
62
|
+
trace.droppedTools = trace.droppedTools.slice(0, 5)
|
|
63
|
+
trace.droppedToolsCount = count
|
|
64
|
+
if (getTraceByteLength(trace) <= maxBytes) return trace
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Level 4: Truncate rationale / context
|
|
68
|
+
if (typeof trace.rationale === 'string' && trace.rationale.length > 120) {
|
|
69
|
+
trace.rationale = `${trace.rationale.slice(0, 117)}...`
|
|
70
|
+
if (getTraceByteLength(trace) <= maxBytes) return trace
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Level 5: Aggressive compaction to minimal essence
|
|
74
|
+
return {
|
|
75
|
+
executionId: trace.executionId,
|
|
76
|
+
roleId: trace.roleId,
|
|
77
|
+
model: trace.model,
|
|
78
|
+
decision: trace.decision || 'delegated',
|
|
79
|
+
droppedCount: Array.isArray(rawTrace.droppedTools) ? rawTrace.droppedTools.length : 0,
|
|
80
|
+
truncated: true,
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Creates a structured Decision Trace object.
|
|
86
|
+
*/
|
|
87
|
+
export function createDecisionTrace({
|
|
88
|
+
executionId,
|
|
89
|
+
roleId,
|
|
90
|
+
roleName,
|
|
91
|
+
requestedModel,
|
|
92
|
+
assignedModel,
|
|
93
|
+
selectionReason = 'preset_match',
|
|
94
|
+
toolSummary,
|
|
95
|
+
droppedTools = [],
|
|
96
|
+
depth = 1,
|
|
97
|
+
durationMs,
|
|
98
|
+
status = 'completed',
|
|
99
|
+
rationale,
|
|
100
|
+
}) {
|
|
101
|
+
const trace = {
|
|
102
|
+
executionId,
|
|
103
|
+
timestamp: Date.now(),
|
|
104
|
+
roleId,
|
|
105
|
+
roleName,
|
|
106
|
+
depth,
|
|
107
|
+
model: {
|
|
108
|
+
requested: requestedModel,
|
|
109
|
+
assigned: assignedModel,
|
|
110
|
+
reason: selectionReason,
|
|
111
|
+
},
|
|
112
|
+
tools: {
|
|
113
|
+
allowedCount: toolSummary?.allowed?.length ?? 0,
|
|
114
|
+
strippedCount: toolSummary?.strippedSecurity?.length ?? 0,
|
|
115
|
+
deniedCount: toolSummary?.denied?.length ?? 0,
|
|
116
|
+
},
|
|
117
|
+
droppedTools: droppedTools.slice(0, 15),
|
|
118
|
+
rationale,
|
|
119
|
+
durationMs,
|
|
120
|
+
status,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return sanitizeDecisionTrace(trace, MAX_TRACE_BYTES)
|
|
124
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task Decomposition Engine for DSH Multi-Agent Orchestrator.
|
|
3
|
+
*
|
|
4
|
+
* Automatically or semi-automatically breaks down tasks received from
|
|
5
|
+
* DSH chat or Gitea Kanban into concrete subtask stages based on chosen or inferred scenario.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getDefaultScenarios, getDefaultRoles, validateScenario } from './scenarios.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Infers scenario complexity based on task description cues.
|
|
12
|
+
*
|
|
13
|
+
* @param {string} taskText
|
|
14
|
+
* @returns {'simple' | 'medium' | 'complex'}
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Natural language complexity triggers for English and Russian input (Issue #133).
|
|
18
|
+
*/
|
|
19
|
+
export const COMPLEXITY_TRIGGERS = {
|
|
20
|
+
complex: /новый плагин|с нуля|полная архитектур|рефакторинг всего|from scratch|new plugin|system design|major refactor|complex pipeline|5-6 этап|сложн/i,
|
|
21
|
+
simple: /баг|опечатк|поправь|быстро|мелк|hotfix|bugfix|typo|quick fix|minor|bump|1-2 этап|прост/i,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function inferScenarioComplexity(taskText = '') {
|
|
25
|
+
const text = (taskText || '').toLowerCase()
|
|
26
|
+
|
|
27
|
+
if (COMPLEXITY_TRIGGERS.complex.test(text)) {
|
|
28
|
+
return 'complex'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (COMPLEXITY_TRIGGERS.simple.test(text)) {
|
|
32
|
+
return 'simple'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return 'medium'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Decomposes a task into a customized execution plan using a scenario preset.
|
|
40
|
+
*
|
|
41
|
+
* @param {object} params
|
|
42
|
+
* @param {string} params.taskTitle
|
|
43
|
+
* @param {string} params.taskDescription
|
|
44
|
+
* @param {string} [params.scenarioId] 'simple' | 'medium' | 'complex' or 'auto'
|
|
45
|
+
* @param {object} [params.customScenarios]
|
|
46
|
+
* @param {object} [params.customRoles]
|
|
47
|
+
* @returns {object} Decomposed pipeline plan { scenarioId, stages, taskTitle, taskDescription }
|
|
48
|
+
*/
|
|
49
|
+
export function decomposeTask({
|
|
50
|
+
taskTitle = '',
|
|
51
|
+
taskDescription = '',
|
|
52
|
+
scenarioId = 'auto',
|
|
53
|
+
customScenarios = null,
|
|
54
|
+
customRoles = null,
|
|
55
|
+
}) {
|
|
56
|
+
const scenarios = customScenarios || getDefaultScenarios()
|
|
57
|
+
const roles = customRoles || getDefaultRoles()
|
|
58
|
+
|
|
59
|
+
let selectedScenarioId = scenarioId
|
|
60
|
+
if (!selectedScenarioId || selectedScenarioId === 'auto') {
|
|
61
|
+
selectedScenarioId = inferScenarioComplexity(`${taskTitle}\n${taskDescription}`)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const scenario = scenarios[selectedScenarioId] || scenarios.medium
|
|
65
|
+
if (!scenario) {
|
|
66
|
+
throw new Error(`Scenario "${selectedScenarioId}" not found and fallback scenario "medium" is missing`)
|
|
67
|
+
}
|
|
68
|
+
validateScenario(scenario)
|
|
69
|
+
|
|
70
|
+
// Clone stages and inject customized task scope
|
|
71
|
+
const instantiatedStages = scenario.stages.map((stage) => {
|
|
72
|
+
const role = roles[stage.roleId] || {}
|
|
73
|
+
let customizedScope = stage.subtaskScope || ''
|
|
74
|
+
|
|
75
|
+
// Contextualize subtask instructions with the overall goal
|
|
76
|
+
customizedScope = [
|
|
77
|
+
`Overall Objective: "${taskTitle}"`,
|
|
78
|
+
`Deliverable Category: ${role.category || stage.roleId}`,
|
|
79
|
+
`Specific Guidance: ${customizedScope}`,
|
|
80
|
+
`Task Details:\n${taskDescription.trim()}`,
|
|
81
|
+
].join('\n\n')
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
...stage,
|
|
85
|
+
subtaskScope: customizedScope,
|
|
86
|
+
roleName: role.name || stage.roleId,
|
|
87
|
+
assignedModel: role.defaultModel || { provider: 'deepseek', model: 'deepseek-chat' },
|
|
88
|
+
skills: role.skills || [],
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
pipelineId: `pipe-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`,
|
|
94
|
+
scenarioId: selectedScenarioId,
|
|
95
|
+
scenarioTitle: scenario.title,
|
|
96
|
+
taskTitle: taskTitle || 'Untitled Pipeline Task',
|
|
97
|
+
taskDescription: taskDescription || '',
|
|
98
|
+
stages: instantiatedStages,
|
|
99
|
+
createdAt: Date.now(),
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|