@superjackfruitlabs/salaam 0.0.1 → 0.0.2
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 +15 -0
- package/README.md +25 -17
- package/dist/src/a2a-backend.js +225 -0
- package/dist/src/a2a-inbound.js +487 -0
- package/dist/src/a2a-peer.js +370 -0
- package/dist/src/a2a-protocol.js +250 -0
- package/dist/src/agent-profiles.js +289 -0
- package/dist/src/children.js +132 -26
- package/dist/src/claude-provider.js +2 -179
- package/dist/src/cli-help.js +107 -6
- package/dist/src/cli.js +477 -13
- package/dist/src/config.js +26 -3
- package/dist/src/credentials.js +7 -2
- package/dist/src/external-runtime.js +1 -0
- package/dist/src/external-work.js +583 -0
- package/dist/src/harness-backend.js +466 -0
- package/dist/src/harness-container.js +450 -0
- package/dist/src/harness-model-gateway.js +104 -0
- package/dist/src/harness-prepare.js +88 -0
- package/dist/src/harness-probe.js +111 -0
- package/dist/src/harness-protocol.js +325 -0
- package/dist/src/harness-readiness.js +50 -0
- package/dist/src/harness-rpc.js +206 -0
- package/dist/src/harness-tool-identity.js +87 -0
- package/dist/src/harness-wire-bridge.js +387 -0
- package/dist/src/host.js +6 -1
- package/dist/src/house-extension.js +147 -64
- package/dist/src/live-provider.js +301 -0
- package/dist/src/managed.js +33 -3
- package/dist/src/protocol.js +9 -5
- package/dist/src/provider-auth.js +300 -0
- package/dist/src/provider-config.js +65 -17
- package/dist/src/provider-model.js +78 -0
- package/dist/src/provider-payload.js +264 -0
- package/dist/src/provider-transport.js +36 -0
- package/dist/src/provider-usage.js +59 -0
- package/dist/src/repository-snapshots.js +219 -0
- package/dist/src/service.js +274 -9
- package/dist/src/store.js +44 -2
- package/dist/src/supervisor.js +246 -35
- package/dist/src/task-budget.js +17 -8
- package/dist/src/task-context.js +15 -0
- package/dist/src/task-driver.js +38 -14
- package/dist/src/task-policy.js +20 -1
- package/dist/src/task-verification.js +3 -0
- package/dist/src/task-work.js +31 -0
- package/dist/src/team-work.js +378 -0
- package/dist/src/tui-view.js +65 -10
- package/dist/src/tui.js +4 -2
- package/dist/src/usage.js +18 -0
- package/dist/src/worker-boundary.js +298 -0
- package/dist/src/worker-workspaces.js +274 -0
- package/dist/src/workspaces.js +6 -2
- package/harness-runtime/codex-pins.json +10 -0
- package/harness-runtime/package-lock.json +560 -0
- package/harness-runtime/package.json +9 -0
- package/npm-shrinkwrap.json +57 -2
- package/package.json +2 -1
- package/src/a2a-backend.ts +254 -0
- package/src/a2a-inbound.ts +585 -0
- package/src/a2a-peer.ts +441 -0
- package/src/a2a-protocol.ts +286 -0
- package/src/agent-profiles.ts +379 -0
- package/src/children.ts +168 -27
- package/src/claude-provider.ts +2 -189
- package/src/cli-help.ts +107 -6
- package/src/cli.ts +602 -12
- package/src/config.ts +34 -3
- package/src/credentials.ts +9 -2
- package/src/external-runtime.ts +54 -0
- package/src/external-work.ts +663 -0
- package/src/harness-backend.ts +557 -0
- package/src/harness-container.ts +543 -0
- package/src/harness-model-gateway.ts +138 -0
- package/src/harness-prepare.ts +113 -0
- package/src/harness-probe.ts +150 -0
- package/src/harness-protocol.ts +388 -0
- package/src/harness-readiness.ts +71 -0
- package/src/harness-rpc.ts +244 -0
- package/src/harness-tool-identity.ts +91 -0
- package/src/harness-wire-bridge.ts +418 -0
- package/src/host.ts +11 -6
- package/src/house-extension.ts +165 -66
- package/src/live-provider.ts +347 -0
- package/src/managed.ts +43 -4
- package/src/protocol.ts +36 -8
- package/src/provider-auth.ts +336 -0
- package/src/provider-config.ts +89 -18
- package/src/provider-model.ts +86 -0
- package/src/provider-payload.ts +263 -0
- package/src/provider-transport.ts +51 -0
- package/src/provider-usage.ts +64 -0
- package/src/repository-snapshots.ts +259 -0
- package/src/service.ts +446 -11
- package/src/store.ts +60 -4
- package/src/supervisor.ts +294 -41
- package/src/task-budget.ts +24 -11
- package/src/task-context.ts +17 -0
- package/src/task-driver.ts +51 -15
- package/src/task-policy.ts +23 -3
- package/src/task-verification.ts +2 -0
- package/src/task-work.ts +46 -1
- package/src/team-work.ts +554 -0
- package/src/tui-client.ts +5 -0
- package/src/tui-view.ts +164 -12
- package/src/tui.ts +5 -2
- package/src/usage.ts +17 -0
- package/src/worker-boundary.ts +395 -0
- package/src/worker-workspaces.ts +390 -0
- package/src/workspaces.ts +4 -2
package/src/team-work.ts
ADDED
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import type { Store } from './store.js';
|
|
3
|
+
import type { TaskWork } from './task-work.js';
|
|
4
|
+
import type { TaskBudget } from './task-budget.js';
|
|
5
|
+
import type { ChildWork } from './children.js';
|
|
6
|
+
import type { AgentProfiles } from './agent-profiles.js';
|
|
7
|
+
import type { WorkerBinding } from './worker-boundary.js';
|
|
8
|
+
import { digest, ServiceError, type Controller, type ChildTask } from './protocol.js';
|
|
9
|
+
|
|
10
|
+
export interface TeamAssignment {
|
|
11
|
+
id: string;
|
|
12
|
+
task_id: string;
|
|
13
|
+
step_id: string;
|
|
14
|
+
plan_revision: number;
|
|
15
|
+
profile_id: string;
|
|
16
|
+
grant_id: string;
|
|
17
|
+
budget: number;
|
|
18
|
+
generation: number;
|
|
19
|
+
child_id: string | null;
|
|
20
|
+
superseded: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Assignment claims extend the task ledger. They are never an independent scheduler. */
|
|
24
|
+
export class TeamWork {
|
|
25
|
+
constructor(
|
|
26
|
+
readonly store: Store,
|
|
27
|
+
readonly tasks: TaskWork,
|
|
28
|
+
readonly budget: TaskBudget,
|
|
29
|
+
readonly children: ChildWork,
|
|
30
|
+
readonly profiles: AgentProfiles,
|
|
31
|
+
) {
|
|
32
|
+
store.workerBoundary.assertAssignment = (binding) => this.assert(binding);
|
|
33
|
+
store.teamSnapshot = (session) => {
|
|
34
|
+
const task = tasks.forSession(session);
|
|
35
|
+
return task ? this.view(task.id) : null;
|
|
36
|
+
};
|
|
37
|
+
tasks.delegatedStep = (task, step) => !!this.forStep(task, step);
|
|
38
|
+
tasks.advanceDelegate = (id) => {
|
|
39
|
+
const grant = store.db.prepare('SELECT * FROM task_grants WHERE id=?').get(id)!;
|
|
40
|
+
const assignment = this.forStep(String(grant.task_id), String(grant.step_id));
|
|
41
|
+
if (!assignment)
|
|
42
|
+
throw new ServiceError('team_assignment_missing', 'Step assignment disappeared');
|
|
43
|
+
return this.run(
|
|
44
|
+
assignment.id,
|
|
45
|
+
'task-grant:' + id,
|
|
46
|
+
Number(grant.expected_revision),
|
|
47
|
+
assignment.generation,
|
|
48
|
+
null,
|
|
49
|
+
null,
|
|
50
|
+
null,
|
|
51
|
+
id,
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
forStep(task: string, step: string): TeamAssignment | undefined {
|
|
56
|
+
return this.store.db
|
|
57
|
+
.prepare('SELECT * FROM team_assignments WHERE task_id=? AND step_id=? AND superseded=0')
|
|
58
|
+
.get(task, step) as unknown as TeamAssignment | undefined;
|
|
59
|
+
}
|
|
60
|
+
get(id: string): TeamAssignment {
|
|
61
|
+
const row = this.store.db
|
|
62
|
+
.prepare('SELECT * FROM team_assignments WHERE id=?')
|
|
63
|
+
.get(id) as unknown as TeamAssignment | undefined;
|
|
64
|
+
if (!row) throw new ServiceError('not_found', 'Team assignment not found', 404);
|
|
65
|
+
return row;
|
|
66
|
+
}
|
|
67
|
+
private expect(task: string, revision: number): void {
|
|
68
|
+
if (this.tasks.get(task).revision !== revision)
|
|
69
|
+
throw new ServiceError('task_revision_conflict', 'Task revision changed');
|
|
70
|
+
}
|
|
71
|
+
assign(
|
|
72
|
+
taskId: string,
|
|
73
|
+
stepId: string,
|
|
74
|
+
profile: string,
|
|
75
|
+
requests: number,
|
|
76
|
+
request: string,
|
|
77
|
+
expected: number,
|
|
78
|
+
controller: Controller,
|
|
79
|
+
runtime?: string,
|
|
80
|
+
): TeamAssignment {
|
|
81
|
+
return this.store.request(
|
|
82
|
+
'local-operator',
|
|
83
|
+
request,
|
|
84
|
+
{
|
|
85
|
+
kind: 'team-assign',
|
|
86
|
+
taskId,
|
|
87
|
+
stepId,
|
|
88
|
+
profile,
|
|
89
|
+
requests,
|
|
90
|
+
expected,
|
|
91
|
+
...(runtime ? { runtime } : {}),
|
|
92
|
+
},
|
|
93
|
+
() => {
|
|
94
|
+
const task = this.tasks.get(taskId);
|
|
95
|
+
this.store.authorizeControl(task.session_id, controller);
|
|
96
|
+
this.expect(taskId, expected);
|
|
97
|
+
if (['cancelled', 'succeeded', 'succeeded_with_exception'].includes(task.state))
|
|
98
|
+
throw new ServiceError('task_settled', 'Revise the task before assigning workers');
|
|
99
|
+
const step = task.steps.find((s) => s.id === stepId);
|
|
100
|
+
if (!step || step.attempt_id || step.state !== 'pending')
|
|
101
|
+
throw new ServiceError('invalid_step_state', 'Assign an unattempted pending step');
|
|
102
|
+
const old = this.forStep(taskId, stepId);
|
|
103
|
+
if (old) {
|
|
104
|
+
if (old.plan_revision === task.plan_revision || old.generation !== 0)
|
|
105
|
+
throw new ServiceError(
|
|
106
|
+
'team_assignment_exists',
|
|
107
|
+
'An immutable assignment already owns this step',
|
|
108
|
+
);
|
|
109
|
+
this.store.db.prepare('UPDATE team_assignments SET superseded=1 WHERE id=?').run(old.id);
|
|
110
|
+
}
|
|
111
|
+
const grant = this.profiles.resolve(task.session_id, profile);
|
|
112
|
+
if (
|
|
113
|
+
!Number.isSafeInteger(requests) ||
|
|
114
|
+
requests < 1 ||
|
|
115
|
+
requests > (grant.profile.max_requests ?? 8)
|
|
116
|
+
)
|
|
117
|
+
throw new ServiceError('invalid_budget', 'Assignment exceeds profile request bounds');
|
|
118
|
+
const id = randomUUID();
|
|
119
|
+
this.store.db
|
|
120
|
+
.prepare('INSERT INTO team_assignments VALUES (?,?,?,?,?,?,?,0,NULL,0)')
|
|
121
|
+
.run(id, taskId, stepId, task.plan_revision, profile, grant.id, requests);
|
|
122
|
+
if (runtime) {
|
|
123
|
+
if (!this.children.external || requests !== 1)
|
|
124
|
+
throw new ServiceError(
|
|
125
|
+
'runtime_unavailable',
|
|
126
|
+
'External assignments require one invocation and a configured runtime',
|
|
127
|
+
);
|
|
128
|
+
this.children.external.assign(id, task, runtime, grant.profile.tools);
|
|
129
|
+
}
|
|
130
|
+
this.changed(taskId, 'assigned', {
|
|
131
|
+
assignment_id: id,
|
|
132
|
+
step_id: stepId,
|
|
133
|
+
profile_id: profile,
|
|
134
|
+
});
|
|
135
|
+
return this.get(id);
|
|
136
|
+
},
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
run(
|
|
140
|
+
id: string,
|
|
141
|
+
request: string,
|
|
142
|
+
expected: number,
|
|
143
|
+
generation: number,
|
|
144
|
+
instruction: string | null,
|
|
145
|
+
continuation: string | null,
|
|
146
|
+
controller: Controller | null,
|
|
147
|
+
grantId?: string,
|
|
148
|
+
peerAdmission?: {
|
|
149
|
+
scope: string;
|
|
150
|
+
digest: string;
|
|
151
|
+
authorize: () => void;
|
|
152
|
+
committed: (child: ChildTask) => void;
|
|
153
|
+
},
|
|
154
|
+
): TeamAssignment {
|
|
155
|
+
let launch: ChildTask | undefined;
|
|
156
|
+
const result = this.store.request(
|
|
157
|
+
peerAdmission?.scope ?? 'local-operator',
|
|
158
|
+
request,
|
|
159
|
+
{
|
|
160
|
+
kind: 'team-run',
|
|
161
|
+
id,
|
|
162
|
+
expected,
|
|
163
|
+
...(!grantId ? { generation } : {}),
|
|
164
|
+
instruction,
|
|
165
|
+
continuation,
|
|
166
|
+
...(grantId ? { grantId } : {}),
|
|
167
|
+
...(peerAdmission ? { peer_admission: peerAdmission.digest } : {}),
|
|
168
|
+
},
|
|
169
|
+
() => {
|
|
170
|
+
const row = this.get(id),
|
|
171
|
+
task = this.tasks.get(row.task_id);
|
|
172
|
+
if (peerAdmission) peerAdmission.authorize();
|
|
173
|
+
else if (controller) this.store.authorizeControl(task.session_id, controller);
|
|
174
|
+
else this.tasks.authorizeGrant(task, row.step_id, grantId);
|
|
175
|
+
this.expect(task.id, expected);
|
|
176
|
+
if (
|
|
177
|
+
row.superseded ||
|
|
178
|
+
row.plan_revision !== task.plan_revision ||
|
|
179
|
+
row.generation !== generation
|
|
180
|
+
)
|
|
181
|
+
throw new ServiceError('team_claim_stale', 'Assignment plan or generation changed');
|
|
182
|
+
if (generation >= 8)
|
|
183
|
+
throw new ServiceError(
|
|
184
|
+
'team_claim_limit',
|
|
185
|
+
'At most eight explicitly admitted generations per assignment',
|
|
186
|
+
);
|
|
187
|
+
if (['cancelled', 'succeeded', 'succeeded_with_exception'].includes(task.state))
|
|
188
|
+
throw new ServiceError('task_settled', 'Task no longer admits work');
|
|
189
|
+
const step = task.steps.find((s) => s.id === row.step_id)!;
|
|
190
|
+
if (!['pending', 'interrupted', 'failed', 'cancelled'].includes(step.state))
|
|
191
|
+
throw new ServiceError(
|
|
192
|
+
'invalid_step_state',
|
|
193
|
+
'Assignment is active or already awaiting review',
|
|
194
|
+
);
|
|
195
|
+
if (
|
|
196
|
+
step.depends_on.some(
|
|
197
|
+
(key) => task.steps.find((s) => s.key === key)?.state !== 'completed',
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
throw new ServiceError('dependency_pending', 'Complete predecessor steps first');
|
|
201
|
+
const interrupted =
|
|
202
|
+
step.attempt_id && this.store.attempt(step.attempt_id).state === 'interrupted'
|
|
203
|
+
? step.attempt_id
|
|
204
|
+
: null;
|
|
205
|
+
if (continuation !== interrupted || (row.child_id && !instruction))
|
|
206
|
+
throw new ServiceError(
|
|
207
|
+
'invalid_continuation',
|
|
208
|
+
'Follow-up requires a new instruction and the exact interrupted attempt, if any',
|
|
209
|
+
);
|
|
210
|
+
if (instruction !== null && (!instruction.trim() || instruction.length > 4096))
|
|
211
|
+
throw new ServiceError('invalid_field', 'Supply a bounded follow-up instruction', 400);
|
|
212
|
+
if (row.child_id) {
|
|
213
|
+
const prior = this.children.inspect(task.session_id, row.child_id);
|
|
214
|
+
if (
|
|
215
|
+
prior.state === 'accepted' ||
|
|
216
|
+
(prior.worker?.binding.external && !this.children.external?.stopped(prior.id)) ||
|
|
217
|
+
this.store.db
|
|
218
|
+
.prepare("SELECT 1 FROM workers WHERE session_id=? AND state!='exited'")
|
|
219
|
+
.get(prior.child_session)
|
|
220
|
+
)
|
|
221
|
+
throw new ServiceError(
|
|
222
|
+
'team_worker_uncertain',
|
|
223
|
+
'Confirm the previous process stopped before claiming a new generation',
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
const agent = this.profiles.resolve(task.session_id, row.profile_id);
|
|
227
|
+
if (agent.id !== row.grant_id)
|
|
228
|
+
throw new ServiceError(
|
|
229
|
+
'team_claim_stale',
|
|
230
|
+
'Assignment requires its original profile admission',
|
|
231
|
+
);
|
|
232
|
+
this.budget.check(task.session_id);
|
|
233
|
+
this.children.external?.validateAssignment(id, task);
|
|
234
|
+
if (this.budget.active(task) >= task.limits.concurrency)
|
|
235
|
+
throw new ServiceError('task_busy', 'Root task concurrency is full');
|
|
236
|
+
if (
|
|
237
|
+
this.store.db
|
|
238
|
+
.prepare(
|
|
239
|
+
"SELECT 1 FROM attempts WHERE session_id=? AND state IN ('accepted','dispatching','running','interrupted') AND resolved_by IS NULL",
|
|
240
|
+
)
|
|
241
|
+
.get(task.session_id)
|
|
242
|
+
)
|
|
243
|
+
throw new ServiceError('task_busy', 'Root session has pending work');
|
|
244
|
+
const envelope = randomUUID(),
|
|
245
|
+
submission = randomUUID();
|
|
246
|
+
// A delegated envelope has never been dispatched to a model. Dispatcher excludes this state.
|
|
247
|
+
this.store.db
|
|
248
|
+
.prepare(
|
|
249
|
+
"INSERT INTO submissions(id,session_id,text,state,attempt_id) VALUES (?,?,?,'delegated',?)",
|
|
250
|
+
)
|
|
251
|
+
.run(submission, task.session_id, step.instruction, envelope);
|
|
252
|
+
this.store.db
|
|
253
|
+
.prepare(
|
|
254
|
+
"INSERT INTO attempts(id,session_id,submission_id,state) VALUES (?,?,?,'delegated')",
|
|
255
|
+
)
|
|
256
|
+
.run(envelope, task.session_id, submission);
|
|
257
|
+
const next = generation + 1;
|
|
258
|
+
const child = this.children.spawn(
|
|
259
|
+
task.session_id,
|
|
260
|
+
0,
|
|
261
|
+
envelope,
|
|
262
|
+
`team:${id}:${next}`,
|
|
263
|
+
instruction ?? step.instruction,
|
|
264
|
+
row.budget,
|
|
265
|
+
[],
|
|
266
|
+
row.profile_id,
|
|
267
|
+
{
|
|
268
|
+
authorize: () => {},
|
|
269
|
+
defer: true,
|
|
270
|
+
assignment: { id, generation: next },
|
|
271
|
+
runtime: this.children.external?.assignment(id),
|
|
272
|
+
},
|
|
273
|
+
);
|
|
274
|
+
this.store.db
|
|
275
|
+
.prepare('UPDATE team_assignments SET generation=?,child_id=? WHERE id=?')
|
|
276
|
+
.run(next, child.id, id);
|
|
277
|
+
this.store.db
|
|
278
|
+
.prepare('INSERT INTO task_step_attempts VALUES (?,?,?,?)')
|
|
279
|
+
.run(child.child_attempt, step.id, task.plan_revision, instruction ?? step.instruction);
|
|
280
|
+
this.store.db
|
|
281
|
+
.prepare("UPDATE task_steps SET attempt_id=?,state='queued',review=NULL WHERE id=?")
|
|
282
|
+
.run(child.child_attempt, step.id);
|
|
283
|
+
if (interrupted)
|
|
284
|
+
this.store.db
|
|
285
|
+
.prepare('UPDATE attempts SET resolved_by=? WHERE id=?')
|
|
286
|
+
.run(child.child_attempt, interrupted);
|
|
287
|
+
this.store.db
|
|
288
|
+
.prepare('UPDATE task_runtime SET started_at=coalesce(started_at,?) WHERE task_id=?')
|
|
289
|
+
.run(Date.now(), task.id);
|
|
290
|
+
this.store.db
|
|
291
|
+
.prepare(
|
|
292
|
+
"UPDATE durable_tasks SET state='running',reason='Team assignment claimed' WHERE id=?",
|
|
293
|
+
)
|
|
294
|
+
.run(task.id);
|
|
295
|
+
if (grantId)
|
|
296
|
+
this.store.db.prepare("UPDATE task_grants SET state='accepted' WHERE id=?").run(grantId);
|
|
297
|
+
this.changed(task.id, 'claimed', {
|
|
298
|
+
assignment_id: id,
|
|
299
|
+
generation: next,
|
|
300
|
+
child_id: child.id,
|
|
301
|
+
attempt_id: child.child_attempt,
|
|
302
|
+
});
|
|
303
|
+
peerAdmission?.committed(child);
|
|
304
|
+
launch = child;
|
|
305
|
+
return this.get(id);
|
|
306
|
+
},
|
|
307
|
+
);
|
|
308
|
+
if (launch) this.children.launchPrepared(launch);
|
|
309
|
+
return result;
|
|
310
|
+
}
|
|
311
|
+
reject(
|
|
312
|
+
id: string,
|
|
313
|
+
request: string,
|
|
314
|
+
expected: number,
|
|
315
|
+
generation: number,
|
|
316
|
+
reason: string,
|
|
317
|
+
controller: Controller,
|
|
318
|
+
): TeamAssignment {
|
|
319
|
+
return this.store.request(
|
|
320
|
+
'local-operator',
|
|
321
|
+
request,
|
|
322
|
+
{ kind: 'team-reject', id, expected, generation, reason },
|
|
323
|
+
() => {
|
|
324
|
+
const assignment = this.get(id),
|
|
325
|
+
task = this.tasks.get(assignment.task_id),
|
|
326
|
+
step = task.steps.find((s) => s.id === assignment.step_id)!;
|
|
327
|
+
this.store.authorizeControl(task.session_id, controller);
|
|
328
|
+
this.expect(task.id, expected);
|
|
329
|
+
if (
|
|
330
|
+
task.state === 'cancelled' ||
|
|
331
|
+
assignment.generation !== generation ||
|
|
332
|
+
!step.attempt_id ||
|
|
333
|
+
!['awaiting_review', 'failed', 'interrupted'].includes(step.state)
|
|
334
|
+
)
|
|
335
|
+
throw new ServiceError('team_claim_stale', 'Reject the current settled claim only');
|
|
336
|
+
if (!reason.trim() || reason.length > 4096)
|
|
337
|
+
throw new ServiceError('invalid_field', 'Record a bounded rejection reason', 400);
|
|
338
|
+
this.store.db
|
|
339
|
+
.prepare('INSERT INTO team_rejections VALUES (?,?,?,?)')
|
|
340
|
+
.run(step.id, step.attempt_id, request, reason);
|
|
341
|
+
this.store.db
|
|
342
|
+
.prepare("UPDATE task_steps SET state='failed',review=? WHERE id=?")
|
|
343
|
+
.run(reason, step.id);
|
|
344
|
+
this.store.db
|
|
345
|
+
.prepare('UPDATE task_runtime SET auto_enabled=0 WHERE task_id=?')
|
|
346
|
+
.run(task.id);
|
|
347
|
+
this.store.db
|
|
348
|
+
.prepare(
|
|
349
|
+
"UPDATE durable_tasks SET state='blocked',reason='Operator rejected worker proposal; explicit follow-up required' WHERE id=?",
|
|
350
|
+
)
|
|
351
|
+
.run(task.id);
|
|
352
|
+
this.changed(task.id, 'proposal_rejected', {
|
|
353
|
+
assignment_id: id,
|
|
354
|
+
generation,
|
|
355
|
+
attempt_id: step.attempt_id,
|
|
356
|
+
reason,
|
|
357
|
+
});
|
|
358
|
+
return this.get(id);
|
|
359
|
+
},
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
assert(binding: WorkerBinding): TeamAssignment {
|
|
363
|
+
const row = this.get(binding.assignment!.id),
|
|
364
|
+
task = this.tasks.get(row.task_id),
|
|
365
|
+
step = task.steps.find((s) => s.id === row.step_id);
|
|
366
|
+
if (
|
|
367
|
+
task.state === 'cancelled' ||
|
|
368
|
+
row.plan_revision !== task.plan_revision ||
|
|
369
|
+
row.generation !== binding.assignment!.generation ||
|
|
370
|
+
row.child_id === null ||
|
|
371
|
+
this.children.inspect(task.session_id, row.child_id).child_attempt !== binding.attempt_id ||
|
|
372
|
+
step?.attempt_id !== binding.attempt_id ||
|
|
373
|
+
binding.owner_task !== task.id ||
|
|
374
|
+
binding.owner_session !== task.session_id ||
|
|
375
|
+
binding.agent?.id !== row.grant_id
|
|
376
|
+
)
|
|
377
|
+
throw new ServiceError(
|
|
378
|
+
'team_claim_stale',
|
|
379
|
+
'Worker no longer owns the current task-step claim',
|
|
380
|
+
);
|
|
381
|
+
return row;
|
|
382
|
+
}
|
|
383
|
+
private changed(task: string, kind: string, detail: unknown): void {
|
|
384
|
+
this.store.db.prepare('UPDATE durable_tasks SET revision=revision+1 WHERE id=?').run(task);
|
|
385
|
+
this.store.event(this.tasks.get(task).session_id, 'team.' + kind, {
|
|
386
|
+
task_id: task,
|
|
387
|
+
...(detail as object),
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
view(task: string): unknown {
|
|
391
|
+
const current = this.tasks.get(task);
|
|
392
|
+
const assignments = (
|
|
393
|
+
this.store.db
|
|
394
|
+
.prepare('SELECT * FROM team_assignments WHERE task_id=? ORDER BY rowid')
|
|
395
|
+
.all(task) as unknown as TeamAssignment[]
|
|
396
|
+
).map((a) => ({
|
|
397
|
+
...a,
|
|
398
|
+
stale: a.plan_revision !== current.plan_revision,
|
|
399
|
+
step_state: current.steps.find((s) => s.id === a.step_id)?.state,
|
|
400
|
+
worker: a.child_id ? this.children.inspect(current.session_id, a.child_id).worker : null,
|
|
401
|
+
runtime: this.children.external?.assignment(a.id) ?? null,
|
|
402
|
+
execution:
|
|
403
|
+
a.child_id && this.children.inspect(current.session_id, a.child_id).worker?.binding.external
|
|
404
|
+
? {
|
|
405
|
+
state: this.children.external!.row(a.child_id).state,
|
|
406
|
+
stopped: this.children.external!.stopped(a.child_id),
|
|
407
|
+
stop_scope:
|
|
408
|
+
this.children.external!.assignment(a.id)?.kind === 'a2a'
|
|
409
|
+
? (() => {
|
|
410
|
+
const receipt = this.children.external!.row(a.child_id).receipt;
|
|
411
|
+
return receipt
|
|
412
|
+
? (JSON.parse(receipt).scope ?? 'remote-terminal-receipt')
|
|
413
|
+
: 'remote-terminal-receipt';
|
|
414
|
+
})()
|
|
415
|
+
: 'owned-processes',
|
|
416
|
+
peer: (() => {
|
|
417
|
+
const row = this.children.external!.row(a.child_id);
|
|
418
|
+
const cp = row.checkpoint ? JSON.parse(row.checkpoint) : null;
|
|
419
|
+
return cp?.dialect === 'a2a'
|
|
420
|
+
? {
|
|
421
|
+
peer_id: cp.peer_id,
|
|
422
|
+
task_id: cp.task_id,
|
|
423
|
+
context_id: cp.context_id,
|
|
424
|
+
cancel_state: cp.cancel_state ?? null,
|
|
425
|
+
remote_state: cp.result?.status?.state ?? null,
|
|
426
|
+
}
|
|
427
|
+
: null;
|
|
428
|
+
})(),
|
|
429
|
+
}
|
|
430
|
+
: null,
|
|
431
|
+
}));
|
|
432
|
+
const messages = this.store.db
|
|
433
|
+
.prepare(
|
|
434
|
+
'SELECT id,sender,recipient,created_at,delivered_at,acknowledged_at FROM team_messages WHERE task_id=? ORDER BY rowid DESC LIMIT 128',
|
|
435
|
+
)
|
|
436
|
+
.all(task);
|
|
437
|
+
return {
|
|
438
|
+
task_id: task,
|
|
439
|
+
assignments,
|
|
440
|
+
messages,
|
|
441
|
+
bounds: {
|
|
442
|
+
root_concurrency: current.limits.concurrency,
|
|
443
|
+
session_workers: 2,
|
|
444
|
+
daemon_workers: 8,
|
|
445
|
+
wake_on_message: false,
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
send(
|
|
450
|
+
taskId: string,
|
|
451
|
+
sender: string,
|
|
452
|
+
recipient: string,
|
|
453
|
+
text: string,
|
|
454
|
+
messageId: string,
|
|
455
|
+
authorize: () => void,
|
|
456
|
+
): unknown {
|
|
457
|
+
return this.store.request(
|
|
458
|
+
`team-message:${taskId}:${sender}`,
|
|
459
|
+
messageId,
|
|
460
|
+
{ recipient, text },
|
|
461
|
+
() => {
|
|
462
|
+
authorize();
|
|
463
|
+
const task = this.tasks.get(taskId);
|
|
464
|
+
if (task.state === 'cancelled') throw new ServiceError('task_settled', 'Task is cancelled');
|
|
465
|
+
if (
|
|
466
|
+
this.get(recipient).task_id !== taskId ||
|
|
467
|
+
(sender !== 'operator' && this.get(sender).task_id !== taskId)
|
|
468
|
+
)
|
|
469
|
+
throw new ServiceError('team_message_scope', 'Messages stay within their owning task');
|
|
470
|
+
if (!text.trim() || Buffer.byteLength(text) > 4096 || !messageId || messageId.length > 128)
|
|
471
|
+
throw new ServiceError(
|
|
472
|
+
'invalid_field',
|
|
473
|
+
'Bound messages to 4 KiB and IDs to 128 characters',
|
|
474
|
+
400,
|
|
475
|
+
);
|
|
476
|
+
const n = Number(
|
|
477
|
+
this.store.db
|
|
478
|
+
.prepare('SELECT count(*) AS n FROM team_messages WHERE task_id=?')
|
|
479
|
+
.get(taskId)!.n,
|
|
480
|
+
);
|
|
481
|
+
if (
|
|
482
|
+
n >= 256 ||
|
|
483
|
+
Number(
|
|
484
|
+
this.store.db
|
|
485
|
+
.prepare(
|
|
486
|
+
'SELECT count(*) AS n FROM team_messages WHERE recipient=? AND acknowledged_at IS NULL',
|
|
487
|
+
)
|
|
488
|
+
.get(recipient)!.n,
|
|
489
|
+
) >= 32
|
|
490
|
+
)
|
|
491
|
+
throw new ServiceError('team_backpressure', 'Team message quota reached');
|
|
492
|
+
const id = randomUUID();
|
|
493
|
+
this.store.db
|
|
494
|
+
.prepare('INSERT INTO team_messages VALUES (?,?,?,?,?,?,?,NULL,NULL)')
|
|
495
|
+
.run(
|
|
496
|
+
id,
|
|
497
|
+
taskId,
|
|
498
|
+
sender,
|
|
499
|
+
recipient,
|
|
500
|
+
text,
|
|
501
|
+
digest({ sender, recipient, text }),
|
|
502
|
+
Date.now(),
|
|
503
|
+
);
|
|
504
|
+
this.store.event(task.session_id, 'team.message_saved', {
|
|
505
|
+
message_id: id,
|
|
506
|
+
sender,
|
|
507
|
+
recipient,
|
|
508
|
+
wakes_worker: false,
|
|
509
|
+
});
|
|
510
|
+
return { id, stored: true, delivered: false, acknowledged: false, wakes_worker: false };
|
|
511
|
+
},
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
receive(recipient: string, request: string, authorize: () => void): unknown {
|
|
515
|
+
return this.store.request('team-receive:' + recipient, request, {}, () => {
|
|
516
|
+
authorize();
|
|
517
|
+
const row = this.get(recipient),
|
|
518
|
+
now = Date.now();
|
|
519
|
+
const messages = this.store.db
|
|
520
|
+
.prepare(
|
|
521
|
+
'SELECT * FROM team_messages WHERE recipient=? AND acknowledged_at IS NULL ORDER BY rowid LIMIT 32',
|
|
522
|
+
)
|
|
523
|
+
.all(recipient);
|
|
524
|
+
for (const m of messages)
|
|
525
|
+
this.store.db
|
|
526
|
+
.prepare('UPDATE team_messages SET delivered_at=coalesce(delivered_at,?) WHERE id=?')
|
|
527
|
+
.run(now, String(m.id));
|
|
528
|
+
this.store.event(this.tasks.get(row.task_id).session_id, 'team.messages_delivered', {
|
|
529
|
+
recipient,
|
|
530
|
+
message_ids: messages.map((m) => m.id),
|
|
531
|
+
});
|
|
532
|
+
return messages.map((m) => ({ ...m, delivered_at: m.delivered_at ?? now }));
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
acknowledge(recipient: string, id: string, request: string, authorize: () => void): unknown {
|
|
536
|
+
return this.store.request('team-ack:' + recipient, request, { id }, () => {
|
|
537
|
+
authorize();
|
|
538
|
+
const row = this.get(recipient),
|
|
539
|
+
message = this.store.db
|
|
540
|
+
.prepare('SELECT * FROM team_messages WHERE id=? AND recipient=?')
|
|
541
|
+
.get(id, recipient);
|
|
542
|
+
if (!message?.delivered_at)
|
|
543
|
+
throw new ServiceError('message_not_delivered', 'Receive the message before acknowledging');
|
|
544
|
+
this.store.db
|
|
545
|
+
.prepare('UPDATE team_messages SET acknowledged_at=coalesce(acknowledged_at,?) WHERE id=?')
|
|
546
|
+
.run(Date.now(), id);
|
|
547
|
+
this.store.event(this.tasks.get(row.task_id).session_id, 'team.message_acknowledged', {
|
|
548
|
+
recipient,
|
|
549
|
+
message_id: id,
|
|
550
|
+
});
|
|
551
|
+
return { id, acknowledged: true };
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
}
|
package/src/tui-client.ts
CHANGED
|
@@ -18,6 +18,11 @@ import type { Workspace } from './workspaces.js';
|
|
|
18
18
|
export interface OperatorHealth {
|
|
19
19
|
service: string;
|
|
20
20
|
provider_route: { provider: string; model: string; max_tokens: number };
|
|
21
|
+
provider_capabilities?: {
|
|
22
|
+
output_token_cap: boolean;
|
|
23
|
+
durable_tasks: boolean;
|
|
24
|
+
warning: string | null;
|
|
25
|
+
};
|
|
21
26
|
coding_root?: string | null;
|
|
22
27
|
execution?: { commands?: boolean; available: boolean; backend?: Record<string, unknown> };
|
|
23
28
|
}
|