@p-dsh-market/graph-job-orchestrator 0.1.0

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/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # Graph Job 多 Subagent 任务图
2
+
3
+ `@p-dsh-market/graph-job-orchestrator` 实现评审稿中的阶段 0~4 基础闭环:
4
+
5
+ - Graph Draft/Revision、Agent Profile snapshot、DAG validator、静态并行预览、受限 Planner patch;
6
+ - `%LOCALAPPDATA%/dsh-desktop/plugin-data/graph-job-orchestrator/` 下的原子 JSON 与 append-only JSONL 存储;
7
+ - DSH in-process Subagent executor、merge barrier、read 并行/write 串行、失败暂停、有限 transport/rate-limit retry、取消和终止;
8
+ - `artifactRefs` workspace-relative 校验、插件 Skill/Graph Job 递归保护、`/graphjob` 命令、Web API、SSE 和任务图编辑面板;
9
+ - Codex provider 的能力发现、model/reasoningEffort 快照与 capability mismatch 错误。
10
+
11
+ 运行时必须先安装并注册 Codex Subagent provider,阶段 4 才会显示 Codex executor 可用;没有 provider 时不会伪装成可运行,而是在能力快照和运行错误中明确说明。当前仓库的本地 runtime 只有 DSH `spawn/fork` provider,因此不会在安装插件时自动修改 profile。
12
+
13
+ 需要 Codex 时,在目标 DSH profile 安装官方 provider Bundle 后重启该 profile:
14
+
15
+ ```powershell
16
+ dsh plugin --profile <name> add @deepseek-ai/dsh-subagent-codex
17
+ ```
18
+
19
+ 插件只负责发现和校验 provider,不会代替用户安装 Bundle、登录 Codex 或修改 Codex 原生权限配置。
20
+
21
+ 主要端点:
22
+
23
+ - `GET /graph-job-orchestrator/capabilities`
24
+ - `GET/PUT /graph-job-orchestrator/profiles`
25
+ - `GET/POST/PATCH /graph-job-orchestrator/graphs`
26
+ - `POST /graph-job-orchestrator/graphs/:id/preview|confirm|run`
27
+ - `GET /graph-job-orchestrator/graphs/:id/preview/:previewId`
28
+ - `GET /graph-job-orchestrator/templates`
29
+ - `GET /graph-job-orchestrator/templates/previews/:previewId`
30
+ - `POST /graph-job-orchestrator/templates/preview|confirm|bind`
31
+ - `GET /graph-job-orchestrator/runs/:runId/events`
32
+ - `POST /graph-job-orchestrator/runs/:runId/retry|cancel|terminate`
33
+
34
+ Planner 只接受当前会话的 roster、Graph JSON Schema 和受限 patch;候选图必须先 preview,再由用户确认。已有手工锁定图时,Planner 请求必须明确 `templateMode: "saveAs"` 或 `"overwrite"`,不会隐式改写当前模板。运行时输出严格投影为 `text` 和 `artifactRefs`,子会话 ID 只保存在运行状态和事件中。
35
+
36
+ 模板 manifest 支持 `scope: "workspace" | "global"`,默认是当前 workspace;workspace 模板不会出现在其他工作区的模板列表中。切换模板会创建新的 Graph Instance,不会把可变模板文件直接绑定为活动 Graph。
37
+
38
+ 验证命令:
39
+
40
+ ```powershell
41
+ npm run check:graph-job
42
+ npm test
43
+ npm run catalog:validate
44
+ Push-Location market/graph-job-orchestrator
45
+ npm pack --dry-run --json
46
+ Pop-Location
47
+ ```
@@ -0,0 +1,4 @@
1
+ - insert:
2
+ - id: p-dsh-market-graph-job-orchestrator
3
+ name: '@p-dsh-market/graph-job-orchestrator'
4
+ inject: [agentDefaultModel, agentPresets, agents, commands, llm, sessions, skills, subagents, systemPrompt, tools, webServer]
@@ -0,0 +1,216 @@
1
+ import { GRAPH_SCHEMA_VERSION } from './graph-schema.js'
2
+
3
+ async function callService(service, method, ...args) {
4
+ const fn = service?.[method]
5
+ if (typeof fn !== 'function') return undefined
6
+ try {
7
+ return await fn.call(service, ...args)
8
+ } catch {
9
+ return undefined
10
+ }
11
+ }
12
+
13
+ function asArray(value) {
14
+ if (Array.isArray(value)) return value
15
+ if (Array.isArray(value?.items)) return value.items
16
+ if (Array.isArray(value?.providers)) return value.providers
17
+ if (Array.isArray(value?.models)) return value.models
18
+ if (Array.isArray(value?.skills)) return value.skills
19
+ return []
20
+ }
21
+
22
+ function stringArray(value) {
23
+ if (typeof value === 'string') return value.trim() ? [value.trim()] : []
24
+ return Array.isArray(value) ? value.map((item) => String(item || '').trim()).filter(Boolean) : []
25
+ }
26
+
27
+ function providerOf(item) {
28
+ return String(item?.provider || item?.providerName || item?.sourceProvider || '').trim()
29
+ }
30
+
31
+ export function isPluginSkill(candidate = {}) {
32
+ return Boolean(
33
+ candidate.metadata?.plugin ||
34
+ candidate.plugin ||
35
+ candidate.metadata?.group === 'market' ||
36
+ providerOf(candidate) === 'desktop-skills' ||
37
+ /^@p-dsh-market\//i.test(providerOf(candidate)) ||
38
+ /graph-job-orchestrator/i.test(providerOf(candidate)) ||
39
+ ['plugin', 'market', 'desktop-plugin'].includes(String(candidate.source || '').toLowerCase())
40
+ )
41
+ }
42
+
43
+ export function normalizeSkillCandidate(candidate = {}) {
44
+ if (typeof candidate === 'string') candidate = { name: candidate }
45
+ const plugin = isPluginSkill(candidate)
46
+ const source = String(candidate.source || '').trim()
47
+ const provider = providerOf(candidate)
48
+ let origin = 'unknown'
49
+ if (plugin) origin = 'plugin'
50
+ else if (['runtime', 'builtin', 'user', 'workspace', 'local', 'project-dsh', 'project-agents', 'user-dsh', 'user-agents', 'bundled'].includes(source.toLowerCase())) {
51
+ const normalizedSource = source.toLowerCase()
52
+ if (['local', 'project-dsh', 'project-agents'].includes(normalizedSource)) origin = 'workspace'
53
+ else if (['user-dsh', 'user-agents'].includes(normalizedSource)) origin = 'user'
54
+ else if (normalizedSource === 'bundled') origin = 'runtime'
55
+ else origin = normalizedSource
56
+ }
57
+ else if (provider === 'runtime' || provider === 'builtin') origin = provider
58
+ const name = String(candidate.name || candidate.id || '').trim()
59
+ return {
60
+ name,
61
+ description: String(candidate.description || '').trim(),
62
+ source,
63
+ provider,
64
+ origin,
65
+ allowed: origin !== 'plugin' && origin !== 'unknown',
66
+ metadata: candidate.metadata && typeof candidate.metadata === 'object' ? { ...candidate.metadata } : undefined,
67
+ locator: candidate.locator || undefined,
68
+ path: candidate.path || undefined
69
+ }
70
+ }
71
+
72
+ export function filterSkills(candidates, allowlist = []) {
73
+ const allowedNames = new Set(stringArray(allowlist))
74
+ const normalized = (Array.isArray(candidates) ? candidates : []).map(normalizeSkillCandidate).filter((item) => item.name)
75
+ const allowed = normalized.filter((item) => allowedNames.has(item.name) && item.allowed)
76
+ const denied = normalized.filter((item) => !allowedNames.has(item.name) || !item.allowed)
77
+ return { allowed, denied }
78
+ }
79
+
80
+ function normalizeToolName(item) {
81
+ if (typeof item === 'string') return item.trim()
82
+ return String(item?.name || item?.id || item?.key || '').trim()
83
+ }
84
+
85
+ async function listTools(tools) {
86
+ const values = await callService(tools, 'schemas') ?? await callService(tools, 'list') ?? await callService(tools, 'getAll')
87
+ return [...new Set(asArray(values).map(normalizeToolName).filter(Boolean))].sort()
88
+ }
89
+
90
+ async function listSkills(skills, cwd) {
91
+ // dsh-skill accepts an optional cwd and defaults to the union of its
92
+ // registered layers. Do not invent a scope key here: the runtime's scope
93
+ // type is intentionally not the string "all".
94
+ const values = await callService(skills, 'list', { cwd }) ?? await callService(skills, 'list')
95
+ return asArray(values).map(normalizeSkillCandidate).filter((item) => item.name)
96
+ }
97
+
98
+ function providerCapabilities(item) {
99
+ const value = item?.capabilities || item?.capability || {}
100
+ return {
101
+ outputSchema: Boolean(value.outputSchema),
102
+ depthLimit: Boolean(value.depthLimit),
103
+ toolFilter: Boolean(value.toolFilter),
104
+ skillFilter: Boolean(value.skillFilter),
105
+ persona: Boolean(value.persona),
106
+ permissionMode: Boolean(value.permissionMode || value.permissionModes),
107
+ permissionModes: stringArray(value.permissionModes),
108
+ reasoningEfforts: stringArray(value.reasoningEfforts || value.reasoningEffort)
109
+ }
110
+ }
111
+
112
+ function normalizeProvider(item) {
113
+ if (typeof item === 'string') return { name: item, capabilities: {} }
114
+ return {
115
+ name: String(item?.name || item?.id || item?.providerName || '').trim(),
116
+ inheritsParentContext: item?.inheritsParentContext === true,
117
+ models: asArray(item?.models || item?.capabilities?.models).map((model) => typeof model === 'string' ? model : String(model?.id || model?.name || '').trim()).filter(Boolean),
118
+ capabilities: providerCapabilities(item)
119
+ }
120
+ }
121
+
122
+ async function listSubagentProviders(subagents) {
123
+ const values = asArray(await callService(subagents, 'list'))
124
+ const result = []
125
+ for (const item of values) {
126
+ const name = typeof item === 'string' ? item.trim() : String(item?.name || item?.id || item?.providerName || '').trim()
127
+ if (!name) continue
128
+ const descriptor = typeof item === 'object' ? item : await callService(subagents, 'getProvider', name)
129
+ result.push(normalizeProvider({ ...(descriptor || {}), name }))
130
+ }
131
+ return result
132
+ }
133
+
134
+ function extractReasoningEfforts(value) {
135
+ if (!value || typeof value !== 'object') return []
136
+ return stringArray(value.reasoningEfforts || value.reasoningEffort || value.capabilities?.reasoningEfforts || value.capabilities?.reasoningEffort)
137
+ }
138
+
139
+ async function discoverModels(llm) {
140
+ const providers = asArray(await callService(llm, 'listProviders'))
141
+ const result = []
142
+ for (const providerEntry of providers) {
143
+ const provider = typeof providerEntry === 'string' ? providerEntry : String(providerEntry?.name || providerEntry?.id || '').trim()
144
+ if (!provider) continue
145
+ const models = asArray(await callService(llm, 'listModels', provider))
146
+ for (const modelEntry of models) {
147
+ const model = typeof modelEntry === 'string' ? modelEntry : String(modelEntry?.id || modelEntry?.name || '').trim()
148
+ if (model) result.push({ provider, model, reasoningEfforts: extractReasoningEfforts(modelEntry) })
149
+ }
150
+ }
151
+ return result
152
+ }
153
+
154
+ function deepFreeze(value) {
155
+ if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value
156
+ for (const child of Object.values(value)) deepFreeze(child)
157
+ return Object.freeze(value)
158
+ }
159
+
160
+ export async function buildCapabilityCatalog(options = {}) {
161
+ const cwd = String(options.cwd || '').trim()
162
+ const selection = await callService(options.agentDefaultModel, 'currentSelection') || {}
163
+ const defaultModel = {
164
+ provider: String(selection.provider || selection.providerName || '').trim(),
165
+ model: String(selection.model || selection.modelName || '').trim(),
166
+ reasoningEffort: String(selection.reasoningEffort || '').trim()
167
+ }
168
+ const subagentProviders = await listSubagentProviders(options.subagents)
169
+ const skills = await listSkills(options.skills, cwd)
170
+ const tools = await listTools(options.tools)
171
+ const models = await discoverModels(options.llm)
172
+ const codexProviders = subagentProviders.filter((item) => /codex/i.test(item.name))
173
+ const codexReasoning = [...new Set(codexProviders.flatMap((item) => item.capabilities.reasoningEfforts))]
174
+ const modelReasoning = models.flatMap((item) => item.reasoningEfforts)
175
+ const reasoningEfforts = [...new Set([...codexReasoning, ...modelReasoning])]
176
+ const catalog = {
177
+ schemaVersion: GRAPH_SCHEMA_VERSION,
178
+ capturedAt: Date.now(),
179
+ runtimeVersion: String(options.runtimeVersion || process.env.DSH_RUNTIME_VERSION || 'unknown'),
180
+ defaultModel,
181
+ executors: {
182
+ dsh: {
183
+ available: typeof options.agents?.create === 'function' || subagentProviders.some((item) => item.name === 'spawn'),
184
+ provider: subagentProviders.find((item) => item.name === 'spawn')?.name || 'spawn'
185
+ },
186
+ codex: {
187
+ available: codexProviders.length > 0,
188
+ providers: codexProviders.map((item) => item.name),
189
+ reasoningEfforts
190
+ }
191
+ },
192
+ subagentProviders,
193
+ models,
194
+ tools,
195
+ skills,
196
+ skillPolicy: {
197
+ allowlist: 'explicit',
198
+ pluginSkills: 'deny',
199
+ unknownSource: 'deny',
200
+ runtimeFallback: 'deny-all-when-scoped-filter-unavailable'
201
+ },
202
+ recursionGuard: {
203
+ deniedTools: ['graphjob_plan', 'graphjob_run', 'graphjob_patch', 'graph_job'],
204
+ deniedSkillOrigins: ['plugin', 'unknown']
205
+ }
206
+ }
207
+ return deepFreeze(catalog)
208
+ }
209
+
210
+ export function profileSkillSelection(catalog, names = []) {
211
+ const { allowed, denied } = filterSkills(catalog?.skills || [], names)
212
+ return {
213
+ names: allowed.map((item) => item.name).sort(),
214
+ denied: denied.map((item) => ({ name: item.name, origin: item.origin, source: item.source }))
215
+ }
216
+ }