@khorsheed/dsh-room 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.i18n.yaml +6 -0
- package/README.md +48 -0
- package/README.zh.md +48 -0
- package/cordis.patch.yml +6 -0
- package/lib/client.js +11481 -0
- package/lib/index.js +3231 -0
- package/lib/invariant.js +26 -0
- package/lib/journal-CuRnWk66.js +435 -0
- package/lib/tool.js +350 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +2615 -0
- package/lib/typert.remote-client.d.ts +55 -0
- package/lib/typert.remote-client.js +1711 -0
- package/lib/types/adapter.d.ts +42 -0
- package/lib/types/adapter.js +41 -0
- package/lib/types/agent-setup.d.ts +141 -0
- package/lib/types/agent-setup.js +145 -0
- package/lib/types/client/InviteAgentAction.d.ts +13 -0
- package/lib/types/client/InviteAgentAction.js +45 -0
- package/lib/types/client/InviteDialog.d.ts +90 -0
- package/lib/types/client/InviteDialog.js +216 -0
- package/lib/types/client/MemberCard.d.ts +33 -0
- package/lib/types/client/MemberCard.js +32 -0
- package/lib/types/client/MembersView.d.ts +5 -0
- package/lib/types/client/MembersView.js +168 -0
- package/lib/types/client/RoomComposer.d.ts +21 -0
- package/lib/types/client/RoomComposer.js +211 -0
- package/lib/types/client/RoomDockCapsules.d.ts +64 -0
- package/lib/types/client/RoomDockCapsules.js +277 -0
- package/lib/types/client/RoomEventView.d.ts +11 -0
- package/lib/types/client/RoomEventView.js +17 -0
- package/lib/types/client/RoomModelPicker.d.ts +18 -0
- package/lib/types/client/RoomModelPicker.js +134 -0
- package/lib/types/client/RoomPlanView.d.ts +17 -0
- package/lib/types/client/RoomPlanView.js +88 -0
- package/lib/types/client/RoomQueueStrip.d.ts +26 -0
- package/lib/types/client/RoomQueueStrip.js +28 -0
- package/lib/types/client/RoomRecoveryView.d.ts +11 -0
- package/lib/types/client/RoomRecoveryView.js +38 -0
- package/lib/types/client/RoomRelayView.d.ts +12 -0
- package/lib/types/client/RoomRelayView.js +20 -0
- package/lib/types/client/RoomRunView.d.ts +15 -0
- package/lib/types/client/RoomRunView.js +50 -0
- package/lib/types/client/RoomSpeechView.d.ts +18 -0
- package/lib/types/client/RoomSpeechView.js +62 -0
- package/lib/types/client/RoomStatsLine.d.ts +61 -0
- package/lib/types/client/RoomStatsLine.js +138 -0
- package/lib/types/client/RoomTaskLineView.d.ts +13 -0
- package/lib/types/client/RoomTaskLineView.js +21 -0
- package/lib/types/client/RoomTodoStrip.d.ts +24 -0
- package/lib/types/client/RoomTodoStrip.js +64 -0
- package/lib/types/client/format.d.ts +31 -0
- package/lib/types/client/format.js +48 -0
- package/lib/types/client/icons.d.ts +27 -0
- package/lib/types/client/icons.js +11 -0
- package/lib/types/client/index.d.ts +39 -0
- package/lib/types/client/index.js +471 -0
- package/lib/types/client/locales.d.ts +422 -0
- package/lib/types/client/locales.js +413 -0
- package/lib/types/client/member-color.d.ts +13 -0
- package/lib/types/client/member-color.js +22 -0
- package/lib/types/client/name-pool.d.ts +17 -0
- package/lib/types/client/name-pool.js +25 -0
- package/lib/types/client/nodes.d.ts +136 -0
- package/lib/types/client/nodes.js +212 -0
- package/lib/types/client/preset-visibility.d.ts +91 -0
- package/lib/types/client/preset-visibility.js +113 -0
- package/lib/types/client/request-ids.d.ts +7 -0
- package/lib/types/client/request-ids.js +21 -0
- package/lib/types/client/room-store.d.ts +137 -0
- package/lib/types/client/room-store.js +244 -0
- package/lib/types/client/slots.d.ts +319 -0
- package/lib/types/client/slots.js +1 -0
- package/lib/types/deliveries.d.ts +5 -0
- package/lib/types/deliveries.js +20 -0
- package/lib/types/dispatch.d.ts +105 -0
- package/lib/types/dispatch.js +467 -0
- package/lib/types/index.d.ts +301 -0
- package/lib/types/index.js +1200 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/invariant.js +24 -0
- package/lib/types/journal.d.ts +117 -0
- package/lib/types/journal.js +434 -0
- package/lib/types/plan-service.d.ts +34 -0
- package/lib/types/plan-service.js +266 -0
- package/lib/types/plan.d.ts +168 -0
- package/lib/types/plan.js +494 -0
- package/lib/types/tool.d.ts +56 -0
- package/lib/types/tool.js +389 -0
- package/lib/types/types.d.ts +813 -0
- package/lib/types/types.js +1 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +131 -0
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
const nonempty = (value, field) => {
|
|
2
|
+
if (typeof value !== 'string' || value.trim() === '' || value.length > 32000)
|
|
3
|
+
throw new Error(`${field} must be non-empty and at most 32000 characters`);
|
|
4
|
+
};
|
|
5
|
+
const identifier = (value) => {
|
|
6
|
+
if (typeof value !== 'string' || !/^[a-zA-Z0-9][a-zA-Z0-9_.:-]{0,127}$/.test(value))
|
|
7
|
+
throw new Error('Invalid plan identity');
|
|
8
|
+
};
|
|
9
|
+
const references = (values, field, required = false) => {
|
|
10
|
+
if (!Array.isArray(values) || values.length > 100 || (required && values.length === 0))
|
|
11
|
+
throw new Error(`${field} requires ${required ? '1' : '0'}–100 entries`);
|
|
12
|
+
for (const value of values)
|
|
13
|
+
nonempty(value, field);
|
|
14
|
+
};
|
|
15
|
+
function evidence(value) {
|
|
16
|
+
nonempty(value.summary, 'Evidence summary');
|
|
17
|
+
references(value.references, 'Evidence references', true);
|
|
18
|
+
references(value.artifacts, 'Artifact locations');
|
|
19
|
+
}
|
|
20
|
+
function validateBudget(value) {
|
|
21
|
+
for (const [key, maximum] of [['maxParallel', 8], ['maxAttempts', 100], ['maxAttemptsPerTask', 10], ['maxActiveMs', 86400000]]) {
|
|
22
|
+
if (!Number.isSafeInteger(value[key]) || value[key] < 1 || value[key] > maximum)
|
|
23
|
+
throw new Error(`Invalid goal budget ${key}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const clone = (plan) => structuredClone(plan);
|
|
27
|
+
function stopClock(plan, now) {
|
|
28
|
+
if (plan.resumedAt !== undefined)
|
|
29
|
+
plan.activeMs += Math.max(0, now - plan.resumedAt);
|
|
30
|
+
delete plan.resumedAt;
|
|
31
|
+
}
|
|
32
|
+
export function activePlanMs(plan, now) {
|
|
33
|
+
return plan.activeMs + (plan.resumedAt === undefined ? 0 : Math.max(0, now - plan.resumedAt));
|
|
34
|
+
}
|
|
35
|
+
export function latestAttempt(task) { return task.attempts.at(-1); }
|
|
36
|
+
export function acceptedTask(plan, taskId) {
|
|
37
|
+
const task = plan.tasks.find(task => task.id === taskId);
|
|
38
|
+
if (task === undefined)
|
|
39
|
+
return false;
|
|
40
|
+
if (task.kind === 'task')
|
|
41
|
+
return task.status === 'accepted';
|
|
42
|
+
const children = plan.tasks.filter(child => child.parentId === task.id);
|
|
43
|
+
return children.length > 0 && children.every(child => acceptedTask(plan, child.id));
|
|
44
|
+
}
|
|
45
|
+
function dependenciesOf(plan, task) {
|
|
46
|
+
const dependencies = [...task.dependsOn];
|
|
47
|
+
const seen = new Set([task.id]);
|
|
48
|
+
let parentId = task.parentId;
|
|
49
|
+
while (parentId !== undefined) {
|
|
50
|
+
if (seen.has(parentId))
|
|
51
|
+
throw new Error('Parent cycle');
|
|
52
|
+
seen.add(parentId);
|
|
53
|
+
const parent = plan.tasks.find(task => task.id === parentId);
|
|
54
|
+
if (parent === undefined)
|
|
55
|
+
throw new Error('Missing parent');
|
|
56
|
+
dependencies.push(...parent.dependsOn);
|
|
57
|
+
parentId = parent.parentId;
|
|
58
|
+
}
|
|
59
|
+
return dependencies;
|
|
60
|
+
}
|
|
61
|
+
/** Accepted leaves behind direct and inherited group prerequisites, once each. */
|
|
62
|
+
export function dependencyResults(plan, task) {
|
|
63
|
+
const leaves = new Map();
|
|
64
|
+
const visit = (id) => {
|
|
65
|
+
const dependency = plan.tasks.find(candidate => candidate.id === id);
|
|
66
|
+
if (dependency === undefined || !acceptedTask(plan, id))
|
|
67
|
+
return;
|
|
68
|
+
if (dependency.kind === 'task')
|
|
69
|
+
leaves.set(id, dependency);
|
|
70
|
+
else
|
|
71
|
+
for (const child of plan.tasks.filter(candidate => candidate.parentId === id))
|
|
72
|
+
visit(child.id);
|
|
73
|
+
};
|
|
74
|
+
for (const id of dependenciesOf(plan, task))
|
|
75
|
+
visit(id);
|
|
76
|
+
return [...leaves.values()].map(dependency => {
|
|
77
|
+
const submission = dependency.attempts.at(-1)?.submission;
|
|
78
|
+
return { taskId: dependency.id, ...submission === undefined ? {} : { submission } };
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
export function readyPlanTasks(plan) {
|
|
82
|
+
if (plan.status !== 'running')
|
|
83
|
+
return [];
|
|
84
|
+
return plan.tasks.filter(task => task.kind === 'task' && task.status === 'pending' && dependenciesOf(plan, task).every(id => acceptedTask(plan, id)));
|
|
85
|
+
}
|
|
86
|
+
export function activePlanAttempts(plan) {
|
|
87
|
+
return plan.tasks.flatMap(task => task.attempts.filter(attempt => attempt.settledAt === undefined && ['queued', 'running', 'submitted', 'uncertain'].includes(attempt.status)));
|
|
88
|
+
}
|
|
89
|
+
function taskAndAttempt(plan, taskId, attemptId) {
|
|
90
|
+
const task = plan.tasks.find(task => task.id === taskId);
|
|
91
|
+
const attempt = task?.attempts.at(-1);
|
|
92
|
+
if (task === undefined || attempt?.id !== attemptId)
|
|
93
|
+
throw new Error('Attempt is missing or superseded');
|
|
94
|
+
return [task, attempt];
|
|
95
|
+
}
|
|
96
|
+
function validateGraph(plan, memberIds) {
|
|
97
|
+
const stages = new Set();
|
|
98
|
+
for (const stage of plan.stages) {
|
|
99
|
+
identifier(stage.id);
|
|
100
|
+
nonempty(stage.title, 'Stage title');
|
|
101
|
+
if (stages.has(stage.id))
|
|
102
|
+
throw new Error('Duplicate stage');
|
|
103
|
+
stages.add(stage.id);
|
|
104
|
+
}
|
|
105
|
+
const tasks = new Map();
|
|
106
|
+
if (plan.tasks.length > 200 || plan.stages.length > 50)
|
|
107
|
+
throw new Error('Plan exceeds 200 tasks or 50 stages');
|
|
108
|
+
for (const task of plan.tasks) {
|
|
109
|
+
identifier(task.id);
|
|
110
|
+
nonempty(task.title, 'Task title');
|
|
111
|
+
if (tasks.has(task.id) || !stages.has(task.stageId))
|
|
112
|
+
throw new Error('Duplicate task or missing stage');
|
|
113
|
+
if (task.kind !== 'group' && task.kind !== 'task')
|
|
114
|
+
throw new Error('Invalid task kind');
|
|
115
|
+
if (task.kind === 'task') {
|
|
116
|
+
if (task.ownerMemberId === undefined || !memberIds.has(task.ownerMemberId))
|
|
117
|
+
throw new Error('Task owner is not in the room');
|
|
118
|
+
nonempty(task.instruction, 'Execution instruction');
|
|
119
|
+
references(task.criteria, 'Acceptance criteria', true);
|
|
120
|
+
}
|
|
121
|
+
references(task.inputRefs, 'Input references');
|
|
122
|
+
references(task.artifactPaths, 'Planned artifacts');
|
|
123
|
+
references(task.dependsOn, 'Task dependencies');
|
|
124
|
+
tasks.set(task.id, task);
|
|
125
|
+
}
|
|
126
|
+
const edges = new Map();
|
|
127
|
+
for (const task of plan.tasks) {
|
|
128
|
+
if (task.dependsOn.some(id => !tasks.has(id)))
|
|
129
|
+
throw new Error('Missing task dependency');
|
|
130
|
+
edges.set(task.id, dependenciesOf(plan, task));
|
|
131
|
+
if (task.parentId !== undefined) {
|
|
132
|
+
const parent = tasks.get(task.parentId);
|
|
133
|
+
if (parent?.kind !== 'group' || parent.stageId !== task.stageId)
|
|
134
|
+
throw new Error('Parent must be a group in the same stage');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// A group becomes accepted through its children. These edges must participate
|
|
138
|
+
// in cycle checks, otherwise a child waiting on its parent deadlocks forever.
|
|
139
|
+
for (const task of plan.tasks)
|
|
140
|
+
if (task.parentId !== undefined)
|
|
141
|
+
edges.get(task.parentId).push(task.id);
|
|
142
|
+
const visited = new Set();
|
|
143
|
+
const visiting = new Set();
|
|
144
|
+
const visit = (id) => {
|
|
145
|
+
if (visiting.has(id))
|
|
146
|
+
throw new Error('Task dependency or parent cycle');
|
|
147
|
+
if (visited.has(id))
|
|
148
|
+
return;
|
|
149
|
+
visiting.add(id);
|
|
150
|
+
for (const dependency of edges.get(id) ?? [])
|
|
151
|
+
visit(dependency);
|
|
152
|
+
visiting.delete(id);
|
|
153
|
+
visited.add(id);
|
|
154
|
+
};
|
|
155
|
+
for (const id of tasks.keys())
|
|
156
|
+
visit(id);
|
|
157
|
+
}
|
|
158
|
+
/** Pure transition: the service persists the returned revision before dispatch. */
|
|
159
|
+
export function changePlan(current, command, context) {
|
|
160
|
+
identifier(command.requestId);
|
|
161
|
+
const signature = planCommandSignature(command);
|
|
162
|
+
const receipt = current?.requests.find(receipt => receipt.id === command.requestId);
|
|
163
|
+
if (receipt !== undefined) {
|
|
164
|
+
if (receipt.signature !== signature)
|
|
165
|
+
throw new Error('Plan request identity was reused for different input');
|
|
166
|
+
return clone(current);
|
|
167
|
+
}
|
|
168
|
+
if (context.actor.kind === 'worker' && command.action !== 'submit')
|
|
169
|
+
throw new Error('Only the coordinator or human may organize or review a plan');
|
|
170
|
+
// Concurrent submissions must not defeat a human stop request. Fence it
|
|
171
|
+
// to the displayed goal so a stale browser cannot pause a replacement.
|
|
172
|
+
if (command.action === 'pause' && command.goalId !== undefined && command.goalId !== current?.id)
|
|
173
|
+
throw new Error('Goal changed; read the current plan before pausing');
|
|
174
|
+
const humanPause = context.actor.kind === 'human' && command.action === 'pause' && command.goalId === current?.id && current !== undefined;
|
|
175
|
+
if (command.expectedRevision !== (current?.revision ?? 0) && context.actor.kind !== 'worker' && !humanPause)
|
|
176
|
+
throw new Error('Plan revision changed; read the current plan before retrying');
|
|
177
|
+
let plan;
|
|
178
|
+
if (command.action === 'create') {
|
|
179
|
+
if (current !== undefined && !['completed', 'cancelled'].includes(current.status))
|
|
180
|
+
throw new Error('An unfinished plan already exists');
|
|
181
|
+
if (current?.id === command.id)
|
|
182
|
+
throw new Error('A new goal requires a new identity');
|
|
183
|
+
identifier(command.id);
|
|
184
|
+
nonempty(command.objective, 'Goal');
|
|
185
|
+
validateBudget(command.budget);
|
|
186
|
+
plan = { version: 1, id: command.id, revision: (current?.revision ?? 0) + 1, objective: command.objective,
|
|
187
|
+
status: command.mode === 'execute' ? 'running' : 'draft', budget: structuredClone(command.budget), activeMs: 0,
|
|
188
|
+
...command.mode === 'execute' ? { resumedAt: context.now } : {}, stages: [], tasks: [], requests: [] };
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
if (current === undefined)
|
|
192
|
+
throw new Error('No formal goal exists');
|
|
193
|
+
plan = clone(current);
|
|
194
|
+
if (['completed', 'cancelled'].includes(plan.status) && command.action !== 'reconcile')
|
|
195
|
+
throw new Error('The plan is closed');
|
|
196
|
+
switch (command.action) {
|
|
197
|
+
case 'extend':
|
|
198
|
+
plan.stages.push(...structuredClone(command.stages));
|
|
199
|
+
for (const task of command.tasks)
|
|
200
|
+
plan.tasks.push({ ...structuredClone(task), status: 'pending', attempts: [] });
|
|
201
|
+
validateGraph(plan, context.memberIds);
|
|
202
|
+
// Never invalidate the accepted output of a parent by appending work below it.
|
|
203
|
+
for (const task of command.tasks)
|
|
204
|
+
if (task.parentId !== undefined && acceptedTask(current, task.parentId))
|
|
205
|
+
throw new Error('Cannot extend an accepted group; add a new group');
|
|
206
|
+
break;
|
|
207
|
+
case 'pause':
|
|
208
|
+
nonempty(command.reason, 'Pause reason');
|
|
209
|
+
stopClock(plan, context.now);
|
|
210
|
+
plan.status = 'paused';
|
|
211
|
+
plan.reason = command.reason;
|
|
212
|
+
break;
|
|
213
|
+
case 'budget':
|
|
214
|
+
if (context.actor.kind !== 'human')
|
|
215
|
+
throw new Error('Only a human may change the authorized budget');
|
|
216
|
+
validateBudget(command.budget);
|
|
217
|
+
plan.budget = structuredClone(command.budget);
|
|
218
|
+
break;
|
|
219
|
+
case 'resume':
|
|
220
|
+
if (activePlanAttempts(plan).some(attempt => attempt.status === 'uncertain'))
|
|
221
|
+
throw new Error('Reconcile uncertain attempts before resuming');
|
|
222
|
+
if (activePlanMs(plan, context.now) >= plan.budget.maxActiveMs)
|
|
223
|
+
throw new Error('The active-time budget is exhausted');
|
|
224
|
+
if (plan.status !== 'running')
|
|
225
|
+
plan.resumedAt = context.now;
|
|
226
|
+
plan.status = 'running';
|
|
227
|
+
delete plan.reason;
|
|
228
|
+
break;
|
|
229
|
+
case 'cancel':
|
|
230
|
+
nonempty(command.reason, 'Cancellation reason');
|
|
231
|
+
stopClock(plan, context.now);
|
|
232
|
+
plan.status = 'cancelled';
|
|
233
|
+
plan.reason = command.reason;
|
|
234
|
+
for (const task of plan.tasks)
|
|
235
|
+
if (task.status === 'pending') {
|
|
236
|
+
task.status = 'cancelled';
|
|
237
|
+
for (const attempt of task.attempts)
|
|
238
|
+
if (attempt.status === 'queued') {
|
|
239
|
+
attempt.status = 'cancelled';
|
|
240
|
+
attempt.settledAt = context.now;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
break;
|
|
244
|
+
case 'submit': {
|
|
245
|
+
const [task, attempt] = taskAndAttempt(plan, command.taskId, command.attemptId);
|
|
246
|
+
if (context.actor.kind === 'worker' && task.ownerMemberId !== context.actor.memberId)
|
|
247
|
+
throw new Error('Workers may only submit their own current attempt');
|
|
248
|
+
if (attempt.status !== 'running')
|
|
249
|
+
throw new Error('Only the running attempt may submit evidence');
|
|
250
|
+
evidence(command.evidence);
|
|
251
|
+
attempt.submission = structuredClone(command.evidence);
|
|
252
|
+
attempt.status = 'submitted';
|
|
253
|
+
task.status = 'submitted';
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
case 'review': {
|
|
257
|
+
const [task, attempt] = taskAndAttempt(plan, command.taskId, command.attemptId);
|
|
258
|
+
if (attempt.status !== 'submitted' || attempt.settledAt === undefined || attempt.submission === undefined)
|
|
259
|
+
throw new Error('Review requires a settled submission');
|
|
260
|
+
nonempty(command.reason, 'Review decision');
|
|
261
|
+
references(command.references, 'Review evidence', true);
|
|
262
|
+
attempt.review = { decision: command.decision, by: context.actor.memberId, reason: command.reason, references: [...command.references], time: context.now };
|
|
263
|
+
attempt.status = command.decision === 'accepted' ? 'accepted' : 'rejected';
|
|
264
|
+
task.status = command.decision === 'accepted' ? 'accepted' : 'pending';
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
case 'retry': {
|
|
268
|
+
const task = plan.tasks.find(task => task.id === command.taskId);
|
|
269
|
+
if (task?.kind !== 'task' || task.status !== 'failed')
|
|
270
|
+
throw new Error('Only a failed task can be retried');
|
|
271
|
+
nonempty(command.reason, 'Retry reason');
|
|
272
|
+
task.status = 'pending';
|
|
273
|
+
const attempt = latestAttempt(task);
|
|
274
|
+
if (attempt !== undefined)
|
|
275
|
+
attempt.retry = { by: context.actor.memberId, reason: command.reason, time: context.now };
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
case 'reconcile': {
|
|
279
|
+
if (context.actor.kind !== 'human')
|
|
280
|
+
throw new Error('Only a human may reconcile an uncertain execution');
|
|
281
|
+
const [task, attempt] = taskAndAttempt(plan, command.taskId, command.attemptId);
|
|
282
|
+
if (attempt.status !== 'uncertain')
|
|
283
|
+
throw new Error('Attempt is not uncertain');
|
|
284
|
+
evidence(command.evidence);
|
|
285
|
+
attempt.submission = structuredClone(command.evidence);
|
|
286
|
+
attempt.status = command.outcome;
|
|
287
|
+
attempt.settledAt = context.now;
|
|
288
|
+
delete attempt.error;
|
|
289
|
+
task.status = command.outcome === 'submitted' ? 'submitted' : 'failed';
|
|
290
|
+
if (plan.reason === 'Reconcile uncertain attempts before continuing' && !activePlanAttempts(plan).some(attempt => attempt.status === 'uncertain')) {
|
|
291
|
+
plan.reason = 'Interrupted attempts reconciled; review and resume explicitly';
|
|
292
|
+
}
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
case 'complete':
|
|
296
|
+
if (!plan.tasks.some(task => task.kind === 'task' && task.status === 'accepted') || plan.tasks.some(task => task.kind === 'task' && !['accepted', 'cancelled'].includes(task.status)))
|
|
297
|
+
throw new Error('All remaining leaf tasks must be accepted before completing the goal');
|
|
298
|
+
evidence(command.evidence);
|
|
299
|
+
stopClock(plan, context.now);
|
|
300
|
+
plan.status = 'completed';
|
|
301
|
+
plan.completion = structuredClone(command.evidence);
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
plan.revision++;
|
|
305
|
+
}
|
|
306
|
+
plan.requests.push({ id: command.requestId, signature });
|
|
307
|
+
return plan;
|
|
308
|
+
}
|
|
309
|
+
/** Reserve a single executable leaf and count the attempt before side effects. */
|
|
310
|
+
export function queuePlanAttempt(current, taskId, attemptId, deliveryId, now) {
|
|
311
|
+
const plan = clone(current);
|
|
312
|
+
if (!readyPlanTasks(plan).some(task => task.id === taskId))
|
|
313
|
+
throw new Error('Task is not ready for execution');
|
|
314
|
+
if (activePlanAttempts(plan).length >= plan.budget.maxParallel)
|
|
315
|
+
throw new Error('Goal concurrency is full');
|
|
316
|
+
const task = plan.tasks.find(task => task.id === taskId);
|
|
317
|
+
if (activePlanMs(plan, now) >= plan.budget.maxActiveMs || task.attempts.length >= plan.budget.maxAttemptsPerTask
|
|
318
|
+
|| plan.tasks.reduce((count, task) => count + task.attempts.length, 0) >= plan.budget.maxAttempts) {
|
|
319
|
+
stopClock(plan, now);
|
|
320
|
+
plan.status = 'paused';
|
|
321
|
+
plan.reason = 'Execution budget exhausted';
|
|
322
|
+
plan.revision++;
|
|
323
|
+
return plan;
|
|
324
|
+
}
|
|
325
|
+
identifier(attemptId);
|
|
326
|
+
nonempty(deliveryId, 'Delivery identity');
|
|
327
|
+
if (plan.tasks.some(task => task.attempts.some(attempt => attempt.id === attemptId || attempt.deliveryId === deliveryId)))
|
|
328
|
+
throw new Error('Attempt or delivery identity already exists');
|
|
329
|
+
task.attempts.push({ id: attemptId, deliveryId, number: task.attempts.length + 1, status: 'queued', createdAt: now });
|
|
330
|
+
task.status = 'running';
|
|
331
|
+
plan.revision++;
|
|
332
|
+
return plan;
|
|
333
|
+
}
|
|
334
|
+
/** Durable admission; a pause after reservation prevents the native side effect. */
|
|
335
|
+
export function admitPlanAttempt(current, taskId, attemptId, now) {
|
|
336
|
+
const plan = clone(current);
|
|
337
|
+
if (plan.status !== 'running' || activePlanMs(plan, now) >= plan.budget.maxActiveMs)
|
|
338
|
+
throw new Error('Goal is paused or its active-time budget is exhausted');
|
|
339
|
+
const [, attempt] = taskAndAttempt(plan, taskId, attemptId);
|
|
340
|
+
if (attempt.status !== 'queued')
|
|
341
|
+
throw new Error('Attempt is not queued');
|
|
342
|
+
attempt.status = 'running';
|
|
343
|
+
attempt.startedAt = now;
|
|
344
|
+
plan.revision++;
|
|
345
|
+
return plan;
|
|
346
|
+
}
|
|
347
|
+
/** A normal native result is a submission to review, never implicit acceptance. */
|
|
348
|
+
export function settlePlanAttempt(current, taskId, attemptId, result, now) {
|
|
349
|
+
const plan = clone(current);
|
|
350
|
+
const task = plan.tasks.find(task => task.id === taskId);
|
|
351
|
+
const attempt = task?.attempts.at(-1);
|
|
352
|
+
// Duplicate or late results cannot rewrite a reviewed or newer attempt.
|
|
353
|
+
if (task === undefined || attempt?.id !== attemptId || attempt.settledAt !== undefined || attempt.status === 'uncertain')
|
|
354
|
+
return plan;
|
|
355
|
+
if (attempt.startedAt === undefined && current.status === 'paused') {
|
|
356
|
+
attempt.status = 'cancelled';
|
|
357
|
+
attempt.error = 'Paused before native admission';
|
|
358
|
+
attempt.settledAt = now;
|
|
359
|
+
task.status = 'pending';
|
|
360
|
+
plan.revision++;
|
|
361
|
+
return plan;
|
|
362
|
+
}
|
|
363
|
+
if (result.state === 'done') {
|
|
364
|
+
if (result.evidence !== undefined && attempt.submission === undefined) {
|
|
365
|
+
evidence(result.evidence);
|
|
366
|
+
attempt.submission = structuredClone(result.evidence);
|
|
367
|
+
}
|
|
368
|
+
if (attempt.submission === undefined) {
|
|
369
|
+
attempt.status = 'failed';
|
|
370
|
+
task.status = 'failed';
|
|
371
|
+
attempt.error = 'Execution finished without submission evidence';
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
attempt.status = 'submitted';
|
|
375
|
+
task.status = 'submitted';
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
attempt.status = result.state;
|
|
380
|
+
task.status = result.state === 'cancelled' ? 'cancelled' : 'failed';
|
|
381
|
+
if (result.error !== undefined)
|
|
382
|
+
attempt.error = result.error;
|
|
383
|
+
}
|
|
384
|
+
attempt.settledAt = now;
|
|
385
|
+
plan.revision++;
|
|
386
|
+
return plan;
|
|
387
|
+
}
|
|
388
|
+
/** Restart always pauses automation and fences native executions with unknown outcomes. */
|
|
389
|
+
export function recoverPlan(current, now) {
|
|
390
|
+
const plan = clone(current);
|
|
391
|
+
if (plan.status === 'completed')
|
|
392
|
+
return plan;
|
|
393
|
+
if (plan.status === 'running') {
|
|
394
|
+
stopClock(plan, now);
|
|
395
|
+
plan.status = 'paused';
|
|
396
|
+
plan.reason = 'Host restarted; review and resume explicitly';
|
|
397
|
+
}
|
|
398
|
+
for (const task of plan.tasks) {
|
|
399
|
+
const attempt = latestAttempt(task);
|
|
400
|
+
if (attempt !== undefined && attempt.settledAt === undefined && ['running', 'submitted'].includes(attempt.status)) {
|
|
401
|
+
attempt.status = 'uncertain';
|
|
402
|
+
attempt.error = 'Execution outcome is unknown after restart';
|
|
403
|
+
task.status = 'failed';
|
|
404
|
+
if (plan.status !== 'cancelled')
|
|
405
|
+
plan.status = 'paused';
|
|
406
|
+
plan.reason = 'Reconcile uncertain attempts before continuing';
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
plan.revision++;
|
|
410
|
+
return plan;
|
|
411
|
+
}
|
|
412
|
+
const shortText = { type: 'string', minLength: 1, maxLength: 32000 };
|
|
413
|
+
const idSchema = { type: 'string', pattern: '^[a-zA-Z0-9][a-zA-Z0-9_.:-]{0,127}$' };
|
|
414
|
+
const strings = { type: 'array', items: shortText, maxItems: 100 };
|
|
415
|
+
const object = (properties, optional = []) => ({ type: 'object', properties, required: Object.keys(properties).filter(key => !optional.includes(key)), additionalProperties: false });
|
|
416
|
+
const enumeration = (...values) => ({ type: 'string', enum: values });
|
|
417
|
+
const integer = (minimum, maximum) => ({ type: 'integer', minimum, maximum });
|
|
418
|
+
const evidenceSchema = object({ summary: shortText, references: { ...strings, minItems: 1 }, artifacts: strings });
|
|
419
|
+
const budgetSchema = object({ maxParallel: integer(1, 8), maxAttempts: integer(1, 100), maxAttemptsPerTask: integer(1, 10), maxActiveMs: integer(1, 86400000) });
|
|
420
|
+
const taskSchema = object({ id: idSchema, title: shortText, stageId: idSchema, parentId: idSchema, kind: enumeration('task', 'group'), ownerMemberId: idSchema, instruction: { type: 'string', maxLength: 32000 }, criteria: strings, inputRefs: strings, artifactPaths: strings, dependsOn: { type: 'array', items: idSchema, maxItems: 100 } }, ['parentId', 'ownerMemberId']);
|
|
421
|
+
const base = { requestId: idSchema, expectedRevision: integer(0, Number.MAX_SAFE_INTEGER) };
|
|
422
|
+
const commandSchema = { oneOf: [
|
|
423
|
+
object({ ...base, action: enumeration('create'), id: idSchema, objective: shortText, mode: enumeration('draft', 'execute'), budget: budgetSchema }),
|
|
424
|
+
object({ ...base, action: enumeration('extend'), stages: { type: 'array', items: object({ id: idSchema, title: shortText }), maxItems: 50 }, tasks: { type: 'array', items: taskSchema, maxItems: 200 } }),
|
|
425
|
+
object({ ...base, action: enumeration('pause'), reason: shortText, goalId: idSchema }, ['goalId']),
|
|
426
|
+
object({ ...base, action: enumeration('resume') }),
|
|
427
|
+
object({ ...base, action: enumeration('budget'), budget: budgetSchema }),
|
|
428
|
+
object({ ...base, action: enumeration('cancel'), reason: shortText }),
|
|
429
|
+
object({ ...base, action: enumeration('submit'), taskId: idSchema, attemptId: idSchema, evidence: evidenceSchema }),
|
|
430
|
+
object({ ...base, action: enumeration('review'), taskId: idSchema, attemptId: idSchema, decision: enumeration('accepted', 'rework'), reason: shortText, references: { ...strings, minItems: 1 } }),
|
|
431
|
+
object({ ...base, action: enumeration('retry'), taskId: idSchema, reason: shortText }),
|
|
432
|
+
object({ ...base, action: enumeration('reconcile'), taskId: idSchema, attemptId: idSchema, outcome: enumeration('failed', 'submitted'), evidence: evidenceSchema }),
|
|
433
|
+
object({ ...base, action: enumeration('complete'), evidence: evidenceSchema }),
|
|
434
|
+
] };
|
|
435
|
+
/** Validate only the small JSON-schema vocabulary used by the published contract. */
|
|
436
|
+
function validateWire(value, schema, path) {
|
|
437
|
+
const fail = () => { throw new Error(`Invalid plan command at ${path}`); };
|
|
438
|
+
if (schema.oneOf !== undefined) {
|
|
439
|
+
for (const alternative of schema.oneOf) {
|
|
440
|
+
try {
|
|
441
|
+
validateWire(value, alternative, path);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
catch { /* try the next action */ }
|
|
445
|
+
}
|
|
446
|
+
fail();
|
|
447
|
+
}
|
|
448
|
+
if (schema.type === 'string') {
|
|
449
|
+
if (typeof value !== 'string')
|
|
450
|
+
fail();
|
|
451
|
+
const text = value;
|
|
452
|
+
if ((schema.minLength !== undefined && text.length < schema.minLength) || (schema.maxLength !== undefined && text.length > schema.maxLength)
|
|
453
|
+
|| (schema.enum !== undefined && !schema.enum.includes(text)) || (schema.pattern !== undefined && !new RegExp(schema.pattern).test(text)))
|
|
454
|
+
fail();
|
|
455
|
+
}
|
|
456
|
+
else if (schema.type === 'integer') {
|
|
457
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < schema.minimum || value > schema.maximum)
|
|
458
|
+
fail();
|
|
459
|
+
}
|
|
460
|
+
else if (schema.type === 'array') {
|
|
461
|
+
if (!Array.isArray(value))
|
|
462
|
+
fail();
|
|
463
|
+
const items = value;
|
|
464
|
+
if (items.length < (schema.minItems ?? 0) || items.length > (schema.maxItems ?? Infinity))
|
|
465
|
+
fail();
|
|
466
|
+
for (const [index, item] of items.entries())
|
|
467
|
+
validateWire(item, schema.items, `${path}[${index}]`);
|
|
468
|
+
}
|
|
469
|
+
else if (schema.type === 'object') {
|
|
470
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
471
|
+
fail();
|
|
472
|
+
const record = value;
|
|
473
|
+
if (schema.required?.some(key => !Object.hasOwn(record, key)) || Object.keys(record).some(key => !Object.hasOwn(schema.properties, key)))
|
|
474
|
+
fail();
|
|
475
|
+
for (const [key, item] of Object.entries(record))
|
|
476
|
+
validateWire(item, schema.properties[key], `${path}.${key}`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
/** One bounded, strict wire grammar is shared by native tools, MCP and the UI. */
|
|
480
|
+
export function parsePlanCommand(text) {
|
|
481
|
+
if (typeof text !== 'string' || text.length > 131072)
|
|
482
|
+
throw new Error('Plan command exceeds 128 KiB');
|
|
483
|
+
const value = JSON.parse(text);
|
|
484
|
+
validateWire(value, commandSchema, 'command');
|
|
485
|
+
return value;
|
|
486
|
+
}
|
|
487
|
+
/** JSON schema supplied through room_read so both harness families see one contract. */
|
|
488
|
+
export function planCommandContract() { return structuredClone(commandSchema); }
|
|
489
|
+
/** Object field order is not part of a retry identity; array order remains meaningful. */
|
|
490
|
+
export function planCommandSignature(command) {
|
|
491
|
+
const stable = (value) => Array.isArray(value) ? value.map(stable)
|
|
492
|
+
: value !== null && typeof value === 'object' ? Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => [key, stable(item)])) : value;
|
|
493
|
+
return JSON.stringify(stable(command));
|
|
494
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { RoomAddTaskRequest, RoomAddTaskResult, RoomCloseTaskRequest, RoomCloseTaskResult, RoomGetStateRequest, RoomGetStateResult, RoomInviteRequest, RoomInviteResult, RoomMessageRequest, RoomMessageResult, RoomUpdateTaskRequest, RoomUpdateTaskResult } from './types.ts';
|
|
2
|
+
/** The slice of RoomService the tool drives. */
|
|
3
|
+
export interface RoomInviteToolBackend {
|
|
4
|
+
/** Validate and journal an invitation (see RoomService.inviteMember). */
|
|
5
|
+
inviteMember(request: RoomInviteRequest, invitedBy: 'human' | 'agent'): Promise<RoomInviteResult>;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Build the room_invite tool definition bound to the room service.
|
|
9
|
+
* @param backend - the room service.
|
|
10
|
+
* @returns a registry-ready tool definition.
|
|
11
|
+
*/
|
|
12
|
+
export declare function roomInviteTool(backend: RoomInviteToolBackend): import("@deepseek-ai/dsh-tools").ToolDefinition;
|
|
13
|
+
/** The slice of RoomService the room_task tool drives. */
|
|
14
|
+
export interface RoomTaskToolBackend {
|
|
15
|
+
/** Open a pending task on a member's lane (see RoomService.addTask). */
|
|
16
|
+
addTask(request: RoomAddTaskRequest): Promise<RoomAddTaskResult>;
|
|
17
|
+
/** Close an open task (see RoomService.closeTask). */
|
|
18
|
+
closeTask(request: RoomCloseTaskRequest): Promise<RoomCloseTaskResult>;
|
|
19
|
+
/** Edit an open task's title/blockedBy (see RoomService.updateTask). */
|
|
20
|
+
updateTask(request: RoomUpdateTaskRequest): Promise<RoomUpdateTaskResult>;
|
|
21
|
+
/** Replay the room state (the error texts read the roster/open tasks from it). */
|
|
22
|
+
getState(request: RoomGetStateRequest): Promise<RoomGetStateResult>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Build the room_task tool definition bound to the room service. Every write
|
|
26
|
+
* goes through the same host functions as the capsule UI, so a task the agent
|
|
27
|
+
* opens is a public commitment indistinguishable from a human-added one.
|
|
28
|
+
* @param backend - the room service.
|
|
29
|
+
* @returns a registry-ready tool definition.
|
|
30
|
+
*/
|
|
31
|
+
export declare function roomTaskTool(backend: RoomTaskToolBackend): import("@deepseek-ai/dsh-tools").ToolDefinition;
|
|
32
|
+
/** The slice of RoomService the room_message tool drives. */
|
|
33
|
+
export interface RoomMessageToolBackend {
|
|
34
|
+
/** Dispatch one message to one member (see RoomService.messageMember). */
|
|
35
|
+
messageMember(request: RoomMessageRequest): Promise<RoomMessageResult>;
|
|
36
|
+
/** Replay the room state (the error text reads the live roster from it). */
|
|
37
|
+
getState(request: RoomGetStateRequest): Promise<RoomGetStateResult>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Build the room_message tool definition bound to the room service: the main
|
|
41
|
+
* agent's way to hand a member a message/task — the model-facing equivalent
|
|
42
|
+
* of the human's `@member text`, minus the user bubble (the caller is the
|
|
43
|
+
* room's own agent, not the human).
|
|
44
|
+
* @param backend - the room service.
|
|
45
|
+
* @returns a registry-ready tool definition.
|
|
46
|
+
*/
|
|
47
|
+
export declare function roomMessageTool(backend: RoomMessageToolBackend): import("@deepseek-ai/dsh-tools").ToolDefinition;
|
|
48
|
+
/** Native room tools use the same bounded context reader as the external MCP bridge. */
|
|
49
|
+
export declare function roomReadTool(backend: {
|
|
50
|
+
readRoomContext(sessionId: string): Promise<string>;
|
|
51
|
+
}): import("@deepseek-ai/dsh-tools").ToolDefinition;
|
|
52
|
+
/** Formal planning uses the same strict command grammar exposed by room_read. */
|
|
53
|
+
export declare function roomPlanTool(backend: {
|
|
54
|
+
commandPlan(sessionId: string, command: string): Promise<string>;
|
|
55
|
+
}): import("@deepseek-ai/dsh-tools").ToolDefinition;
|
|
56
|
+
//# sourceMappingURL=tool.d.ts.map
|