@mars.liu/dsh-cybernetics 0.2.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -2
- package/cordis.patch.yml +18 -1
- package/docs/protocol.md +219 -3
- package/lib/client.js +231 -13
- package/package.json +13 -1
- package/src/controller.ts +113 -3
- package/src/executor.ts +414 -0
- package/src/index.ts +610 -42
- 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
package/lib/client.js
CHANGED
|
@@ -3,8 +3,13 @@
|
|
|
3
3
|
// host — no plugin HTTP route needed. Hand-written ModuleLoader wrapper, same
|
|
4
4
|
// format the host bundler emits (see dsh-context / dsh-maze client.js).
|
|
5
5
|
/* global window */
|
|
6
|
+
// Registration id = the package name in package.json, NOT the short plugin row
|
|
7
|
+
// id: the client-modules boot graph keys every row by the package manifest name
|
|
8
|
+
// (`@mars.liu/dsh-cybernetics`) and only materializes a row whose bundle
|
|
9
|
+
// registered under exactly that id — a stale `dsh-cybernetics` here executes
|
|
10
|
+
// the batch script fine, then reports "loaded without registering".
|
|
6
11
|
window.__ModuleLoader__.load({
|
|
7
|
-
id: 'dsh-cybernetics',
|
|
12
|
+
id: '@mars.liu/dsh-cybernetics',
|
|
8
13
|
factory: (require) => {
|
|
9
14
|
var module = { exports: {} };
|
|
10
15
|
var exports = module.exports;
|
|
@@ -35,6 +40,28 @@ window.__ModuleLoader__.load({
|
|
|
35
40
|
'agent.noData': '目标数据未同步',
|
|
36
41
|
'agents.empty': '本会话当前没有子 agent。',
|
|
37
42
|
'agents.count': '{running}/{total} 个 agent 运行中',
|
|
43
|
+
// v0.6 multi-goal projection (`cyberneticsGoals`) — every goal of the
|
|
44
|
+
// session in one tab, updated in place.
|
|
45
|
+
'goals.count': '{count} 个目标',
|
|
46
|
+
'goal.round': '第 {round} 轮',
|
|
47
|
+
'goal.subgoal': '当前子目标',
|
|
48
|
+
'goal.depth.quick': '轻量',
|
|
49
|
+
'goal.depth.full': '完整',
|
|
50
|
+
'goal.executor.ephemeral': '临时执行者',
|
|
51
|
+
'goal.executor.persistent': '持久执行者',
|
|
52
|
+
'goalStatus.pending': '待启动',
|
|
53
|
+
'goalStatus.running': '进行中',
|
|
54
|
+
'goalStatus.paused': '已暂停',
|
|
55
|
+
'goalStatus.met': '已完成',
|
|
56
|
+
'goalStatus.cancelled': '已取消',
|
|
57
|
+
'goalStatus.superseded': '已被取代',
|
|
58
|
+
'goalStatus.failed': '受阻',
|
|
59
|
+
// v0.6 append-only goal lineage lines (server sets these fields)
|
|
60
|
+
'goal.supersededBy': '已被「{title}」取代',
|
|
61
|
+
'goal.revisedFrom': '修订自「{title}」',
|
|
62
|
+
// v0.6 executor line: the durable teammate's name (persistent only)
|
|
63
|
+
'goal.member': '成员 {name}',
|
|
64
|
+
'control.goals': '目标',
|
|
38
65
|
};
|
|
39
66
|
const en = {
|
|
40
67
|
'view.goal': 'Goal',
|
|
@@ -57,6 +84,24 @@ window.__ModuleLoader__.load({
|
|
|
57
84
|
'agent.noData': 'Goal data not synced',
|
|
58
85
|
'agents.empty': 'No child agents in this session yet.',
|
|
59
86
|
'agents.count': '{running}/{total} agents running',
|
|
87
|
+
'goals.count': '{count} goals',
|
|
88
|
+
'goal.round': 'round {round}',
|
|
89
|
+
'goal.subgoal': 'Current subgoal',
|
|
90
|
+
'goal.depth.quick': 'Quick',
|
|
91
|
+
'goal.depth.full': 'Full',
|
|
92
|
+
'goal.executor.ephemeral': 'Ephemeral executor',
|
|
93
|
+
'goal.executor.persistent': 'Persistent executor',
|
|
94
|
+
'goalStatus.pending': 'Pending',
|
|
95
|
+
'goalStatus.running': 'Running',
|
|
96
|
+
'goalStatus.paused': 'Paused',
|
|
97
|
+
'goalStatus.met': 'Complete',
|
|
98
|
+
'goalStatus.cancelled': 'Cancelled',
|
|
99
|
+
'goalStatus.superseded': 'Superseded',
|
|
100
|
+
'goalStatus.failed': 'Blocked',
|
|
101
|
+
'goal.supersededBy': 'Superseded by "{title}"',
|
|
102
|
+
'goal.revisedFrom': 'Revised from "{title}"',
|
|
103
|
+
'goal.member': 'Member {name}',
|
|
104
|
+
'control.goals': 'Goals',
|
|
60
105
|
};
|
|
61
106
|
//#endregion
|
|
62
107
|
|
|
@@ -68,12 +113,104 @@ window.__ModuleLoader__.load({
|
|
|
68
113
|
};
|
|
69
114
|
|
|
70
115
|
//#region goal view component
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
|
|
116
|
+
// v0.6: the tab is driven by the plugin's own `cyberneticsGoals`
|
|
117
|
+
// projection — the WHOLE multi-goal board of this session, pushed live
|
|
118
|
+
// by the host (one whole-value `cybernetics/goals` event per change, no
|
|
119
|
+
// polling). When that key is absent (older plugin/host) or carries an
|
|
120
|
+
// empty board, fall back to dsh-goal's single-slot `goal` projection,
|
|
121
|
+
// which is exactly the pre-v0.6 rendering.
|
|
122
|
+
const GOAL_STATUS_KEY = {
|
|
123
|
+
pending: 'goalStatus.pending',
|
|
124
|
+
running: 'goalStatus.running',
|
|
125
|
+
paused: 'goalStatus.paused',
|
|
126
|
+
met: 'goalStatus.met',
|
|
127
|
+
cancelled: 'goalStatus.cancelled',
|
|
128
|
+
superseded: 'goalStatus.superseded',
|
|
129
|
+
failed: 'goalStatus.failed',
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
function goalStatusStyle(status) {
|
|
133
|
+
if (status === 'running') return { color: '#3fb950' };
|
|
134
|
+
if (status === 'failed') return { color: '#d29922' };
|
|
135
|
+
if (status === 'met') return { color: '#58a6ff' };
|
|
136
|
+
return { color: '#8b949e' };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// End-of-life statuses: the same two-segment rule the goal tab uses
|
|
140
|
+
// (unfinished first, finished after). The server already sorts that way;
|
|
141
|
+
// this stable partition keeps the invariant even if a view arrives with a
|
|
142
|
+
// different order, without re-sorting anything inside a segment.
|
|
143
|
+
const COMPLETED_GOAL_STATUSES = ['met', 'cancelled', 'superseded'];
|
|
144
|
+
function groupGoals(goals) {
|
|
145
|
+
const open = [];
|
|
146
|
+
const done = [];
|
|
147
|
+
for (const g of goals) {
|
|
148
|
+
if (COMPLETED_GOAL_STATUSES.indexOf(g.status) >= 0) done.push(g);
|
|
149
|
+
else open.push(g);
|
|
150
|
+
}
|
|
151
|
+
return open.concat(done);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// One goal row: title + status badge + round + current subgoal + tags.
|
|
155
|
+
// Append-only lineage (v0.6): a superseded goal names its successor, a
|
|
156
|
+
// revised goal names where it came from — titles resolved from the board
|
|
157
|
+
// snapshot, falling back to the goalId when the successor is not listed.
|
|
158
|
+
function GoalRow(props) {
|
|
159
|
+
const t = props.t;
|
|
160
|
+
const g = props.goal;
|
|
161
|
+
const titles = props.titles || {};
|
|
162
|
+
const statusKey = GOAL_STATUS_KEY[g.status] ?? 'goalStatus.pending';
|
|
163
|
+
const depthKey = g.depth === 'quick' ? 'goal.depth.quick' : 'goal.depth.full';
|
|
164
|
+
const executorKey = g.executor === 'persistent' ? 'goal.executor.persistent' : 'goal.executor.ephemeral';
|
|
165
|
+
return jsx.jsxs('div', { style: styles.goalCard, children: [
|
|
166
|
+
jsx.jsxs('div', { style: styles.goalHead, children: [
|
|
167
|
+
jsx.jsx('span', { style: styles.goalTitle, children: g.title || g.goalId }),
|
|
168
|
+
jsx.jsx('span', { style: { ...styles.badge, ...goalStatusStyle(g.status) }, children: t(statusKey) }),
|
|
169
|
+
] }),
|
|
170
|
+
jsx.jsxs('div', { style: styles.goalMetaRow, children: [
|
|
171
|
+
jsx.jsx('span', { style: styles.meta, children: t('goal.round', { round: g.round ?? 0 }) }),
|
|
172
|
+
jsx.jsx('span', { style: styles.tag, children: t(depthKey) }),
|
|
173
|
+
jsx.jsx('span', { style: styles.tag, children: t(executorKey) }),
|
|
174
|
+
g.memberName
|
|
175
|
+
? jsx.jsx('span', { style: styles.tag, children: t('goal.member', { name: g.memberName }) })
|
|
176
|
+
: null,
|
|
177
|
+
] }),
|
|
178
|
+
g.supersededBy
|
|
179
|
+
? jsx.jsx('div', { style: styles.lineage, children: t('goal.supersededBy', { title: titles[g.supersededBy] || g.supersededBy }) })
|
|
180
|
+
: null,
|
|
181
|
+
g.parentGoalId
|
|
182
|
+
? jsx.jsx('div', { style: styles.lineage, children: t('goal.revisedFrom', { title: titles[g.parentGoalId] || g.parentGoalId }) })
|
|
183
|
+
: null,
|
|
184
|
+
g.subgoal
|
|
185
|
+
? jsx.jsxs('div', { style: styles.goalSubgoal, children: [t('goal.subgoal') + ':', g.subgoal] })
|
|
186
|
+
: null,
|
|
187
|
+
] });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Multi-goal list. Order is the projection's (server-side two segments:
|
|
191
|
+
// unfinished goals first, finished after; newest change first inside each),
|
|
192
|
+
// so the client renders verbatim and never re-sorts.
|
|
193
|
+
function GoalsList(props) {
|
|
194
|
+
const t = props.t;
|
|
195
|
+
const goals = props.goals;
|
|
196
|
+
const titles = {};
|
|
197
|
+
for (const g of goals) titles[g.goalId] = g.title || g.goalId;
|
|
198
|
+
return jsx.jsxs('div', { style: styles.root, children: [
|
|
199
|
+
jsx.jsx('div', { style: styles.badgeRow, children:
|
|
200
|
+
jsx.jsx('span', { style: styles.badge, children: t('goals.count', { count: goals.length }) }) }),
|
|
201
|
+
...goals.map(function (g) {
|
|
202
|
+
return jsx.jsx(GoalRow, { goal: g, t: t, titles: titles }, g.goalId + ':' + (g.updatedAt ?? 0));
|
|
203
|
+
}),
|
|
204
|
+
jsx.jsx('div', { style: styles.hint, children: t('loopHint') }),
|
|
205
|
+
] });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// props.raw: the session `goal` projection snapshot value. It may be the
|
|
209
|
+
// raw state ({current: GoalProjection | null, ...}) or the projection
|
|
210
|
+
// value itself — normalize both; undefined = not yet arrived.
|
|
211
|
+
function SingleGoalView(props) {
|
|
75
212
|
const t = props.t;
|
|
76
|
-
const raw =
|
|
213
|
+
const raw = props.raw;
|
|
77
214
|
if (raw === undefined) return null;
|
|
78
215
|
const goal = raw !== null && typeof raw === 'object' && 'current' in raw ? raw.current : raw;
|
|
79
216
|
if (goal === null || goal === undefined) {
|
|
@@ -93,6 +230,16 @@ window.__ModuleLoader__.load({
|
|
|
93
230
|
] });
|
|
94
231
|
}
|
|
95
232
|
|
|
233
|
+
function GoalView(props) {
|
|
234
|
+
const goalsRaw = react.useSyncExternalStore(props.goals.subscribe, props.goals.getSnapshot);
|
|
235
|
+
const singleRaw = react.useSyncExternalStore(props.goal.subscribe, props.goal.getSnapshot);
|
|
236
|
+
const goals = goalsRaw !== null && typeof goalsRaw === 'object' && Array.isArray(goalsRaw.goals)
|
|
237
|
+
? goalsRaw.goals
|
|
238
|
+
: [];
|
|
239
|
+
if (goals.length > 0) return jsx.jsx(GoalsList, { t: props.t, goals: goals });
|
|
240
|
+
return jsx.jsx(SingleGoalView, { t: props.t, raw: singleRaw });
|
|
241
|
+
}
|
|
242
|
+
|
|
96
243
|
var styles = {
|
|
97
244
|
root: { display: 'flex', flexDirection: 'column', gap: 8, padding: '12px 16px', overflow: 'auto' },
|
|
98
245
|
empty: { padding: '12px 16px', opacity: 0.6 },
|
|
@@ -102,6 +249,17 @@ window.__ModuleLoader__.load({
|
|
|
102
249
|
blocked: { color: '#d29922', fontSize: 12 },
|
|
103
250
|
meta: { opacity: 0.65, fontSize: 12 },
|
|
104
251
|
hint: { opacity: 0.45, fontSize: 11 },
|
|
252
|
+
// v0.6 multi-goal rows
|
|
253
|
+
goalCard: { display: 'flex', flexDirection: 'column', gap: 4, padding: '8px 10px', border: '1px solid rgba(127,127,127,0.25)', borderRadius: 6 },
|
|
254
|
+
goalHead: { display: 'flex', alignItems: 'center', gap: 6, minWidth: 0 },
|
|
255
|
+
goalTitle: { fontSize: 13, fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
|
|
256
|
+
goalMetaRow: { display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' },
|
|
257
|
+
tag: { fontSize: 10, padding: '0 6px', borderRadius: 999, border: '1px solid rgba(127,127,127,0.35)', opacity: 0.75 },
|
|
258
|
+
goalSubgoal: { fontSize: 12, opacity: 0.85, whiteSpace: 'pre-wrap', lineHeight: 1.45 },
|
|
259
|
+
lineage: { fontSize: 11, opacity: 0.6, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
|
|
260
|
+
// v0.6 control tab executor rows
|
|
261
|
+
controlGoalRow: { display: 'flex', alignItems: 'center', gap: 6, minWidth: 0, flexWrap: 'wrap', padding: '2px 0' },
|
|
262
|
+
goalCards: { display: 'flex', flexDirection: 'column', gap: 4, marginBottom: 4 },
|
|
105
263
|
agentRow: { borderLeft: '2px solid transparent', borderRadius: 4 },
|
|
106
264
|
agentMain: { display: 'flex', flexDirection: 'column', gap: 2 },
|
|
107
265
|
agentHead: { display: 'flex', alignItems: 'center', gap: 6, minWidth: 0 },
|
|
@@ -122,13 +280,39 @@ window.__ModuleLoader__.load({
|
|
|
122
280
|
//#endregion
|
|
123
281
|
|
|
124
282
|
//#region control view component
|
|
125
|
-
// The「控制 / Control」tab: every
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
283
|
+
// The「控制 / Control」tab: every goal of the session with its EXECUTOR
|
|
284
|
+
// line (substrate + the durable teammate's name when it runs on one),
|
|
285
|
+
// plus every agent of the current session — the lead itself and the
|
|
286
|
+
// subagent catalog (recursively) — with its run state and its own durable
|
|
287
|
+
// goal (dsh-goal projection values). Goal data comes from the same
|
|
288
|
+
// `cyberneticsGoals` whole-value projection the goal tab reads; the agent
|
|
289
|
+
// tree comes from the session list store, updated live by the Session
|
|
290
|
+
// Controller via the control stream (running flips, goal/control frames).
|
|
130
291
|
const MAX_TREE_DEPTH = 3;
|
|
131
292
|
|
|
293
|
+
// No goal face (older host/plugin): keep the agent tree rendering exactly
|
|
294
|
+
// as before instead of throwing.
|
|
295
|
+
const NOOP_GOALS_FACE = { subscribe: () => () => {}, getSnapshot: () => undefined };
|
|
296
|
+
|
|
297
|
+
// One goal row of the control tab: status + title + executor + durable
|
|
298
|
+
// member name + round. Same fields as the goal tab, restated as the
|
|
299
|
+
// operator's line — who is executing this goal and how far it got.
|
|
300
|
+
function ControlGoalRow(props) {
|
|
301
|
+
const t = props.t;
|
|
302
|
+
const g = props.goal;
|
|
303
|
+
const statusKey = GOAL_STATUS_KEY[g.status] ?? 'goalStatus.pending';
|
|
304
|
+
const executorKey = g.executor === 'persistent' ? 'goal.executor.persistent' : 'goal.executor.ephemeral';
|
|
305
|
+
return jsx.jsxs('div', { style: styles.controlGoalRow, children: [
|
|
306
|
+
jsx.jsx('span', { style: { ...styles.badge, ...goalStatusStyle(g.status) }, children: t(statusKey) }),
|
|
307
|
+
jsx.jsx('span', { style: styles.agentName, children: g.title || g.goalId }),
|
|
308
|
+
jsx.jsx('span', { style: styles.tag, children: t(executorKey) }),
|
|
309
|
+
g.memberName
|
|
310
|
+
? jsx.jsx('span', { style: styles.tag, children: t('goal.member', { name: g.memberName }) })
|
|
311
|
+
: null,
|
|
312
|
+
jsx.jsx('span', { style: styles.meta, children: t('goal.round', { round: g.round ?? 0 }) }),
|
|
313
|
+
] });
|
|
314
|
+
}
|
|
315
|
+
|
|
132
316
|
/** Depth-first agent rows rooted at `rootId`: lead + subagentCatalog, recursed. */
|
|
133
317
|
function collectAgents(list, rootId) {
|
|
134
318
|
const rows = [];
|
|
@@ -184,9 +368,23 @@ window.__ModuleLoader__.load({
|
|
|
184
368
|
const t = props.t;
|
|
185
369
|
const sessions = props.sessions;
|
|
186
370
|
const list = react.useSyncExternalStore(sessions.list.subscribe, sessions.list.getSnapshot);
|
|
371
|
+
const goalsFace = props.goals ?? NOOP_GOALS_FACE;
|
|
372
|
+
const goalsRaw = react.useSyncExternalStore(goalsFace.subscribe, goalsFace.getSnapshot);
|
|
373
|
+
const goals = goalsRaw !== null && typeof goalsRaw === 'object' && Array.isArray(goalsRaw.goals)
|
|
374
|
+
? groupGoals(goalsRaw.goals)
|
|
375
|
+
: [];
|
|
187
376
|
const agents = collectAgents(list, props.sessionId);
|
|
188
377
|
const runningCount = agents.filter((a) => a.running).length;
|
|
189
378
|
return jsx.jsxs('div', { style: styles.root, children: [
|
|
379
|
+
goals.length > 0
|
|
380
|
+
? jsx.jsxs('div', { style: styles.goalCards, children: [
|
|
381
|
+
jsx.jsxs('div', { style: styles.badgeRow, children: [
|
|
382
|
+
jsx.jsx('span', { style: styles.meta, children: t('control.goals') }),
|
|
383
|
+
jsx.jsx('span', { style: styles.badge, children: t('goals.count', { count: goals.length }) }),
|
|
384
|
+
] }),
|
|
385
|
+
...goals.map((g) => jsx.jsx(ControlGoalRow, { goal: g, t }, 'goal:' + g.goalId + ':' + (g.updatedAt ?? 0))),
|
|
386
|
+
] })
|
|
387
|
+
: null,
|
|
190
388
|
jsx.jsxs('div', { style: styles.badgeRow, children: [
|
|
191
389
|
jsx.jsx('span', { style: styles.badge, children: t('agents.count', { total: agents.length, running: runningCount }) }),
|
|
192
390
|
] }),
|
|
@@ -218,19 +416,39 @@ window.__ModuleLoader__.load({
|
|
|
218
416
|
inject: (sessionId) => {
|
|
219
417
|
const binding = ctx.sessions.binding(sessionId);
|
|
220
418
|
if (binding === undefined) throw new Error('dsh-cybernetics: session "' + sessionId + '" unavailable');
|
|
221
|
-
|
|
419
|
+
// v0.6: both faces. `cyberneticsGoals` carries the whole goal
|
|
420
|
+
// board (live, pushed by the host); `goal` stays as the
|
|
421
|
+
// backward-compatible fallback for older plugin/host builds.
|
|
422
|
+
return {
|
|
423
|
+
goals: binding.session.projections.faceOf('cyberneticsGoals'),
|
|
424
|
+
goal: binding.session.projections.faceOf('goal'),
|
|
425
|
+
};
|
|
222
426
|
},
|
|
223
427
|
}, GoalView));
|
|
224
428
|
|
|
225
429
|
// Control tab: goals + run states of every agent in this session,
|
|
226
430
|
// driven by the session list store (live via the control stream).
|
|
431
|
+
// The goal face is optional (older host/plugin): the agent tree then
|
|
432
|
+
// renders exactly as before.
|
|
227
433
|
ctx.slots.inject('conversation.view', () => ctx.slots.register({
|
|
228
434
|
name: 'conversation.view',
|
|
229
435
|
id: 'cybernetics-control',
|
|
230
436
|
order: 16,
|
|
231
437
|
locale: NS,
|
|
232
438
|
label: () => t('view.control'),
|
|
233
|
-
inject: (sessionId) =>
|
|
439
|
+
inject: (sessionId) => {
|
|
440
|
+
let goals = NOOP_GOALS_FACE;
|
|
441
|
+
try {
|
|
442
|
+
const binding = ctx.sessions.binding(sessionId);
|
|
443
|
+
const face = binding && binding.session && binding.session.projections
|
|
444
|
+
? binding.session.projections.faceOf('cyberneticsGoals')
|
|
445
|
+
: undefined;
|
|
446
|
+
if (face) goals = face;
|
|
447
|
+
} catch {
|
|
448
|
+
// No multi-goal projection: keep the control tab's agent tree.
|
|
449
|
+
}
|
|
450
|
+
return { sessionId, sessions: ctx.sessions, goals };
|
|
451
|
+
},
|
|
234
452
|
}, ControlView));
|
|
235
453
|
}
|
|
236
454
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mars.liu/dsh-cybernetics",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"private": false,
|
|
5
|
+
"files": [
|
|
6
|
+
"src",
|
|
7
|
+
"lib",
|
|
8
|
+
"workflows",
|
|
9
|
+
"docs",
|
|
10
|
+
"locale",
|
|
11
|
+
"cordis.patch.yml",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
5
14
|
"description": "Cybernetic work protocol for DSH: the session goal (dsh-goal) driven loop of task -> verify -> triage agents, implicitly advanced via one workflow run per round. Server plugin + client goal tab.",
|
|
6
15
|
"type": "module",
|
|
7
16
|
"main": "src/index.ts",
|
|
@@ -29,5 +38,8 @@
|
|
|
29
38
|
"platform": "web",
|
|
30
39
|
"immediately": true
|
|
31
40
|
}
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"@deepseek-ai/dsh-session": "*"
|
|
32
44
|
}
|
|
33
45
|
}
|
package/src/controller.ts
CHANGED
|
@@ -22,6 +22,8 @@ export interface RoundResult {
|
|
|
22
22
|
verdict?: unknown
|
|
23
23
|
triage?: unknown
|
|
24
24
|
failed?: string
|
|
25
|
+
/** Execution substrate used for this round (v0.6 audit). */
|
|
26
|
+
executor?: 'ephemeral' | 'persistent'
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
/** LLM target overrides for one sub-agent role; workflow `agent()` opts subset. */
|
|
@@ -70,9 +72,60 @@ export interface LoopState {
|
|
|
70
72
|
role: string
|
|
71
73
|
/** Detailed per-round work order extracted by the calling model; passed to the task agent verbatim. */
|
|
72
74
|
brief?: string
|
|
73
|
-
/**
|
|
74
|
-
|
|
75
|
+
/** Round depth: 'quick' = task agent only (lightweight analysis); 'full' = task→verify→triage (default). */
|
|
76
|
+
depth?: 'quick' | 'full'
|
|
77
|
+
/** Execution substrate: 'ephemeral' (workflow subagents, default) or 'persistent' (Agent Teams teammate). */
|
|
78
|
+
executor?: 'ephemeral' | 'persistent'
|
|
79
|
+
/** Persistent teammate name, set on the first persistent round and reused afterwards. */
|
|
80
|
+
memberName?: string
|
|
81
|
+
/**
|
|
82
|
+
* Lifecycle: running (a round in flight) / paused / met (verify passed, awaits
|
|
83
|
+
* user) / cancelled / superseded (a revision created a successor goal; this
|
|
84
|
+
* goal keeps its final state and is never driven again).
|
|
85
|
+
*/
|
|
86
|
+
status?: 'running' | 'paused' | 'met' | 'cancelled' | 'superseded'
|
|
75
87
|
paused?: { reason: ResumableReason; note: string }
|
|
88
|
+
/**
|
|
89
|
+
* Append-only goal lineage (v0.6, user 2026-11-02): a revision never edits the
|
|
90
|
+
* objective in place — it creates a NEW goal, and the old one keeps its final
|
|
91
|
+
* state. `parentGoalId` = the goal this one was revised from.
|
|
92
|
+
*/
|
|
93
|
+
parentGoalId?: string
|
|
94
|
+
/** Set on a superseded goal: the id of the successor that replaced it. */
|
|
95
|
+
supersededBy?: string
|
|
96
|
+
/**
|
|
97
|
+
* In-place revision log kept for wire compatibility (pre-append-only shape).
|
|
98
|
+
* v0.6 append-only model: a revision creates a successor goal instead, so new
|
|
99
|
+
* revisions are recorded as `parentGoalId`/`supersededBy` plus the
|
|
100
|
+
* `.cybernetics/revisions-<newGoalId>.jsonl` audit line, and this stays empty.
|
|
101
|
+
*/
|
|
102
|
+
revisions?: Array<{ version: number; reason: string; at: number }>
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** One `revise` transition as written to `.cybernetics/revisions-<newGoalId>.jsonl`. */
|
|
106
|
+
export interface GoalRevisionRecord {
|
|
107
|
+
/** Superseded goal id. */
|
|
108
|
+
from: string
|
|
109
|
+
/** Newly created successor goal id. */
|
|
110
|
+
to: string
|
|
111
|
+
/** Why the goal changed (required on the `revise` action). */
|
|
112
|
+
reason: string
|
|
113
|
+
/** Epoch milliseconds of the revision. */
|
|
114
|
+
at: number
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Input for the append-only goal creation path. */
|
|
118
|
+
export interface GoalSeed {
|
|
119
|
+
goalId: string
|
|
120
|
+
title?: string
|
|
121
|
+
/** Overrides the inherited objective when present. */
|
|
122
|
+
objective?: string
|
|
123
|
+
/** Goal this one was revised from (lineage link). */
|
|
124
|
+
parentGoalId?: string
|
|
125
|
+
depth?: 'quick' | 'full'
|
|
126
|
+
executor?: 'ephemeral' | 'persistent'
|
|
127
|
+
role?: string
|
|
128
|
+
subgoal?: string
|
|
76
129
|
}
|
|
77
130
|
|
|
78
131
|
/** Append one round's structured result to the per-workspace audit log. */
|
|
@@ -85,6 +138,60 @@ async function writeRoundLog(root: string, goalId: string | undefined, round: nu
|
|
|
85
138
|
await writeFile(join(root, STATE_DIR, `round-${slug}-${round}.json`), JSON.stringify(result, null, 2))
|
|
86
139
|
}
|
|
87
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Append one revision line to `.cybernetics/revisions-<newGoalId>.jsonl`.
|
|
143
|
+
* One line per transition, so a revision chain is readable (and greppable)
|
|
144
|
+
* without reconstructing it from per-goal fields. The directory is created
|
|
145
|
+
* lazily; the caller treats a write failure as non-fatal (see `revise`).
|
|
146
|
+
*/
|
|
147
|
+
export async function writeRevisionLog(root: string, newGoalId: string, record: GoalRevisionRecord): Promise<string> {
|
|
148
|
+
await mkdir(join(root, STATE_DIR), { recursive: true })
|
|
149
|
+
const slug = newGoalId.replace(/[^a-zA-Z0-9._-]/g, '_')
|
|
150
|
+
const file = join(root, STATE_DIR, `revisions-${slug}.jsonl`)
|
|
151
|
+
await writeFile(file, `${JSON.stringify(record)}\n`, { flag: 'a' })
|
|
152
|
+
return file
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Build the successor goal of an append-only revision. Everything the old goal
|
|
157
|
+
* owned that is not per-round state — title, objective, depth, executor, role —
|
|
158
|
+
* is inherited unless a seed field overrides it; the new goal starts at round 0
|
|
159
|
+
* and carries `parentGoalId`. Pure: it neither mutates nor registers anything.
|
|
160
|
+
*/
|
|
161
|
+
export function createGoal(previous: LoopState | undefined, seed: GoalSeed): LoopState {
|
|
162
|
+
const objective = seed.objective ?? previous?.objective ?? previous?.title ?? ''
|
|
163
|
+
const title = seed.title ?? previous?.title ?? objective.split('\n')[0].slice(0, 60)
|
|
164
|
+
const goal: LoopState = {
|
|
165
|
+
goalId: seed.goalId,
|
|
166
|
+
title,
|
|
167
|
+
objective,
|
|
168
|
+
round: 0,
|
|
169
|
+
doneSummary: '',
|
|
170
|
+
subgoal: seed.subgoal ?? (objective.length > 0 ? objective : title),
|
|
171
|
+
role: seed.role ?? previous?.role ?? 'default',
|
|
172
|
+
status: 'running',
|
|
173
|
+
}
|
|
174
|
+
const depth = seed.depth ?? previous?.depth
|
|
175
|
+
if (depth !== undefined) goal.depth = depth
|
|
176
|
+
const executor = seed.executor ?? previous?.executor
|
|
177
|
+
if (executor !== undefined) goal.executor = executor
|
|
178
|
+
const parentGoalId = seed.parentGoalId ?? previous?.goalId
|
|
179
|
+
if (parentGoalId !== undefined) goal.parentGoalId = parentGoalId
|
|
180
|
+
return goal
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Freeze a goal as superseded by a successor. Mutates the loop in place (the
|
|
185
|
+
* registry holds the same object); the paused marker is dropped so the view
|
|
186
|
+
* does not mix "awaiting human" with "replaced".
|
|
187
|
+
*/
|
|
188
|
+
export function supersedeGoal(previous: LoopState, newGoalId: string): LoopState {
|
|
189
|
+
previous.status = 'superseded'
|
|
190
|
+
previous.supersededBy = newGoalId
|
|
191
|
+
previous.paused = undefined
|
|
192
|
+
return previous
|
|
193
|
+
}
|
|
194
|
+
|
|
88
195
|
/**
|
|
89
196
|
* Start one round's workflow run without awaiting it. Returns the round
|
|
90
197
|
* number and the live run; the caller either awaits `run.result` itself
|
|
@@ -109,6 +216,7 @@ export async function startRound(
|
|
|
109
216
|
state: { round, doneSummary: state.doneSummary },
|
|
110
217
|
subgoal: state.subgoal,
|
|
111
218
|
brief: state.brief,
|
|
219
|
+
depth: state.depth ?? 'full',
|
|
112
220
|
role: state.role,
|
|
113
221
|
roles: config.roles ?? {},
|
|
114
222
|
},
|
|
@@ -137,7 +245,9 @@ export async function foldRound(
|
|
|
137
245
|
} else {
|
|
138
246
|
result = settled.value as RoundResult
|
|
139
247
|
}
|
|
140
|
-
|
|
248
|
+
// v0.6 audit: record which substrate ran the round.
|
|
249
|
+
const logged: RoundResult = { ...result, executor: state.executor ?? 'ephemeral' }
|
|
250
|
+
await writeRoundLog(root, state.goalId, round, logged)
|
|
141
251
|
return { result, decision: decide(result, state, config) }
|
|
142
252
|
}
|
|
143
253
|
|