@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/dist/src/service.js
CHANGED
|
@@ -14,7 +14,7 @@ import { Store } from './store.js';
|
|
|
14
14
|
import { acquireStateLock } from './state-lock.js';
|
|
15
15
|
import { Artifacts } from './artifacts.js';
|
|
16
16
|
import { Supervisor } from './supervisor.js';
|
|
17
|
-
import { canonical, integer, object, ServiceError, string } from './protocol.js';
|
|
17
|
+
import { canonical, digest, integer, object, ServiceError, string } from './protocol.js';
|
|
18
18
|
import { readJson } from './transport.js';
|
|
19
19
|
import { Dispatcher } from './dispatcher.js';
|
|
20
20
|
import { ManagedWork } from './managed.js';
|
|
@@ -24,7 +24,17 @@ import { engineRequest } from './execution-boundary.js';
|
|
|
24
24
|
import { DockerCodingExecutor } from './coding-executor.js';
|
|
25
25
|
import { Workspaces } from './workspaces.js';
|
|
26
26
|
import { CodingWork } from './coding.js';
|
|
27
|
+
import { AgentProfiles } from './agent-profiles.js';
|
|
28
|
+
import { RepositorySnapshots } from './repository-snapshots.js';
|
|
29
|
+
import { TeamWork } from './team-work.js';
|
|
30
|
+
import { WorkerWorkspaces } from './worker-workspaces.js';
|
|
31
|
+
import { ExternalWork } from './external-work.js';
|
|
32
|
+
import { DockerHarnessBackend } from './harness-backend.js';
|
|
33
|
+
import { A2AInbound } from './a2a-inbound.js';
|
|
34
|
+
import { A2ABackend, ExternalBackends } from './a2a-backend.js';
|
|
27
35
|
export class SalaamService {
|
|
36
|
+
external;
|
|
37
|
+
a2aInbound;
|
|
28
38
|
stateDir;
|
|
29
39
|
socket;
|
|
30
40
|
bridgeSocket;
|
|
@@ -43,6 +53,10 @@ export class SalaamService {
|
|
|
43
53
|
dispatcher;
|
|
44
54
|
managed;
|
|
45
55
|
children;
|
|
56
|
+
agents;
|
|
57
|
+
repositories;
|
|
58
|
+
team;
|
|
59
|
+
workerWorkspaces;
|
|
46
60
|
workspaces;
|
|
47
61
|
coding;
|
|
48
62
|
codingSocket;
|
|
@@ -72,6 +86,13 @@ export class SalaamService {
|
|
|
72
86
|
this.tasks = new TaskContext(this.store, this.artifacts);
|
|
73
87
|
this.taskWork = new TaskWork(this.store, this.tasks);
|
|
74
88
|
this.taskBudget = new TaskBudget(this.store, this.taskWork);
|
|
89
|
+
if (options.admitModelRequest) {
|
|
90
|
+
const taskAdmission = this.store.taskModelAdmission;
|
|
91
|
+
this.store.taskModelAdmission = (session, attempt, input, output) => {
|
|
92
|
+
taskAdmission(session, attempt, input, output);
|
|
93
|
+
options.admitModelRequest(session, attempt, input, output);
|
|
94
|
+
};
|
|
95
|
+
}
|
|
75
96
|
this.workspaces = new Workspaces(this.store, this.artifacts, options.codingRoot);
|
|
76
97
|
if (options.codingSocket && !options.codingRoot)
|
|
77
98
|
throw new Error('Command execution requires an explicit --coding-root');
|
|
@@ -79,6 +100,23 @@ export class SalaamService {
|
|
|
79
100
|
(options.codingSocket
|
|
80
101
|
? new DockerCodingExecutor(engineRequest(options.codingSocket))
|
|
81
102
|
: undefined);
|
|
103
|
+
if (options.harnessConfig &&
|
|
104
|
+
(options.externalBackend ||
|
|
105
|
+
!(executor instanceof DockerCodingExecutor) ||
|
|
106
|
+
!options.codingSocket ||
|
|
107
|
+
!options.codingRoot))
|
|
108
|
+
throw Error('Harness configuration requires the real configured Docker executor and an exclusive backend');
|
|
109
|
+
if (options.a2aConfig && (options.externalBackend || !options.codingRoot || !executor))
|
|
110
|
+
throw Error('A2A configuration requires an owned coding root/executor and an exclusive backend');
|
|
111
|
+
const externalBackend = options.externalBackend ??
|
|
112
|
+
new ExternalBackends([
|
|
113
|
+
...(options.harnessConfig
|
|
114
|
+
? [
|
|
115
|
+
new DockerHarnessBackend(resolve(options.harnessConfig), join(resolve(options.codingRoot), 'external'), options.codingSocket, executor),
|
|
116
|
+
]
|
|
117
|
+
: []),
|
|
118
|
+
...(options.a2aConfig ? [new A2ABackend(resolve(options.a2aConfig))] : []),
|
|
119
|
+
]);
|
|
82
120
|
this.coding = new CodingWork(this.store, this.workspaces, this.artifacts, executor, (id, error) => this.supervisor.failControl(id, error));
|
|
83
121
|
this.taskVerification = new TaskVerification(this.store, this.taskWork, this.taskBudget, this.workspaces, this.coding, this.artifacts);
|
|
84
122
|
this.taskCheckpoints = new TaskCheckpoints(this.store, this.taskWork, this.taskVerification, this.workspaces, this.artifacts);
|
|
@@ -89,22 +127,59 @@ export class SalaamService {
|
|
|
89
127
|
effectsEnabled: !!options.effectConnector,
|
|
90
128
|
});
|
|
91
129
|
this.supervisor.tasks = this.tasks;
|
|
130
|
+
this.taskWork.providerCheck = (limits) => this.supervisor.assertTaskProviderSupported(limits);
|
|
92
131
|
this.supervisor.coding = this.coding;
|
|
93
132
|
this.managed = new ManagedWork(this.store, {
|
|
94
133
|
connector: options.effectConnector,
|
|
95
134
|
approvalTtlMs: options.approvalTtlMs,
|
|
96
|
-
assertActive: (id, epoch, attempt) => this.
|
|
135
|
+
assertActive: (id, epoch, attempt) => this.external?.owns(id)
|
|
136
|
+
? this.external.assertActive(id, epoch, attempt)
|
|
137
|
+
: this.supervisor.assertActive(id, epoch, attempt),
|
|
97
138
|
reply: (id, epoch, request, confirmed) => this.supervisor.replyUI(id, epoch, request, confirmed),
|
|
98
|
-
failed: (id, error) => this.
|
|
139
|
+
failed: (id, error) => this.external?.owns(id)
|
|
140
|
+
? this.external.failControl(id, error)
|
|
141
|
+
: this.supervisor.failControl(id, error),
|
|
99
142
|
});
|
|
100
143
|
this.supervisor.managed = this.managed;
|
|
101
|
-
this.
|
|
144
|
+
this.agents = new AgentProfiles(this.store, this.supervisor.route, async (route, session) => {
|
|
145
|
+
const prepared = await this.supervisor.prepareProviderRoute(route);
|
|
146
|
+
const task = this.taskWork.forSession(session);
|
|
147
|
+
if (task)
|
|
148
|
+
this.supervisor.assertTaskProviderSupported(task.limits, prepared.output_cap);
|
|
149
|
+
return { route: prepared.route, output_cap: prepared.output_cap };
|
|
150
|
+
});
|
|
151
|
+
this.store.workerBoundary.profiles = this.agents;
|
|
152
|
+
this.agents.validateFrame = (session) => {
|
|
153
|
+
this.tasks.frame(session, 'x'.repeat(32));
|
|
154
|
+
};
|
|
155
|
+
this.repositories = new RepositorySnapshots(this.store, this.artifacts, this.workspaces);
|
|
156
|
+
this.supervisor.repositories = this.repositories;
|
|
157
|
+
this.children = new ChildWork(this.store, this.artifacts, this.supervisor, this.repositories);
|
|
102
158
|
this.supervisor.children = this.children;
|
|
159
|
+
this.workerWorkspaces = new WorkerWorkspaces(this.store, this.workspaces, this.repositories, this.artifacts, this.coding, this.children);
|
|
160
|
+
this.children.workspaces = this.workerWorkspaces;
|
|
161
|
+
this.agents.assertExecution = () => {
|
|
162
|
+
this.workerWorkspaces.execution();
|
|
163
|
+
};
|
|
164
|
+
this.store.workerBoundary.assertIsolation = (binding) => this.workerWorkspaces.assert(binding);
|
|
165
|
+
this.taskWork.assertStepReview = (task, step) => this.workerWorkspaces.assertStepReview(task, step);
|
|
166
|
+
this.team = new TeamWork(this.store, this.taskWork, this.taskBudget, this.children, this.agents);
|
|
167
|
+
this.supervisor.team = this.team;
|
|
168
|
+
this.external = new ExternalWork(this.store, this.artifacts, this.children, this.taskBudget, this.managed, this.workspaces, externalBackend, options.admitModelRequest);
|
|
169
|
+
this.children.external = this.external;
|
|
170
|
+
const guardSubmission = this.store.guardTaskSubmission;
|
|
171
|
+
this.store.guardTaskSubmission = (session, step) => {
|
|
172
|
+
const child = this.children.owned(session);
|
|
173
|
+
if (child && this.store.workerBoundary.view(child.id).binding.external)
|
|
174
|
+
throw new ServiceError('external_worker_owned', 'Use the owning team assignment and an explicit continuation');
|
|
175
|
+
guardSubmission(session, step);
|
|
176
|
+
};
|
|
103
177
|
this.dispatcher = new Dispatcher(this.store, this.supervisor);
|
|
104
178
|
this.taskDriver = new TaskDriver(this.store, this.taskWork, this.taskBudget, this.taskVerification, this.supervisor, this.children, this.coding);
|
|
105
179
|
this.jobs = new JobWork(this.store, this.taskWork, this.taskDriver, this.taskVerification, this.managed);
|
|
106
180
|
this.recurring = new RecurringWork(this.jobs);
|
|
107
181
|
this.inbox = new OperatorInbox(this.store, (error) => {
|
|
182
|
+
this.external.failAll(error);
|
|
108
183
|
for (const session of this.store.sessions())
|
|
109
184
|
this.supervisor.failControl(session.id, error);
|
|
110
185
|
});
|
|
@@ -164,19 +239,27 @@ export class SalaamService {
|
|
|
164
239
|
(service.coding.executor?.profileDigest !== options.expectedExecution.profile_digest ||
|
|
165
240
|
service.coding.executor?.engineId !== options.expectedExecution.engine_id))
|
|
166
241
|
throw new Error('Host executor does not match reviewed manifest');
|
|
242
|
+
await service.supervisor.providerAuth();
|
|
243
|
+
await service.external.backend?.prepare?.();
|
|
167
244
|
await service.supervisor.recover();
|
|
245
|
+
await service.external.recover();
|
|
168
246
|
service.store.recoverWork();
|
|
169
247
|
service.taskWork.recover();
|
|
170
248
|
service.tasks.recover();
|
|
171
249
|
service.workspaces.recover();
|
|
172
250
|
service.coding.recover();
|
|
173
251
|
service.children.recover();
|
|
252
|
+
service.workerWorkspaces.recover();
|
|
174
253
|
service.managed.recover();
|
|
175
254
|
await service.managed.reconcileAll();
|
|
176
255
|
service.taskCheckpoints.recover();
|
|
177
256
|
await service.jobs.recover();
|
|
178
257
|
await service.recurring.recover();
|
|
179
258
|
service.inbox.reconcile();
|
|
259
|
+
if (options.a2aListenConfig) {
|
|
260
|
+
service.a2aInbound = new A2AInbound(service, resolve(options.a2aListenConfig));
|
|
261
|
+
await service.a2aInbound.listen();
|
|
262
|
+
}
|
|
180
263
|
await service.listen(service.bridgeServer, service.bridgeSocket);
|
|
181
264
|
await service.listen(service.operator, service.socket);
|
|
182
265
|
service.taskDriver.start();
|
|
@@ -213,6 +296,46 @@ export class SalaamService {
|
|
|
213
296
|
throw new ServiceError('stopping', 'Service is stopping', 503);
|
|
214
297
|
const url = new URL(req.url ?? '/', 'http://local');
|
|
215
298
|
const parts = url.pathname.split('/').filter(Boolean);
|
|
299
|
+
if (!bridge &&
|
|
300
|
+
req.method === 'GET' &&
|
|
301
|
+
parts[0] === 'v1' &&
|
|
302
|
+
parts[1] === 'a2a' &&
|
|
303
|
+
parts[2] === 'grants' &&
|
|
304
|
+
parts[3] &&
|
|
305
|
+
parts.length === 4) {
|
|
306
|
+
this.send(res, 200, A2AInbound.binding(this, parts[3]));
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (!bridge && parts[0] === 'v1' && parts[1] === 'harnesses') {
|
|
310
|
+
if (req.method === 'GET' && parts.length === 2) {
|
|
311
|
+
this.send(res, 200, this.external.runtimes());
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (parts[2]) {
|
|
315
|
+
const raw = this.store.db
|
|
316
|
+
.prepare('SELECT * FROM child_tasks WHERE id=?')
|
|
317
|
+
.get(parts[2]);
|
|
318
|
+
if (!raw || !this.store.workerBoundary.view(raw.id).binding.external)
|
|
319
|
+
throw new ServiceError('not_found', 'External worker not found', 404);
|
|
320
|
+
if (req.method === 'GET' && parts.length === 3) {
|
|
321
|
+
this.send(res, 200, {
|
|
322
|
+
child: this.children.inspect(raw.parent_session, raw.id),
|
|
323
|
+
execution: this.external.view(raw.id),
|
|
324
|
+
});
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (req.method === 'POST' && parts.length === 4 && parts[3] === 'reconcile') {
|
|
328
|
+
const body = object(await readJson(req));
|
|
329
|
+
const result = await this.external.reconcile(raw, string(body.request_id, 'request_id', 128), string(body.run_id, 'run_id', 128), () => this.store.authorizeControl(raw.parent_session, {
|
|
330
|
+
client_id: string(body.client_id, 'client_id', 128),
|
|
331
|
+
token: string(body.token, 'token', 128),
|
|
332
|
+
}));
|
|
333
|
+
this.send(res, 200, result);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
throw new ServiceError('not_found', 'External worker route not found', 404);
|
|
338
|
+
}
|
|
216
339
|
if (bridge) {
|
|
217
340
|
if (req.method !== 'POST' || parts.length !== 3 || parts[0] !== 'worker')
|
|
218
341
|
throw new ServiceError('not_found', 'Bridge route not found', 404);
|
|
@@ -332,7 +455,95 @@ export class SalaamService {
|
|
|
332
455
|
}
|
|
333
456
|
throw new ServiceError('not_found', 'Job route not found', 404);
|
|
334
457
|
}
|
|
458
|
+
if (parts[0] === 'v1' &&
|
|
459
|
+
parts[1] === 'integrations' &&
|
|
460
|
+
parts[2] &&
|
|
461
|
+
parts.length === 4 &&
|
|
462
|
+
parts[3] === 'resolve' &&
|
|
463
|
+
req.method === 'POST') {
|
|
464
|
+
const body = object(await readJson(req)), controller = {
|
|
465
|
+
client_id: string(body.client_id, 'client_id', 128),
|
|
466
|
+
token: string(body.token, 'token', 128),
|
|
467
|
+
}, parent = this.workerWorkspaces.owner(parts[2]);
|
|
468
|
+
this.send(res, 200, this.workerWorkspaces.resolve(parts[2], string(body.request_id, 'request_id', 128), integer(body.workspace_revision, 'workspace_revision'), string(body.workspace_digest, 'workspace_digest', 128), () => this.store.authorizeControl(parent, controller)));
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (parts[0] === 'v1' &&
|
|
472
|
+
parts[1] === 'integrations' &&
|
|
473
|
+
parts[2] &&
|
|
474
|
+
parts.length === 3 &&
|
|
475
|
+
req.method === 'GET') {
|
|
476
|
+
this.send(res, 200, this.workerWorkspaces.inspect(parts[2]));
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
if (parts[0] === 'v1' && parts[1] === 'workers' && parts[2] && parts.length === 4) {
|
|
480
|
+
const child = this.store.db
|
|
481
|
+
.prepare('SELECT parent_session FROM child_tasks WHERE id=?')
|
|
482
|
+
.get(parts[2]);
|
|
483
|
+
if (!child)
|
|
484
|
+
throw new ServiceError('not_found', 'Worker not found', 404);
|
|
485
|
+
const parent = String(child.parent_session);
|
|
486
|
+
if (parts[3] === 'proposal' && req.method === 'GET') {
|
|
487
|
+
this.send(res, 200, this.workerWorkspaces.proposal(parent, parts[2], integer(Number(url.searchParams.get('offset') ?? '0'), 'offset')));
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
if (parts[3] === 'integrate' && req.method === 'POST') {
|
|
491
|
+
const body = object(await readJson(req)), controller = {
|
|
492
|
+
client_id: string(body.client_id, 'client_id', 128),
|
|
493
|
+
token: string(body.token, 'token', 128),
|
|
494
|
+
};
|
|
495
|
+
this.send(res, 200, this.workerWorkspaces.integrate(parent, parts[2], string(body.request_id, 'request_id', 128), integer(body.workspace_revision, 'workspace_revision'), string(body.workspace_digest, 'workspace_digest', 128), string(body.patch_digest, 'patch_digest', 128), () => this.store.authorizeControl(parent, controller)));
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
335
499
|
if (parts[0] === 'v1' && parts[1] === 'tasks') {
|
|
500
|
+
if (parts[2] && parts[3] === 'team') {
|
|
501
|
+
const task = this.taskWork.get(parts[2]);
|
|
502
|
+
if (req.method === 'GET' && parts.length === 4) {
|
|
503
|
+
this.send(res, 200, this.team.view(task.id));
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
if (req.method === 'POST') {
|
|
507
|
+
const body = object(await readJson(req));
|
|
508
|
+
const request = string(body.request_id, 'request_id', 128);
|
|
509
|
+
const controller = {
|
|
510
|
+
client_id: string(body.client_id, 'client_id', 128),
|
|
511
|
+
token: string(body.token, 'token', 128),
|
|
512
|
+
};
|
|
513
|
+
const authorize = () => this.store.authorizeControl(task.session_id, controller);
|
|
514
|
+
let result;
|
|
515
|
+
if (parts.length === 5 && parts[4] === 'assign') {
|
|
516
|
+
if (body.runtime !== undefined && body.requests !== undefined)
|
|
517
|
+
throw new ServiceError('worker_budget_scope', 'Use invocations for an external runtime; requests always means model requests', 400);
|
|
518
|
+
result = this.team.assign(task.id, string(body.step_id, 'step_id', 128), string(body.profile_id, 'profile_id', 128), integer(body.runtime === undefined ? body.requests : body.invocations, body.runtime === undefined ? 'requests' : 'invocations'), request, integer(body.expected_revision, 'expected_revision'), controller, body.runtime === undefined ? undefined : string(body.runtime, 'runtime', 128));
|
|
519
|
+
}
|
|
520
|
+
else if (parts.length === 5 && parts[4] === 'send') {
|
|
521
|
+
result = this.team.send(task.id, 'operator', string(body.recipient, 'recipient', 128), string(body.text, 'text', 4096), request, authorize);
|
|
522
|
+
}
|
|
523
|
+
else if (parts.length === 6 && parts[4]) {
|
|
524
|
+
const assignment = this.team.get(parts[4]);
|
|
525
|
+
if (assignment.task_id !== task.id)
|
|
526
|
+
throw new ServiceError('team_message_scope', 'Assignment belongs to another task');
|
|
527
|
+
if (parts[5] === 'run')
|
|
528
|
+
result = this.team.run(assignment.id, request, integer(body.expected_revision, 'expected_revision'), integer(body.generation, 'generation'), body.instruction == null ? null : string(body.instruction, 'instruction', 4096), body.continues_attempt_id == null
|
|
529
|
+
? null
|
|
530
|
+
: string(body.continues_attempt_id, 'continues_attempt_id', 128), controller);
|
|
531
|
+
else if (parts[5] === 'reject')
|
|
532
|
+
result = this.team.reject(assignment.id, request, integer(body.expected_revision, 'expected_revision'), integer(body.generation, 'generation'), string(body.reason, 'reason', 4096), controller);
|
|
533
|
+
else if (parts[5] === 'receive')
|
|
534
|
+
result = this.team.receive(assignment.id, request, authorize);
|
|
535
|
+
else if (parts[5] === 'ack')
|
|
536
|
+
result = this.team.acknowledge(assignment.id, string(body.message_id, 'message_id', 128), request, authorize);
|
|
537
|
+
else
|
|
538
|
+
throw new ServiceError('not_found', 'Team action not found', 404);
|
|
539
|
+
}
|
|
540
|
+
else
|
|
541
|
+
throw new ServiceError('not_found', 'Team route not found', 404);
|
|
542
|
+
this.send(res, 200, result);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
throw new ServiceError('not_found', 'Team route not found', 404);
|
|
546
|
+
}
|
|
336
547
|
if (req.method === 'GET' && parts.length === 2) {
|
|
337
548
|
const session = string(url.searchParams.get('session_id'), 'session_id', 128);
|
|
338
549
|
this.store.session(session);
|
|
@@ -430,6 +641,22 @@ export class SalaamService {
|
|
|
430
641
|
const sessions = this.store.sessions().map((x) => this.supervisor.inspect(x.id));
|
|
431
642
|
this.send(res, 200, {
|
|
432
643
|
service: 'healthy',
|
|
644
|
+
a2a: {
|
|
645
|
+
inbound_enabled: !!this.a2aInbound,
|
|
646
|
+
listener: this.a2aInbound?.server.address() ?? null,
|
|
647
|
+
wire_version: '1.0',
|
|
648
|
+
remote_execution: 'unobserved',
|
|
649
|
+
inbound_authority: 'one-use-native-analysis-assignment',
|
|
650
|
+
},
|
|
651
|
+
external_workers: {
|
|
652
|
+
control: this.external.status(),
|
|
653
|
+
runtimes: this.external.runtimes(),
|
|
654
|
+
opaque_budget: 'explicit-invocations-only',
|
|
655
|
+
unknown_model_usage: true,
|
|
656
|
+
uncertain_stops: Number(this.store.db
|
|
657
|
+
.prepare("SELECT count(*) AS n FROM external_runs WHERE stopped=0 AND state='unknown'")
|
|
658
|
+
.get().n),
|
|
659
|
+
},
|
|
433
660
|
job_execution: {
|
|
434
661
|
durable_jobs: true,
|
|
435
662
|
one_shot_triggers: !!this.coding.executor,
|
|
@@ -441,16 +668,18 @@ export class SalaamService {
|
|
|
441
668
|
control_error: this.jobs.controlError,
|
|
442
669
|
},
|
|
443
670
|
task_execution: {
|
|
444
|
-
durable_tasks:
|
|
671
|
+
durable_tasks: this.supervisor.providerCapabilities().durable_tasks,
|
|
445
672
|
explicit_steps: true,
|
|
446
673
|
automatic_progression: 'operator-bounded-opt-in',
|
|
447
674
|
verified_completion: !!this.coding.executor,
|
|
448
675
|
verification_supported: true,
|
|
449
676
|
task_budgets: true,
|
|
677
|
+
task_token_budgets: this.supervisor.providerCapabilities().output_token_cap,
|
|
450
678
|
checkpoints: true,
|
|
451
679
|
deterministic_compaction: 'operator-bounded-idle-only',
|
|
452
680
|
},
|
|
453
681
|
provider_route: this.supervisor.route,
|
|
682
|
+
provider_capabilities: this.supervisor.providerCapabilities(),
|
|
454
683
|
effect_connector: this.managed.options.connector?.id ?? null,
|
|
455
684
|
coding_root: this.workspaces.root ?? null,
|
|
456
685
|
coding_socket: this.codingSocket,
|
|
@@ -524,6 +753,40 @@ export class SalaamService {
|
|
|
524
753
|
}
|
|
525
754
|
if (parts[0] === 'v1' && parts[1] === 'sessions' && parts[2]) {
|
|
526
755
|
const id = parts[2];
|
|
756
|
+
if (parts[3] === 'agents') {
|
|
757
|
+
if (req.method === 'GET') {
|
|
758
|
+
const profile = url.searchParams.get('profile');
|
|
759
|
+
this.send(res, 200, profile
|
|
760
|
+
? {
|
|
761
|
+
...this.agents.inspect(id, profile),
|
|
762
|
+
digest: digest(this.agents.inspect(id, profile)),
|
|
763
|
+
}
|
|
764
|
+
: this.agents.list(id).map((p) => ({ ...p, digest: digest(p) })));
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
const body = object(await readJson(req));
|
|
768
|
+
const controller = {
|
|
769
|
+
client_id: string(body.client_id, 'client_id', 128),
|
|
770
|
+
token: string(body.token, 'token', 128),
|
|
771
|
+
};
|
|
772
|
+
const request = string(body.request_id, 'request_id', 128);
|
|
773
|
+
if (parts[4] === 'admit') {
|
|
774
|
+
const facts = body.fact_ids === undefined
|
|
775
|
+
? []
|
|
776
|
+
: Array.isArray(body.fact_ids)
|
|
777
|
+
? body.fact_ids.map((x) => string(x, 'fact_id', 128))
|
|
778
|
+
: (() => {
|
|
779
|
+
throw new ServiceError('invalid_field', 'fact_ids must be an array');
|
|
780
|
+
})();
|
|
781
|
+
this.send(res, 201, await this.agents.admit(id, request, string(body.profile_id, 'profile_id', 128), string(body.expected_digest, 'expected_digest', 64), facts, controller));
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
if (parts[4] === 'revoke') {
|
|
785
|
+
this.send(res, 200, this.agents.revoke(id, string(body.grant_id, 'grant_id', 128), request, controller));
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
throw new ServiceError('not_found', 'Agent operation not found', 404);
|
|
789
|
+
}
|
|
527
790
|
if (parts[3] === 'activity' && req.method === 'GET') {
|
|
528
791
|
this.store.session(id);
|
|
529
792
|
const rows = this.store.db
|
|
@@ -693,10 +956,10 @@ export class SalaamService {
|
|
|
693
956
|
token: string(body.token, 'token', 128),
|
|
694
957
|
});
|
|
695
958
|
this.coding.cancelAttempt(attempt);
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
.catch((error) => this.supervisor.failControl(this.store.attempt(attempt).session_id, error));
|
|
959
|
+
const owned = this.children.owned(this.store.attempt(attempt).session_id);
|
|
960
|
+
void (owned && this.store.workerBoundary.view(owned.id).binding.external
|
|
961
|
+
? this.external.cancel(owned)
|
|
962
|
+
: this.children.cancelOwned(attempt).then(() => this.supervisor.cancelAttempt(attempt))).catch((error) => this.supervisor.failControl(this.store.attempt(attempt).session_id, error));
|
|
700
963
|
this.send(res, 202, receipt);
|
|
701
964
|
return;
|
|
702
965
|
}
|
|
@@ -905,6 +1168,8 @@ export class SalaamService {
|
|
|
905
1168
|
this.children.pause();
|
|
906
1169
|
const errors = [];
|
|
907
1170
|
try {
|
|
1171
|
+
await this.a2aInbound?.close();
|
|
1172
|
+
await this.external.close();
|
|
908
1173
|
await this.coding.close();
|
|
909
1174
|
try {
|
|
910
1175
|
await this.children.close();
|
package/dist/src/store.js
CHANGED
|
@@ -3,6 +3,7 @@ import { chmodSync } from 'node:fs';
|
|
|
3
3
|
import { randomUUID, randomBytes } from 'node:crypto';
|
|
4
4
|
import { EventEmitter } from 'node:events';
|
|
5
5
|
import { UsageLedger } from './usage.js';
|
|
6
|
+
import { WorkerBoundary } from './worker-boundary.js';
|
|
6
7
|
import { OFFLINE_ROUTE } from './provider-config.js';
|
|
7
8
|
import { canonical } from './protocol.js';
|
|
8
9
|
import { digest, ServiceError, textDigest, } from './protocol.js';
|
|
@@ -10,9 +11,11 @@ export class Store {
|
|
|
10
11
|
db;
|
|
11
12
|
changes = new EventEmitter();
|
|
12
13
|
usage = new UsageLedger(this);
|
|
14
|
+
workerBoundary = new WorkerBoundary(this);
|
|
13
15
|
reconcileCompactions;
|
|
14
16
|
finishChild;
|
|
15
17
|
projectTaskAttempt;
|
|
18
|
+
teamSnapshot;
|
|
16
19
|
guardTaskSubmission;
|
|
17
20
|
cancelChildren;
|
|
18
21
|
recordTaskMutation;
|
|
@@ -32,7 +35,7 @@ export class Store {
|
|
|
32
35
|
this.db.exec(`PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL; PRAGMA foreign_keys=ON; PRAGMA busy_timeout=1000;`);
|
|
33
36
|
const version = this.db.prepare('PRAGMA user_version').get()
|
|
34
37
|
.user_version;
|
|
35
|
-
if (version >
|
|
38
|
+
if (version > 17) {
|
|
36
39
|
this.db.close();
|
|
37
40
|
throw new ServiceError('schema_too_new', 'Database schema is newer than this service');
|
|
38
41
|
}
|
|
@@ -158,6 +161,38 @@ export class Store {
|
|
|
158
161
|
CREATE TABLE recurring_occurrences(id TEXT PRIMARY KEY,job_id TEXT NOT NULL REFERENCES recurring_jobs(id),occurrence_key TEXT NOT NULL,session_id TEXT NOT NULL UNIQUE REFERENCES sessions(id),job_run_id TEXT UNIQUE REFERENCES jobs(id),schedule_revision INTEGER NOT NULL,due_at INTEGER,state TEXT NOT NULL,error TEXT,created_at INTEGER NOT NULL,UNIQUE(job_id,occurrence_key));
|
|
159
162
|
CREATE TABLE operator_inbox(id TEXT PRIMARY KEY,session_id TEXT NOT NULL REFERENCES sessions(id),source_key TEXT NOT NULL UNIQUE,kind TEXT NOT NULL,detail TEXT NOT NULL,revision INTEGER NOT NULL,state TEXT NOT NULL,ack_revision INTEGER NOT NULL DEFAULT 0,updated_at INTEGER NOT NULL);
|
|
160
163
|
PRAGMA user_version=11; COMMIT;`);
|
|
164
|
+
if (this.db.prepare('PRAGMA user_version').get().user_version < 12) {
|
|
165
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
166
|
+
this.workerBoundary.migrate();
|
|
167
|
+
this.db.exec('PRAGMA user_version=12; COMMIT;');
|
|
168
|
+
}
|
|
169
|
+
if (this.db.prepare('PRAGMA user_version').get().user_version < 13)
|
|
170
|
+
this.db.exec(`BEGIN IMMEDIATE;
|
|
171
|
+
CREATE TABLE agent_grants(id TEXT PRIMARY KEY,owner_session TEXT NOT NULL REFERENCES sessions(id),profile_id TEXT NOT NULL,document TEXT NOT NULL,digest TEXT NOT NULL,state TEXT NOT NULL);
|
|
172
|
+
CREATE TABLE specialist_reports(worker_id TEXT PRIMARY KEY,input_digest TEXT NOT NULL,artifact_id TEXT NOT NULL REFERENCES artifacts(id));
|
|
173
|
+
PRAGMA user_version=13; COMMIT;`);
|
|
174
|
+
if (this.db.prepare('PRAGMA user_version').get().user_version < 14)
|
|
175
|
+
this.db.exec(`BEGIN IMMEDIATE;
|
|
176
|
+
CREATE TABLE team_assignments(id TEXT PRIMARY KEY,task_id TEXT NOT NULL REFERENCES durable_tasks(id),step_id TEXT NOT NULL REFERENCES task_steps(id),plan_revision INTEGER NOT NULL,profile_id TEXT NOT NULL,grant_id TEXT NOT NULL REFERENCES agent_grants(id),budget INTEGER NOT NULL,generation INTEGER NOT NULL DEFAULT 0,child_id TEXT REFERENCES child_tasks(id),superseded INTEGER NOT NULL DEFAULT 0);
|
|
177
|
+
CREATE UNIQUE INDEX team_active_step ON team_assignments(step_id) WHERE superseded=0;
|
|
178
|
+
CREATE TABLE team_messages(id TEXT PRIMARY KEY,task_id TEXT NOT NULL REFERENCES durable_tasks(id),sender TEXT NOT NULL,recipient TEXT NOT NULL REFERENCES team_assignments(id),text TEXT NOT NULL,input_digest TEXT NOT NULL,created_at INTEGER NOT NULL,delivered_at INTEGER,acknowledged_at INTEGER);
|
|
179
|
+
PRAGMA user_version=14; COMMIT;`);
|
|
180
|
+
if (this.db.prepare('PRAGMA user_version').get().user_version < 15)
|
|
181
|
+
this.db.exec(`BEGIN IMMEDIATE;
|
|
182
|
+
CREATE TABLE worker_integrations(id TEXT PRIMARY KEY,parent_session TEXT NOT NULL REFERENCES sessions(id),child_id TEXT NOT NULL UNIQUE REFERENCES child_tasks(id),worker_id TEXT NOT NULL,patch_digest TEXT NOT NULL,before_revision INTEGER NOT NULL,before_digest TEXT NOT NULL,after_artifact TEXT NOT NULL REFERENCES artifacts(id),state TEXT NOT NULL,mutation_id TEXT REFERENCES workspace_mutations(id),mutation_key TEXT NOT NULL UNIQUE,generation INTEGER NOT NULL DEFAULT 1);
|
|
183
|
+
CREATE TABLE team_rejections(step_id TEXT NOT NULL REFERENCES task_steps(id),attempt_id TEXT NOT NULL REFERENCES attempts(id),request_id TEXT NOT NULL,reason TEXT NOT NULL,PRIMARY KEY(step_id,attempt_id));
|
|
184
|
+
PRAGMA user_version=15; COMMIT;`);
|
|
185
|
+
if (this.db.prepare('PRAGMA user_version').get().user_version < 16)
|
|
186
|
+
this.db.exec(`BEGIN IMMEDIATE;
|
|
187
|
+
CREATE TABLE external_assignments(assignment_id TEXT PRIMARY KEY REFERENCES team_assignments(id),runtime TEXT NOT NULL,digest TEXT NOT NULL);
|
|
188
|
+
CREATE TABLE external_runs(child_id TEXT PRIMARY KEY REFERENCES child_tasks(id),run_id TEXT NOT NULL UNIQUE,state TEXT NOT NULL,epoch INTEGER NOT NULL,checkpoint TEXT,receipt TEXT,stopped INTEGER NOT NULL DEFAULT 0,error TEXT,result TEXT,import_artifact TEXT REFERENCES artifacts(id));
|
|
189
|
+
CREATE TABLE task_invocations(attempt_id TEXT PRIMARY KEY REFERENCES attempts(id),task_id TEXT NOT NULL REFERENCES durable_tasks(id),runtime_id TEXT NOT NULL,input_digest TEXT NOT NULL,created_at INTEGER NOT NULL);
|
|
190
|
+
CREATE TABLE external_model_requests(id TEXT PRIMARY KEY,child_id TEXT NOT NULL REFERENCES external_runs(child_id),input_bytes INTEGER NOT NULL,output_bound INTEGER NOT NULL);
|
|
191
|
+
PRAGMA user_version=16; COMMIT;`);
|
|
192
|
+
if (this.db.prepare('PRAGMA user_version').get().user_version < 17)
|
|
193
|
+
this.db.exec(`BEGIN IMMEDIATE;
|
|
194
|
+
CREATE TABLE a2a_inbound(id TEXT PRIMARY KEY,peer TEXT NOT NULL,tenant TEXT NOT NULL,message_id TEXT NOT NULL,input_digest TEXT NOT NULL,assignment_id TEXT NOT NULL UNIQUE REFERENCES team_assignments(id),grant_digest TEXT NOT NULL,context_id TEXT NOT NULL UNIQUE,child_id TEXT NOT NULL UNIQUE REFERENCES child_tasks(id),created_at INTEGER NOT NULL,skill TEXT NOT NULL,cancel_requested INTEGER NOT NULL DEFAULT 0,UNIQUE(peer,tenant,message_id));
|
|
195
|
+
PRAGMA user_version=17; COMMIT;`);
|
|
161
196
|
}
|
|
162
197
|
catch (error) {
|
|
163
198
|
if (this.db.isTransaction)
|
|
@@ -641,7 +676,14 @@ export class Store {
|
|
|
641
676
|
operations: this.operations(id),
|
|
642
677
|
children: this.db
|
|
643
678
|
.prepare('SELECT * FROM child_tasks WHERE parent_session=? ORDER BY rowid DESC LIMIT 100')
|
|
644
|
-
.all(id)
|
|
679
|
+
.all(id).map((child) => ({
|
|
680
|
+
...child,
|
|
681
|
+
worker: this.workerBoundary.view(child.id),
|
|
682
|
+
})),
|
|
683
|
+
...(this.workerBoundary.profiles
|
|
684
|
+
? { agents: this.workerBoundary.profiles.snapshot(id) }
|
|
685
|
+
: {}),
|
|
686
|
+
...(this.teamSnapshot ? { team: this.teamSnapshot(id) } : {}),
|
|
645
687
|
};
|
|
646
688
|
}
|
|
647
689
|
cancelled(id) {
|