@mars.liu/dsh-cybernetics 0.2.0 → 0.7.8
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 +133 -2
- package/cordis.patch.yml +18 -1
- package/docs/issue-browser-connection-pool.md +79 -0
- package/docs/listBundles-hang-investigation-20261103.md +93 -0
- package/docs/protocol.md +253 -3
- package/lib/client.js +517 -15
- package/package.json +18 -2
- package/src/controller.ts +193 -3
- package/src/executor.ts +414 -0
- package/src/index.ts +776 -49
- package/src/projection.ts +763 -0
- package/src/upstream.d.ts +22 -0
- package/workflows/round.js +101 -1
- package/test/v03-offline.mjs +0 -203
- package/tsconfig.json +0 -16
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal ambient declarations for the DSH host packages this plugin is
|
|
3
|
+
* `link:`-mounted against.
|
|
4
|
+
*
|
|
5
|
+
* Rationale: the plugin is deliberately zero-dependency (see src/index.ts), so
|
|
6
|
+
* `npx tsc --noEmit` in this checkout has no node_modules to resolve the host
|
|
7
|
+
* packages from. The only upstream surfaces the plugin needs to merge into are
|
|
8
|
+
* the two merge-extensible type tables below (their real declarations in
|
|
9
|
+
* @deepseek-ai/dsh-session-projection / @deepseek-ai/dsh-session are exactly
|
|
10
|
+
* these empty interfaces). Declaring them ambiently keeps the augmentation in
|
|
11
|
+
* src/projection.ts a valid augmentation instead of a TS2664 error, and keeps
|
|
12
|
+
* the standalone typecheck at its pre-existing baseline (only the known
|
|
13
|
+
* TS2307/TS5097 diagnostics from the type-only imports).
|
|
14
|
+
*/
|
|
15
|
+
declare module '@deepseek-ai/dsh-session-projection/types' {
|
|
16
|
+
export interface SessionProjectionMap {}
|
|
17
|
+
export interface SessionProjectionStateMap {}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
declare module '@deepseek-ai/dsh-session/types' {
|
|
21
|
+
export interface SessionEventMap {}
|
|
22
|
+
}
|
package/workflows/round.js
CHANGED
|
@@ -10,10 +10,17 @@
|
|
|
10
10
|
// // primary work order (falls back to the objective)
|
|
11
11
|
// role: string, // task agent role key (default 'default')
|
|
12
12
|
// roles: { [role]: { provider?, model? } }, // per-role LLM overrides (deployment config)
|
|
13
|
+
// taskReport?: object, // v0.6 TeamExecutor: the persistent teammate's
|
|
14
|
+
// // report — when present the task agent is skipped
|
|
15
|
+
// // and verify/triage run over it directly
|
|
16
|
+
// depth: 'quick' | 'full', // quick = task agent only (lightweight analysis:
|
|
17
|
+
// // one agent, one round, direct feedback — no
|
|
18
|
+
// // verify/triage); full = the whole loop (default,
|
|
19
|
+
// // for complex analysis and any world change)
|
|
13
20
|
// }
|
|
14
21
|
// Returns { round, report, verdict, triage } — the controller folds this into the decision.
|
|
15
22
|
|
|
16
|
-
const { goal, state, subgoal, brief, role, roles } = args
|
|
23
|
+
const { goal, state, subgoal, brief, role, roles, depth, taskReport } = args
|
|
17
24
|
|
|
18
25
|
// One triaged step; shared by the four triage buckets (plain data, no $ref).
|
|
19
26
|
const stepsItem = {
|
|
@@ -29,6 +36,81 @@ const specFor = (key) => {
|
|
|
29
36
|
: {}
|
|
30
37
|
}
|
|
31
38
|
|
|
39
|
+
// v0.6 TeamExecutor: the persistent teammate already did the task work; the
|
|
40
|
+
// controller hands its report in as `taskReport`. Skip the task agent entirely
|
|
41
|
+
// and go straight to verify/triage (quick depth returns the derived verdict).
|
|
42
|
+
if (taskReport && typeof taskReport === 'object') {
|
|
43
|
+
const report = taskReport
|
|
44
|
+
if (depth === 'quick') {
|
|
45
|
+
return {
|
|
46
|
+
round: state.round,
|
|
47
|
+
report,
|
|
48
|
+
verdict: {
|
|
49
|
+
met: report.done === true,
|
|
50
|
+
quick: true,
|
|
51
|
+
criteria: [],
|
|
52
|
+
gaps: report.done === true ? [] : ['task agent 报告 done=false(quick 轮无 verify,详见 report)'],
|
|
53
|
+
goalItselfQuestionable: false,
|
|
54
|
+
},
|
|
55
|
+
triage: { auto: [], defer: [], reject: [], human: [] },
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
await phase('verify')
|
|
59
|
+
const verdict = await agent(
|
|
60
|
+
[
|
|
61
|
+
`你是独立的 verify agent:你没有、也不应获得 task agent 的叙述过程。`,
|
|
62
|
+
`下面是目标、工作委托书和 task agent 的报告,请基于证据判定本轮是否达成目标。`,
|
|
63
|
+
``,
|
|
64
|
+
`【目标】`,
|
|
65
|
+
goal.objective,
|
|
66
|
+
``,
|
|
67
|
+
`【工作委托书】`,
|
|
68
|
+
(brief && brief.trim()) || goal.objective,
|
|
69
|
+
``,
|
|
70
|
+
`【task agent 的报告】`,
|
|
71
|
+
JSON.stringify(report, null, 2),
|
|
72
|
+
``,
|
|
73
|
+
`判定标准:证据是否支撑报告的每一项结论;是否覆盖了本轮子目标;是否存在未验证的断言。`,
|
|
74
|
+
].join('\n'),
|
|
75
|
+
{ label: 'verify', ...specFor('verify'), schema: {
|
|
76
|
+
type: 'object', additionalProperties: false,
|
|
77
|
+
required: ['met', 'criteria', 'gaps', 'goalItselfQuestionable'],
|
|
78
|
+
properties: {
|
|
79
|
+
met: { type: 'boolean' },
|
|
80
|
+
criteria: { type: 'array', items: { type: 'object', additionalProperties: false, required: ['criterion', 'pass', 'evidence'], properties: { criterion: { type: 'string' }, pass: { type: 'boolean' }, evidence: { type: 'string' } } } },
|
|
81
|
+
gaps: { type: 'array', items: { type: 'string' } },
|
|
82
|
+
goalItselfQuestionable: { type: 'boolean' },
|
|
83
|
+
},
|
|
84
|
+
} },
|
|
85
|
+
)
|
|
86
|
+
await phase('triage')
|
|
87
|
+
const triage = await agent(
|
|
88
|
+
[
|
|
89
|
+
`你是 triage agent:把 verify 的缺口拆成四类处置建议。`,
|
|
90
|
+
``,
|
|
91
|
+
`【verify 判定】`,
|
|
92
|
+
JSON.stringify(verdict, null, 2),
|
|
93
|
+
``,
|
|
94
|
+
`【本轮子目标】`,
|
|
95
|
+
subgoal || '',
|
|
96
|
+
].join('\n'),
|
|
97
|
+
{ label: 'triage', ...specFor('triage'), schema: {
|
|
98
|
+
type: 'object', additionalProperties: false,
|
|
99
|
+
required: ['auto', 'defer', 'reject', 'human'],
|
|
100
|
+
properties: {
|
|
101
|
+
auto: { type: 'array', items: { type: 'object', additionalProperties: false, required: ['step', 'rationale'], properties: { step: { type: 'string' }, rationale: { type: 'string' } } } },
|
|
102
|
+
defer: { type: 'array', items: { type: 'object', additionalProperties: false, required: ['step', 'rationale'], properties: { step: { type: 'string' }, rationale: { type: 'string' } } } },
|
|
103
|
+
reject: { type: 'array', items: { type: 'object', additionalProperties: false, required: ['step', 'rationale'], properties: { step: { type: 'string' }, rationale: { type: 'string' } } } },
|
|
104
|
+
human: { type: 'array', items: { type: 'object', additionalProperties: false, required: ['step', 'rationale'], properties: { step: { type: 'string' }, rationale: { type: 'string' }, question: { type: 'string' } } } },
|
|
105
|
+
},
|
|
106
|
+
} },
|
|
107
|
+
)
|
|
108
|
+
if (verdict === null || triage === null) {
|
|
109
|
+
return { round: state.round, report, failed: 'verify/triage agent 失败,本轮无结论' }
|
|
110
|
+
}
|
|
111
|
+
return { round: state.round, report, verdict, triage }
|
|
112
|
+
}
|
|
113
|
+
|
|
32
114
|
await phase('task')
|
|
33
115
|
|
|
34
116
|
// The task agent is an INDEPENDENT subagent: it inherits no conversation
|
|
@@ -76,6 +158,24 @@ if (report === null) {
|
|
|
76
158
|
return { round: state.round, failed: 'task agent 失败,本轮无结果' }
|
|
77
159
|
}
|
|
78
160
|
|
|
161
|
+
// quick depth: lightweight analysis — one agent, one round, direct feedback.
|
|
162
|
+
// Skip verify/triage entirely; derive a verdict-shaped outcome from the
|
|
163
|
+
// task report so the controller's decision logic works unchanged.
|
|
164
|
+
if (depth === 'quick') {
|
|
165
|
+
return {
|
|
166
|
+
round: state.round,
|
|
167
|
+
report,
|
|
168
|
+
verdict: {
|
|
169
|
+
met: report.done === true,
|
|
170
|
+
quick: true,
|
|
171
|
+
criteria: [],
|
|
172
|
+
gaps: report.done === true ? [] : ['task agent 报告 done=false(quick 轮无 verify,详见 report)'],
|
|
173
|
+
goalItselfQuestionable: false,
|
|
174
|
+
},
|
|
175
|
+
triage: { auto: [], defer: [], reject: [], human: [] },
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
79
179
|
await phase('verify')
|
|
80
180
|
|
|
81
181
|
const verdict = await agent(
|
package/test/v03-offline.mjs
DELETED
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
// Offline harness test for dsh-cybernetics v0.3: exercises every tool action
|
|
2
|
-
// (list / round / start / collect / cancel) against a fake host ctx and a
|
|
3
|
-
// controllable fake workflow engine — no DSH runtime required.
|
|
4
|
-
// Run: node test/v03-offline.mjs
|
|
5
|
-
|
|
6
|
-
import assert from 'node:assert/strict'
|
|
7
|
-
import { mkdtemp, readFile } from 'node:fs/promises'
|
|
8
|
-
import { tmpdir } from 'node:os'
|
|
9
|
-
import { join } from 'node:path'
|
|
10
|
-
import { apply } from '../src/index.ts'
|
|
11
|
-
|
|
12
|
-
// --- fake workflow engine ---------------------------------------------------
|
|
13
|
-
function fakeEngine() {
|
|
14
|
-
const runs = []
|
|
15
|
-
return {
|
|
16
|
-
runs,
|
|
17
|
-
start(request) {
|
|
18
|
-
let settle
|
|
19
|
-
const result = new Promise((resolve) => { settle = resolve })
|
|
20
|
-
const run = {
|
|
21
|
-
request,
|
|
22
|
-
result,
|
|
23
|
-
cancelled: false,
|
|
24
|
-
disposed: false,
|
|
25
|
-
settle(value) { settle(value) },
|
|
26
|
-
cancel(reason) {
|
|
27
|
-
run.cancelled = true
|
|
28
|
-
settle({ stopReason: 'cancelled', error: reason })
|
|
29
|
-
},
|
|
30
|
-
async dispose() { run.disposed = true },
|
|
31
|
-
}
|
|
32
|
-
runs.push(run)
|
|
33
|
-
return run
|
|
34
|
-
},
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// --- fake host ctx ------------------------------------------------------------
|
|
39
|
-
function fakeHost(engine, { activeGoal } = {}) {
|
|
40
|
-
const tools = []
|
|
41
|
-
const followups = []
|
|
42
|
-
const createdGoals = []
|
|
43
|
-
const parent = {
|
|
44
|
-
session: { id: 'sess-test' },
|
|
45
|
-
followup(message) { followups.push(message) },
|
|
46
|
-
}
|
|
47
|
-
const rootPromise = mkdtemp(join(tmpdir(), 'cyb-test-'))
|
|
48
|
-
const ctx = {
|
|
49
|
-
tools: { register: (d) => { tools.push(d); return () => {} } },
|
|
50
|
-
systemPrompt: { section() {}, getSectionOrder: () => 0 },
|
|
51
|
-
goals: {
|
|
52
|
-
get: () => activeGoal ?? null,
|
|
53
|
-
create: (_agent, input) => { createdGoals.push(input); return { id: 'goal-focused' } },
|
|
54
|
-
},
|
|
55
|
-
workingDirectory: { get: () => '__PENDING__' },
|
|
56
|
-
agentPresets: { serviceFor: (_agent, name) => (name === 'workflowEngine' ? engine : undefined) },
|
|
57
|
-
}
|
|
58
|
-
// workingDirectory.get must be sync in the plugin; pre-resolve the tmp dir.
|
|
59
|
-
return { ctx, tools, followups, createdGoals, parent, rootPromise }
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const engine = fakeEngine()
|
|
63
|
-
const { ctx, tools, followups, createdGoals, parent } = fakeHost(engine)
|
|
64
|
-
const root = await mkdtemp(join(tmpdir(), 'cyb-test-'))
|
|
65
|
-
ctx.workingDirectory = { get: () => root }
|
|
66
|
-
|
|
67
|
-
let section
|
|
68
|
-
ctx.systemPrompt.section = (s) => { section = s }
|
|
69
|
-
apply(ctx, {})
|
|
70
|
-
const tool = tools[0]
|
|
71
|
-
const exec = { agent: parent, signal: undefined }
|
|
72
|
-
|
|
73
|
-
// 1. list on an empty board
|
|
74
|
-
{
|
|
75
|
-
const out = await tool.execute({ action: 'list' }, exec)
|
|
76
|
-
assert.deepEqual(out.board, [], 'empty board')
|
|
77
|
-
console.log('✓ 1 list: empty board')
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// 2. round with no goal anywhere → guard error
|
|
81
|
-
{
|
|
82
|
-
await assert.rejects(
|
|
83
|
-
() => tool.execute({ action: 'round' }, exec),
|
|
84
|
-
/no active session goal/,
|
|
85
|
-
)
|
|
86
|
-
console.log('✓ 2 guard: no goal resolved → structured error')
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// 3. start creates a new goal and launches a background round
|
|
90
|
-
let goalId
|
|
91
|
-
{
|
|
92
|
-
const out = await tool.execute({
|
|
93
|
-
action: 'start', title: '测试任务A', brief: '完整工作委托书:做 X,验收 Y',
|
|
94
|
-
}, exec)
|
|
95
|
-
assert.equal(out.started, true)
|
|
96
|
-
assert.match(out.goalId, /^cyb-/)
|
|
97
|
-
assert.equal(out.round, 1)
|
|
98
|
-
goalId = out.goalId
|
|
99
|
-
assert.equal(engine.runs.length, 1, 'one workflow run started')
|
|
100
|
-
assert.equal(engine.runs[0].request.args.brief, '完整工作委托书:做 X,验收 Y', 'brief passed through')
|
|
101
|
-
assert.equal(createdGoals.length, 1, 'focus goal synced into dsh-goal (slot was free)')
|
|
102
|
-
assert.match(createdGoals[0].objective, /测试任务A/)
|
|
103
|
-
console.log('✓ 3 start: goal created, background round launched, brief透传, focus synced')
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// 4. collect before settle → pending
|
|
107
|
-
{
|
|
108
|
-
const out = await tool.execute({ action: 'collect', goalId }, exec)
|
|
109
|
-
assert.equal(out.pending, true)
|
|
110
|
-
console.log('✓ 4 collect (running): pending')
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// 5. settle the run → completion notice queued into the session inbox
|
|
114
|
-
{
|
|
115
|
-
const roundResult = {
|
|
116
|
-
round: 1,
|
|
117
|
-
report: { summary: '做完了一半', done: false, artifacts: [], evidence: 'e', nextStepThoughts: ['继续另一半'] },
|
|
118
|
-
verdict: { met: false, criteria: [], gaps: ['另一半没做'], goalItselfQuestionable: false },
|
|
119
|
-
triage: { auto: [], defer: [], reject: [], human: [] },
|
|
120
|
-
}
|
|
121
|
-
engine.runs[0].settle({ stopReason: 'completed', value: roundResult })
|
|
122
|
-
await new Promise((r) => setImmediate(r))
|
|
123
|
-
assert.equal(followups.length, 1, 'one completion notice queued')
|
|
124
|
-
assert.equal(followups[0].role, 'user')
|
|
125
|
-
assert.equal(followups[0].source.kind, 'cybernetics')
|
|
126
|
-
assert.equal(followups[0].source.goalId, goalId)
|
|
127
|
-
assert.match(followups[0].content[0].text, /collect/)
|
|
128
|
-
console.log('✓ 5 settle: agent.followup completion notice queued')
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// 6. collect folds the settled round into a continue decision + audit log
|
|
132
|
-
{
|
|
133
|
-
const out = await tool.execute({ action: 'collect', goalId }, exec)
|
|
134
|
-
assert.equal(out.decision.action, 'continue')
|
|
135
|
-
assert.match(out.decision.subgoal, /另一半没做/, 'subgoal derived from gaps')
|
|
136
|
-
const log = JSON.parse(await readFile(join(root, '.cybernetics', `round-${goalId}-1.json`), 'utf8'))
|
|
137
|
-
assert.equal(log.report.summary, '做完了一半', 'audit log written (goal-scoped filename)')
|
|
138
|
-
console.log(`✓ 6 collect: continue decision from gaps, round-${goalId}-1.json audit log落盘`)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// 7. start a second round then cancel it (correction-interrupt path)
|
|
142
|
-
{
|
|
143
|
-
const out = await tool.execute({ action: 'start', goalId, brief: '修正:方向改为 Z' }, exec)
|
|
144
|
-
assert.equal(out.round, 2)
|
|
145
|
-
const c = await tool.execute({ action: 'cancel', goalId, brief: '用户打断:方向改为 Z' }, exec)
|
|
146
|
-
assert.ok(c.cancelled)
|
|
147
|
-
await new Promise((r) => setImmediate(r))
|
|
148
|
-
const collected = await tool.execute({ action: 'collect', goalId }, exec)
|
|
149
|
-
assert.equal(collected.decision.action, 'pause')
|
|
150
|
-
assert.equal(collected.decision.reason, 'failed')
|
|
151
|
-
assert.match(collected.result.failed, /cancelled/)
|
|
152
|
-
console.log('✓ 7 cancel: in-flight round aborted, collect folds cancelled outcome')
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// 8. blocking round on a SECOND goal; met verdict → pause(met), status tracked
|
|
156
|
-
// (blocking: schedule the settle first, then await the execute promise)
|
|
157
|
-
{
|
|
158
|
-
const p = tool.execute({ action: 'round', title: '测试任务B', brief: '委托书B' }, exec)
|
|
159
|
-
// Wait until the execute path has actually started the run (readFile is async).
|
|
160
|
-
const before = engine.runs.length
|
|
161
|
-
for (let i = 0; i < 100 && engine.runs.length === before; i++) {
|
|
162
|
-
await new Promise((r) => setImmediate(r))
|
|
163
|
-
}
|
|
164
|
-
const runB = engine.runs.at(-1)
|
|
165
|
-
runB.settle({
|
|
166
|
-
stopReason: 'completed',
|
|
167
|
-
value: {
|
|
168
|
-
round: 1,
|
|
169
|
-
report: { summary: 'B 全部完成', done: true, artifacts: ['x'], evidence: 'e', nextStepThoughts: [] },
|
|
170
|
-
verdict: { met: true, criteria: [], gaps: [], goalItselfQuestionable: false },
|
|
171
|
-
triage: { auto: [], defer: [], reject: [], human: [] },
|
|
172
|
-
},
|
|
173
|
-
})
|
|
174
|
-
const out = await p
|
|
175
|
-
assert.equal(out.decision.action, 'pause')
|
|
176
|
-
assert.equal(out.decision.reason, 'met')
|
|
177
|
-
assert.ok(out.goalId !== goalId, 'second goal has its own id')
|
|
178
|
-
console.log('✓ 8 round (blocking): second goal, met → pause(met)')
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// 9. list shows both goals with divergent status
|
|
182
|
-
{
|
|
183
|
-
const out = await tool.execute({ action: 'list' }, exec)
|
|
184
|
-
assert.equal(out.board.length, 2)
|
|
185
|
-
const a = out.board.find((g) => g.goalId === goalId)
|
|
186
|
-
const b = out.board.find((g) => g.goalId !== goalId)
|
|
187
|
-
assert.equal(a.round, 2)
|
|
188
|
-
assert.equal(a.running, false)
|
|
189
|
-
assert.match(a.subgoal, /另一半没做/, 'goal A keeps its own subgoal across B\'s rounds')
|
|
190
|
-
assert.equal(b.status, 'met')
|
|
191
|
-
console.log('✓ 9 list: two goals coexist with independent state')
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// 10. guidance section encodes the orchestrator protocol
|
|
195
|
-
{
|
|
196
|
-
assert.match(section.text, /never execute task work/i)
|
|
197
|
-
assert.match(section.text, /NEW task/)
|
|
198
|
-
assert.match(section.text, /CORRECTION/)
|
|
199
|
-
assert.match(section.text, /cancel/)
|
|
200
|
-
console.log('✓ 10 guidance: orchestrator protocol injected')
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
console.log('\nALL 10 CHECKS PASSED')
|
package/tsconfig.json
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "NodeNext",
|
|
5
|
-
"moduleResolution": "NodeNext",
|
|
6
|
-
"lib": ["ES2022"],
|
|
7
|
-
"strict": true,
|
|
8
|
-
"declaration": true,
|
|
9
|
-
"declarationMap": true,
|
|
10
|
-
"outDir": "lib",
|
|
11
|
-
"rootDir": "src",
|
|
12
|
-
"verbatimModuleSyntax": true,
|
|
13
|
-
"skipLibCheck": true
|
|
14
|
-
},
|
|
15
|
-
"include": ["src/**/*.ts"]
|
|
16
|
-
}
|