@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/routes.js
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP REST API Routes for DSH Multi-Agent Orchestrator.
|
|
3
|
+
*
|
|
4
|
+
* Implements:
|
|
5
|
+
* - Fail-closed origin/host/loopback check on state mutating routes (/dispatch, /delegate, /cancel, /config, /snapshots/save-as-preset) (Issue #113)
|
|
6
|
+
* - Strict body payload limit (1MB max, 413 Payload Too Large on overflow) (Issue #113)
|
|
7
|
+
* - Safe read-only /status route (no mutations, public metrics only) (Issue #113)
|
|
8
|
+
* - Snapshots listing & One-Click Save as Preset (Issue #109)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readModelSelection, fetchModelCatalog } from './pipeline/model-selection.js'
|
|
12
|
+
import { executeDirectDelegation } from './pipeline/delegation.js'
|
|
13
|
+
import { rejectUntrustedRequest, parseBoundedJsonBody } from './http-guard.js'
|
|
14
|
+
import { SnapshotManager } from './pipeline/snapshots.js'
|
|
15
|
+
import { validateScenario } from './pipeline/scenarios.js'
|
|
16
|
+
|
|
17
|
+
function jsonReply(res, data, status = 200) {
|
|
18
|
+
res.statusCode = status
|
|
19
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8')
|
|
20
|
+
res.end(JSON.stringify(data))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function registerOrchestratorRoutes(ctx, services) {
|
|
24
|
+
const { store, runner, getConfig, updateConfig, callLlm, snapshotManager = new SnapshotManager() } = services
|
|
25
|
+
|
|
26
|
+
// 1. Status & Global Metrics (Read-only status overview)
|
|
27
|
+
// Conscious architectural choice (Issue #113): Public read-only endpoint returning non-sensitive metrics
|
|
28
|
+
ctx.effect(() => ctx.webServer.register({
|
|
29
|
+
kind: 'exact',
|
|
30
|
+
path: '/dsh-agent-orchestrator/status',
|
|
31
|
+
handler: (req, res) => {
|
|
32
|
+
jsonReply(res, {
|
|
33
|
+
activePipelines: store.getActivePipelines(),
|
|
34
|
+
metrics: store.getMetrics(),
|
|
35
|
+
recentPipelines: store.getAllPipelines().slice(0, 10),
|
|
36
|
+
})
|
|
37
|
+
},
|
|
38
|
+
}), 'dsh-agent-orchestrator: status route')
|
|
39
|
+
|
|
40
|
+
// 2. Dispatch a new pipeline (multi-stage DAG) - Guarded (Issue #113)
|
|
41
|
+
ctx.effect(() => ctx.webServer.register({
|
|
42
|
+
kind: 'exact',
|
|
43
|
+
path: '/dsh-agent-orchestrator/dispatch',
|
|
44
|
+
handler: async (req, res) => {
|
|
45
|
+
if (req.method !== 'POST') {
|
|
46
|
+
return jsonReply(res, { error: 'Method not allowed' }, 405)
|
|
47
|
+
}
|
|
48
|
+
if (rejectUntrustedRequest(req, res)) return
|
|
49
|
+
|
|
50
|
+
let body
|
|
51
|
+
try {
|
|
52
|
+
body = await parseBoundedJsonBody(req)
|
|
53
|
+
} catch (err) {
|
|
54
|
+
return jsonReply(res, { error: err.message }, err.statusCode || 400)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const pipeline = await runner.startPipeline({
|
|
59
|
+
taskTitle: body.taskTitle || body.title || 'Manual Pipeline Run',
|
|
60
|
+
taskDescription: body.taskDescription || body.description || body.prompt || '',
|
|
61
|
+
scenarioId: body.scenarioId || 'auto',
|
|
62
|
+
taskId: body.taskId, // optional linked kanban task
|
|
63
|
+
})
|
|
64
|
+
jsonReply(res, { success: true, pipeline })
|
|
65
|
+
} catch (err) {
|
|
66
|
+
jsonReply(res, { error: err?.message || String(err) }, 400)
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
}), 'dsh-agent-orchestrator: dispatch route')
|
|
70
|
+
|
|
71
|
+
// 3. Direct Specialist Delegation (single subagent) - Guarded (Issue #113)
|
|
72
|
+
ctx.effect(() => ctx.webServer.register({
|
|
73
|
+
kind: 'exact',
|
|
74
|
+
path: '/dsh-agent-orchestrator/delegate',
|
|
75
|
+
handler: async (req, res) => {
|
|
76
|
+
if (req.method !== 'POST') {
|
|
77
|
+
return jsonReply(res, { error: 'Method not allowed' }, 405)
|
|
78
|
+
}
|
|
79
|
+
if (rejectUntrustedRequest(req, res)) return
|
|
80
|
+
|
|
81
|
+
let body
|
|
82
|
+
try {
|
|
83
|
+
body = await parseBoundedJsonBody(req)
|
|
84
|
+
} catch (err) {
|
|
85
|
+
return jsonReply(res, { error: err.message }, err.statusCode || 400)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!body.task) {
|
|
89
|
+
return jsonReply(res, { error: 'Missing required field: task' }, 400)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const selection = readModelSelection(ctx)
|
|
94
|
+
const result = await executeDirectDelegation({
|
|
95
|
+
roleId: body.roleId || 'architecture',
|
|
96
|
+
task: body.task,
|
|
97
|
+
context: body.context || '',
|
|
98
|
+
roles: getConfig().roles,
|
|
99
|
+
config: getConfig(),
|
|
100
|
+
callLlm,
|
|
101
|
+
allowedRoutes: selection.allowedRoutes,
|
|
102
|
+
})
|
|
103
|
+
jsonReply(res, { success: true, deliverable: result })
|
|
104
|
+
} catch (err) {
|
|
105
|
+
jsonReply(res, { error: err?.message || String(err) }, 500)
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
}), 'dsh-agent-orchestrator: direct delegate route')
|
|
109
|
+
|
|
110
|
+
// 4. Pipeline details by ID
|
|
111
|
+
ctx.effect(() => ctx.webServer.register({
|
|
112
|
+
kind: 'prefix',
|
|
113
|
+
path: '/dsh-agent-orchestrator/pipeline',
|
|
114
|
+
handler: (req, res) => {
|
|
115
|
+
const parts = req.url.split('?')[0].split('/')
|
|
116
|
+
const pipelineId = parts[parts.length - 1]
|
|
117
|
+
const pipeline = store.getPipeline(pipelineId)
|
|
118
|
+
if (!pipeline) {
|
|
119
|
+
return jsonReply(res, { error: 'Pipeline not found' }, 404)
|
|
120
|
+
}
|
|
121
|
+
jsonReply(res, { pipeline })
|
|
122
|
+
},
|
|
123
|
+
}), 'dsh-agent-orchestrator: pipeline detail route')
|
|
124
|
+
|
|
125
|
+
// 4b. Cancel a running pipeline - Guarded (Issue #113)
|
|
126
|
+
ctx.effect(() => ctx.webServer.register({
|
|
127
|
+
kind: 'exact',
|
|
128
|
+
path: '/dsh-agent-orchestrator/cancel',
|
|
129
|
+
handler: async (req, res) => {
|
|
130
|
+
if (req.method !== 'POST') {
|
|
131
|
+
return jsonReply(res, { error: 'Method not allowed' }, 405)
|
|
132
|
+
}
|
|
133
|
+
if (rejectUntrustedRequest(req, res)) return
|
|
134
|
+
|
|
135
|
+
let body
|
|
136
|
+
try {
|
|
137
|
+
body = await parseBoundedJsonBody(req)
|
|
138
|
+
} catch (err) {
|
|
139
|
+
return jsonReply(res, { error: err.message }, err.statusCode || 400)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const pipelineId = body.pipelineId
|
|
143
|
+
const p = store.getPipeline(pipelineId)
|
|
144
|
+
if (p && (p.status === 'running' || p.status === 'pending')) {
|
|
145
|
+
store.updatePipeline(pipelineId, {
|
|
146
|
+
status: 'failed',
|
|
147
|
+
completedAt: Date.now(),
|
|
148
|
+
error: 'Cancelled by user',
|
|
149
|
+
})
|
|
150
|
+
return jsonReply(res, { success: true })
|
|
151
|
+
}
|
|
152
|
+
jsonReply(res, { success: false, error: 'Pipeline not running or not found' }, 404)
|
|
153
|
+
},
|
|
154
|
+
}), 'dsh-agent-orchestrator: cancel route')
|
|
155
|
+
|
|
156
|
+
// 5. Configuration (roles, scenarios, kanbanSync) - Guarded (Issue #113)
|
|
157
|
+
ctx.effect(() => ctx.webServer.register({
|
|
158
|
+
kind: 'exact',
|
|
159
|
+
path: '/dsh-agent-orchestrator/config',
|
|
160
|
+
handler: async (req, res) => {
|
|
161
|
+
if (req.method === 'POST') {
|
|
162
|
+
if (rejectUntrustedRequest(req, res)) return
|
|
163
|
+
let body
|
|
164
|
+
try {
|
|
165
|
+
body = await parseBoundedJsonBody(req)
|
|
166
|
+
} catch (err) {
|
|
167
|
+
return jsonReply(res, { error: err.message }, err.statusCode || 400)
|
|
168
|
+
}
|
|
169
|
+
if (body?.scenarios && typeof body.scenarios === 'object') {
|
|
170
|
+
for (const [sId, sc] of Object.entries(body.scenarios)) {
|
|
171
|
+
try {
|
|
172
|
+
validateScenario(sc)
|
|
173
|
+
} catch (vErr) {
|
|
174
|
+
return jsonReply(res, { error: `Invalid scenario "${sId}": ${vErr.message}` }, 400)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
await updateConfig(body)
|
|
179
|
+
return jsonReply(res, { success: true, config: getConfig() })
|
|
180
|
+
}
|
|
181
|
+
jsonReply(res, { config: getConfig() })
|
|
182
|
+
},
|
|
183
|
+
}), 'dsh-agent-orchestrator: config route')
|
|
184
|
+
|
|
185
|
+
// 6. Available models query with allowedModels integration
|
|
186
|
+
ctx.effect(() => ctx.webServer.register({
|
|
187
|
+
kind: 'exact',
|
|
188
|
+
path: '/dsh-agent-orchestrator/models',
|
|
189
|
+
handler: async (req, res) => {
|
|
190
|
+
const selection = readModelSelection(ctx)
|
|
191
|
+
const catalog = await fetchModelCatalog(ctx, {
|
|
192
|
+
allowedRoutes: selection.allowedRoutes,
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
jsonReply(res, {
|
|
196
|
+
models: catalog,
|
|
197
|
+
authorizedConstraint: Boolean(selection.allowedRoutes && selection.allowedRoutes.length > 0),
|
|
198
|
+
})
|
|
199
|
+
},
|
|
200
|
+
}), 'dsh-agent-orchestrator: models route')
|
|
201
|
+
|
|
202
|
+
// 6b. Live Model & Subagent Catalog query (Issue #76)
|
|
203
|
+
ctx.effect(() => ctx.webServer.register({
|
|
204
|
+
kind: 'exact',
|
|
205
|
+
path: '/dsh-agent-orchestrator/catalog/models',
|
|
206
|
+
handler: async (req, res) => {
|
|
207
|
+
try {
|
|
208
|
+
const urlObj = new URL(req.url, 'http://localhost')
|
|
209
|
+
const provider = urlObj.searchParams.get('provider') || undefined
|
|
210
|
+
const capability = urlObj.searchParams.get('capability') || undefined
|
|
211
|
+
|
|
212
|
+
const catalog = await fetchModelCatalog(ctx, { provider, capability })
|
|
213
|
+
jsonReply(res, {
|
|
214
|
+
total: catalog.length,
|
|
215
|
+
catalog,
|
|
216
|
+
})
|
|
217
|
+
} catch (err) {
|
|
218
|
+
jsonReply(res, { error: err?.message || String(err) }, 500)
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
}), 'dsh-agent-orchestrator: catalog models route')
|
|
222
|
+
|
|
223
|
+
// 7. Dispatch Snapshots Listing (Issue #109)
|
|
224
|
+
ctx.effect(() => ctx.webServer.register({
|
|
225
|
+
kind: 'exact',
|
|
226
|
+
path: '/dsh-agent-orchestrator/snapshots',
|
|
227
|
+
handler: (req, res) => {
|
|
228
|
+
try {
|
|
229
|
+
const snapshots = snapshotManager.listSnapshots()
|
|
230
|
+
jsonReply(res, { snapshots, total: snapshots.length })
|
|
231
|
+
} catch (err) {
|
|
232
|
+
jsonReply(res, { error: err?.message || String(err) }, 500)
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
}), 'dsh-agent-orchestrator: snapshots list route')
|
|
236
|
+
|
|
237
|
+
// 8. One-Click Save as Preset (Issue #109) - Guarded (Issue #113)
|
|
238
|
+
ctx.effect(() => ctx.webServer.register({
|
|
239
|
+
kind: 'exact',
|
|
240
|
+
path: '/dsh-agent-orchestrator/snapshots/save-as-preset',
|
|
241
|
+
handler: async (req, res) => {
|
|
242
|
+
if (req.method !== 'POST') {
|
|
243
|
+
return jsonReply(res, { error: 'Method not allowed' }, 405)
|
|
244
|
+
}
|
|
245
|
+
if (rejectUntrustedRequest(req, res)) return
|
|
246
|
+
|
|
247
|
+
let body
|
|
248
|
+
try {
|
|
249
|
+
body = await parseBoundedJsonBody(req)
|
|
250
|
+
} catch (err) {
|
|
251
|
+
return jsonReply(res, { error: err.message }, err.statusCode || 400)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const { snapshotId, name, description } = body || {}
|
|
255
|
+
if (!snapshotId) {
|
|
256
|
+
return jsonReply(res, { error: 'Missing required field: snapshotId' }, 400)
|
|
257
|
+
}
|
|
258
|
+
if (typeof snapshotId !== 'string' || !/^[a-zA-Z0-9_-]{1,64}$/.test(snapshotId)) {
|
|
259
|
+
return jsonReply(res, { error: 'Invalid or unsafe snapshotId' }, 400)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
const preset = snapshotManager.saveAsPreset(snapshotId, { name, description })
|
|
264
|
+
validateScenario(preset)
|
|
265
|
+
|
|
266
|
+
// Idempotently persist to configured scenarios
|
|
267
|
+
const currentScenarios = getConfig().scenarios || {}
|
|
268
|
+
currentScenarios[preset.id] = preset
|
|
269
|
+
await updateConfig({ scenarios: currentScenarios })
|
|
270
|
+
|
|
271
|
+
jsonReply(res, { success: true, preset })
|
|
272
|
+
} catch (err) {
|
|
273
|
+
jsonReply(res, { error: err?.message || String(err) }, 400)
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
}), 'dsh-agent-orchestrator: save-as-preset route')
|
|
277
|
+
}
|
package/lib/store.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline & Metrics Store for DSH Multi-Agent Orchestrator.
|
|
3
|
+
*
|
|
4
|
+
* In-memory state with JSON persistence for recovery across turns.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'
|
|
8
|
+
import { join, dirname } from 'node:path'
|
|
9
|
+
import { homedir } from 'node:os'
|
|
10
|
+
|
|
11
|
+
export class OrchestratorStore {
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
const opts = typeof options === 'string' ? { storageDir: options } : (options || {})
|
|
14
|
+
const baseDir = opts.storageDir || join(homedir(), '.dsh')
|
|
15
|
+
this.storagePath = join(baseDir, 'orchestrator-pipelines.json')
|
|
16
|
+
this.logger = opts.logger || null
|
|
17
|
+
this.pipelines = new Map()
|
|
18
|
+
this.globalMetrics = {
|
|
19
|
+
totalPipelines: 0,
|
|
20
|
+
completedPipelines: 0,
|
|
21
|
+
failedPipelines: 0,
|
|
22
|
+
totalPromptTokens: 0,
|
|
23
|
+
totalCacheHitTokens: 0,
|
|
24
|
+
totalCacheMissTokens: 0,
|
|
25
|
+
overallHitRatio: 0,
|
|
26
|
+
totalDurationMs: 0,
|
|
27
|
+
}
|
|
28
|
+
this._load()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_load() {
|
|
32
|
+
try {
|
|
33
|
+
if (existsSync(this.storagePath)) {
|
|
34
|
+
const raw = readFileSync(this.storagePath, 'utf8')
|
|
35
|
+
const data = JSON.parse(raw)
|
|
36
|
+
if (Array.isArray(data.pipelines)) {
|
|
37
|
+
for (const p of data.pipelines) {
|
|
38
|
+
this.pipelines.set(p.pipelineId, p)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (data.globalMetrics) {
|
|
42
|
+
this.globalMetrics = { ...this.globalMetrics, ...data.globalMetrics }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
} catch (e) {
|
|
46
|
+
if (this.logger?.warn) this.logger.warn('[dsh-agent-orchestrator] Store load warning:', e?.message || e)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
save() {
|
|
51
|
+
try {
|
|
52
|
+
mkdirSync(dirname(this.storagePath), { recursive: true })
|
|
53
|
+
const serialized = {
|
|
54
|
+
pipelines: Array.from(this.pipelines.values()).slice(-50), // keep last 50
|
|
55
|
+
globalMetrics: this.globalMetrics,
|
|
56
|
+
updatedAt: Date.now(),
|
|
57
|
+
}
|
|
58
|
+
writeFileSync(this.storagePath, JSON.stringify(serialized, null, 2), 'utf8')
|
|
59
|
+
} catch (e) {
|
|
60
|
+
if (this.logger?.warn) this.logger.warn('[dsh-agent-orchestrator] Store save warning:', e?.message || e)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
recordPipeline(pipelineData) {
|
|
65
|
+
this.pipelines.set(pipelineData.pipelineId, pipelineData)
|
|
66
|
+
this.globalMetrics.totalPipelines++
|
|
67
|
+
this.save()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
updatePipeline(pipelineId, updateFnOrPartial) {
|
|
71
|
+
const existing = this.pipelines.get(pipelineId)
|
|
72
|
+
if (existing) {
|
|
73
|
+
const updated = typeof updateFnOrPartial === 'function'
|
|
74
|
+
? (updateFnOrPartial(existing) || existing)
|
|
75
|
+
: { ...existing, ...updateFnOrPartial }
|
|
76
|
+
this.pipelines.set(pipelineId, updated)
|
|
77
|
+
|
|
78
|
+
if (updated.status === 'completed' && existing.status !== 'completed') {
|
|
79
|
+
this.globalMetrics.completedPipelines++
|
|
80
|
+
this.globalMetrics.totalDurationMs += (updated.durationMs || 0)
|
|
81
|
+
} else if (updated.status === 'failed' && existing.status !== 'failed') {
|
|
82
|
+
this.globalMetrics.failedPipelines++
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.save()
|
|
86
|
+
return updated
|
|
87
|
+
}
|
|
88
|
+
return null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
updateStage(pipelineId, stageId, updateFnOrPartial) {
|
|
92
|
+
const pipeline = this.pipelines.get(pipelineId)
|
|
93
|
+
if (!pipeline) return null
|
|
94
|
+
|
|
95
|
+
if (!pipeline.stateMap) pipeline.stateMap = {}
|
|
96
|
+
const existing = pipeline.stateMap[stageId] || { id: stageId, status: 'pending' }
|
|
97
|
+
const updated = typeof updateFnOrPartial === 'function'
|
|
98
|
+
? (updateFnOrPartial(existing) || existing)
|
|
99
|
+
: { ...existing, ...updateFnOrPartial }
|
|
100
|
+
|
|
101
|
+
pipeline.stateMap[stageId] = updated
|
|
102
|
+
|
|
103
|
+
if (Array.isArray(pipeline.stages)) {
|
|
104
|
+
const idx = pipeline.stages.findIndex((s) => s.id === stageId)
|
|
105
|
+
if (idx !== -1) {
|
|
106
|
+
pipeline.stages[idx] = { ...pipeline.stages[idx], ...updated }
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
this.save()
|
|
111
|
+
return updated
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
appendStageLog(pipelineId, stageId, logDelta) {
|
|
115
|
+
const pipeline = this.pipelines.get(pipelineId)
|
|
116
|
+
if (!pipeline) return
|
|
117
|
+
if (!pipeline.stageLogs) pipeline.stageLogs = {}
|
|
118
|
+
if (!pipeline.stageLogs[stageId]) pipeline.stageLogs[stageId] = ''
|
|
119
|
+
pipeline.stageLogs[stageId] += String(logDelta || '')
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
recordCompletion(pipelineId, summary) {
|
|
123
|
+
const p = this.pipelines.get(pipelineId)
|
|
124
|
+
if (p) {
|
|
125
|
+
p.status = summary.success ? 'completed' : 'failed'
|
|
126
|
+
p.completedAt = Date.now()
|
|
127
|
+
p.durationMs = summary.durationMs || 0
|
|
128
|
+
p.stateMap = summary.stateMap || {}
|
|
129
|
+
p.artifacts = summary.artifacts || {}
|
|
130
|
+
|
|
131
|
+
if (summary.success) {
|
|
132
|
+
this.globalMetrics.completedPipelines++
|
|
133
|
+
} else {
|
|
134
|
+
this.globalMetrics.failedPipelines++
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
this.globalMetrics.totalDurationMs += p.durationMs
|
|
138
|
+
|
|
139
|
+
// Aggregate cache metrics across all stages
|
|
140
|
+
for (const node of Object.values(summary.stateMap || {})) {
|
|
141
|
+
const m = node.metrics
|
|
142
|
+
if (m) {
|
|
143
|
+
this.globalMetrics.totalPromptTokens += m.promptTokens || 0
|
|
144
|
+
this.globalMetrics.totalCacheHitTokens += m.cacheHitTokens || 0
|
|
145
|
+
this.globalMetrics.totalCacheMissTokens += m.cacheMissTokens || 0
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (this.globalMetrics.totalPromptTokens > 0) {
|
|
150
|
+
this.globalMetrics.overallHitRatio = parseFloat(
|
|
151
|
+
(this.globalMetrics.totalCacheHitTokens / this.globalMetrics.totalPromptTokens).toFixed(4)
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
this.save()
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
getPipeline(pipelineId) {
|
|
160
|
+
return this.pipelines.get(pipelineId) || null
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
getActivePipelines() {
|
|
164
|
+
return Array.from(this.pipelines.values()).filter(
|
|
165
|
+
(p) => p.status === 'running' || p.status === 'pending'
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
getAllPipelines() {
|
|
170
|
+
return Array.from(this.pipelines.values()).sort((a, b) => b.createdAt - a.createdAt)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
getMetrics() {
|
|
174
|
+
return { ...this.globalMetrics }
|
|
175
|
+
}
|
|
176
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@goodandready/dsh-agent-orchestrator",
|
|
3
|
+
"version": "0.1.6",
|
|
4
|
+
"description": "Multi-agent task decomposition, DAG workflow orchestration, and prompt caching optimizer for DeepSeek Harness.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./lib/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./lib/index.js",
|
|
9
|
+
"./client": "./lib/client.js",
|
|
10
|
+
"./package.json": "./package.json",
|
|
11
|
+
"./cordis.patch.yml": "./cordis.patch.yml"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"lib/",
|
|
15
|
+
"cordis.patch.yml",
|
|
16
|
+
"README.md",
|
|
17
|
+
"README.zh.md",
|
|
18
|
+
"README.ru.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"test": "node --test test/*.test.mjs"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"dsh",
|
|
26
|
+
"dsh-plugin",
|
|
27
|
+
"deepseek-harness",
|
|
28
|
+
"orchestrator",
|
|
29
|
+
"multi-agent",
|
|
30
|
+
"prompt-caching",
|
|
31
|
+
"dag-workflow",
|
|
32
|
+
"kanban"
|
|
33
|
+
],
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/GooDAnDReaDY/dsh-agent-orchestrator.git"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/GooDAnDReaDY/dsh-agent-orchestrator#readme",
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/GooDAnDReaDY/dsh-agent-orchestrator/issues"
|
|
41
|
+
},
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=20"
|
|
45
|
+
},
|
|
46
|
+
"dsh": {
|
|
47
|
+
"bundle": {
|
|
48
|
+
"patch": "./cordis.patch.yml"
|
|
49
|
+
},
|
|
50
|
+
"client": {
|
|
51
|
+
"platform": "web",
|
|
52
|
+
"inject": [
|
|
53
|
+
"@deepseek-ai/dsh-client-locale",
|
|
54
|
+
"@deepseek-ai/dsh-client-ui-slots"
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"peerDependencies": {
|
|
59
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
60
|
+
"@deepseek-ai/dsh-settings": ">=0.1.0-rc.6 <1.0.0",
|
|
61
|
+
"@deepseek-ai/dsh-host-webserver": ">=0.1.0-rc.6 <1.0.0",
|
|
62
|
+
"@deepseek-ai/dsh-llm": ">=0.1.0-rc.6 <1.0.0",
|
|
63
|
+
"@deepseek-ai/dsh-tools": ">=0.1.0-rc.6 <1.0.0",
|
|
64
|
+
"@deepseek-ai/dsh-session": ">=0.1.0-rc.6 <1.0.0",
|
|
65
|
+
"@deepseek-ai/schemastery": "^3.18.1"
|
|
66
|
+
},
|
|
67
|
+
"peerDependenciesMeta": {
|
|
68
|
+
"@deepseek-ai/cordis": {
|
|
69
|
+
"optional": true
|
|
70
|
+
},
|
|
71
|
+
"@deepseek-ai/dsh-settings": {
|
|
72
|
+
"optional": true
|
|
73
|
+
},
|
|
74
|
+
"@deepseek-ai/dsh-host-webserver": {
|
|
75
|
+
"optional": true
|
|
76
|
+
},
|
|
77
|
+
"@deepseek-ai/dsh-llm": {
|
|
78
|
+
"optional": true
|
|
79
|
+
},
|
|
80
|
+
"@deepseek-ai/dsh-tools": {
|
|
81
|
+
"optional": true
|
|
82
|
+
},
|
|
83
|
+
"@deepseek-ai/dsh-session": {
|
|
84
|
+
"optional": true
|
|
85
|
+
},
|
|
86
|
+
"@deepseek-ai/schemastery": {
|
|
87
|
+
"optional": true
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
"publishConfig": {
|
|
91
|
+
"registry": "https://registry.npmjs.org",
|
|
92
|
+
"access": "public"
|
|
93
|
+
}
|
|
94
|
+
}
|