@goodandready/dsh-agent-orchestrator 0.1.9 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +12 -3
- package/lib/index.js +41 -9
- package/lib/pipeline/dag-engine.js +44 -4
- package/lib/pipeline/delegation.js +11 -1
- package/lib/pipeline/intent.js +4 -1
- package/lib/pipeline/model-selection.js +0 -1
- package/lib/pipeline/snapshots.js +9 -2
- package/lib/pipeline/worker-pool.js +1 -0
- package/lib/routes.js +22 -0
- package/lib/store.js +16 -3
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -897,7 +897,14 @@ window.__ModuleLoader__.load({
|
|
|
897
897
|
if (res.ok) {
|
|
898
898
|
const data = await res.json()
|
|
899
899
|
const active = (data.activePipelines || [])[0] || null
|
|
900
|
-
setActivePipeline(
|
|
900
|
+
setActivePipeline((prev) => {
|
|
901
|
+
if (!prev && !active) return null
|
|
902
|
+
if (!prev || !active) return active
|
|
903
|
+
if (prev.pipelineId === active.pipelineId && prev.status === active.status && prev.durationMs === active.durationMs) {
|
|
904
|
+
return prev
|
|
905
|
+
}
|
|
906
|
+
return active
|
|
907
|
+
})
|
|
901
908
|
|
|
902
909
|
// If no active, check recent completed/failed within last 60 seconds
|
|
903
910
|
if (!active && data.recentPipelines && data.recentPipelines.length > 0) {
|
|
@@ -917,12 +924,14 @@ window.__ModuleLoader__.load({
|
|
|
917
924
|
}
|
|
918
925
|
}, [dismissedId])
|
|
919
926
|
|
|
927
|
+
const hasActive = Boolean(activePipeline)
|
|
928
|
+
|
|
920
929
|
useEffect(() => {
|
|
921
930
|
pollStatus()
|
|
922
|
-
const intervalMs =
|
|
931
|
+
const intervalMs = hasActive ? 2000 : 5000
|
|
923
932
|
const timer = setInterval(pollStatus, intervalMs)
|
|
924
933
|
return () => clearInterval(timer)
|
|
925
|
-
}, [
|
|
934
|
+
}, [hasActive, pollStatus])
|
|
926
935
|
|
|
927
936
|
const handleCancel = async (pipelineId) => {
|
|
928
937
|
try {
|
package/lib/index.js
CHANGED
|
@@ -292,10 +292,22 @@ export function apply(ctx, config = {}) {
|
|
|
292
292
|
}
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
+
const activeAbortControllers = new Map()
|
|
296
|
+
|
|
295
297
|
/**
|
|
296
298
|
* Runner object to start, execute, and monitor pipelines
|
|
297
299
|
*/
|
|
298
300
|
const runner = {
|
|
301
|
+
cancelPipeline(pipelineId) {
|
|
302
|
+
const controller = activeAbortControllers.get(pipelineId)
|
|
303
|
+
if (controller) {
|
|
304
|
+
controller.abort()
|
|
305
|
+
activeAbortControllers.delete(pipelineId)
|
|
306
|
+
return true
|
|
307
|
+
}
|
|
308
|
+
return false
|
|
309
|
+
},
|
|
310
|
+
|
|
299
311
|
async startPipeline({ taskTitle, taskDescription, scenarioId, taskId }) {
|
|
300
312
|
const cfg = getConfig()
|
|
301
313
|
const plan = decomposeTask({
|
|
@@ -369,17 +381,24 @@ export function apply(ctx, config = {}) {
|
|
|
369
381
|
}
|
|
370
382
|
}
|
|
371
383
|
|
|
384
|
+
const abortController = new AbortController()
|
|
385
|
+
activeAbortControllers.set(plan.pipelineId, abortController)
|
|
386
|
+
|
|
372
387
|
executeDAG({
|
|
373
388
|
stages: plan.stages,
|
|
374
389
|
executor,
|
|
375
390
|
concurrency: 3,
|
|
391
|
+
signal: abortController.signal,
|
|
376
392
|
})
|
|
377
393
|
.then(async (dagResult) => {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
394
|
+
activeAbortControllers.delete(plan.pipelineId)
|
|
395
|
+
const currentP = store.getPipeline(plan.pipelineId)
|
|
396
|
+
if (currentP?.status === 'failed' && currentP?.error === 'Cancelled by user') {
|
|
397
|
+
return
|
|
398
|
+
}
|
|
399
|
+
store.recordCompletion(plan.pipelineId, {
|
|
400
|
+
...dagResult,
|
|
381
401
|
durationMs: Date.now() - pipelineData.startedAt,
|
|
382
|
-
artifacts: dagResult.artifacts,
|
|
383
402
|
})
|
|
384
403
|
|
|
385
404
|
// Sync to Kanban if enabled
|
|
@@ -399,7 +418,7 @@ export function apply(ctx, config = {}) {
|
|
|
399
418
|
if (syncConfig.createChecklist) {
|
|
400
419
|
const checklistItems = plan.stages.map((s) => ({
|
|
401
420
|
title: `${s.name} (${s.roleName})`,
|
|
402
|
-
completed: dagResult
|
|
421
|
+
completed: dagResult?.stateMap?.[s.id]?.status === 'completed',
|
|
403
422
|
}))
|
|
404
423
|
await kanbanBridge.syncStageChecklist(targetTaskId, checklistItems)
|
|
405
424
|
}
|
|
@@ -419,7 +438,7 @@ export function apply(ctx, config = {}) {
|
|
|
419
438
|
id: plan.pipelineId,
|
|
420
439
|
title: plan.taskTitle,
|
|
421
440
|
scenarioId: plan.scenarioId,
|
|
422
|
-
status: dagResult
|
|
441
|
+
status: dagResult?.success ? 'completed' : 'failed',
|
|
423
442
|
stages: plan.stages,
|
|
424
443
|
durationMs: Date.now() - pipelineData.startedAt,
|
|
425
444
|
})
|
|
@@ -428,9 +447,13 @@ export function apply(ctx, config = {}) {
|
|
|
428
447
|
}
|
|
429
448
|
})
|
|
430
449
|
.catch((dagErr) => {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
450
|
+
activeAbortControllers.delete(plan.pipelineId)
|
|
451
|
+
const currentP = store.getPipeline(plan.pipelineId)
|
|
452
|
+
if (currentP?.status === 'failed' && currentP?.error === 'Cancelled by user') {
|
|
453
|
+
return
|
|
454
|
+
}
|
|
455
|
+
store.recordCompletion(plan.pipelineId, {
|
|
456
|
+
success: false,
|
|
434
457
|
durationMs: Date.now() - pipelineData.startedAt,
|
|
435
458
|
error: dagErr?.message || String(dagErr),
|
|
436
459
|
})
|
|
@@ -813,8 +836,11 @@ export function apply(ctx, config = {}) {
|
|
|
813
836
|
}
|
|
814
837
|
}
|
|
815
838
|
|
|
839
|
+
let hasCommandsService = false
|
|
840
|
+
|
|
816
841
|
// Chat slash commands: /orchestrate and /orc via commands service
|
|
817
842
|
ctx.inject(['commands'], (cctx) => {
|
|
843
|
+
hasCommandsService = true
|
|
818
844
|
try {
|
|
819
845
|
if (typeof cctx.commands?.register !== 'function') return
|
|
820
846
|
|
|
@@ -904,6 +930,7 @@ export function apply(ctx, config = {}) {
|
|
|
904
930
|
|
|
905
931
|
if (typeof cctx.effect === 'function') {
|
|
906
932
|
cctx.effect(() => () => {
|
|
933
|
+
hasCommandsService = false
|
|
907
934
|
try {
|
|
908
935
|
if (typeof unregister1 === 'function') unregister1()
|
|
909
936
|
if (typeof unregister2 === 'function') unregister2()
|
|
@@ -938,6 +965,11 @@ export function apply(ctx, config = {}) {
|
|
|
938
965
|
const text = String(event.text || event.content || '').trim()
|
|
939
966
|
const intent = detectOrchestratorIntent(text)
|
|
940
967
|
|
|
968
|
+
// Prevent duplicate dispatch: if commands service is active, slash commands are handled by cctx.commands (#148)
|
|
969
|
+
if (hasCommandsService && intent.triggerKind === 'slash') {
|
|
970
|
+
return
|
|
971
|
+
}
|
|
972
|
+
|
|
941
973
|
if (intent.isTrigger && intent.action === 'on') {
|
|
942
974
|
await handlePipelineDispatch({
|
|
943
975
|
taskTitle: intent.taskTitle || 'Interactive Orchestrated Task',
|
|
@@ -149,12 +149,14 @@ export async function executeDAG(stagesOrOptions, executorArg, optionsArg = {})
|
|
|
149
149
|
let onNodeComplete = () => {}
|
|
150
150
|
let onNodeFail = () => {}
|
|
151
151
|
let onNodeBlocked = () => {}
|
|
152
|
+
let signal = null
|
|
152
153
|
|
|
153
154
|
if (Array.isArray(stagesOrOptions)) {
|
|
154
155
|
stages = stagesOrOptions
|
|
155
156
|
executor = executorArg
|
|
156
157
|
concurrency = optionsArg.concurrency || optionsArg.maxConcurrency || 4
|
|
157
158
|
initialContext = optionsArg.initialContext || {}
|
|
159
|
+
signal = optionsArg.signal || null
|
|
158
160
|
if (typeof optionsArg.onNodeStart === 'function') onNodeStart = optionsArg.onNodeStart
|
|
159
161
|
if (typeof optionsArg.onNodeComplete === 'function') onNodeComplete = optionsArg.onNodeComplete
|
|
160
162
|
if (typeof optionsArg.onNodeFail === 'function') onNodeFail = optionsArg.onNodeFail
|
|
@@ -164,6 +166,7 @@ export async function executeDAG(stagesOrOptions, executorArg, optionsArg = {})
|
|
|
164
166
|
executor = stagesOrOptions.executor
|
|
165
167
|
concurrency = stagesOrOptions.concurrency || stagesOrOptions.maxConcurrency || 4
|
|
166
168
|
initialContext = stagesOrOptions.initialContext || {}
|
|
169
|
+
signal = stagesOrOptions.signal || null
|
|
167
170
|
if (typeof stagesOrOptions.onNodeStart === 'function') onNodeStart = stagesOrOptions.onNodeStart
|
|
168
171
|
if (typeof stagesOrOptions.onNodeComplete === 'function') onNodeComplete = stagesOrOptions.onNodeComplete
|
|
169
172
|
if (typeof stagesOrOptions.onNodeFail === 'function') onNodeFail = stagesOrOptions.onNodeFail
|
|
@@ -220,13 +223,33 @@ export async function executeDAG(stagesOrOptions, executorArg, optionsArg = {})
|
|
|
220
223
|
function triggerNext() {
|
|
221
224
|
if (isDone) return
|
|
222
225
|
|
|
226
|
+
if (signal?.aborted) {
|
|
227
|
+
if (!isDone && runningCount === 0) {
|
|
228
|
+
isDone = true
|
|
229
|
+
for (const s of stages) {
|
|
230
|
+
const st = stateMap.get(s.id)
|
|
231
|
+
if (st.status === NODE_STATUS.PENDING) {
|
|
232
|
+
st.status = NODE_STATUS.SKIPPED
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
resolvePromise({
|
|
236
|
+
success: false,
|
|
237
|
+
cancelled: true,
|
|
238
|
+
durationMs: Date.now() - startTime,
|
|
239
|
+
stateMap: Object.fromEntries(stateMap),
|
|
240
|
+
artifacts: Object.fromEntries(artifacts),
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
|
|
223
246
|
// Propagate blockers first
|
|
224
247
|
if (markBlockedStages(stages, stateMap)) {
|
|
225
248
|
for (const s of stages) {
|
|
226
249
|
const st = stateMap.get(s.id)
|
|
227
250
|
if (st.status === NODE_STATUS.BLOCKED && !st.notifiedBlocked) {
|
|
228
251
|
st.notifiedBlocked = true
|
|
229
|
-
onNodeBlocked(st)
|
|
252
|
+
try { onNodeBlocked(st) } catch (_) { /* Safe ignore notification error */ }
|
|
230
253
|
}
|
|
231
254
|
}
|
|
232
255
|
}
|
|
@@ -241,7 +264,7 @@ export async function executeDAG(stagesOrOptions, executorArg, optionsArg = {})
|
|
|
241
264
|
nodeState.startedAt = Date.now()
|
|
242
265
|
runningCount++
|
|
243
266
|
|
|
244
|
-
onNodeStart(nodeState)
|
|
267
|
+
try { onNodeStart(nodeState) } catch (_) { /* Safe ignore notification error */ }
|
|
245
268
|
|
|
246
269
|
// Run task asynchronously
|
|
247
270
|
const upstreamOutputs = {}
|
|
@@ -266,17 +289,28 @@ export async function executeDAG(stagesOrOptions, executorArg, optionsArg = {})
|
|
|
266
289
|
|
|
267
290
|
artifacts.set(nextStage.id, nodeState.output)
|
|
268
291
|
runningCount--
|
|
269
|
-
|
|
292
|
+
try {
|
|
293
|
+
onNodeComplete(nodeState)
|
|
294
|
+
} catch (_) {
|
|
295
|
+
/* Callback error must not alter completed state or double-decrement runningCount */
|
|
296
|
+
}
|
|
270
297
|
triggerNext()
|
|
271
298
|
})
|
|
272
299
|
.catch((err) => {
|
|
300
|
+
if (nodeState.status === NODE_STATUS.COMPLETED) {
|
|
301
|
+
return
|
|
302
|
+
}
|
|
273
303
|
nodeState.status = NODE_STATUS.FAILED
|
|
274
304
|
nodeState.completedAt = Date.now()
|
|
275
305
|
nodeState.durationMs = nodeState.completedAt - nodeState.startedAt
|
|
276
306
|
nodeState.error = err?.message || String(err)
|
|
277
307
|
|
|
278
308
|
runningCount--
|
|
279
|
-
|
|
309
|
+
try {
|
|
310
|
+
onNodeFail(nodeState, err)
|
|
311
|
+
} catch (_) {
|
|
312
|
+
/* Callback error must not crash the engine */
|
|
313
|
+
}
|
|
280
314
|
triggerNext()
|
|
281
315
|
})
|
|
282
316
|
}
|
|
@@ -284,6 +318,12 @@ export async function executeDAG(stagesOrOptions, executorArg, optionsArg = {})
|
|
|
284
318
|
checkCompletion()
|
|
285
319
|
}
|
|
286
320
|
|
|
321
|
+
if (signal) {
|
|
322
|
+
signal.addEventListener('abort', () => {
|
|
323
|
+
triggerNext()
|
|
324
|
+
}, { once: true })
|
|
325
|
+
}
|
|
326
|
+
|
|
287
327
|
// Initial trigger
|
|
288
328
|
triggerNext()
|
|
289
329
|
})
|
|
@@ -152,8 +152,18 @@ export function normalizeRoleId(input, availableRoles = []) {
|
|
|
152
152
|
if (role.displayName && role.displayName.toLowerCase() === lower) return role.id
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
function matchesRoleAlias(str, alias) {
|
|
156
|
+
if (str === alias) return true
|
|
157
|
+
if (alias.length <= 3) {
|
|
158
|
+
const escaped = alias.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
159
|
+
const regex = new RegExp('(^|[^a-z0-9а-яё])' + escaped + '([^a-z0-9а-яё]|$)', 'i')
|
|
160
|
+
return regex.test(str)
|
|
161
|
+
}
|
|
162
|
+
return str.includes(alias)
|
|
163
|
+
}
|
|
164
|
+
|
|
155
165
|
for (const [alias, id] of Object.entries(ROLE_INPUT_ALIASES)) {
|
|
156
|
-
if (lower
|
|
166
|
+
if (matchesRoleAlias(lower, alias)) {
|
|
157
167
|
const found = rolesList.find((r) => r.id === id)
|
|
158
168
|
if (found) return found.id
|
|
159
169
|
if (id === 'code') {
|
package/lib/pipeline/intent.js
CHANGED
|
@@ -51,12 +51,13 @@ export function detectOrchestratorIntent(text) {
|
|
|
51
51
|
if (slashMatch) {
|
|
52
52
|
const rest = (slashMatch[1] || '').trim()
|
|
53
53
|
if (rest.toLowerCase() === 'off') {
|
|
54
|
-
return { isTrigger: true, action: 'off' }
|
|
54
|
+
return { isTrigger: true, action: 'off', triggerKind: 'slash' }
|
|
55
55
|
}
|
|
56
56
|
const { scenarioId, taskTitle } = extractScenarioAndTask(rest)
|
|
57
57
|
return {
|
|
58
58
|
isTrigger: true,
|
|
59
59
|
action: 'on',
|
|
60
|
+
triggerKind: 'slash',
|
|
60
61
|
scenarioId,
|
|
61
62
|
taskTitle,
|
|
62
63
|
}
|
|
@@ -73,6 +74,7 @@ export function detectOrchestratorIntent(text) {
|
|
|
73
74
|
return {
|
|
74
75
|
isTrigger: true,
|
|
75
76
|
action: 'on',
|
|
77
|
+
triggerKind: 'nl',
|
|
76
78
|
scenarioId,
|
|
77
79
|
taskTitle,
|
|
78
80
|
}
|
|
@@ -89,6 +91,7 @@ export function detectOrchestratorIntent(text) {
|
|
|
89
91
|
return {
|
|
90
92
|
isTrigger: true,
|
|
91
93
|
action: 'on',
|
|
94
|
+
triggerKind: 'nl',
|
|
92
95
|
scenarioId,
|
|
93
96
|
taskTitle,
|
|
94
97
|
}
|
|
@@ -569,7 +569,6 @@ export async function fetchModelCatalog(ctx, options = {}) {
|
|
|
569
569
|
const defaultPool = [
|
|
570
570
|
{ provider: 'deepseek-official', model: 'deepseek-chat', maxTokens: 4096 },
|
|
571
571
|
{ provider: 'deepseek-official', model: 'deepseek-reasoner', maxTokens: 8192 },
|
|
572
|
-
{ provider: 'anthropic', model: 'claude-3-5-sonnet', maxTokens: 8192 },
|
|
573
572
|
]
|
|
574
573
|
for (const item of defaultPool) {
|
|
575
574
|
const capabilities = inferModelCapabilities(item.model, item.provider)
|
|
@@ -23,6 +23,7 @@ export class SnapshotManager {
|
|
|
23
23
|
this.baseDir = baseDir || path.join(os.homedir(), '.dsh', 'orchestrator-snapshots')
|
|
24
24
|
this.maxSnapshots = maxSnapshots
|
|
25
25
|
this.logger = logger || null
|
|
26
|
+
this._snapshotsCache = null
|
|
26
27
|
this._ensureDir()
|
|
27
28
|
}
|
|
28
29
|
|
|
@@ -65,6 +66,9 @@ export class SnapshotManager {
|
|
|
65
66
|
*/
|
|
66
67
|
listSnapshots() {
|
|
67
68
|
this._ensureDir()
|
|
69
|
+
if (this._snapshotsCache) {
|
|
70
|
+
return [...this._snapshotsCache]
|
|
71
|
+
}
|
|
68
72
|
try {
|
|
69
73
|
const files = fs.readdirSync(this.baseDir)
|
|
70
74
|
const snapshots = []
|
|
@@ -91,7 +95,8 @@ export class SnapshotManager {
|
|
|
91
95
|
return timeB - timeA
|
|
92
96
|
})
|
|
93
97
|
|
|
94
|
-
|
|
98
|
+
this._snapshotsCache = snapshots
|
|
99
|
+
return [...snapshots]
|
|
95
100
|
} catch (err) {
|
|
96
101
|
if (this.logger?.error) {
|
|
97
102
|
this.logger.error('[SnapshotManager] Error listing snapshots:', err?.message || err)
|
|
@@ -158,7 +163,7 @@ export class SnapshotManager {
|
|
|
158
163
|
const tmpPath = path.join(this.baseDir, `${snapshotId}.tmp-${Date.now()}`)
|
|
159
164
|
|
|
160
165
|
try {
|
|
161
|
-
fs.writeFileSync(tmpPath, JSON.stringify(record
|
|
166
|
+
fs.writeFileSync(tmpPath, JSON.stringify(record), 'utf8')
|
|
162
167
|
fs.renameSync(tmpPath, filePath)
|
|
163
168
|
} catch (err) {
|
|
164
169
|
try {
|
|
@@ -169,6 +174,7 @@ export class SnapshotManager {
|
|
|
169
174
|
throw err
|
|
170
175
|
}
|
|
171
176
|
|
|
177
|
+
this._snapshotsCache = null
|
|
172
178
|
// Enforce FIFO retention policy: if file count > maxSnapshots, prune oldest
|
|
173
179
|
this._enforceRetention()
|
|
174
180
|
|
|
@@ -179,6 +185,7 @@ export class SnapshotManager {
|
|
|
179
185
|
* Prunes oldest snapshot files when total exceeds maxSnapshots ceiling.
|
|
180
186
|
*/
|
|
181
187
|
_enforceRetention() {
|
|
188
|
+
this._snapshotsCache = null
|
|
182
189
|
try {
|
|
183
190
|
const files = fs.readdirSync(this.baseDir).filter((f) => f.endsWith('.json'))
|
|
184
191
|
if (files.length <= this.maxSnapshots) return
|
|
@@ -84,6 +84,7 @@ export async function executeStageWorker(stage, context) {
|
|
|
84
84
|
output: artContent || '',
|
|
85
85
|
})
|
|
86
86
|
}
|
|
87
|
+
priorArtifactsList.sort((a, b) => a.stageId.localeCompare(b.stageId))
|
|
87
88
|
const cumulativeArtifacts = formatCumulativeArtifacts(priorArtifactsList)
|
|
88
89
|
|
|
89
90
|
// 4. Assemble canonical prompt (Layer 1 + 2 + 3 in System, Layer 4 in User)
|
package/lib/routes.js
CHANGED
|
@@ -29,6 +29,9 @@ export function registerOrchestratorRoutes(ctx, services) {
|
|
|
29
29
|
kind: 'exact',
|
|
30
30
|
path: '/dsh-agent-orchestrator/status',
|
|
31
31
|
handler: (req, res) => {
|
|
32
|
+
if (req.method !== 'GET') {
|
|
33
|
+
return jsonReply(res, { error: 'Method not allowed' }, 405)
|
|
34
|
+
}
|
|
32
35
|
jsonReply(res, {
|
|
33
36
|
activePipelines: store.getActivePipelines(),
|
|
34
37
|
metrics: store.getMetrics(),
|
|
@@ -112,6 +115,10 @@ export function registerOrchestratorRoutes(ctx, services) {
|
|
|
112
115
|
kind: 'prefix',
|
|
113
116
|
path: '/dsh-agent-orchestrator/pipeline',
|
|
114
117
|
handler: (req, res) => {
|
|
118
|
+
if (req.method !== 'GET') {
|
|
119
|
+
return jsonReply(res, { error: 'Method not allowed' }, 405)
|
|
120
|
+
}
|
|
121
|
+
if (rejectUntrustedRequest(req, res)) return
|
|
115
122
|
const parts = req.url.split('?')[0].split('/')
|
|
116
123
|
const pipelineId = parts[parts.length - 1]
|
|
117
124
|
const pipeline = store.getPipeline(pipelineId)
|
|
@@ -140,6 +147,7 @@ export function registerOrchestratorRoutes(ctx, services) {
|
|
|
140
147
|
}
|
|
141
148
|
|
|
142
149
|
const pipelineId = body.pipelineId
|
|
150
|
+
runner?.cancelPipeline?.(pipelineId)
|
|
143
151
|
const p = store.getPipeline(pipelineId)
|
|
144
152
|
if (p && (p.status === 'running' || p.status === 'pending')) {
|
|
145
153
|
store.updatePipeline(pipelineId, {
|
|
@@ -178,6 +186,10 @@ export function registerOrchestratorRoutes(ctx, services) {
|
|
|
178
186
|
await updateConfig(body)
|
|
179
187
|
return jsonReply(res, { success: true, config: getConfig() })
|
|
180
188
|
}
|
|
189
|
+
if (req.method !== 'GET') {
|
|
190
|
+
return jsonReply(res, { error: 'Method not allowed' }, 405)
|
|
191
|
+
}
|
|
192
|
+
if (rejectUntrustedRequest(req, res)) return
|
|
181
193
|
jsonReply(res, { config: getConfig() })
|
|
182
194
|
},
|
|
183
195
|
}), 'dsh-agent-orchestrator: config route')
|
|
@@ -187,6 +199,9 @@ export function registerOrchestratorRoutes(ctx, services) {
|
|
|
187
199
|
kind: 'exact',
|
|
188
200
|
path: '/dsh-agent-orchestrator/models',
|
|
189
201
|
handler: async (req, res) => {
|
|
202
|
+
if (req.method !== 'GET') {
|
|
203
|
+
return jsonReply(res, { error: 'Method not allowed' }, 405)
|
|
204
|
+
}
|
|
190
205
|
const selection = readModelSelection(ctx)
|
|
191
206
|
const catalog = await fetchModelCatalog(ctx, {
|
|
192
207
|
allowedRoutes: selection.allowedRoutes,
|
|
@@ -204,6 +219,9 @@ export function registerOrchestratorRoutes(ctx, services) {
|
|
|
204
219
|
kind: 'exact',
|
|
205
220
|
path: '/dsh-agent-orchestrator/catalog/models',
|
|
206
221
|
handler: async (req, res) => {
|
|
222
|
+
if (req.method !== 'GET') {
|
|
223
|
+
return jsonReply(res, { error: 'Method not allowed' }, 405)
|
|
224
|
+
}
|
|
207
225
|
try {
|
|
208
226
|
const urlObj = new URL(req.url, 'http://localhost')
|
|
209
227
|
const provider = urlObj.searchParams.get('provider') || undefined
|
|
@@ -225,6 +243,10 @@ export function registerOrchestratorRoutes(ctx, services) {
|
|
|
225
243
|
kind: 'exact',
|
|
226
244
|
path: '/dsh-agent-orchestrator/snapshots',
|
|
227
245
|
handler: (req, res) => {
|
|
246
|
+
if (req.method !== 'GET') {
|
|
247
|
+
return jsonReply(res, { error: 'Method not allowed' }, 405)
|
|
248
|
+
}
|
|
249
|
+
if (rejectUntrustedRequest(req, res)) return
|
|
228
250
|
try {
|
|
229
251
|
const snapshots = snapshotManager.listSnapshots()
|
|
230
252
|
jsonReply(res, { snapshots, total: snapshots.length })
|
package/lib/store.js
CHANGED
|
@@ -12,9 +12,10 @@ export class OrchestratorStore {
|
|
|
12
12
|
constructor(options = {}) {
|
|
13
13
|
const opts = typeof options === 'string' ? { storageDir: options } : (options || {})
|
|
14
14
|
const baseDir = opts.storageDir || join(homedir(), '.dsh')
|
|
15
|
-
this.storagePath = join(baseDir, 'orchestrator-pipelines.json')
|
|
15
|
+
this.storagePath = opts.storagePath || join(baseDir, 'orchestrator-pipelines.json')
|
|
16
16
|
this.logger = opts.logger || null
|
|
17
17
|
this.pipelines = new Map()
|
|
18
|
+
this._saveTimer = null
|
|
18
19
|
this.globalMetrics = {
|
|
19
20
|
totalPipelines: 0,
|
|
20
21
|
completedPipelines: 0,
|
|
@@ -55,12 +56,23 @@ export class OrchestratorStore {
|
|
|
55
56
|
globalMetrics: this.globalMetrics,
|
|
56
57
|
updatedAt: Date.now(),
|
|
57
58
|
}
|
|
58
|
-
writeFileSync(this.storagePath, JSON.stringify(serialized
|
|
59
|
+
writeFileSync(this.storagePath, JSON.stringify(serialized), 'utf8')
|
|
59
60
|
} catch (e) {
|
|
60
61
|
if (this.logger?.warn) this.logger.warn('[dsh-agent-orchestrator] Store save warning:', e?.message || e)
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
saveDebounced(delayMs = 150) {
|
|
66
|
+
if (this._saveTimer) return
|
|
67
|
+
this._saveTimer = setTimeout(() => {
|
|
68
|
+
this._saveTimer = null
|
|
69
|
+
this.save()
|
|
70
|
+
}, delayMs)
|
|
71
|
+
if (this._saveTimer && typeof this._saveTimer.unref === 'function') {
|
|
72
|
+
this._saveTimer.unref()
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
64
76
|
recordPipeline(pipelineData) {
|
|
65
77
|
this.pipelines.set(pipelineData.pipelineId, pipelineData)
|
|
66
78
|
this.globalMetrics.totalPipelines++
|
|
@@ -107,7 +119,7 @@ export class OrchestratorStore {
|
|
|
107
119
|
}
|
|
108
120
|
}
|
|
109
121
|
|
|
110
|
-
this.
|
|
122
|
+
this.saveDebounced()
|
|
111
123
|
return updated
|
|
112
124
|
}
|
|
113
125
|
|
|
@@ -127,6 +139,7 @@ export class OrchestratorStore {
|
|
|
127
139
|
p.durationMs = summary.durationMs || 0
|
|
128
140
|
p.stateMap = summary.stateMap || {}
|
|
129
141
|
p.artifacts = summary.artifacts || {}
|
|
142
|
+
if (summary.error) p.error = summary.error
|
|
130
143
|
|
|
131
144
|
if (summary.success) {
|
|
132
145
|
this.globalMetrics.completedPipelines++
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-agent-orchestrator",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "Multi-agent task decomposition, DAG workflow orchestration, and prompt caching optimizer for DeepSeek Harness.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|