@p-dsh-market/graph-job-orchestrator 0.1.2 → 0.1.3

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 CHANGED
@@ -6,7 +6,7 @@
6
6
  - `%LOCALAPPDATA%/dsh-desktop/plugin-data/graph-job-orchestrator/` 下的原子 JSON 与 append-only JSONL 存储;
7
7
  - DSH in-process Subagent executor、merge barrier、read 并行/write 串行、失败暂停、有限 transport/rate-limit retry、取消和终止;
8
8
  - `artifactRefs` workspace-relative 校验、插件 Skill/Graph Job 递归保护、`/graphjob` 命令、Web API、SSE 和任务图编辑面板;
9
- - Codex provider 的能力发现、model/reasoningEffort 快照与 capability mismatch 错误。
9
+ - Codex provider 的能力发现、model/reasoningEffort 快照与 capability mismatch 错误;官方 `subagent-codex` 按 one-shot 运行,不会误走 DSH continuable 路径。
10
10
 
11
11
  运行时必须先安装并注册 Codex Subagent provider,阶段 4 才会显示 Codex executor 可用;没有 provider 时不会伪装成可运行,而是在能力快照和运行错误中明确说明。当前仓库的本地 runtime 只有 DSH `spawn/fork` provider,因此不会在安装插件时自动修改 profile。
12
12
 
@@ -18,6 +18,26 @@ dsh plugin --profile <name> add @deepseek-ai/dsh-subagent-codex
18
18
 
19
19
  插件只负责发现和校验 provider,不会代替用户安装 Bundle、登录 Codex 或修改 Codex 原生权限配置。
20
20
 
21
+ 官方 Codex Bundle 的 `model` 是 provider 实例级静态配置,不是一次调用时的动态参数。若希望在 Graph Job 设置中选择多个 Codex 模型,可在目标 Profile 的 `cordis.patch.yml` 挂载多个命名实例,例如:
22
+
23
+ ```yaml
24
+ - insert:
25
+ - id: graph-job-codex-luna
26
+ name: '@deepseek-ai/dsh-subagent-codex'
27
+ config:
28
+ providerName: codex-luna
29
+ model: gpt-5.6-luna
30
+ permissionMode: never
31
+ - id: graph-job-codex-sol
32
+ name: '@deepseek-ai/dsh-subagent-codex'
33
+ config:
34
+ providerName: codex-sol
35
+ model: gpt-5.6-sol
36
+ permissionMode: never
37
+ ```
38
+
39
+ Graph Job 会从 runtime capability snapshot 读取这些 provider/model;`reasoningEffort` 只有 Codex provider 明确公布可选值时才显示和发送。
40
+
21
41
  主要端点:
22
42
 
23
43
  - `GET /graph-job-orchestrator/capabilities`
@@ -29,12 +49,13 @@ dsh plugin --profile <name> add @deepseek-ai/dsh-subagent-codex
29
49
  - `GET /graph-job-orchestrator/templates/previews/:previewId`
30
50
  - `POST /graph-job-orchestrator/templates/preview|confirm|bind`
31
51
  - `GET/POST /graph-job-orchestrator/subagents`
52
+ - `POST /graph-job-orchestrator/planner/decision`
32
53
  - `GET /graph-job-orchestrator/runs/:runId/events`
33
54
  - `POST /graph-job-orchestrator/runs/:runId/retry|cancel|terminate`
34
55
 
35
- 带目标的 `/graphjob <目标>` 会向当前主对话排入一次 Planner follow-up;空 `/graphjob` 只打开或创建空图。Planner 只接受当前会话的 roster、Graph JSON Schema 和受限 patch;候选图必须先 preview,再由用户确认。已有手工锁定图时,Planner 请求必须明确 `templateMode: "saveAs"` 或 `"overwrite"`,不会隐式改写当前模板。运行时输出严格投影为 `text` 和 `artifactRefs`,子会话 ID 只保存在运行状态和事件中。
56
+ 带目标的 `/graphjob <目标>` 会向当前主对话排入一次 Planner follow-up;空 `/graphjob` 只打开或创建空图。首次打开时生成的空手工占位图不会阻挡第一份模型草案;真正编辑过的手工锁定图会在任务图面板要求选择 `saveAs` 或 `overwrite`,选择通过 `/planner/decision` 提交,不会隐式改写当前模板。Planner 只接受当前会话的 roster、Graph JSON Schema 和受限 patch;候选图必须先 preview,再由用户确认。运行时输出严格投影为 `text` 和 `artifactRefs`,子会话 ID 只保存在运行状态和事件中。
36
57
 
37
- `POST /graph-job-orchestrator/subagents` 创建可继续对话的独立 Subagent,并按所选 Agent Profile 快照 Provider/Model;客户端会在创建后打开对应的子会话。该入口默认过滤 `skill` 和 Graph Job 管理工具。Host 和客户端会输出 `[graph-job-orchestrator]` 前缀的脱敏诊断日志,只记录会话短标识、长度、状态、耗时和错误代码,不记录用户任务正文或 token。
58
+ `POST /graph-job-orchestrator/subagents` 按所选 Agent Profile 创建 Subagent。DSH provider 创建可继续对话的子会话;官方 Codex provider 使用 one-shot `start`,等待单轮结果后直接显示最终正文和脱敏诊断,不创建可继续的 Codex 会话。官方 provider 的 model、权限、MCP 和 Skills 由 Profile Bundle/原生 Codex 配置决定;只有 provider 明确公布动态能力时才允许按 Profile 传入对应字段。该入口默认过滤 `skill` 和 Graph Job 管理工具。Host 和客户端会输出 `[graph-job-orchestrator]` 前缀的脱敏诊断日志,只记录会话短标识、长度、状态、耗时和错误代码,不记录用户任务正文或 token。
38
59
 
39
60
  模板 manifest 支持 `scope: "workspace" | "global"`,默认是当前 workspace;workspace 模板不会出现在其他工作区的模板列表中。切换模板会创建新的 Graph Instance,不会把可变模板文件直接绑定为活动 Graph。
40
61
 
@@ -105,16 +105,23 @@ function providerCapabilities(item) {
105
105
  persona: Boolean(value.persona),
106
106
  permissionMode: Boolean(value.permissionMode || value.permissionModes),
107
107
  permissionModes: stringArray(value.permissionModes),
108
- reasoningEfforts: stringArray(value.reasoningEfforts || value.reasoningEffort)
108
+ reasoningEfforts: stringArray(value.reasoningEfforts || value.reasoningEffort),
109
+ dynamicModel: Boolean(value.dynamicModel || value.modelOverride),
110
+ dynamicReasoning: Boolean(value.dynamicReasoning || value.reasoningEffortOverride)
109
111
  }
110
112
  }
111
113
 
112
114
  function normalizeProvider(item) {
113
115
  if (typeof item === 'string') return { name: item, capabilities: {} }
116
+ const config = item?.config && typeof item.config === 'object' ? item.config : {}
117
+ const staticModel = String(item?.staticModel || item?.model || config.model || '').trim()
118
+ const staticPermissionMode = String(item?.staticPermissionMode || config.permissionMode || '').trim()
114
119
  return {
115
120
  name: String(item?.name || item?.id || item?.providerName || '').trim(),
116
121
  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),
122
+ models: asArray(item?.models || item?.capabilities?.models || (staticModel ? [staticModel] : [])).map((model) => typeof model === 'string' ? model : String(model?.id || model?.name || '').trim()).filter(Boolean),
123
+ ...(staticModel ? { staticModel } : {}),
124
+ ...(staticPermissionMode ? { staticPermissionMode } : {}),
118
125
  capabilities: providerCapabilities(item)
119
126
  }
120
127
  }
@@ -171,8 +178,9 @@ export async function buildCapabilityCatalog(options = {}) {
171
178
  const models = await discoverModels(options.llm)
172
179
  const codexProviders = subagentProviders.filter((item) => /codex/i.test(item.name))
173
180
  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])]
181
+ // Reasoning is an executor/provider capability. LLM model metadata from an
182
+ // unrelated DSH adapter must never make the Codex selector appear supported.
183
+ const reasoningEfforts = [...new Set(codexReasoning)]
176
184
  const catalog = {
177
185
  schemaVersion: GRAPH_SCHEMA_VERSION,
178
186
  capturedAt: Date.now(),
package/lib/client.js CHANGED
@@ -12,6 +12,7 @@ window.__ModuleLoader__.load({
12
12
  var PLUGIN_ID = '@p-dsh-market/graph-job-orchestrator'
13
13
  var BASE = '/graph-job-orchestrator'
14
14
  var DESKTOP_RPC_METHOD = 'graphJobOrchestrator.open'
15
+ var DESKTOP_SUBAGENT_RPC_METHOD = 'graphJobOrchestrator.openSubagent'
15
16
  var sessionsService = null
16
17
  var plannerWaitUntil = {}
17
18
  var subagentOverlayStore = { open: false, version: 0, initialPrompt: '' }
@@ -86,7 +87,10 @@ window.__ModuleLoader__.load({
86
87
  if (!response.ok || body.ok === false) {
87
88
  reportedFailure = true
88
89
  clientDiagnostic('request.failed', { method: method, path: path, status: response.status, durationMs: Date.now() - startedAt, code: body.code || 'HTTP_ERROR' })
89
- throw new Error(body.error || 'Graph Job 请求失败。')
90
+ var requestError = new Error(body.error || 'Graph Job 请求失败。')
91
+ requestError.code = body.code || 'HTTP_ERROR'
92
+ requestError.status = response.status
93
+ throw requestError
90
94
  }
91
95
  clientDiagnostic('request.done', { method: method, path: path, status: response.status, durationMs: Date.now() - startedAt })
92
96
  return body
@@ -246,7 +250,7 @@ window.__ModuleLoader__.load({
246
250
 
247
251
  function GraphJobView(props) {
248
252
  var sessionId = useSessionId(props)
249
- var state = React.useState({ graph: null, profiles: [], capabilities: null, templates: [], templateId: '', templateScope: 'workspace', run: null, preview: null, templatePreview: null, loading: true, planning: false, saving: false, error: '', message: '', selectedId: '', connectFrom: '' })
253
+ var state = React.useState({ graph: null, profiles: [], capabilities: null, templates: [], templateId: '', templateScope: 'workspace', templateDecisionMode: 'saveAs', templateName: '', planner: null, run: null, preview: null, templatePreview: null, loading: true, planning: false, saving: false, error: '', message: '', selectedId: '', connectFrom: '' })
250
254
  var store = state[0]
251
255
  var setStore = state[1]
252
256
  function patch(next) { setStore(function (current) { return Object.assign({}, current, next) }) }
@@ -257,12 +261,13 @@ window.__ModuleLoader__.load({
257
261
  Promise.all([request('/graphs?sessionId=' + encodeURIComponent(sessionId)), request('/profiles'), request('/capabilities'), request('/templates?sessionId=' + encodeURIComponent(sessionId))]).then(function (values) {
258
262
  var body = values[0]
259
263
  var planner = body.planner || null
260
- if (planner && planner.state === 'running' && !plannerWaitUntil[sessionId]) plannerWaitUntil[sessionId] = Date.now() + 90000
261
- var waiting = Boolean((plannerWaitUntil[sessionId] || 0) > Date.now())
262
- var freshPreview = planner && planner.state === 'ready' && body.preview ? body.preview : (waiting ? store.preview : body.preview)
263
- var plannerFailed = planner && planner.state === 'failed'
264
- patch({ graph: freshPreview && freshPreview.graph ? layoutGraph(freshPreview.graph) : (body.graph ? layoutGraph(body.graph) : null), preview: freshPreview || null, run: body.binding && body.binding.activeRunId ? null : null, profiles: values[1].profiles || [], capabilities: values[2].capabilities || null, templates: values[3].templates || [], planning: waiting && !plannerFailed, loading: false, error: plannerFailed ? planner.error : '', message: planner && planner.state === 'ready' && body.preview ? 'Planner 草案已生成;请检查 DAG 后确认 revision。' : '' })
265
- if ((planner && (planner.state === 'ready' || planner.state === 'failed')) || !waiting) delete plannerWaitUntil[sessionId]
264
+ if (planner && ['queued', 'running'].indexOf(planner.state) >= 0 && !plannerWaitUntil[sessionId]) plannerWaitUntil[sessionId] = Date.now() + 90000
265
+ var waiting = Boolean((plannerWaitUntil[sessionId] || 0) > Date.now())
266
+ var plannerNeedsDecision = planner && planner.state === 'needs-decision'
267
+ var freshPreview = plannerNeedsDecision ? null : (planner && planner.state === 'ready' && body.preview ? body.preview : (waiting ? store.preview : body.preview))
268
+ var plannerFailed = planner && planner.state === 'failed'
269
+ patch({ graph: freshPreview && freshPreview.graph ? layoutGraph(freshPreview.graph) : (body.graph ? layoutGraph(body.graph) : null), preview: freshPreview || null, templatePreview: body.templatePreview || null, planner: planner, run: body.binding && body.binding.activeRunId ? null : null, profiles: values[1].profiles || [], capabilities: values[2].capabilities || null, templates: values[3].templates || [], planning: waiting && !plannerFailed && !plannerNeedsDecision, loading: false, error: plannerFailed || plannerNeedsDecision ? planner.error || '模型修订需要先选择 saveAs 或 overwrite 模板。' : '', message: planner && planner.state === 'ready' && body.preview ? 'Planner 草案已生成;请检查 DAG 后确认 revision。' : plannerNeedsDecision ? '手工 Graph 已锁定;请选择模型候选的模板保存方式。' : '' })
270
+ if ((planner && (planner.state === 'ready' || planner.state === 'failed' || planner.state === 'needs-decision')) || !waiting) delete plannerWaitUntil[sessionId]
266
271
  else setTimeout(load, 1000)
267
272
  if (body.binding && body.binding.activeRunId) request('/runs/' + encodeURIComponent(body.binding.activeRunId)).then(function (runBody) { patch({ run: runBody.run }) }).catch(function () {})
268
273
  }).catch(function (error) { patch({ loading: false, error: error.message || String(error) }) })
@@ -273,14 +278,27 @@ window.__ModuleLoader__.load({
273
278
  var detail = event && event.detail || {}
274
279
  if (detail.sessionId && detail.sessionId !== sessionId) return
275
280
  plannerWaitUntil[sessionId] = Date.now() + 90000
276
- patch({ preview: null, planning: true, error: '', message: '已收到 /graphjob 目标,正在生成 Graph Job 草案…' })
281
+ patch({ preview: null, templatePreview: null, planner: { state: 'queued' }, planning: true, error: '', message: '已收到 /graphjob 目标,正在生成 Graph Job 草案…' })
277
282
  load()
278
283
  }
279
284
  window.addEventListener('gjo:planner-start', onPlannerStart)
280
285
  return function () { window.removeEventListener('gjo:planner-start', onPlannerStart) }
281
286
  }, [sessionId])
282
287
 
283
- function save() {
288
+ function submitPlannerDecision() {
289
+ var mode = store.templateDecisionMode || 'saveAs'
290
+ if (mode === 'overwrite' && !store.templateId) { patch({ error: '覆盖模板前必须选择一个模板。' }); return }
291
+ plannerWaitUntil[sessionId] = Date.now() + 90000
292
+ patch({ planning: true, error: '', message: '正在按所选模板方式生成候选…' })
293
+ request('/planner/decision', { method: 'POST', body: { sessionId: sessionId, mode: mode, templateId: mode === 'overwrite' ? store.templateId : '', templateScope: store.templateScope || 'workspace', templateName: store.templateName || (store.graph && store.graph.goal) || 'Graph Job 模板' } }).then(function (body) {
294
+ var next = { planning: body.state === 'queued', error: '', message: body.state === 'queued' ? '已提交模板选择,正在生成候选…' : '候选模板已生成;请确认后保存。' }
295
+ if (body.templatePreview) next.templatePreview = body.templatePreview
296
+ if (body.state === 'ready') { next.planner = Object.assign({}, store.planner || {}, { state: 'ready', previewId: body.preview && body.preview.previewId || '' }); next.planning = false }
297
+ patch(next)
298
+ if (body.state === 'queued') setTimeout(load, 600)
299
+ }).catch(function (error) { delete plannerWaitUntil[sessionId]; patch({ planning: false, error: error.message || String(error) }) })
300
+ }
301
+ function save() {
284
302
  if (!store.graph || store.saving) return
285
303
  patch({ saving: true, error: '', message: '' })
286
304
  request('/graphs/' + encodeURIComponent(store.graph.graphId), { method: 'PATCH', body: { sessionId: sessionId, graph: store.graph } }).then(function (body) {
@@ -340,13 +358,23 @@ window.__ModuleLoader__.load({
340
358
  }
341
359
  function removeEdge(edge) { patch({ graph: Object.assign({}, clone(store.graph), { edges: (store.graph.edges || []).filter(function (item) { return item.from !== edge.from || item.to !== edge.to }) }), preview: null }) }
342
360
  function updateGoal(event) { patch({ graph: Object.assign({}, store.graph, { goal: event.target.value }), preview: null }) }
343
- var selected = nodeById(store.graph, store.selectedId)
361
+ var selected = nodeById(store.graph, store.selectedId)
362
+ var plannerNeedsDecision = store.planner && store.planner.state === 'needs-decision'
344
363
  if (store.loading) return React.createElement('div', { className: 'gjo-shell gjo-empty' }, '正在加载 Graph Job…')
345
364
  if (!store.graph) return React.createElement('div', { className: 'gjo-shell gjo-empty' }, [React.createElement('div', { key: 'icon', className: 'gjo-icon' }, '◇'), React.createElement('h2', { key: 'h' }, '当前会话还没有 Graph Job'), React.createElement('p', { key: 'p' }, '从空白任务图开始,或让当前对话中的 Planner 先提出一个受限草案。'), React.createElement('button', { key: 'create', type: 'button', className: 'gjo-primary', onClick: newGraph }, '创建任务图')])
346
365
  return React.createElement('div', { className: 'gjo-shell' }, [
347
366
  React.createElement('header', { key: 'header', className: 'gjo-header' }, [React.createElement('div', { key: 'copy' }, [React.createElement('div', { key: 'eyebrow', className: 'gjo-eyebrow' }, 'GRAPH JOB / SESSION ' + sessionId.slice(0, 12)), React.createElement('h2', { key: 'title' }, '多 Subagent 任务图'), React.createElement('p', { key: 'sub' }, 'Revision ' + store.graph.revision + ' · ' + (store.graph.manualLock ? '手工锁定' : 'Planner 草案')), React.createElement('div', { key: 'actions', className: 'gjo-header-actions' }, [React.createElement('button', { key: 'new', type: 'button', className: 'gjo-secondary', onClick: newGraph }, '新建'), React.createElement('button', { key: 'refresh', type: 'button', className: 'gjo-secondary', onClick: load }, '刷新'), React.createElement('button', { key: 'save', type: 'button', className: 'gjo-secondary', disabled: store.saving, onClick: save }, store.saving ? '保存中…' : '保存 revision'), React.createElement('button', { key: 'template', type: 'button', className: 'gjo-secondary', onClick: function () { previewTemplate('saveAs') } }, '另存模板'), React.createElement('select', { key: 'scope', className: 'gjo-template-select', value: store.templateScope || 'workspace', onChange: function (event) { patch({ templateScope: event.target.value }) } }, [React.createElement('option', { key: 'workspace', value: 'workspace' }, 'workspace 模板'), React.createElement('option', { key: 'global', value: 'global' }, 'global 模板')]), store.templates.length ? React.createElement('select', { key: 'template-select', className: 'gjo-template-select', value: store.templateId || '', onChange: function (event) { var id = event.target.value; var selected = (store.templates || []).filter(function (template) { return template.id === id })[0]; patch({ templateId: id, templateScope: selected && selected.scope || 'workspace' }) } }, [React.createElement('option', { key: 'empty', value: '' }, '选择覆盖模板'), store.templates.map(function (template) { return React.createElement('option', { key: template.id, value: template.id }, (template.name || template.id) + ' · ' + (template.scope || 'workspace')) })]) : null, React.createElement('button', { key: 'bind', type: 'button', className: 'gjo-secondary', disabled: !store.templateId, onClick: bindTemplate }, '切换到模板'), React.createElement('button', { key: 'overwrite', type: 'button', className: 'gjo-secondary', disabled: !store.templateId, onClick: function () { previewTemplate('overwrite') } }, '覆盖模板'), React.createElement('button', { key: 'preview', type: 'button', className: 'gjo-primary', onClick: preview }, '预览 DAG')])])]),
348
367
  React.createElement('div', { key: 'goal', className: 'gjo-goal-bar' }, [React.createElement('label', { key: 'label' }, '目标'), React.createElement('textarea', { key: 'input', rows: 2, value: store.graph.goal || '', onChange: updateGoal, placeholder: '这个 Graph 要完成什么?' }), React.createElement('div', { key: 'limit', className: 'gjo-limit-copy' }, 'maxParallel ' + (store.graph.limits && store.graph.limits.maxParallel || 4) + ' · write 串行 · 失败暂停')]),
349
- store.error ? React.createElement('div', { key: 'error', className: 'gjo-error' }, store.error) : null,
368
+ store.error ? React.createElement('div', { key: 'error', className: 'gjo-error' }, store.error) : null,
369
+ plannerNeedsDecision ? React.createElement('section', { key: 'planner-decision', className: 'gjo-decision-panel' }, [
370
+ React.createElement('strong', { key: 'title' }, '选择模型候选的保存方式'),
371
+ React.createElement('p', { key: 'copy' }, '当前会话的 Graph 已被手工编辑锁定。模型只能生成候选,必须另存为新模板或覆盖已有模板。'),
372
+ React.createElement('label', { key: 'mode' }, ['保存方式', React.createElement('select', { key: 'input', value: store.templateDecisionMode || 'saveAs', onChange: function (event) { patch({ templateDecisionMode: event.target.value }) } }, [React.createElement('option', { key: 'saveAs', value: 'saveAs' }, 'saveAs:另存为新模板'), React.createElement('option', { key: 'overwrite', value: 'overwrite' }, 'overwrite:覆盖已有模板')])]),
373
+ store.templateDecisionMode === 'overwrite' ? React.createElement('label', { key: 'template' }, ['目标模板', React.createElement('select', { key: 'input', value: store.templateId || '', onChange: function (event) { patch({ templateId: event.target.value }) } }, [React.createElement('option', { key: 'empty', value: '' }, '请选择模板'), (store.templates || []).map(function (template) { return React.createElement('option', { key: template.id, value: template.id }, (template.name || template.id) + ' · ' + (template.scope || 'workspace')) })])]) : null,
374
+ React.createElement('label', { key: 'name' }, ['模板名称', React.createElement('input', { key: 'input', value: store.templateName || (store.graph && store.graph.goal) || '', onChange: function (event) { patch({ templateName: event.target.value }) } })]),
375
+ React.createElement('label', { key: 'scope' }, ['模板作用域', React.createElement('select', { key: 'input', value: store.templateScope || 'workspace', onChange: function (event) { patch({ templateScope: event.target.value }) } }, [React.createElement('option', { key: 'workspace', value: 'workspace' }, 'workspace'), React.createElement('option', { key: 'global', value: 'global' }, 'global')])]),
376
+ React.createElement('button', { key: 'submit', type: 'button', className: 'gjo-primary', disabled: store.planning, onClick: submitPlannerDecision }, store.planning ? '生成中…' : '按选择生成候选')
377
+ ]) : null,
350
378
  store.message ? React.createElement('div', { key: 'message', className: 'gjo-message' }, store.message) : null,
351
379
  store.planning ? React.createElement('div', { key: 'planning', className: 'gjo-message' }, 'Planner 正在生成草案;完成后会显示预览。') : null,
352
380
  store.preview ? React.createElement(ValidationPanel, { key: 'validation', validation: store.preview.validation }) : null,
@@ -384,21 +412,28 @@ window.__ModuleLoader__.load({
384
412
  return Object.assign({}, value, { draft: draft })
385
413
  })
386
414
  }
415
+ function providerEntry(name) {
416
+ return ((store.capabilities && store.capabilities.subagentProviders) || []).filter(function (item) { return item.name === name })[0] || null
417
+ }
387
418
  function listValues(value) {
388
419
  return Array.from(new Set((value || []).map(function (item) { return String(item || '').trim() }).filter(Boolean)))
389
420
  }
390
421
  function providersFor(profile) {
391
422
  var capabilities = store.capabilities || {}
392
- var providers = (capabilities.subagentProviders || []).filter(function (item) { return profile.executor !== 'codex' || /codex/i.test(item.name || '') }).map(function (item) { return item.name }).filter(Boolean)
423
+ var providers = profile.executor === 'codex'
424
+ ? (capabilities.subagentProviders || []).filter(function (item) { return /codex/i.test(item.name || '') }).map(function (item) { return item.name }).filter(Boolean)
425
+ : (capabilities.models || []).map(function (item) { return typeof item === 'string' ? item : item.provider }).filter(Boolean)
393
426
  if (profile.executor === 'codex') providers = listValues((capabilities.executors && capabilities.executors.codex && capabilities.executors.codex.providers) || providers)
394
- else if (capabilities.executors && capabilities.executors.dsh && capabilities.executors.dsh.provider) providers.unshift(capabilities.executors.dsh.provider)
427
+ else if (capabilities.defaultModel && capabilities.defaultModel.provider) providers.unshift(capabilities.defaultModel.provider)
395
428
  if (profile.provider) providers.unshift(profile.provider)
396
429
  return listValues(providers)
397
430
  }
398
431
  function modelsFor(profile) {
399
- var values = (store.capabilities && store.capabilities.models || []).filter(function (item) { return typeof item === 'string' || !profile.provider || item.provider === profile.provider }).map(function (item) { return typeof item === 'string' ? item : item.model })
400
432
  var provider = (store.capabilities && store.capabilities.subagentProviders || []).filter(function (item) { return item.name === profile.provider })[0]
401
- values = values.concat(provider && provider.models || [])
433
+ var values = profile.executor === 'codex'
434
+ ? (provider && provider.models || [])
435
+ : (store.capabilities && store.capabilities.models || []).filter(function (item) { return typeof item === 'string' || !profile.provider || item.provider === profile.provider }).map(function (item) { return typeof item === 'string' ? item : item.model })
436
+ values = values.concat(provider && provider.staticModel || [])
402
437
  if (profile.model) values.unshift(profile.model)
403
438
  return listValues(values)
404
439
  }
@@ -408,22 +443,58 @@ window.__ModuleLoader__.load({
408
443
  var values = provider && provider.capabilities && provider.capabilities.reasoningEfforts || (store.capabilities && store.capabilities.executors && store.capabilities.executors.codex && store.capabilities.executors.codex.reasoningEfforts) || []
409
444
  return listValues(values)
410
445
  }
446
+ function changeProvider(index, providerName) {
447
+ var profile = (store.draft || [])[index] || {}
448
+ var next = { provider: providerName }
449
+ if (profile.executor === 'codex') {
450
+ var provider = providerEntry(providerName)
451
+ var models = listValues(provider && (provider.models || []).concat(provider.staticModel || []))
452
+ next.model = models.length ? models[0] : ''
453
+ next.reasoningEffort = ''
454
+ }
455
+ update(index, next)
456
+ }
411
457
  function changeExecutor(index, executor) {
412
458
  var profile = (store.draft || [])[index] || {}
413
- var next = Object.assign({}, profile, { executor: executor, reasoningEffort: executor === 'codex' ? profile.reasoningEffort || '' : '' })
459
+ var next = Object.assign({}, profile, { executor: executor, provider: profile.provider || '', model: '', reasoningEffort: '' })
414
460
  var providers = providersFor(next)
461
+ if (executor === 'codex') {
462
+ var providerEntries = (store.capabilities && store.capabilities.subagentProviders || []).filter(function (item) { return /codex/i.test(item.name || '') })
463
+ var configured = providerEntries.filter(function (item) { return (item.models && item.models.length) || item.staticModel })[0]
464
+ if (configured) next.provider = configured.name
465
+ } else {
466
+ var dshProviders = providers
467
+ if (dshProviders.length && dshProviders.indexOf(next.provider) < 0) next.provider = dshProviders[0]
468
+ }
415
469
  if (providers.length && providers.indexOf(next.provider) < 0) next.provider = providers[0]
416
470
  var models = modelsFor(next)
417
471
  if (models.length && models.indexOf(next.model) < 0) next.model = models[0]
418
472
  update(index, next)
419
473
  }
474
+ function newProfile() {
475
+ setStore(function (value) {
476
+ var draft = clone(value.draft || [])
477
+ var ids = {}
478
+ draft.forEach(function (profile) { ids[profile.id] = true })
479
+ var index = draft.length + 1
480
+ var id = 'agent-' + index
481
+ while (ids[id]) { index += 1; id = 'agent-' + index }
482
+ var capabilities = value.capabilities || {}
483
+ var codexEntries = (capabilities.subagentProviders || []).filter(function (item) { return /codex/i.test(item.name || '') })
484
+ var codex = codexEntries.filter(function (item) { return (item.models && item.models.length) || item.staticModel })[0] || codexEntries[0]
485
+ var useCodex = Boolean(capabilities.executors && capabilities.executors.codex && capabilities.executors.codex.available && codex)
486
+ var model = useCodex ? listValues((codex.models || []).concat(codex.staticModel || []))[0] || '' : ''
487
+ draft.push({ id: id, name: useCodex ? 'Codex Agent ' + index : 'DSH Agent ' + index, executor: useCodex ? 'codex' : 'dsh', provider: useCodex ? codex.name : '', model: model, reasoningEffort: '', persona: '', permissionMode: 'default', maxOutputTokens: 4096, capabilities: { tools: [], skills: [] }, enabled: true })
488
+ return Object.assign({}, value, { draft: draft, message: useCodex ? '已新增 Codex Profile,请检查 Model 后保存。' : '已新增 DSH Profile,请检查配置后保存。' })
489
+ })
490
+ }
420
491
  function save() {
421
492
  setStore(function (value) { return Object.assign({}, value, { saving: true, error: '' }) })
422
493
  request('/profiles', { method: 'PUT', body: { profiles: store.draft } }).then(function (body) { setStore(function (value) { return Object.assign({}, value, { profiles: body.profiles, draft: clone(body.profiles), saving: false, message: 'Agent Profile 已保存。' }) }) }).catch(function (error) { setStore(function (value) { return Object.assign({}, value, { saving: false, error: error.message || String(error) }) }) })
423
494
  }
424
495
  if (store.loading) return React.createElement('div', { className: 'gjo-settings gjo-empty' }, '正在读取 Agent Profile…')
425
496
  return React.createElement('div', { className: 'gjo-settings' }, [
426
- React.createElement('div', { key: 'head', className: 'gjo-settings-head' }, [React.createElement('div', { key: 'copy' }, [React.createElement('h2', { key: 'h' }, 'Graph Job Agent Profiles'), React.createElement('p', { key: 'p' }, 'revision 创建时会快照 Provider/Model;普通 DSH 不显示 reasoningEffort,Codex 只显示能力快照公布的值。')]), React.createElement('button', { key: 'save', type: 'button', className: 'gjo-primary', disabled: store.saving, onClick: save }, store.saving ? '保存中…' : '保存')]),
497
+ React.createElement('div', { key: 'head', className: 'gjo-settings-head' }, [React.createElement('div', { key: 'copy' }, [React.createElement('h2', { key: 'h' }, 'Graph Job Agent Profiles'), React.createElement('p', { key: 'p' }, 'revision 创建时会快照 Provider/Model;普通 DSH 不显示 reasoningEffort,Codex 只显示能力快照公布的值。')]), React.createElement('div', { key: 'actions', className: 'gjo-header-actions' }, [React.createElement('button', { key: 'new', type: 'button', className: 'gjo-secondary', onClick: newProfile }, '新增 Profile'), React.createElement('button', { key: 'save', type: 'button', className: 'gjo-primary', disabled: store.saving, onClick: save }, store.saving ? '保存中…' : '保存')])]),
427
498
  store.error ? React.createElement('div', { key: 'error', className: 'gjo-error' }, store.error) : null,
428
499
  store.message ? React.createElement('div', { key: 'message', className: 'gjo-message' }, store.message) : null,
429
500
  React.createElement('div', { key: 'list', className: 'gjo-profile-list' }, (store.draft || []).map(function (profile, index) {
@@ -433,10 +504,10 @@ window.__ModuleLoader__.load({
433
504
  return React.createElement('article', { key: profile.id, className: 'gjo-profile-card' }, [
434
505
  React.createElement('div', { key: 'top', className: 'gjo-profile-top' }, [React.createElement('strong', { key: 'name' }, profile.name || profile.id), React.createElement('span', { key: 'executor', className: 'gjo-pill' }, profile.executor)]),
435
506
  React.createElement('label', { key: 'executor' }, ['Executor', React.createElement('select', { key: 'input', value: profile.executor || 'dsh', onChange: function (event) { changeExecutor(index, event.target.value) } }, [React.createElement('option', { key: 'dsh', value: 'dsh' }, 'DSH in-process'), React.createElement('option', { key: 'codex', value: 'codex', disabled: !(store.capabilities && store.capabilities.executors && store.capabilities.executors.codex && store.capabilities.executors.codex.available) }, 'Codex')])]),
436
- React.createElement('label', { key: 'provider' }, ['Provider', providers.length ? React.createElement('select', { key: 'input', value: profile.provider || '', onChange: function (event) { update(index, { provider: event.target.value }) } }, [React.createElement('option', { key: 'empty', value: '' }, '选择 Provider'), providers.map(function (item) { return React.createElement('option', { key: item, value: item }, item) })]) : React.createElement('input', { key: 'input', value: profile.provider || '', placeholder: 'revision 时继承默认', onChange: function (event) { update(index, { provider: event.target.value }) } })]),
507
+ React.createElement('label', { key: 'provider' }, ['Provider', providers.length ? React.createElement('select', { key: 'input', value: profile.provider || '', onChange: function (event) { changeProvider(index, event.target.value) } }, [React.createElement('option', { key: 'empty', value: '' }, '选择 Provider'), providers.map(function (item) { return React.createElement('option', { key: item, value: item }, item) })]) : React.createElement('input', { key: 'input', value: profile.provider || '', placeholder: 'revision 时继承默认', onChange: function (event) { update(index, { provider: event.target.value }) } })]),
437
508
  React.createElement('label', { key: 'model' }, ['Model', models.length ? React.createElement('select', { key: 'input', value: profile.model || '', onChange: function (event) { update(index, { model: event.target.value }) } }, [React.createElement('option', { key: 'empty', value: '' }, '选择 Model'), models.map(function (item) { return React.createElement('option', { key: item, value: item }, item) })]) : React.createElement('input', { key: 'input', value: profile.model || '', placeholder: 'revision 时继承默认', onChange: function (event) { update(index, { model: event.target.value }) } })]),
438
509
  profile.executor === 'codex' ? React.createElement('label', { key: 'reasoning' }, ['Reasoning effort', reasoning.length ? React.createElement('select', { key: 'input', value: profile.reasoningEffort || '', onChange: function (event) { update(index, { reasoningEffort: event.target.value }) } }, [React.createElement('option', { key: 'empty', value: '' }, '不设置'), reasoning.map(function (item) { return React.createElement('option', { key: item, value: item }, item) })]) : React.createElement('span', { key: 'empty', className: 'gjo-muted' }, '当前 Codex capability 未公布可选值')]) : null,
439
- React.createElement('label', { key: 'permission' }, ['Permission mode', React.createElement('select', { key: 'input', value: profile.permissionMode || 'default', onChange: function (event) { update(index, { permissionMode: event.target.value }) } }, [React.createElement('option', { key: 'default', value: 'default' }, 'default'), React.createElement('option', { key: 'never', value: 'never' }, 'never'), React.createElement('option', { key: 'approve', value: 'approve-for-me' }, 'approve-for-me')])]),
510
+ React.createElement('label', { key: 'permission' }, ['Permission mode', React.createElement('select', { key: 'input', value: profile.permissionMode || 'default', onChange: function (event) { update(index, { permissionMode: event.target.value }) } }, [React.createElement('option', { key: 'default', value: 'default' }, 'default'), React.createElement('option', { key: 'never', value: 'never' }, 'never'), React.createElement('option', { key: 'approve', value: 'approve-for-me' }, 'approve-for-me'), React.createElement('option', { key: 'dangerous', value: 'dangerously-bypass-approvals-and-sandbox' }, 'dangerously-bypass-approvals-and-sandbox')])]),
440
511
  React.createElement('label', { key: 'persona' }, ['Persona', React.createElement('textarea', { key: 'input', rows: 3, value: profile.persona || '', onChange: function (event) { update(index, { persona: event.target.value }) } } )]),
441
512
  React.createElement('p', { key: 'caps', className: 'gjo-muted' }, '工具 ' + ((profile.capabilities && profile.capabilities.tools || []).join(', ') || '无') + ' · Skills ' + ((profile.capabilities && profile.capabilities.skills || []).join(', ') || '无') + (profile.executor === 'codex' ? ' · reasoningEffort 由 capability 校验' : ''))
442
513
  ])
@@ -465,19 +536,22 @@ window.__ModuleLoader__.load({
465
536
  var sessions = props.sessions || sessionsService
466
537
  var sessionId = useSessionId({ sessions: sessions })
467
538
  var overlay = useSubagentOverlay()
468
- var state = React.useState({ label: '新建 Subagent', prompt: overlay.initialPrompt || '', profileId: '', profiles: [], loading: false, busy: false, error: '', message: '' })
539
+ var state = React.useState({ label: '新建 Subagent', prompt: overlay.initialPrompt || '', profileId: '', profiles: [], capabilities: null, loading: false, busy: false, error: '', message: '', oneShotResult: null })
469
540
  var store = state[0]
470
541
  var setStore = state[1]
471
542
  React.useEffect(function () {
472
543
  if (!overlay.open) return undefined
473
- setStore({ label: '新建 Subagent', prompt: overlay.initialPrompt || '', profileId: '', profiles: [], loading: true, busy: false, error: '', message: '' })
474
- request('/profiles').then(function (body) {
475
- var profiles = (body.profiles || []).filter(function (profile) { return profile.enabled !== false })
476
- setStore(function (value) { return Object.assign({}, value, { profiles: profiles, profileId: profiles[0] && profiles[0].id || '', loading: false }) })
477
- }).catch(function (error) { setStore(function (value) { return Object.assign({}, value, { loading: false, error: error.message || String(error) }) }) })
544
+ setStore({ label: '新建 Subagent', prompt: overlay.initialPrompt || '', profileId: '', profiles: [], capabilities: null, loading: true, busy: false, error: '', message: '', oneShotResult: null })
545
+ Promise.all([request('/profiles'), request('/capabilities')]).then(function (values) {
546
+ var body = values[0]
547
+ var profiles = (body.profiles || []).filter(function (profile) { return profile.enabled !== false })
548
+ setStore(function (value) { return Object.assign({}, value, { profiles: profiles, capabilities: values[1].capabilities || null, profileId: profiles[0] && profiles[0].id || '', loading: false }) })
549
+ }).catch(function (error) { setStore(function (value) { return Object.assign({}, value, { loading: false, error: error.message || String(error) }) }) })
478
550
  return undefined
479
551
  }, [overlay.open, overlay.version, sessionId])
480
- function update(patch) { setStore(function (value) { return Object.assign({}, value, patch) }) }
552
+ function update(patch) { setStore(function (value) { return Object.assign({}, value, patch) }) }
553
+ var selectedProfile = (store.profiles || []).filter(function (profile) { return profile.id === store.profileId })[0] || null
554
+ var selectedCodex = selectedProfile && selectedProfile.executor === 'codex'
481
555
  function openChild(child) {
482
556
  var childId = child && (child.childId || child.id)
483
557
  if (!childId) throw new Error('Runtime 没有返回子会话 ID。')
@@ -499,24 +573,34 @@ window.__ModuleLoader__.load({
499
573
  event.preventDefault()
500
574
  if (store.busy || !sessionId || !store.prompt.trim()) return
501
575
  update({ busy: true, error: '', message: '' })
502
- request('/subagents', { method: 'POST', body: { sessionId: sessionId, label: store.label, prompt: store.prompt, profileId: store.profileId } }).then(function (body) {
503
- update({ message: 'Subagent 已创建,正在打开独立会话。' })
504
- return openChild(body.child || {})
505
- }).then(function () {
506
- setSubagentOverlayOpen(false)
507
- }).catch(function (error) { update({ busy: false, error: error.message || String(error) }) })
576
+ request('/subagents', { method: 'POST', body: { sessionId: sessionId, label: store.label, prompt: store.prompt, profileId: store.profileId } }).then(function (body) {
577
+ var child = body.child || {}
578
+ if (child.mode === 'one-shot') {
579
+ update({ busy: false, message: 'Codex one-shot 已完成;结果保留在这里,未创建可继续会话。', oneShotResult: child.result || null })
580
+ return null
581
+ }
582
+ update({ message: 'Subagent 已创建,正在打开独立会话。' })
583
+ return openChild(child).then(function () { setSubagentOverlayOpen(false) })
584
+ }).catch(function (error) { update({ busy: false, error: error.message || String(error) }) })
508
585
  }
509
586
  if (!overlay.open) return null
510
587
  return React.createElement('div', { className: 'gjo-subagent-overlay', onMouseDown: function (event) { if (event.target === event.currentTarget) setSubagentOverlayOpen(false) } }, React.createElement('aside', { className: 'gjo-subagent-panel', role: 'dialog', 'aria-modal': 'true', 'aria-label': '新建 Subagent' }, [
511
588
  React.createElement('div', { key: 'head', className: 'gjo-subagent-head' }, [React.createElement('div', { key: 'copy' }, [React.createElement('span', { key: 'eyebrow', className: 'gjo-eyebrow' }, 'SUBAGENT'), React.createElement('h2', { key: 'title' }, '新建 Subagent')]), React.createElement('button', { key: 'close', type: 'button', className: 'gjo-subagent-close', onClick: function () { setSubagentOverlayOpen(false) } }, '×')]),
512
- React.createElement('p', { key: 'intro', className: 'gjo-subagent-intro' }, '创建一个可继续对话的独立 Agent。创建后会自动切换到它的会话,当前主会话仍保留。'),
513
- store.error ? React.createElement('div', { key: 'error', className: 'gjo-error' }, store.error) : null,
589
+ React.createElement('p', { key: 'intro', className: 'gjo-subagent-intro' }, selectedCodex ? 'Codex 通过官方 one-shot provider 运行一次任务;它会使用 Profile Bundle/原生配置中的模型和权限,完成后在此显示结果。' : '创建一个可继续对话的独立 Agent。创建后会自动切换到它的会话,当前主会话仍保留。'),
590
+ store.error ? React.createElement('div', { key: 'error', className: 'gjo-error' }, store.error) : null,
591
+ selectedProfile ? React.createElement('div', { key: 'profile-summary', className: 'gjo-subagent-profile-summary' }, [
592
+ React.createElement('strong', { key: 'title' }, '当前 Profile 能力'),
593
+ React.createElement('span', { key: 'provider' }, 'Executor:' + (selectedProfile.executor || 'dsh') + ' · Provider:' + (selectedProfile.provider || '默认')),
594
+ React.createElement('span', { key: 'model' }, 'Model:' + (selectedProfile.model || (selectedCodex ? '由 Codex 原生配置决定' : '由 DSH 默认配置决定'))),
595
+ selectedCodex ? React.createElement('span', { key: 'reasoning' }, 'Reasoning:' + (selectedProfile.reasoningEffort || '未设置;官方 provider 未公布动态推理档次')) : null
596
+ ]) : null,
597
+ store.oneShotResult ? React.createElement('div', { key: 'one-shot-result', className: 'gjo-subagent-result' }, [React.createElement('strong', { key: 'title' }, 'Codex 结果'), React.createElement('pre', { key: 'text' }, store.oneShotResult.text || '(无正文输出)'), store.oneShotResult.diagnostic ? React.createElement('small', { key: 'diagnostic' }, '诊断:' + store.oneShotResult.diagnostic) : null]) : null,
514
598
  React.createElement('form', { key: 'form', onSubmit: create }, [
515
599
  React.createElement('label', { key: 'label' }, ['名称', React.createElement('input', { key: 'input', value: store.label, maxLength: 120, onChange: function (event) { update({ label: event.target.value }) }, placeholder: '例如:资料检索员' })]),
516
600
  React.createElement('label', { key: 'profile' }, ['Agent Profile', store.loading ? React.createElement('span', { key: 'loading', className: 'gjo-muted' }, '读取中…') : React.createElement('select', { key: 'select', value: store.profileId, onChange: function (event) { update({ profileId: event.target.value }) } }, [React.createElement('option', { key: 'empty', value: '' }, '使用默认 Profile'), store.profiles.map(function (profile) { return React.createElement('option', { key: profile.id, value: profile.id }, (profile.name || profile.id) + ' · ' + profile.executor) })])]),
517
601
  React.createElement('label', { key: 'prompt' }, ['初始任务', React.createElement('textarea', { key: 'textarea', rows: 10, maxLength: 20000, value: store.prompt, onChange: function (event) { update({ prompt: event.target.value }) }, placeholder: '告诉 Subagent 要完成什么,以及需要关注的约束。' })]),
518
- React.createElement('p', { key: 'note', className: 'gjo-note' }, 'Subagent 默认不能使用 Graph Job 管理工具或 Skill 工具;模型、Provider 和权限沿用所选 Profile 的受控配置。'),
519
- React.createElement('div', { key: 'actions', className: 'gjo-subagent-actions' }, [React.createElement('button', { key: 'cancel', type: 'button', className: 'gjo-secondary', onClick: function () { setSubagentOverlayOpen(false) } }, '取消'), React.createElement('button', { key: 'submit', type: 'submit', className: 'gjo-primary', disabled: store.busy || !sessionId || !store.prompt.trim() }, store.busy ? '创建中…' : '创建并打开')])
602
+ React.createElement('p', { key: 'note', className: 'gjo-note' }, selectedCodex ? '官方 Codex provider 不接受 per-call agentOptions、Tools、Skills、persona 或动态 reasoning;需要先在 Profile Bundle/原生 Codex 配置中设置。' : 'Subagent 默认不能使用 Graph Job 管理工具或 Skill 工具;模型、Provider 和权限沿用所选 Profile 的受控配置。'),
603
+ React.createElement('div', { key: 'actions', className: 'gjo-subagent-actions' }, [React.createElement('button', { key: 'cancel', type: 'button', className: 'gjo-secondary', onClick: function () { setSubagentOverlayOpen(false) } }, '取消'), React.createElement('button', { key: 'submit', type: 'submit', className: 'gjo-primary', disabled: store.busy || !sessionId || !store.prompt.trim() }, store.busy ? '创建中…' : selectedCodex ? '运行 Codex one-shot' : '创建并打开')])
520
604
  ])
521
605
  ]))
522
606
  }
@@ -530,6 +614,10 @@ window.__ModuleLoader__.load({
530
614
  CSS += `
531
615
  .gjo-sidebar-action{display:flex;align-items:center;justify-content:center;width:calc(100% - 20px);margin:6px 10px 0;padding:7px 10px;border:1px solid var(--dsw-alias-border-l2,#30343e);border-radius:7px;background:transparent;color:var(--dsw-alias-label-secondary,#aab1bd);cursor:pointer;font:inherit;font-size:12px}.gjo-sidebar-action:hover{background:var(--dsw-alias-interactive-bg-hover,#ffffff0d);color:var(--dsw-alias-label-primary,#e7e9ee);border-color:var(--dsw-alias-state-business-primary,#4f8cff)}
532
616
  .gjo-subagent-overlay{position:fixed;z-index:1000;inset:0;display:flex;justify-content:flex-end;background:#0007}.gjo-subagent-panel{box-sizing:border-box;width:min(440px,100vw);height:100%;overflow:auto;padding:20px;background:var(--dsw-alias-bg-layer-1,#1d2028);border-left:1px solid var(--dsw-alias-border-l2,#30343e);box-shadow:-8px 0 30px #0006;color:var(--dsw-alias-label-primary,#e7e9ee)}.gjo-subagent-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}.gjo-subagent-head h2{margin:3px 0 0;font-size:19px}.gjo-subagent-close{border:0;background:transparent;color:var(--dsw-alias-label-secondary,#aab1bd);font-size:24px;line-height:1;cursor:pointer}.gjo-subagent-intro{margin:12px 0 18px;color:var(--dsw-alias-label-secondary,#aab1bd);font-size:12px}.gjo-subagent-panel .gjo-error{margin:0 0 12px}.gjo-subagent-panel label{display:flex;flex-direction:column;gap:6px;margin:13px 0;color:var(--dsw-alias-label-secondary,#aab1bd);font-size:11px}.gjo-subagent-panel input,.gjo-subagent-panel select,.gjo-subagent-panel textarea{box-sizing:border-box;width:100%;border:1px solid var(--dsw-alias-border-l2,#30343e);border-radius:7px;background:var(--dsw-specific-input-major,#101217);color:inherit;padding:8px 9px;font:inherit;font-size:12px}.gjo-subagent-panel textarea{resize:vertical}.gjo-subagent-panel .gjo-note{margin-top:16px}.gjo-subagent-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:18px}
617
+ `
618
+
619
+ CSS += `
620
+ .gjo-decision-panel{margin:10px 22px 0;padding:12px;border:1px solid var(--dsw-alias-state-warn-primary,#c77b26);border-radius:8px;background:var(--dsw-alias-state-warn-tertiary,#c77b2614)}.gjo-decision-panel p{margin:5px 0 10px;color:var(--dsw-alias-label-secondary,#aab1bd);font-size:11px}.gjo-decision-panel label{display:flex;flex-direction:column;gap:5px;margin:9px 0;color:var(--dsw-alias-label-secondary,#aab1bd);font-size:11px}.gjo-decision-panel input,.gjo-decision-panel select{box-sizing:border-box;width:100%;border:1px solid var(--dsw-alias-border-l2,#30343e);border-radius:7px;background:var(--dsw-specific-input-major,#101217);color:inherit;padding:7px 9px;font:inherit;font-size:12px}.gjo-decision-panel button{margin-top:4px}.gjo-subagent-profile-summary,.gjo-subagent-result{display:flex;flex-direction:column;gap:4px;margin:10px 0;padding:9px;border:1px solid var(--dsw-alias-border-l2,#30343e);border-radius:7px;background:var(--dsw-alias-bg-layer-2,#ffffff08);font-size:11px}.gjo-subagent-profile-summary span,.gjo-subagent-result small{color:var(--dsw-alias-label-secondary,#aab1bd)}.gjo-subagent-result pre{max-height:260px;overflow:auto;margin:4px 0;white-space:pre-wrap;word-break:break-word;font:11px/1.5 ui-monospace,SFMono-Regular,Consolas,monospace}
533
621
  `
534
622
 
535
623
  function activateConversationView() {
@@ -550,7 +638,9 @@ window.__ModuleLoader__.load({
550
638
  function onDesktopMessage(event) {
551
639
  if (event.source !== window.parent) return
552
640
  var data = event.data
553
- if (data && data.source === 'dsh-desktop' && data.type === 'plugin-rpc' && data.pluginId === PLUGIN_ID && data.method === DESKTOP_RPC_METHOD) activateConversationView()
641
+ if (!data || data.source !== 'dsh-desktop' || data.type !== 'plugin-rpc' || data.pluginId !== PLUGIN_ID) return
642
+ if (data.method === DESKTOP_RPC_METHOD) activateConversationView()
643
+ if (data.method === DESKTOP_SUBAGENT_RPC_METHOD) openSubagentOverlay()
554
644
  }
555
645
  window.addEventListener('message', onDesktopMessage)
556
646
  return function () { window.removeEventListener('message', onDesktopMessage) }
package/lib/executors.js CHANGED
@@ -123,6 +123,25 @@ export function resolveCodexPermission(profile = {}, providerCapabilities = {})
123
123
  return { mode: requested, passThrough: true }
124
124
  }
125
125
 
126
+ function isNativeCodexProvider(providerName, provider, providerCapabilities) {
127
+ return /codex/i.test(String(providerName || '')) &&
128
+ provider?.inheritsParentContext === false &&
129
+ providerCapabilities.toolFilter !== true &&
130
+ providerCapabilities.depthLimit !== true &&
131
+ providerCapabilities.persona !== true &&
132
+ providerCapabilities.outputSchema !== true
133
+ }
134
+
135
+ function staticProviderValue(provider, descriptor, key) {
136
+ return String(
137
+ provider?.[key] ||
138
+ provider?.config?.[key] ||
139
+ descriptor?.[key] ||
140
+ descriptor?.config?.[key] ||
141
+ ''
142
+ ).trim()
143
+ }
144
+
126
145
  function contentText(content) {
127
146
  if (typeof content === 'string') return content.trim()
128
147
  if (content && typeof content === 'object' && !Array.isArray(content)) {
@@ -434,29 +453,63 @@ export class CodexExecutor {
434
453
  }
435
454
  const descriptor = this.capabilityCatalog.subagentProviders?.find((item) => item.name === providerName)
436
455
  const providerCapabilities = provider?.capabilities || descriptor?.capabilities || {}
437
- if (providerCapabilities.toolFilter !== true) throw new CapabilityMismatchError(`Codex provider ${providerName} 未明确支持安全 Tool allowlist。`)
438
- if (providerCapabilities.depthLimit !== true) throw new CapabilityMismatchError(`Codex provider ${providerName} 未明确支持 delegation depth 限制。`)
439
- if (profile.persona && providerCapabilities.persona !== true) throw new CapabilityMismatchError(`Codex provider ${providerName} 未明确支持 per-profile persona。`)
440
- if (profile.reasoningEffort && (!Array.isArray(providerCapabilities.reasoningEfforts) || !providerCapabilities.reasoningEfforts.includes(profile.reasoningEffort))) {
441
- throw new CapabilityMismatchError(`Codex provider ${providerName} 未明确支持 reasoningEffort=${profile.reasoningEffort}。`)
442
- }
443
- const permission = resolveCodexPermission(profile, providerCapabilities)
444
456
  const prompt = buildNodePrompt({ graph, node, profile, predecessorResults })
445
- const toolFilter = buildToolFilter(profile, this.capabilityCatalog.tools || [])
446
- const agentOptions = { model: profile.model, maxTokens: profile.maxOutputTokens }
447
- if (profile.reasoningEffort) agentOptions.reasoningEffort = profile.reasoningEffort
448
- const startRequest = {
449
- label: `graph-job:codex:${graph.graphId}:${node.id}:attempt-${attempt}`,
450
- prompt: [{ type: 'text', text: prompt }],
451
- parent: parentAgent,
452
- signal,
453
- maxDepth: graph.limits.maxDepth,
454
- persona: profile.persona || undefined,
455
- agentOptions
457
+ const nativeCodex = isNativeCodexProvider(providerName, provider, providerCapabilities)
458
+ let startRequest
459
+ if (nativeCodex) {
460
+ // The official subagent-codex bundle is intentionally one-shot and
461
+ // exposes NO_START_CAPABILITIES. It fixes model/permission on the
462
+ // provider instance and rejects shared agentOptions, tool filtering,
463
+ // persona, output schema and depth controls. Do not send generic DSH
464
+ // fields here: doing so makes the provider reject the whole run.
465
+ if (profile.capabilities?.tools?.length || profile.capabilities?.skills?.length) {
466
+ throw new CapabilityMismatchError(`Codex provider ${providerName} 不支持通过 Graph Profile 传入 Tools/Skills;请在 Codex 原生配置中管理它们。`)
467
+ }
468
+ if (profile.persona) throw new CapabilityMismatchError(`Codex provider ${providerName} 不支持 per-profile persona;请在 Codex 原生配置中管理身份。`)
469
+ if (profile.reasoningEffort && (!Array.isArray(providerCapabilities.reasoningEfforts) || !providerCapabilities.reasoningEfforts.includes(profile.reasoningEffort))) {
470
+ throw new CapabilityMismatchError(`Codex provider ${providerName} 未公布 reasoningEffort=${profile.reasoningEffort};当前包不支持按调用指定推理档次。`)
471
+ }
472
+ const staticModel = staticProviderValue(provider, descriptor, 'staticModel') || staticProviderValue(provider, descriptor, 'model')
473
+ if (profile.model && providerCapabilities.dynamicModel !== true && staticModel !== profile.model) {
474
+ throw new CapabilityMismatchError(`Codex provider ${providerName} 的 model 由 Profile Bundle 静态配置;当前运行不能按节点动态覆盖 ${profile.model}。`)
475
+ }
476
+ const staticPermission = staticProviderValue(provider, descriptor, 'staticPermissionMode') || staticProviderValue(provider, descriptor, 'permissionMode')
477
+ if (profile.permissionMode && profile.permissionMode !== 'default' && staticPermission && staticPermission !== profile.permissionMode) {
478
+ throw new CapabilityMismatchError(`Codex provider ${providerName} 的 permissionMode 固定为 ${staticPermission},不能在节点运行时改成 ${profile.permissionMode}。`)
479
+ }
480
+ if (profile.permissionMode && profile.permissionMode !== 'default' && !staticPermission) {
481
+ throw new CapabilityMismatchError(`Codex provider ${providerName} 未公布可动态设置 permissionMode;请在 Profile Bundle 配置后使用 default。`)
482
+ }
483
+ startRequest = {
484
+ label: `graph-job:codex:${graph.graphId}:${node.id}:attempt-${attempt}`,
485
+ prompt: [{ type: 'text', text: prompt }],
486
+ parent: parentAgent,
487
+ signal
488
+ }
489
+ } else {
490
+ if (providerCapabilities.toolFilter !== true) throw new CapabilityMismatchError(`Codex provider ${providerName} 未明确支持安全 Tool allowlist。`)
491
+ if (providerCapabilities.depthLimit !== true) throw new CapabilityMismatchError(`Codex provider ${providerName} 未明确支持 delegation depth 限制。`)
492
+ if (profile.persona && providerCapabilities.persona !== true) throw new CapabilityMismatchError(`Codex provider ${providerName} 未明确支持 per-profile persona。`)
493
+ if (profile.reasoningEffort && (!Array.isArray(providerCapabilities.reasoningEfforts) || !providerCapabilities.reasoningEfforts.includes(profile.reasoningEffort))) {
494
+ throw new CapabilityMismatchError(`Codex provider ${providerName} 未明确支持 reasoningEffort=${profile.reasoningEffort}。`)
495
+ }
496
+ const permission = resolveCodexPermission(profile, providerCapabilities)
497
+ const toolFilter = buildToolFilter(profile, this.capabilityCatalog.tools || [])
498
+ const agentOptions = { model: profile.model, maxTokens: profile.maxOutputTokens }
499
+ if (profile.reasoningEffort) agentOptions.reasoningEffort = profile.reasoningEffort
500
+ startRequest = {
501
+ label: `graph-job:codex:${graph.graphId}:${node.id}:attempt-${attempt}`,
502
+ prompt: [{ type: 'text', text: prompt }],
503
+ parent: parentAgent,
504
+ signal,
505
+ maxDepth: graph.limits.maxDepth,
506
+ persona: profile.persona || undefined,
507
+ agentOptions
508
+ }
509
+ if (permission.passThrough) startRequest.permissionMode = permission.mode
510
+ if (toolFilter) startRequest.toolFilter = toolFilter
511
+ if (providerCapabilities.outputSchema) startRequest.outputSchema = NODE_OUTPUT_SCHEMA
456
512
  }
457
- if (permission.passThrough) startRequest.permissionMode = permission.mode
458
- if (toolFilter) startRequest.toolFilter = toolFilter
459
- if (providerCapabilities.outputSchema) startRequest.outputSchema = NODE_OUTPUT_SCHEMA
460
513
  const run = await start.call(this.services.subagents, providerName, startRequest)
461
514
  const childSessionId = childSessionIdFromRun(run)
462
515
  try {
@@ -13,13 +13,13 @@ const AGENT_PROFILE_SCHEMA = {
13
13
  properties: {
14
14
  id: { type: 'string', minLength: 1, maxLength: 120 },
15
15
  name: { type: 'string', maxLength: 200 },
16
- executor: { enum: [...EXECUTORS] },
16
+ executor: { type: 'string', enum: [...EXECUTORS] },
17
17
  provider: { type: 'string', maxLength: 200 },
18
18
  subagentProvider: { type: 'string', maxLength: 200 },
19
19
  model: { type: 'string', maxLength: 300 },
20
20
  reasoningEffort: { type: 'string', maxLength: 40 },
21
21
  persona: { type: 'string', maxLength: 12000 },
22
- permissionMode: { enum: ['default', 'never', 'approve-for-me'] },
22
+ permissionMode: { type: 'string', enum: ['default', 'never', 'approve-for-me', 'dangerously-bypass-approvals-and-sandbox'] },
23
23
  maxOutputTokens: { type: 'integer', minimum: 256, maximum: 128000 },
24
24
  capabilities: {
25
25
  type: 'object',
@@ -37,7 +37,7 @@ const OUTPUT_CONTRACT_SCHEMA = {
37
37
  type: 'object',
38
38
  additionalProperties: false,
39
39
  properties: {
40
- fields: { type: 'array', items: { enum: ['text', 'artifactRefs'] }, maxItems: 2 },
40
+ fields: { type: 'array', items: { type: 'string', enum: ['text', 'artifactRefs'] }, maxItems: 2 },
41
41
  text: { type: 'boolean' },
42
42
  artifactRefs: { type: 'boolean' },
43
43
  requireText: { type: 'boolean' },
@@ -52,13 +52,13 @@ const NODE_SCHEMA = {
52
52
  required: ['id', 'kind', 'title'],
53
53
  properties: {
54
54
  id: { type: 'string', minLength: 1, maxLength: 120 },
55
- kind: { enum: [...NODE_KINDS] },
55
+ kind: { type: 'string', enum: [...NODE_KINDS] },
56
56
  title: { type: 'string', minLength: 1, maxLength: 300 },
57
57
  instruction: { type: 'string', maxLength: 20000 },
58
58
  agentProfileId: { type: 'string', maxLength: 120 },
59
- access: { enum: [...ACCESS_MODES] },
59
+ access: { type: 'string', enum: [...ACCESS_MODES] },
60
60
  outputContract: OUTPUT_CONTRACT_SCHEMA,
61
- failurePolicy: { enum: [...FAILURE_POLICIES] },
61
+ failurePolicy: { type: 'string', enum: [...FAILURE_POLICIES] },
62
62
  ui: {
63
63
  type: 'object',
64
64
  additionalProperties: false,
@@ -117,7 +117,7 @@ export const GRAPH_PATCH_SCHEMA = Object.freeze({
117
117
  type: 'object',
118
118
  required: ['op'],
119
119
  properties: {
120
- op: { enum: ['addNode', 'removeNode', 'updateNode', 'addEdge', 'removeEdge', 'updateLimits', 'setGoal'] },
120
+ op: { type: 'string', enum: ['addNode', 'removeNode', 'updateNode', 'addEdge', 'removeEdge', 'updateLimits', 'setGoal'] },
121
121
  id: { type: 'string' },
122
122
  nodeId: { type: 'string' },
123
123
  node: NODE_SCHEMA,
@@ -180,9 +180,14 @@ export function normalizeProfile(input = {}, options = {}) {
180
180
  provider = executor === 'codex' ? 'codex' : defaultProvider
181
181
  modelSource = 'default'
182
182
  }
183
- if (!model && options.resolveDefaults) {
183
+ if (!model && options.resolveDefaults && executor !== 'codex') {
184
184
  model = defaultModelName
185
185
  modelSource = 'default'
186
+ } else if (!model && options.resolveDefaults && executor === 'codex') {
187
+ // The official Codex provider may inherit its model from the static
188
+ // provider bundle/native Codex settings. Never inject the DSH chat model
189
+ // into a Codex one-shot request.
190
+ modelSource = 'provider-default'
186
191
  }
187
192
  const reasoningEffort = executor === 'codex' ? text(input.reasoningEffort) : ''
188
193
  return {
package/lib/index.js CHANGED
@@ -55,7 +55,16 @@ function diagnosticLog(logger, level, event, details = {}) {
55
55
  } catch { /* diagnostics must never break the user path */ }
56
56
  }
57
57
 
58
- function makePlannerFollowup(goal) {
58
+ function makePlannerFollowup(goal, decision = {}) {
59
+ const decisionText = decision.templateMode
60
+ ? [
61
+ '用户已经在 Graph Job 面板选择了手工图修订方式。',
62
+ `templateMode:${decision.templateMode}`,
63
+ decision.templateId ? `templateId:${decision.templateId}` : 'templateId:无(请创建新模板)',
64
+ `templateScope:${decision.templateScope || 'workspace'}`,
65
+ decision.templateName ? `templateName:${decision.templateName}` : ''
66
+ ].filter(Boolean).join('\n')
67
+ : ''
59
68
  return {
60
69
  id: `graphjob-command-${randomUUID()}`,
61
70
  role: 'user',
@@ -65,8 +74,9 @@ function makePlannerFollowup(goal) {
65
74
  '用户刚刚通过 /graphjob 提交了一个任务图目标。',
66
75
  '请只调用 graphjob_plan,为这个目标生成受限的 Graph Job 草案;不要运行 Graph,不要执行目标中的业务动作,也不要发送外部消息。',
67
76
  `用户目标:${goal}`,
77
+ decisionText,
68
78
  '生成预览后等待用户在 Graph Job 面板中确认。'
69
- ].join('\n\n')
79
+ ].filter(Boolean).join('\n\n')
70
80
  }],
71
81
  source: { kind: 'plugin', plugin: PLUGIN_ID, form: 'instructions' }
72
82
  }
@@ -108,8 +118,20 @@ function commandDefinition(orchestrator, logger) {
108
118
  }
109
119
  return { kind: 'success', text: 'Graph Job 已打开。请在任务图面板中补充节点,完成预览和确认后再运行。' }
110
120
  }
121
+ orchestrator.setPlannerRequest?.(sessionId, { goal: rawGoal, cwd: agent.session.header.cwd })
111
122
  try {
112
- await Promise.resolve(agent.followup(makePlannerFollowup(rawGoal)))
123
+ // Agent.followup is deliberately a synchronous queue operation. Awaiting
124
+ // a promise here would make the slash-command lifecycle depend on the
125
+ // whole planner/model turn and leaves the composer in "停止生成" state.
126
+ const queuedFollowup = agent.followup(makePlannerFollowup(rawGoal))
127
+ if (queuedFollowup && typeof queuedFollowup.catch === 'function') {
128
+ queuedFollowup.catch((error) => diagnosticLog(logger, 'error', 'command.planner_turn_failed', {
129
+ sessionId: sessionMarker(sessionId),
130
+ code: error?.code || 'UNKNOWN',
131
+ message: diagnosticMessage(error)
132
+ }))
133
+ }
134
+ orchestrator.setPlannerStatus?.(sessionId, { state: 'queued' })
113
135
  diagnosticLog(logger, 'info', 'command.planner_queued', { sessionId: sessionMarker(sessionId), goalLength: rawGoal.length })
114
136
  return { kind: 'success', text: 'Graph Job 已打开,正在生成草案;完成后请在任务图面板预览并确认。' }
115
137
  } catch (error) {
@@ -130,14 +152,16 @@ const planOutputSchema = {
130
152
  confirmationToken: { type: 'string' },
131
153
  summary: { type: 'object' },
132
154
  validation: { type: 'object' },
133
- templateDecision: {
134
- type: 'object',
135
- additionalProperties: false,
136
- required: ['mode'],
155
+ templateDecision: {
156
+ type: 'object',
157
+ additionalProperties: false,
158
+ required: [],
137
159
  properties: {
138
160
  mode: { type: 'string', enum: ['saveAs', 'overwrite'] },
139
161
  templateId: { type: 'string' },
140
- scope: { type: 'string', enum: ['global', 'workspace'] }
162
+ scope: { type: 'string', enum: ['global', 'workspace'] },
163
+ required: { type: 'boolean' },
164
+ message: { type: 'string' }
141
165
  }
142
166
  }
143
167
  }
@@ -165,7 +189,9 @@ function planTool(orchestrator, logger) {
165
189
  },
166
190
  output: {
167
191
  schema: planOutputSchema,
168
- render: (_args, value) => [{ type: 'text', text: `Graph Job 草案已生成,等待用户预览确认。\n${JSON.stringify(value.summary)}` }]
192
+ render: (_args, value) => [{ type: 'text', text: value?.templateDecision?.required
193
+ ? `Graph Job 草案等待用户选择模板保存方式:${value.templateDecision.message || 'saveAs 或 overwrite'}。`
194
+ : `Graph Job 草案已生成,等待用户预览确认。\n${JSON.stringify(value.summary)}` }]
169
195
  },
170
196
  presentCall(args) {
171
197
  return { card: 'generic', kind: 'graph-job-plan', title: '生成 Graph Job 任务图', rawInput: JSON.stringify({ goal: args?.goal || '' }) }
@@ -180,6 +206,13 @@ function planTool(orchestrator, logger) {
180
206
  if (!parentAgent || !header?.id) throw new Error(`${PLAN_TOOL_NAME} 只能从正在运行的 DSH 主对话调用。`)
181
207
  if (header.origin === 'subagent') throw new Error('Subagent 不能递归调用 Graph Job Planner。')
182
208
  const startedAt = Date.now()
209
+ orchestrator.setPlannerRequest?.(header.id, {
210
+ goal: args?.goal,
211
+ cwd: header.cwd,
212
+ draft: args?.draft,
213
+ patch: args?.patch,
214
+ baseGraph: args?.baseGraph
215
+ })
183
216
  orchestrator.setPlannerStatus?.(header.id, { state: 'running' })
184
217
  diagnosticLog(logger, 'info', 'planner.request', {
185
218
  sessionId: sessionMarker(header.id),
@@ -218,7 +251,18 @@ function planTool(orchestrator, logger) {
218
251
  message: diagnosticMessage(error),
219
252
  durationMs: Date.now() - startedAt
220
253
  })
221
- orchestrator.setPlannerStatus?.(header.id, { state: 'failed', error: diagnosticMessage(error) })
254
+ if (error?.code === 'TEMPLATE_DECISION_REQUIRED') {
255
+ const message = diagnosticMessage(error)
256
+ orchestrator.setPlannerStatus?.(header.id, { state: 'needs-decision', code: error.code, error: message })
257
+ return {
258
+ previewId: '',
259
+ confirmationRequired: false,
260
+ summary: { requiresTemplateDecision: true },
261
+ validation: { valid: false, errors: [{ code: error.code, message }], warnings: [] },
262
+ templateDecision: { required: true, message }
263
+ }
264
+ }
265
+ orchestrator.setPlannerStatus?.(header.id, { state: 'failed', code: error?.code || 'UNKNOWN', error: diagnosticMessage(error) })
222
266
  throw error
223
267
  }
224
268
  }
@@ -246,6 +290,147 @@ function graphJobToolFilter(toolNames) {
246
290
  return denied.length ? { deny: denied } : undefined
247
291
  }
248
292
 
293
+ function oneShotText(value) {
294
+ if (typeof value === 'string') return value.trim()
295
+ if (Array.isArray(value)) return value.map(oneShotText).filter(Boolean).join('\n\n').trim()
296
+ if (!value || typeof value !== 'object') return ''
297
+ if (typeof value.text === 'string') return value.text.trim()
298
+ return oneShotText(value.content || value.output || value.message)
299
+ }
300
+
301
+ function oneShotDiagnostic(result) {
302
+ const value = result?.diagnostic || result?.error?.message || result?.error?.code || result?.code || ''
303
+ if (!value) return ''
304
+ if (typeof value === 'string') return diagnosticMessage({ message: value })
305
+ try { return diagnosticMessage({ message: JSON.stringify(value) }) } catch { return diagnosticMessage(value) }
306
+ }
307
+
308
+ function oneShotResult(result) {
309
+ if (!result || typeof result !== 'object') throw codedError('Codex Subagent 没有返回结果 payload。', 'MISSING_SUBAGENT_PAYLOAD')
310
+ const stopReason = String(result.stopReason || 'completed').trim()
311
+ const diagnostic = oneShotDiagnostic(result)
312
+ if (result.isError === true || stopReason !== 'completed') {
313
+ const lower = `${stopReason} ${diagnostic}`.toLowerCase()
314
+ if (/abort|cancel/.test(lower)) throw codedError('Codex Subagent 已取消。', 'ABORTED')
315
+ if (/auth|unauthori[sz]|credential|login|token/.test(lower)) throw codedError(diagnostic || 'Codex Subagent 需要认证。', 'AUTH_REQUIRED')
316
+ if (/rate.?limit|too many requests|\b429\b/.test(lower)) throw codedError(diagnostic || 'Codex Subagent 请求受到限流。', 'RATE_LIMIT')
317
+ if (/timeout|timed.?out|network|transport|connection|econn/.test(lower)) throw codedError(diagnostic || 'Codex Subagent transport 失败。', 'TRANSPORT_ERROR')
318
+ throw codedError(diagnostic || `Codex Subagent 以 ${stopReason || 'error'} 结束。`, 'SUBAGENT_ERROR')
319
+ }
320
+ return {
321
+ stopReason,
322
+ text: oneShotText(result.output || result.text || result.content || result.structured?.text).slice(0, 40000),
323
+ diagnostic: diagnostic.slice(0, 320)
324
+ }
325
+ }
326
+
327
+ function providerStaticValue(provider, descriptor, key) {
328
+ return String(
329
+ provider?.[key] || provider?.config?.[key] ||
330
+ descriptor?.[key] || descriptor?.config?.[key] || ''
331
+ ).trim()
332
+ }
333
+
334
+ function providerModels(provider, descriptor) {
335
+ return [...new Set([
336
+ ...(Array.isArray(provider?.models) ? provider.models : []),
337
+ ...(Array.isArray(descriptor?.models) ? descriptor.models : []),
338
+ providerStaticValue(provider, descriptor, 'staticModel'),
339
+ providerStaticValue(provider, descriptor, 'model')
340
+ ].map((item) => typeof item === 'string' ? item.trim() : String(item?.id || item?.name || '').trim()).filter(Boolean))]
341
+ }
342
+
343
+ async function createCodexSubagent({ services, parentAgent, profile, providerName, provider, catalog, label, prompt, sessionId, logger }) {
344
+ if (!services.subagents || typeof services.subagents.start !== 'function') {
345
+ throw codedError('当前 runtime 未提供 Codex one-shot Subagent 能力。', 'CODEX_PROVIDER_UNAVAILABLE')
346
+ }
347
+ if (!provider) throw codedError(`Codex provider 不可用:${providerName}`, 'NO_PROVIDER')
348
+
349
+ const descriptor = catalog.subagentProviders?.find((item) => item.name === providerName) || {}
350
+ const capabilities = { ...(descriptor.capabilities || {}), ...(provider.capabilities || {}) }
351
+ const staticModel = providerStaticValue(provider, descriptor, 'staticModel') || providerStaticValue(provider, descriptor, 'model')
352
+ const models = providerModels(provider, descriptor)
353
+ const requestedModel = String(profile.model || '').trim()
354
+ const dynamicModel = capabilities.dynamicModel === true || capabilities.modelOverride === true
355
+ if (requestedModel && !dynamicModel && (!models.length || !models.includes(requestedModel))) {
356
+ throw codedError(`Codex provider ${providerName} 的 model 由 Profile Bundle/原生配置静态决定,不能动态指定 ${requestedModel}。请把 Profile 的 Model 留空,或选择与 Bundle 相同的 model。`, 'UNSUPPORTED_CAPABILITY')
357
+ }
358
+ if (requestedModel && staticModel && requestedModel !== staticModel && !dynamicModel) {
359
+ throw codedError(`Codex provider ${providerName} 的 model 固定为 ${staticModel},不能改成 ${requestedModel}。`, 'UNSUPPORTED_CAPABILITY')
360
+ }
361
+
362
+ const requestedReasoning = String(profile.reasoningEffort || '').trim()
363
+ const reasoningEfforts = Array.isArray(capabilities.reasoningEfforts) ? capabilities.reasoningEfforts : []
364
+ if (requestedReasoning && (!reasoningEfforts.includes(requestedReasoning) || capabilities.dynamicReasoning !== true && capabilities.reasoningEffortOverride !== true)) {
365
+ throw codedError(`Codex provider ${providerName} 未公布可按调用指定的 reasoningEffort=${requestedReasoning};当前官方包只支持静态配置。`, 'UNSUPPORTED_CAPABILITY')
366
+ }
367
+ if (profile.capabilities?.tools?.length || profile.capabilities?.skills?.length) {
368
+ throw codedError(`Codex provider ${providerName} 不支持通过 Profile 传入 Tools/Skills;请在 Codex 原生配置中管理它们。`, 'UNSUPPORTED_CAPABILITY')
369
+ }
370
+ if (profile.persona) throw codedError(`Codex provider ${providerName} 不支持 per-profile persona;请在 Codex 原生配置中管理身份。`, 'UNSUPPORTED_CAPABILITY')
371
+
372
+ const requestedPermission = String(profile.permissionMode || 'default').trim()
373
+ const staticPermission = providerStaticValue(provider, descriptor, 'staticPermissionMode') || providerStaticValue(provider, descriptor, 'permissionMode')
374
+ const permissionModes = Array.isArray(capabilities.permissionModes) ? capabilities.permissionModes : []
375
+ if (requestedPermission !== 'default' && staticPermission && staticPermission !== requestedPermission) {
376
+ throw codedError(`Codex provider ${providerName} 的 permissionMode 固定为 ${staticPermission},不能改成 ${requestedPermission}。`, 'UNSUPPORTED_CAPABILITY')
377
+ }
378
+ if (requestedPermission !== 'default' && !staticPermission && !(capabilities.permissionMode === true || permissionModes.includes(requestedPermission))) {
379
+ throw codedError(`Codex provider ${providerName} 未公布可动态设置 permissionMode;请在 Profile Bundle 配置后使用 default。`, 'UNSUPPORTED_CAPABILITY')
380
+ }
381
+
382
+ const request = {
383
+ label,
384
+ prompt: [{ type: 'text', text: prompt }],
385
+ parent: parentAgent,
386
+ signal: new AbortController().signal
387
+ }
388
+ diagnosticLog(logger, 'info', 'subagent.codex_one_shot_requested', {
389
+ parentSessionId: sessionMarker(sessionId),
390
+ labelLength: label.length,
391
+ promptLength: prompt.length,
392
+ profileId: profile.id,
393
+ provider: providerName,
394
+ model: requestedModel ? '[profile]' : '[provider-default]',
395
+ reasoningEffort: requestedReasoning ? '[profile]' : '[provider-default]'
396
+ })
397
+ let run
398
+ try {
399
+ run = await services.subagents.start.call(services.subagents, providerName, request)
400
+ if (!run || run.result === undefined) throw codedError('Codex provider 返回了缺少 result 的 payload。', 'MISSING_SUBAGENT_PAYLOAD')
401
+ const result = oneShotResult(await run.result)
402
+ diagnosticLog(logger, 'info', 'subagent.codex_one_shot_completed', {
403
+ parentSessionId: sessionMarker(sessionId),
404
+ profileId: profile.id,
405
+ provider: providerName,
406
+ stopReason: result.stopReason,
407
+ outputLength: result.text.length
408
+ })
409
+ return {
410
+ mode: 'one-shot',
411
+ runId: String(run.id || '').trim(),
412
+ label,
413
+ parentSessionId: sessionId,
414
+ profileId: profile.id,
415
+ provider: providerName,
416
+ result
417
+ }
418
+ } catch (error) {
419
+ diagnosticLog(logger, 'error', 'subagent.codex_one_shot_failed', {
420
+ parentSessionId: sessionMarker(sessionId),
421
+ profileId: profile.id,
422
+ provider: providerName,
423
+ code: error?.code || 'UNKNOWN',
424
+ message: diagnosticMessage(error)
425
+ })
426
+ throw error
427
+ } finally {
428
+ try { await run?.dispose?.() } catch (error) {
429
+ diagnosticLog(logger, 'warn', 'subagent.codex_one_shot_dispose_failed', { provider: providerName, message: diagnosticMessage(error) })
430
+ }
431
+ }
432
+ }
433
+
249
434
  export async function createSubagent(orchestrator, services, input = {}, logger) {
250
435
  const sessionId = String(input.sessionId || '').trim()
251
436
  if (!sessionId) throw codedError('创建 Subagent 需要当前会话。', 'MISSING_SESSION')
@@ -264,6 +449,9 @@ export async function createSubagent(orchestrator, services, input = {}, logger)
264
449
  const catalog = await orchestrator.capabilities({ cwd })
265
450
  const providerName = String(profile.subagentProvider || (profile.executor === 'codex' ? profile.provider : catalog.executors?.dsh?.provider) || 'spawn').trim()
266
451
  const provider = services.subagents?.getProvider?.(providerName)
452
+ if (profile.executor === 'codex') {
453
+ return createCodexSubagent({ services, parentAgent, profile, providerName, provider, catalog, label, prompt, sessionId, logger })
454
+ }
267
455
  if (!services.subagents || typeof services.subagents.startContinuable !== 'function') {
268
456
  throw codedError('当前 DSH Runtime 未提供 continuable Subagent 能力。', 'CONTINUATION_UNAVAILABLE')
269
457
  }
@@ -431,6 +619,66 @@ async function handleGraphRoutes(req, res, orchestrator) {
431
619
  return jsonResponse(res, 404, { ok: false, error: 'Graph action 不存在。' })
432
620
  }
433
621
 
622
+ async function handlePlannerDecisionRoute(req, res, orchestrator, services, logger) {
623
+ if (methodOf(req) !== 'POST') return jsonResponse(res, 405, { ok: false, error: '仅支持 POST。' })
624
+ const body = await readJson(req)
625
+ const sessionId = sessionIdFrom(req, body)
626
+ if (!sessionId) return jsonResponse(res, 400, { ok: false, error: '缺少 sessionId。' })
627
+ const mode = String(body.mode || '').trim()
628
+ if (!['saveAs', 'overwrite'].includes(mode)) throw codedError('必须选择 saveAs 或 overwrite。', 'TEMPLATE_DECISION_REQUIRED')
629
+ const templateId = String(body.templateId || '').trim()
630
+ if (mode === 'overwrite' && !templateId) throw codedError('覆盖模板必须选择 templateId。', 'TEMPLATE_DECISION_REQUIRED')
631
+ const parentAgent = services.agents?.get?.(sessionId)
632
+ if (!parentAgent?.session?.header?.id) throw codedError('当前会话没有可用的主 Agent;请先让主对话完成启动。', 'PARENT_AGENT_NOT_LIVE')
633
+ const pending = orchestrator.plannerRequestForSession?.(sessionId)
634
+ if (!pending?.goal) throw codedError('当前会话没有等待模板选择的 Planner 草案,请重新发送 /graphjob。', 'PLANNER_REQUEST_NOT_FOUND')
635
+ const decision = {
636
+ templateMode: mode,
637
+ templateId,
638
+ templateScope: String(body.templateScope || 'workspace').trim() || 'workspace',
639
+ templateName: String(body.templateName || '').trim().slice(0, 200)
640
+ }
641
+ diagnosticLog(logger, 'info', 'planner.decision', {
642
+ sessionId: sessionMarker(sessionId),
643
+ mode,
644
+ hasTemplateId: Boolean(templateId)
645
+ })
646
+
647
+ const plannerInput = {
648
+ sessionId,
649
+ cwd: parentAgent.session.header.cwd || pending.cwd,
650
+ goal: pending.goal,
651
+ templateMode: mode,
652
+ ...(templateId ? { templateId } : {}),
653
+ ...(decision.templateScope ? { templateScope: decision.templateScope } : {}),
654
+ ...(decision.templateName ? { templateName: decision.templateName } : {}),
655
+ ...(pending.draft ? { draft: pending.draft } : {}),
656
+ ...(pending.patch ? { patch: pending.patch } : {}),
657
+ ...(pending.baseGraph ? { baseGraph: pending.baseGraph } : {})
658
+ }
659
+ if (pending.draft || pending.patch) {
660
+ orchestrator.setPlannerStatus?.(sessionId, { state: 'running' })
661
+ try {
662
+ const preview = await orchestrator.previewPlanner(plannerInput)
663
+ orchestrator.setPlannerStatus?.(sessionId, { state: 'ready', previewId: preview.previewId })
664
+ return jsonResponse(res, 200, { ok: true, state: 'ready', preview, templatePreview: preview })
665
+ } catch (error) {
666
+ orchestrator.setPlannerStatus?.(sessionId, { state: 'failed', code: error?.code || 'UNKNOWN', error: diagnosticMessage(error) })
667
+ throw error
668
+ }
669
+ }
670
+
671
+ orchestrator.setPlannerStatus?.(sessionId, { state: 'queued' })
672
+ orchestrator.setPlannerRequest?.(sessionId, { goal: pending.goal, cwd: plannerInput.cwd })
673
+ try {
674
+ parentAgent.followup(makePlannerFollowup(pending.goal, decision))
675
+ } catch (error) {
676
+ orchestrator.setPlannerStatus?.(sessionId, { state: 'failed', code: error?.code || 'UNKNOWN', error: diagnosticMessage(error) })
677
+ throw error
678
+ }
679
+ return jsonResponse(res, 202, { ok: true, state: 'queued' })
680
+ }
681
+
434
682
  async function handleSubagentRoutes(req, res, orchestrator, services, logger) {
435
683
  const segments = pathSegments(req, `${BASE_PATH}/subagents`)
436
684
  if (!segments) return false
@@ -575,7 +823,19 @@ export function createHost(options = {}) {
575
823
  const result = await handleSubagentRoutes(req, res, orchestrator, services, logger)
576
824
  if (result === false) return jsonResponse(res, 404, { ok: false, error: 'Not found' })
577
825
  } catch (error) {
578
- const status = ['PARENT_AGENT_NOT_LIVE', 'CONTINUATION_UNAVAILABLE', 'UNSUPPORTED_CAPABILITY', 'NO_PROVIDER'].includes(error?.code) ? 409 : 400
826
+ const status = ['PARENT_AGENT_NOT_LIVE', 'CONTINUATION_UNAVAILABLE', 'CODEX_PROVIDER_UNAVAILABLE', 'UNSUPPORTED_CAPABILITY', 'NO_PROVIDER'].includes(error?.code) ? 409 : 400
827
+ return writeError(res, status, error)
828
+ }
829
+ }
830
+ })
831
+ registerEffect(ctx, services.webServer, {
832
+ kind: 'exact',
833
+ path: `${BASE_PATH}/planner/decision`,
834
+ handler: async (req, res) => {
835
+ try {
836
+ return await handlePlannerDecisionRoute(req, res, orchestrator, services, logger)
837
+ } catch (error) {
838
+ const status = ['PARENT_AGENT_NOT_LIVE', 'PLANNER_REQUEST_NOT_FOUND'].includes(error?.code) ? 409 : error?.code === 'TEMPLATE_DECISION_REQUIRED' ? 412 : 400
579
839
  return writeError(res, status, error)
580
840
  }
581
841
  }
@@ -55,6 +55,17 @@ function templateVisible(template, workspaceId) {
55
55
  return Boolean(workspaceId) && normalizeWorkspaceId(template?.workspaceId) === normalizeWorkspaceId(workspaceId)
56
56
  }
57
57
 
58
+ function isUntouchedManualPlaceholder(graph) {
59
+ if (!graph || graph.source !== 'manual' || graph.manualLock !== true) return false
60
+ if ((graph.nodes || []).length || (graph.edges || []).length) return false
61
+ const goal = String(graph.goal || '').trim()
62
+ return !goal || [
63
+ '请填写任务图目标。',
64
+ '请在任务图编辑器中补充目标。',
65
+ 'Graph Job 已打开。请在任务图面板中补充节点,完成预览和确认后再运行。'
66
+ ].includes(goal)
67
+ }
68
+
58
69
  function publicProfile(profile) {
59
70
  const value = clone(profile)
60
71
  if (!value) return value
@@ -77,6 +88,7 @@ export class GraphJobOrchestrator {
77
88
  this.bindings = {}
78
89
  this.previews = new Map()
79
90
  this.plannerStatuses = new Map()
91
+ this.plannerRequests = new Map()
80
92
  this.templatePreviews = new Map()
81
93
  this.schedulers = new Map()
82
94
  this.startLocks = new Map()
@@ -340,20 +352,20 @@ export class GraphJobOrchestrator {
340
352
  this.initialize()
341
353
  const sessionId = String(input.sessionId || '').trim()
342
354
  const binding = this.getBinding(sessionId)
343
- const manualTemplateEdit = binding?.manualLock === true
355
+ const activeGraph = this.graphForSession(sessionId)
356
+ const manualTemplateEdit = binding?.manualLock === true && !isUntouchedManualPlaceholder(activeGraph)
344
357
  if (manualTemplateEdit && !['saveAs', 'overwrite'].includes(input.templateMode)) {
345
358
  throw errorWithCode('当前会话的 Graph 已被手工编辑锁定;模型修订只能作为候选,并必须选择 saveAs 或 overwrite 模板。', 'TEMPLATE_DECISION_REQUIRED')
346
359
  }
347
360
  const cwd = String(input.cwd || '').trim() || process.cwd()
348
361
  const catalog = await this.capabilities({ cwd })
349
- const activeGraph = this.graphForSession(sessionId)
350
362
  if (input.patch && input.baseGraph && activeGraph && (
351
363
  String(input.baseGraph.graphId || '') !== String(activeGraph.graphId || '') ||
352
364
  Number(input.baseGraph.revision || 0) !== Number(activeGraph.revision || 0)
353
365
  )) {
354
366
  throw errorWithCode('Planner patch 的 baseGraph 不是当前会话活动 revision。', 'GRAPH_REVISION_CONFLICT')
355
367
  }
356
- const baseGraph = input.patch ? activeGraph : (input.baseGraph || null)
368
+ const baseGraph = input.patch ? activeGraph : (input.baseGraph || (isUntouchedManualPlaceholder(activeGraph) ? activeGraph : null))
357
369
  let draft
358
370
  if (input.patch) {
359
371
  if (!baseGraph) throw errorWithCode('Planner patch 找不到当前 Graph。', 'GRAPH_NOT_FOUND')
@@ -474,6 +486,7 @@ export class GraphJobOrchestrator {
474
486
  state: String(status.state || 'idle'),
475
487
  updatedAt: nowValue(this.now),
476
488
  ...(status.previewId ? { previewId: String(status.previewId) } : {}),
489
+ ...(status.code ? { code: String(status.code).slice(0, 120) } : {}),
477
490
  ...(status.error ? { error: String(status.error).slice(0, 320) } : {})
478
491
  }
479
492
  this.plannerStatuses.set(id, value)
@@ -485,6 +498,25 @@ export class GraphJobOrchestrator {
485
498
  return id ? clone(this.plannerStatuses.get(id) || null) : null
486
499
  }
487
500
 
501
+ setPlannerRequest(sessionId, request = {}) {
502
+ const id = String(sessionId || '').trim()
503
+ if (!id) return null
504
+ const value = {
505
+ goal: String(request.goal || '').trim().slice(0, 20000),
506
+ cwd: String(request.cwd || '').trim()
507
+ }
508
+ if (request.draft && typeof request.draft === 'object') value.draft = clone(request.draft)
509
+ if (request.patch && typeof request.patch === 'object') value.patch = clone(request.patch)
510
+ if (request.baseGraph && typeof request.baseGraph === 'object') value.baseGraph = clone(request.baseGraph)
511
+ this.plannerRequests.set(id, value)
512
+ return clone(value)
513
+ }
514
+
515
+ plannerRequestForSession(sessionId) {
516
+ const id = String(sessionId || '').trim()
517
+ return id ? clone(this.plannerRequests.get(id) || null) : null
518
+ }
519
+
488
520
  latestPreviewForSession(sessionId) {
489
521
  this.initialize()
490
522
  const id = String(sessionId || '').trim()
@@ -497,6 +529,18 @@ export class GraphJobOrchestrator {
497
529
  return latest ? this.publicPreview(latest) : null
498
530
  }
499
531
 
532
+ latestTemplatePreviewForSession(sessionId) {
533
+ this.initialize()
534
+ const id = String(sessionId || '').trim()
535
+ if (!id) return null
536
+ let latest = null
537
+ for (const record of this.templatePreviews.values()) {
538
+ if (record.sessionId !== id || record.expiresAt < nowValue(this.now)) continue
539
+ latest = record
540
+ }
541
+ return latest ? this.getTemplatePreview(latest.previewId) : null
542
+ }
543
+
500
544
  saveManualGraph(input = {}) {
501
545
  this.initialize()
502
546
  const graphInput = input.graph || input
@@ -544,7 +588,14 @@ export class GraphJobOrchestrator {
544
588
  this.initialize()
545
589
  const binding = this.getBinding(sessionId)
546
590
  const graph = binding?.activeGraphId ? this.getGraph(binding.activeGraphId, binding.activeRevision) : null
547
- return { binding, graph, preview: this.latestPreviewForSession(sessionId), planner: this.plannerStatusForSession(sessionId), graphs: [...this.graphs.values()].map(clone).filter(Boolean) }
591
+ return {
592
+ binding,
593
+ graph,
594
+ preview: this.latestPreviewForSession(sessionId),
595
+ templatePreview: this.latestTemplatePreviewForSession(sessionId),
596
+ planner: this.plannerStatusForSession(sessionId),
597
+ graphs: [...this.graphs.values()].map(clone).filter(Boolean)
598
+ }
548
599
  }
549
600
 
550
601
  listTemplates(options = {}) {
@@ -847,6 +898,7 @@ export class GraphJobOrchestrator {
847
898
  this.listeners.clear()
848
899
  this.previews.clear()
849
900
  this.plannerStatuses.clear()
901
+ this.plannerRequests.clear()
850
902
  this.templatePreviews.clear()
851
903
  this.startLocks.clear()
852
904
  }
package/lib/validator.js CHANGED
@@ -51,9 +51,11 @@ function reasoningEfforts(capabilities) {
51
51
  function availableModels(capabilities, profile) {
52
52
  if (!capabilities) return null
53
53
  const providerModels = capabilities.subagentProviders?.find?.((item) => item?.name === profile.provider)?.models
54
- const values = profile.executor === 'codex' && Array.isArray(providerModels) && providerModels.length
55
- ? providerModels.map((model) => ({ provider: profile.provider, model: String(model) }))
56
- : Array.isArray(capabilities.models) ? capabilities.models : []
54
+ if (profile.executor === 'codex') {
55
+ if (!Array.isArray(providerModels) || !providerModels.length) return null
56
+ return new Set(providerModels.map((model) => `${profile.provider}\u0000${String(model)}`))
57
+ }
58
+ const values = Array.isArray(capabilities.models) ? capabilities.models : []
57
59
  if (!values.length) return null
58
60
  return new Set(values.map((item) => {
59
61
  if (typeof item === 'string') return `${profile.provider}\u0000${item}`
@@ -95,7 +97,7 @@ export function validateGraph(input, options = {}) {
95
97
  profileMap.set(profile.id, profile)
96
98
  if (!EXECUTORS.has(profile.executor)) push(errors, `agentProfiles[${index}].executor`, `不支持的 executor:${profile.executor}。`, 'EXECUTOR')
97
99
  if (!profile.provider) push(errors, `agentProfiles[${index}].provider`, 'provider 必须在 revision 创建时解析完成。', 'MODEL_UNRESOLVED')
98
- if (!profile.model) push(errors, `agentProfiles[${index}].model`, 'model 必须在 revision 创建时解析完成。', 'MODEL_UNRESOLVED')
100
+ if (!profile.model && profile.executor !== 'codex') push(errors, `agentProfiles[${index}].model`, 'model 必须在 revision 创建时解析完成。', 'MODEL_UNRESOLVED')
99
101
  if (options.enforceExecutorAvailability && capabilities?.executors?.[profile.executor]) {
100
102
  const executor = capabilities.executors[profile.executor]
101
103
  if (executor.available === false) push(errors, `agentProfiles[${index}].executor`, `${profile.executor} executor 当前不可用。`, 'CAPABILITY_MISMATCH')
@@ -107,7 +109,7 @@ export function validateGraph(input, options = {}) {
107
109
  if (modelSet && !modelSet.has(`${profile.provider}\u0000${profile.model}`)) {
108
110
  push(errors, `agentProfiles[${index}].model`, `当前 capability 不提供 ${profile.provider}/${profile.model}。`, 'CAPABILITY_MISMATCH')
109
111
  }
110
- if (!['default', 'never', 'approve-for-me'].includes(profile.permissionMode)) {
112
+ if (!['default', 'never', 'approve-for-me', 'dangerously-bypass-approvals-and-sandbox'].includes(profile.permissionMode)) {
111
113
  push(errors, `agentProfiles[${index}].permissionMode`, `不支持的 permissionMode:${profile.permissionMode}。`, 'PERMISSION_MODE')
112
114
  }
113
115
  for (const tool of profile.capabilities.tools) {
@@ -126,7 +128,7 @@ export function validateGraph(input, options = {}) {
126
128
  }
127
129
  if (profile.executor === 'codex' && profile.reasoningEffort) {
128
130
  const effortSet = reasoningEfforts(capabilities)
129
- if (effortSet && !effortSet.has(profile.reasoningEffort)) {
131
+ if (capabilities && (!effortSet || !effortSet.has(profile.reasoningEffort))) {
130
132
  push(errors, `agentProfiles[${index}].reasoningEffort`, `当前 Codex capability 不支持 reasoningEffort=${profile.reasoningEffort}。`, 'CAPABILITY_MISMATCH')
131
133
  }
132
134
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@p-dsh-market/graph-job-orchestrator",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "DSH Graph Job 多 Subagent 任务图编排器",
5
5
  "keywords": [
6
6
  "dsh",
@@ -72,10 +72,20 @@
72
72
  "type": "pluginRpc",
73
73
  "method": "graphJobOrchestrator.open"
74
74
  }
75
+ },
76
+ {
77
+ "id": "open-graph-job-subagent",
78
+ "slot": "desktop.titlebar.workspaceActions",
79
+ "label": "新建 Subagent",
80
+ "order": 145,
81
+ "when": ["dshRunning", "pluginActive", "restartNotRequired"],
82
+ "action": {
83
+ "type": "pluginRpc",
84
+ "method": "graphJobOrchestrator.openSubagent"
85
+ }
75
86
  }
76
87
  ]
77
88
  }
78
89
  }
79
90
  }
80
91
  }
81
-