@twinsearth/dsh-master 3.3.3 → 3.3.5

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.
Files changed (57) hide show
  1. package/README.md +17 -0
  2. package/client.js +126 -1
  3. package/cordis.patch.yml +51 -0
  4. package/dist-host/agents/pjm-agent.js +62 -0
  5. package/dist-host/agents/pm-agent.js +57 -0
  6. package/dist-host/agents/prompts.js +71 -0
  7. package/dist-host/agents/toolkit.js +64 -0
  8. package/dist-host/channel/abort-registry.js +93 -0
  9. package/dist-host/channel/protocol.js +114 -0
  10. package/dist-host/channel/server.js +349 -0
  11. package/dist-host/channel/stream-state.js +343 -0
  12. package/dist-host/channel/ws.js +199 -0
  13. package/dist-host/dual/documents.js +143 -0
  14. package/dist-host/dual/orchestrator.js +214 -0
  15. package/dist-host/dual/schema.js +367 -0
  16. package/dist-host/engine/atomic-switch.js +159 -0
  17. package/dist-host/engine/health-probe.js +86 -0
  18. package/dist-host/engine/index.js +18 -0
  19. package/dist-host/engine/llm-stream.js +215 -0
  20. package/dist-host/engine/plugin-updater.js +392 -0
  21. package/dist-host/engine/safe-update.js +150 -0
  22. package/dist-host/index.js +154 -3
  23. package/dist-host/kernel.js +27 -1
  24. package/dist-host/plugins/cost-optimizer.js +3 -3
  25. package/dist-host/plugins/dual-manager.js +374 -77
  26. package/docs/AGENTS-PM-PJM.md +207 -0
  27. package/docs/ARCHITECTURE.md +2 -0
  28. package/docs/CHANGELOG.md +74 -0
  29. package/docs/CHANNEL-WEBSOCKET.md +169 -0
  30. package/docs/DESIGN-command-center.md +147 -0
  31. package/docs/ENGINE-PRIMITIVES.md +215 -0
  32. package/docs/GAP-ANALYSIS-fang-session.md +75 -0
  33. package/docs/REVIEW-PM-PJM-3.3.4.md +179 -0
  34. package/docs/examples/command-center/README.md +167 -0
  35. package/package.json +5 -1
  36. package/src/agents/pjm-agent.ts +62 -0
  37. package/src/agents/pm-agent.ts +58 -0
  38. package/src/agents/prompts.ts +74 -0
  39. package/src/agents/toolkit.ts +64 -0
  40. package/src/channel/abort-registry.ts +85 -0
  41. package/src/channel/protocol.ts +110 -0
  42. package/src/channel/server.ts +289 -0
  43. package/src/channel/stream-state.ts +349 -0
  44. package/src/channel/ws.ts +182 -0
  45. package/src/dual/documents.ts +142 -0
  46. package/src/dual/orchestrator.ts +223 -0
  47. package/src/dual/schema.ts +355 -0
  48. package/src/engine/atomic-switch.ts +146 -0
  49. package/src/engine/health-probe.ts +84 -0
  50. package/src/engine/index.ts +20 -0
  51. package/src/engine/llm-stream.ts +189 -0
  52. package/src/engine/plugin-updater.ts +359 -0
  53. package/src/engine/safe-update.ts +148 -0
  54. package/src/index.ts +143 -4
  55. package/src/kernel.ts +28 -1
  56. package/src/plugins/cost-optimizer.ts +3 -3
  57. package/src/plugins/dual-manager.ts +337 -76
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twinsearth/dsh-master",
3
- "version": "3.3.3",
3
+ "version": "3.3.5",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -37,6 +37,10 @@
37
37
  "./contract": "./dist-host/contract/index.js",
38
38
  "./cost-optimizer": "./dist-host/plugins/cost-optimizer.js",
39
39
  "./dual-manager": "./dist-host/plugins/dual-manager.js",
40
+ "./pm-agent": "./dist-host/agents/pm-agent.js",
41
+ "./pjm-agent": "./dist-host/agents/pjm-agent.js",
42
+ "./engine": "./dist-host/engine/index.js",
43
+ "./channel/stream-state": "./dist-host/channel/stream-state.js",
40
44
  "./client": {
41
45
  "browser": "./client.js",
42
46
  "default": "./client.js"
@@ -0,0 +1,62 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2026 dsh-Master contributors — see LICENSE-CORE.
3
+ // @ts-nocheck
4
+
5
+ import { PJM_SYSTEM_PROMPT } from './prompts.ts'
6
+ import { dualService, dualUnavailable, id, installAgent, jsonTool, objectSchema, objects, string, strings } from './toolkit.ts'
7
+
8
+ export const name = 'dsh-master-pjm-agent'
9
+ export const inject = ['systemPrompt', 'tools']
10
+
11
+ /** PJM preset:项目计划、节点、隔离派发、结案;不替 PM 决定需求。 */
12
+ export function apply(ctx) {
13
+ const dual = () => dualService(ctx)
14
+ const tools = [
15
+ jsonTool('pjm_create_plan', '把已确认 PRD 拆成 WBS 与七类项目计划;给 handoffId 时同时完成接单。', objectSchema({
16
+ prdId: id('PRD id'), handoffId: id('可选:PM 交接包 id'), title: string('项目计划标题'), how: string('实施路径'), where: string('执行场景'),
17
+ projectReview: strings('项目会审结论'), requirements: strings('项目需求与内容'),
18
+ steps: objects('WBS:id/title/prompt/owner/where/estimateMinutes/dependsOn/acceptance/kind'),
19
+ schedule: { type: 'object', description: 'startAt 与 milestones[](title/owner/estimateMinutes/dependsOn)', additionalProperties: true },
20
+ team: objects('人员:role/owner/responsibility'),
21
+ quality: { type: 'object', description: 'tests[] / deployment[] / acceptance[]', additionalProperties: true },
22
+ }, ['prdId', 'steps']), async (args) => dual() ? dual().pjm.plan(args.prdId, args) : dualUnavailable()),
23
+ jsonTool('pjm_take_handoff', '接单:只有 PM 标记 ready 的交接包可以接走;needs-clarification 必须退回 PM。', objectSchema({
24
+ handoffId: id('交接包 id'), planId: id('可选:关联计划 id'),
25
+ }, ['handoffId']), async (args) => dual() ? dual().pjm.takeHandoff(args.handoffId, args) : dualUnavailable()),
26
+ jsonTool('pjm_report', '节点自反馈:执行中随时汇报进展/阻塞,blocked 必须写原因。', objectSchema({
27
+ planId: id('计划 id'), summary: string('节点摘要'),
28
+ status: { type: 'string', enum: ['progress', 'blocked', 'done'] },
29
+ stepId: id('可选:关联步骤'), blocker: string('阻塞原因(blocked 必填)'), evidence: strings('证据'),
30
+ }, ['planId', 'summary']), async (args) => dual() ? dual().pjm.report(args.planId, args) : dualUnavailable()),
31
+ jsonTool('pjm_submit_feedback', '项目级自反馈:计划全部步骤完成后提交给 PM 复核,写清交付、偏差、遗留问题与测试。', objectSchema({
32
+ planId: id('计划 id'), summary: string('执行总结(必填)'),
33
+ deviations: strings('相对计划的偏差'), openIssues: strings('遗留问题'), tests: strings('测试证据'),
34
+ }, ['planId', 'summary']), async (args) => dual() ? dual().pjm.submitFeedback(args.planId, args) : dualUnavailable()),
35
+ jsonTool('pjm_dispatch_step', '通过 fangForker.spawn 在隔离分身中执行一个 WBS 步骤;不可用会 fail-closed,不在宿主本地执行。', objectSchema({
36
+ planId: id('计划 id'), stepId: id('步骤 id'),
37
+ }, ['planId', 'stepId']), async (args) => dual() ? await dual().pjm.dispatch(args.planId, { stepId: args.stepId }) : dualUnavailable()),
38
+ jsonTool('pjm_complete_step', '记录步骤完成/失败、证据与实际用时;不得把失败写成完成。', objectSchema({
39
+ planId: id('计划 id'), stepId: id('步骤 id'),
40
+ status: { type: 'string', enum: ['done', 'failed'] }, summary: string('结果摘要'),
41
+ evidence: strings('测试/文件/报告证据'), actualMinutes: { type: 'number', description: '实际用时(分钟)' },
42
+ }, ['planId', 'stepId', 'status']), async (args) => dual() ? dual().pjm.completeStep(args.planId, args.stepId, args) : dualUnavailable()),
43
+ jsonTool('pjm_check_step', '查单个步骤真实状态:派发/执行/汇报、证据、用时与关联报告。结果来自计划与节点汇报,不是 VM 作业队列。', objectSchema({
44
+ planId: id('计划 id'), stepId: id('步骤 id'),
45
+ }, ['planId', 'stepId']), async (args) => dual() ? dual().pjm.checkStep(args.planId, args.stepId) : dualUnavailable()),
46
+ jsonTool('pjm_next_action', '读计划状态,返回 PJM 下一步唯一推荐动作与理由(纯建议,不执行)。', objectSchema({ planId: id('计划 id') }, ['planId']), async (args) => dual() ? dual().pjm.nextAction(args.planId) : dualUnavailable()),
47
+ jsonTool('pjm_project_status', '读取项目看板:完成/进行/待办/失败、估时、实际用时、是否可结案。', objectSchema({ planId: id('计划 id') }, ['planId']), async (args) => dual() ? dual().pjm.progress(args.planId) : dualUnavailable()),
48
+ jsonTool('pjm_write_project', '把项目计划与当前节点写成 workspace/docs/pjm 下的 Markdown。', objectSchema({ planId: id('计划 id') }, ['planId']), async (args, exec) => dual() ? await dual().pjm.writeProject(args.planId, { agent: exec.agent }) : dualUnavailable()),
49
+ jsonTool('pjm_close_project', '全部步骤完成后结案:写入总结、评审报告、验收与资料清单,并可生成结案文档。', objectSchema({
50
+ planId: id('计划 id'), summary: string('结案总结(必填)'), reviewReport: string('评审报告'),
51
+ acceptance: strings('验收记录'), artifacts: strings('项目资料路径'), write: { type: 'boolean', description: '是否同时写结案 Markdown,默认 true' },
52
+ }, ['planId', 'summary']), async (args, exec) => {
53
+ if (!dual()) return dualUnavailable()
54
+ const closed = dual().pjm.close(args.planId, args)
55
+ if (!closed.ok) return closed
56
+ if (args.write === false) return closed
57
+ const document = await dual().pjm.writeClosure(args.planId, { agent: exec.agent })
58
+ return { ...closed, document }
59
+ }),
60
+ ]
61
+ return installAgent(ctx, { prompt: PJM_SYSTEM_PROMPT, tools })
62
+ }
@@ -0,0 +1,58 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2026 dsh-Master contributors — see LICENSE-CORE.
3
+ // @ts-nocheck
4
+
5
+ import { PM_SYSTEM_PROMPT } from './prompts.ts'
6
+ import { dualService, dualUnavailable, id, installAgent, jsonTool, objectSchema, objects, string, strings } from './toolkit.ts'
7
+
8
+ export const name = 'dsh-master-pm-agent'
9
+ export const inject = ['systemPrompt', 'tools']
10
+
11
+ /** PM preset:只提供需求/盘点/文档工具,不提供任何执行或派生能力。 */
12
+ export function apply(ctx) {
13
+ const dual = () => dualService(ctx)
14
+ const tools = [
15
+ jsonTool('pm_create_prd', '创建结构化产品需求文档(WHY/WHAT/模块/约束/验收),返回 PRD id。', objectSchema({
16
+ title: string('需求标题'), goal: string('要解决的问题与目标'),
17
+ why: string('背景与价值'), what: strings('功能范围'), architecture: string('架构说明'),
18
+ outcomes: strings('预期结果'), modules: objects('模块:name/result/architecture/status'),
19
+ priority: { type: 'string', enum: ['low', 'normal', 'high', 'critical'] },
20
+ constraints: strings('约束'), acceptance: strings('验收标准'), nonGoals: strings('非目标'),
21
+ assumptions: strings('假设'), openQuestions: strings('待澄清问题'), requester: string('请求方'),
22
+ }, ['title', 'goal']), async (args) => dual() ? dual().pm.createPrd(args) : dualUnavailable()),
23
+ jsonTool('pm_create_assessment', '保存六类产品现状盘点。completed 每项必须附 evidence;其它类给原因、计划、冲突或歧义。', objectSchema({
24
+ prdId: id('可选:关联的 PRD id'), title: string('盘点标题'), why: string('目标'), what: strings('范围'),
25
+ completed: objects('已完成:title/progress/evidence[](证据必填)'),
26
+ unfinished: objects('未完成:title/reason/nextStep/progress'),
27
+ blocked: objects('受阻:title/reason/blocker/nextStep'),
28
+ planned: objects('已计划:title/plan/estimateMinutes/priority'),
29
+ conflicts: objects('冲突:title/question/options[{value,label,consequence}]'),
30
+ ambiguities: objects('歧义:title/question/currentInterpretation'),
31
+ }, ['title']), async (args) => dual() ? dual().pm.createAssessment(args) : dualUnavailable()),
32
+ jsonTool('pm_write_prd', '把 PRD(可带六类盘点)写成 workspace/docs/pm 下的 Markdown,返回文档路径。', objectSchema({
33
+ prdId: id('PRD id'), assessmentId: id('可选:关联的 Assessment id'),
34
+ }, ['prdId']), async (args, exec) => dual() ? await dual().pm.writePrd(args.prdId, { assessmentId: args.assessmentId, agent: exec.agent }) : dualUnavailable()),
35
+ jsonTool('pm_handoff_to_pjm', '把 PRD(可带盘点)打成交接包交给 PJM。有未决问题或冲突时不给 ready,必须先澄清。', objectSchema({
36
+ prdId: id('PRD id'), assessmentId: id('可选:关联的盘点 id'),
37
+ acceptance: strings('可选:覆盖 PRD 的验收标准'), openQuestions: strings('可选:补充未决问题'),
38
+ }, ['prdId']), async (args) => dual() ? dual().pm.createHandoff(args.prdId, args) : dualUnavailable()),
39
+ jsonTool('pm_review_feedback', '复核 PJM 的执行反馈:逐条验收给 met 布尔与证据,写复盘与总结;结论必须与验收一致。', objectSchema({
40
+ feedbackId: id('PJM 反馈 id'),
41
+ acceptanceChecks: objects('逐条验收:criterion/met(boolean)/evidence[]/note'),
42
+ verdict: { type: 'string', enum: ['accepted', 'changes-requested'] },
43
+ retrospective: { type: 'object', description: 'kept[] / improve[] / lessons[]', additionalProperties: true },
44
+ summary: string('复核总结(必填)'),
45
+ write: { type: 'boolean', description: '是否同时写复核文档,默认 true' },
46
+ }, ['feedbackId', 'acceptanceChecks', 'summary']), async (args, exec) => {
47
+ if (!dual()) return dualUnavailable()
48
+ const reviewed = dual().pm.reviewFeedback(args.feedbackId, args)
49
+ if (!reviewed.ok || args.write === false) return reviewed
50
+ const document = await dual().pm.writeReview(reviewed.review.id, { agent: exec.agent })
51
+ return { ...reviewed, document }
52
+ }),
53
+ jsonTool('pm_pipeline', '读一个 PRD 的闭环状态:drafted → handoff-ready / needs-clarification → planning → feedback → accepted / changes-requested。', objectSchema({ prdId: id('PRD id') }, ['prdId']), async (args) => dual() ? dual().pm.pipeline(args.prdId) : dualUnavailable()),
54
+ jsonTool('pm_next_action', '读闭环状态,返回 PM 下一步唯一推荐动作与理由(纯建议,不执行)。', objectSchema({ prdId: id('PRD id') }, ['prdId']), async (args) => dual() ? dual().pm.nextAction(args.prdId) : dualUnavailable()),
55
+ jsonTool('pm_list_prds', '列出当前 Host 中的 PRD、六类盘点、交接包与复核索引。', objectSchema({}), async () => dual() ? ({ ok: true, prds: dual().pm.listPrds().items, assessments: dual().pm.listAssessments().items, handoffs: dual().pm.listHandoffs().items, reviews: dual().pm.listReviews().items }) : dualUnavailable()),
56
+ ]
57
+ return installAgent(ctx, { prompt: PM_SYSTEM_PROMPT, tools })
58
+ }
@@ -0,0 +1,74 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2026 dsh-Master contributors — see LICENSE-CORE.
3
+
4
+ export const PM_AGENT_ID = 'master-pm'
5
+ export const PJM_AGENT_ID = 'master-pjm'
6
+
7
+ export const PM_SYSTEM_PROMPT = String.raw`# Role
8
+ 你是 DSH-Master「房主」的产品经理 Agent(PM,preset: master-pm)。你把用户和开发者的大白话转换成可核对、可验收的产品需求与文档。
9
+
10
+ # Goals — WHY / WHAT
11
+ 1. WHY:说清解决什么问题、为什么值得做、达成什么目标。
12
+ 2. WHAT:说清功能、模块、架构、预期结果、约束、非目标和验收标准。
13
+ 3. 对现状做证据驱动的六类盘点:已完成、未完成、无法实现/受阻、已计划、冲突、歧义。
14
+
15
+ # Workflow
16
+ 1. 先识别目标、范围、约束和未决项;重大歧义或不可逆选择必须用 ask_user_question 提出一个具体决策点,给 2–4 个选项并标推荐项。不要为琐碎实现细节打断用户。
17
+ 2. 盘点前先用 read/grep/glob 读取真实代码、测试和文档。没有证据的“完成”不得写 100%,不得把设计稿当成已交付。
18
+ 3. 用 pm_create_prd 保存结构化 PRD;用 pm_create_assessment 保存六类盘点。每个 completed 项必须附文件、测试或报告证据。
19
+ 4. 用 pm_write_prd 生成 Markdown 文档;需要用户打开时用 present 交付。
20
+ 5. **交接**:确认 PRD 有验收标准、且没有未决问题/冲突后,用 pm_handoff_to_pjm 交接给 PJM;不 ready 时先回去澄清,不得硬塞。
21
+ 6. **复核闭环**:PJM 提交反馈后用 pm_review_feedback 逐条核对验收(met + 证据),写复盘(kept/improve/lessons)与总结;结论必须与验收一致(有未达标就 changes-requested)。
22
+ 7. 用 pm_pipeline 查闭环阶段(drafted → handoff-ready → planning → feedback → accepted / changes-requested)。
23
+ 8. 不确定下一步时先调 pm_next_action:它给出唯一推荐动作(create_prd / create_assessment / clarify / handoff / review / done / blocked)与理由;按它走,不要自己跳步。
24
+
25
+ # Rules
26
+ - 不臆测、不伪造证据、不把“可以实现”写成“已经完成”。
27
+ - 冲突必须列出冲突双方、影响、可选方案和需要用户重新判断的问题。
28
+ - 歧义必须写出当前理解与待确认问题;用户未回答前不得把该理解当成事实。
29
+ - PM 不写生产代码、不跑 shell、不发布、不安装、不调度 VM/分身。
30
+ - 只有 PM 能复核 PJM 的交付;不得自己给自己复核,也不得替 PJM 编造执行结果。
31
+ - 只写 PRD/盘点/复核文档;不改需求范围外的文件。
32
+
33
+ # Output Format
34
+ 默认用简洁 Markdown 汇报,必须按任务需要包含:
35
+ - WHY & WHAT
36
+ - 六类现状矩阵:completed / unfinished / blocked / planned / conflicts / ambiguities
37
+ - 冲突与歧义的用户决策点
38
+ - 已保存的 PRD/Assessment id 与文档路径
39
+ 只输出已核实内容;未知、未验证、受限分别标明。`
40
+
41
+ export const PJM_SYSTEM_PROMPT = String.raw`# Role
42
+ 你是 DSH-Master「房主」的项目经理 Agent(PJM,preset: master-pjm)。你把已确认的 PRD 变成可执行、可追踪、可验收的项目计划,并按节点汇报直至结案。
43
+
44
+ # Goals — HOW / WHO / WHERE / WHEN
45
+ 1. HOW:把 PRD 拆成 WBS,每步写清 prompt、依赖、验收、估时。
46
+ 2. WHO:给每一步分配 owner 与责任;未分配必须写 unassigned,不得虚构人员。
47
+ 3. WHERE:明确执行场景(宿主、fork、workspace、DSH Web);脏任务只能在隔离执行体中运行。
48
+ 4. WHEN:给里程碑与估时;估时是计划,不是承诺工期。
49
+
50
+ # Workflow
51
+ 1. 项目会审:核对 PRD、验收标准、约束、未决问题;未决项未解决不得自行扩权。
52
+ 2. 接单:收到 PM 交接包后用 pjm_take_handoff 或 pjm_create_plan({handoffId}) 接单;交接包 needs-clarification 时退回 PM,不得硬开工。
53
+ 3. 用 pjm_create_plan 建立七类项目内容:项目会审、WBS、需求内容、时间、人员、测试部署验收、结案。
54
+ 4. 执行前申请隔离资源,通过 pjm_dispatch_step 走 fangForker.spawn;不可用就 fail-closed,绝不在母体/原身本地降级执行,不虚构 vm_create/vm_exec。
55
+ 5. **节点自反馈**:执行中随时用 pjm_report 汇报进展/阻塞(blocked 必须写原因)。
56
+ 6. **提交反馈**:全部步骤完成后用 pjm_submit_feedback 把交付、偏差、遗留问题、测试证据交给 PM 复核;不得在未完成时提交。
57
+ 7. 用 pjm_project_status 汇总节点,用 todo_write 跟踪当前工作;完成步骤时记录 summary/evidence/actualMinutes。
58
+ 8. 用 pjm_write_project 保存计划与节点报告。全部步骤完成后才能 pjm_close_project;结案必须有总结、验收记录、评审报告和资料清单,并交付文档。
59
+ 9. 不确定下一步时先调 pjm_next_action:它给出唯一推荐动作(take_handoff / return_to_pm / create_plan / dispatch / report / complete_feedback / close / done)与理由;按它走,不要跳步或重复派发。
60
+
61
+ # Rules
62
+ - 按时、按质、按量报告真实状态;失败、阻塞、未测量不得包装成完成。
63
+ - 不发布、不部署到生产、不修改 profile、不触碰凭据,除非用户对该动作另行明确授权。
64
+ - 不代替 PM 决定需求,不替用户回答选择题;阻塞时给出原因、影响、可选方案和所需决策。
65
+ - 不给自己复核:交付是否正确由 PM 复核;PM 判 changes-requested 时按新的验收项重做。
66
+ - 不做无限轮询;节点汇报写完成/进行/待办/失败及下一步。
67
+
68
+ # Output Format
69
+ - 项目会审与范围
70
+ - WBS 看板(Todo / In Progress / Done / Failed)
71
+ - Owner、场景、估时、实际用时与依赖
72
+ - 测试/部署/交付/验收证据
73
+ - 结案:评审报告、验收记录、资料汇总
74
+ - 仍需人类决策的事项(如有)`
@@ -0,0 +1,64 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2026 dsh-Master contributors — see LICENSE-CORE.
3
+ // @ts-nocheck
4
+
5
+ import { SERVICE, masterError, safeGet } from '../kernel.ts'
6
+
7
+ export function dualService(ctx) {
8
+ const service = safeGet(ctx, SERVICE.dual)
9
+ return service && service.pm && service.pjm ? service : null
10
+ }
11
+
12
+ export function dualUnavailable() {
13
+ return masterError('dual-service-unavailable', 'dshMasterDual is required; enable master-dual-manager in this Host')
14
+ }
15
+
16
+ /** 注册一个返回结构化 JSON 的模型工具;异常也转成具名错误,不吞掉。 */
17
+ export function jsonTool(name, description, parameters, execute) {
18
+ return {
19
+ name,
20
+ description,
21
+ parameters,
22
+ output: {
23
+ schema: { type: 'object' },
24
+ render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }],
25
+ },
26
+ async execute(args, exec) {
27
+ try { return await execute(args && typeof args === 'object' ? args : {}, exec) }
28
+ catch (error) { return masterError('agent-tool-threw', String(error?.message ?? error), { tool: name }) }
29
+ },
30
+ }
31
+ }
32
+
33
+ /** 在 preset scope 内挂 persona 与 scoped tools,全部纳入 Cordis effect 生命周期。 */
34
+ export function installAgent(ctx, { prompt, tools }) {
35
+ const systemPrompt = safeGet(ctx, 'systemPrompt')
36
+ const registry = safeGet(ctx, 'tools')
37
+ if (!systemPrompt || typeof systemPrompt.section !== 'function') {
38
+ return { ok: false, code: 'system-prompt-unavailable', error: 'ctx.systemPrompt with section() is required' }
39
+ }
40
+ if (!registry || typeof registry.register !== 'function') {
41
+ return { ok: false, code: 'tools-unavailable', error: 'ctx.tools with register() is required' }
42
+ }
43
+ const registered = []
44
+ try {
45
+ ctx.effect(() => systemPrompt.section({ name: 'deployment:persona-prefix', order: 0, text: prompt }))
46
+ } catch (error) {
47
+ return { ok: false, code: 'prompt-registration-failed', error: String(error?.message ?? error) }
48
+ }
49
+ for (const definition of tools) {
50
+ try {
51
+ ctx.effect(() => registry.register(definition))
52
+ registered.push(definition.name)
53
+ } catch (error) {
54
+ return { ok: false, code: 'tool-registration-failed', error: String(error?.message ?? error), tool: definition.name, registered }
55
+ }
56
+ }
57
+ return { ok: true, tools: registered }
58
+ }
59
+
60
+ export const objectSchema = (properties = {}, required = []) => ({ type: 'object', properties, required, additionalProperties: true })
61
+ export const string = (description = '') => ({ type: 'string', description })
62
+ export const strings = (description = '') => ({ type: 'array', items: { type: 'string' }, description })
63
+ export const objects = (description = '') => ({ type: 'array', items: { type: 'object', additionalProperties: true }, description })
64
+ export const id = (description) => ({ type: 'string', description })
@@ -0,0 +1,85 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2026 dsh-Master contributors — see LICENSE-CORE.
3
+ // @ts-nocheck
4
+
5
+ /**
6
+ * 流式生成的取消登记表(3.3.5)。
7
+ *
8
+ * 为什么单独抽出来:中断的正确性取决于「谁在跑、谁负责停、停完是否清理」。
9
+ * 把它写成可测原语后,通道、会话桥、演示生产者都能复用同一套语义。
10
+ *
11
+ * 语义:
12
+ * * `begin(key)` 拿到 `{ key, signal, abort, end }`;同一个 key 重复 begin 会**先中止旧的**(新的一轮覆盖旧的)。
13
+ * * `abort(key)` 触发 signal 并保留登记,直到 `end()` 才真正清理 —— 这样“已中止但尚未收尾”可被观察。
14
+ * * `end(key)` 幂等清理;调用后 `has(key)` 为 false。
15
+ * * `abortAll()` 用于连接关闭 / 插件卸载,保证没有僵尸流。
16
+ * * 所有方法都不抛(除了传入非法 key 返回 false)。
17
+ */
18
+
19
+ export function createAbortRegistry() {
20
+ const entries = new Map()
21
+
22
+ function begin(key, { onAbort } = {}) {
23
+ const id = String(key ?? '')
24
+ if (!id) throw new Error('abort-registry: key is required')
25
+ // 同一 key 上已有生成:先中止旧的,避免两路流同时往同一个消息里写
26
+ const previous = entries.get(id)
27
+ if (previous) {
28
+ previous.abort('superseded')
29
+ }
30
+ const controller = new AbortController()
31
+ const entry = {
32
+ key: id,
33
+ controller,
34
+ signal: controller.signal,
35
+ aborted: false,
36
+ reason: null,
37
+ startedAt: Date.now(),
38
+ endedAt: null,
39
+ }
40
+ controller.signal.addEventListener('abort', () => {
41
+ entry.aborted = true
42
+ try { onAbort?.(entry.reason ?? controller.signal.reason ?? 'aborted') } catch { /* 回调异常不影响中止 */ }
43
+ }, { once: true })
44
+ entry.abort = (reason = 'user-interrupt') => {
45
+ if (controller.signal.aborted) return false
46
+ try { controller.abort(reason) } catch { /* ignore */ }
47
+ entry.reason = String(reason)
48
+ return true
49
+ }
50
+ entry.end = () => {
51
+ if (entries.get(id) === entry) entries.delete(id)
52
+ entry.endedAt = Date.now()
53
+ return true
54
+ }
55
+ entries.set(id, entry)
56
+ return entry
57
+ }
58
+
59
+ return {
60
+ begin,
61
+ has: (key) => entries.has(String(key ?? '')),
62
+ get: (key) => entries.get(String(key ?? '')) ?? null,
63
+ /** 中止并返回是否真的发生了中止。 */
64
+ abort: (key, reason = 'user-interrupt') => {
65
+ const entry = entries.get(String(key ?? ''))
66
+ if (!entry) return false
67
+ return entry.abort(reason)
68
+ },
69
+ abortAll: (reason = 'shutdown') => {
70
+ const aborted = []
71
+ for (const entry of entries.values()) {
72
+ if (entry.abort(reason)) aborted.push(entry.key)
73
+ }
74
+ return aborted
75
+ },
76
+ /** 结束并清理(幂等)。 */
77
+ end: (key) => {
78
+ const entry = entries.get(String(key ?? ''))
79
+ if (!entry) return false
80
+ return entry.end()
81
+ },
82
+ keys: () => [...entries.keys()],
83
+ size: () => entries.size,
84
+ }
85
+ }
@@ -0,0 +1,110 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2026 dsh-Master contributors — see LICENSE-CORE.
3
+ // @ts-nocheck
4
+
5
+ /**
6
+ * 智囊团通道协议(3.3.5)。
7
+ *
8
+ * 客户端 → 服务端:
9
+ * { type: 'ping' }
10
+ * { type: 'resync' } → 重新推一次快照
11
+ * { type: 'chat', content: string, agent?: 'pm'|'pjm' }
12
+ * { type: 'answer', callId: string, value: string } ← 目前会返回 session-prompt-unavailable
13
+ *
14
+ * 服务端 → 客户端:
15
+ * { type: 'hello', ... } 连接建立(含协议版本与可用性)
16
+ * { type: 'dual', snapshot } 交接/计划/反馈/复核快照
17
+ * { type: 'pong', at }
18
+ * { type: 'error', code, message, detail? }
19
+ *
20
+ * 为什么 chat/answer 现在只返回错误:见 docs/CHANNEL-WEBSOCKET.md。
21
+ * 插件侧没有「向指定 preset 会话提交用户消息」的 Host API;假装能聊就是假聊天。
22
+ */
23
+
24
+ export const CHANNEL_PROTOCOL_VERSION = 1
25
+ export const CHANNEL_MAX_MESSAGE_BYTES = 64 * 1024
26
+ export const CLIENT_MESSAGE_TYPES = Object.freeze(['ping', 'resync', 'chat', 'answer', 'interrupt'])
27
+
28
+ function text(value) { return String(value ?? '').trim() }
29
+
30
+ /**
31
+ * 解析并校验一条客户端消息;非法输入给出具名错误而不是抛。
32
+ */
33
+ export function parseClientMessage(raw) {
34
+ if (typeof raw === 'string' && raw.length > CHANNEL_MAX_MESSAGE_BYTES) {
35
+ return { ok: false, code: 'message-too-large', error: `message exceeds ${CHANNEL_MAX_MESSAGE_BYTES} bytes` }
36
+ }
37
+ let body = null
38
+ if (typeof raw === 'string') {
39
+ try { body = JSON.parse(raw) } catch { return { ok: false, code: 'invalid-json', error: 'message must be JSON' } }
40
+ } else if (raw && typeof raw === 'object') {
41
+ body = raw
42
+ } else {
43
+ return { ok: false, code: 'invalid-message', error: 'message must be a JSON object' }
44
+ }
45
+ const type = text(body.type)
46
+ if (!CLIENT_MESSAGE_TYPES.includes(type)) {
47
+ return { ok: false, code: 'unknown-type', error: `unknown message type: ${type || '(empty)'}`, types: CLIENT_MESSAGE_TYPES }
48
+ }
49
+ if (type === 'chat') {
50
+ const content = text(body.content)
51
+ if (!content) return { ok: false, code: 'content-required', error: 'chat requires content' }
52
+ if (content.length > 8_000) return { ok: false, code: 'content-too-long', error: 'chat content exceeds 8000 characters' }
53
+ const agent = text(body.agent) || 'pm'
54
+ if (!['pm', 'pjm'].includes(agent)) return { ok: false, code: 'agent-invalid', error: 'agent must be pm or pjm' }
55
+ return { ok: true, message: { type, content, agent } }
56
+ }
57
+ if (type === 'answer') {
58
+ const callId = text(body.callId)
59
+ const value = text(body.value)
60
+ if (!callId || !value) return { ok: false, code: 'answer-incomplete', error: 'answer requires callId and value' }
61
+ return { ok: true, message: { type, callId, value } }
62
+ }
63
+ if (type === 'interrupt') {
64
+ // messageId 可选:省略表示“这一连接上所有正在生成的流都停”
65
+ const messageId = text(body.messageId)
66
+ return { ok: true, message: { type, messageId: messageId || null } }
67
+ }
68
+ return { ok: true, message: { type } }
69
+ }
70
+
71
+ /** 中断确认:`aborted` 表示是否真的掐断了一条在跑的流。 */
72
+ export function interruptedMessage({ messageId, savedContent = '', aborted = true } = {}) {
73
+ return {
74
+ type: 'interrupted',
75
+ messageId: text(messageId) || null,
76
+ savedContent: String(savedContent ?? ''),
77
+ aborted: aborted === true,
78
+ at: Date.now(),
79
+ }
80
+ }
81
+
82
+ export function serverHello({ channelId, tokenRequired = true } = {}) {
83
+ return {
84
+ type: 'hello',
85
+ protocol: CHANNEL_PROTOCOL_VERSION,
86
+ channelId: text(channelId) || 'dsh-master-command-center',
87
+ capabilities: {
88
+ dualSnapshot: true,
89
+ chat: false,
90
+ answer: false,
91
+ },
92
+ limitations: {
93
+ chat: 'plugin cannot submit a user message into a preset agent session',
94
+ answer: 'user questions are answered inside their owning session',
95
+ },
96
+ tokenRequired: tokenRequired === true,
97
+ serverTime: Date.now(),
98
+ }
99
+ }
100
+
101
+ export function serverError(code, message, detail) {
102
+ const out = { type: 'error', code: text(code) || 'unknown', message: String(message ?? '') }
103
+ if (detail !== undefined) out.detail = detail
104
+ return out
105
+ }
106
+
107
+ /** 把 dual 快照包成通道消息(保持字段原样,不在这里加工业务语义)。 */
108
+ export function dualMessage(snapshot) {
109
+ return { type: 'dual', snapshot: snapshot ?? null }
110
+ }