@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.
Files changed (110) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +25 -17
  3. package/dist/src/a2a-backend.js +225 -0
  4. package/dist/src/a2a-inbound.js +487 -0
  5. package/dist/src/a2a-peer.js +370 -0
  6. package/dist/src/a2a-protocol.js +250 -0
  7. package/dist/src/agent-profiles.js +289 -0
  8. package/dist/src/children.js +132 -26
  9. package/dist/src/claude-provider.js +2 -179
  10. package/dist/src/cli-help.js +107 -6
  11. package/dist/src/cli.js +477 -13
  12. package/dist/src/config.js +26 -3
  13. package/dist/src/credentials.js +7 -2
  14. package/dist/src/external-runtime.js +1 -0
  15. package/dist/src/external-work.js +583 -0
  16. package/dist/src/harness-backend.js +466 -0
  17. package/dist/src/harness-container.js +450 -0
  18. package/dist/src/harness-model-gateway.js +104 -0
  19. package/dist/src/harness-prepare.js +88 -0
  20. package/dist/src/harness-probe.js +111 -0
  21. package/dist/src/harness-protocol.js +325 -0
  22. package/dist/src/harness-readiness.js +50 -0
  23. package/dist/src/harness-rpc.js +206 -0
  24. package/dist/src/harness-tool-identity.js +87 -0
  25. package/dist/src/harness-wire-bridge.js +387 -0
  26. package/dist/src/host.js +6 -1
  27. package/dist/src/house-extension.js +147 -64
  28. package/dist/src/live-provider.js +301 -0
  29. package/dist/src/managed.js +33 -3
  30. package/dist/src/protocol.js +9 -5
  31. package/dist/src/provider-auth.js +300 -0
  32. package/dist/src/provider-config.js +65 -17
  33. package/dist/src/provider-model.js +78 -0
  34. package/dist/src/provider-payload.js +264 -0
  35. package/dist/src/provider-transport.js +36 -0
  36. package/dist/src/provider-usage.js +59 -0
  37. package/dist/src/repository-snapshots.js +219 -0
  38. package/dist/src/service.js +274 -9
  39. package/dist/src/store.js +44 -2
  40. package/dist/src/supervisor.js +246 -35
  41. package/dist/src/task-budget.js +17 -8
  42. package/dist/src/task-context.js +15 -0
  43. package/dist/src/task-driver.js +38 -14
  44. package/dist/src/task-policy.js +20 -1
  45. package/dist/src/task-verification.js +3 -0
  46. package/dist/src/task-work.js +31 -0
  47. package/dist/src/team-work.js +378 -0
  48. package/dist/src/tui-view.js +65 -10
  49. package/dist/src/tui.js +4 -2
  50. package/dist/src/usage.js +18 -0
  51. package/dist/src/worker-boundary.js +298 -0
  52. package/dist/src/worker-workspaces.js +274 -0
  53. package/dist/src/workspaces.js +6 -2
  54. package/harness-runtime/codex-pins.json +10 -0
  55. package/harness-runtime/package-lock.json +560 -0
  56. package/harness-runtime/package.json +9 -0
  57. package/npm-shrinkwrap.json +57 -2
  58. package/package.json +2 -1
  59. package/src/a2a-backend.ts +254 -0
  60. package/src/a2a-inbound.ts +585 -0
  61. package/src/a2a-peer.ts +441 -0
  62. package/src/a2a-protocol.ts +286 -0
  63. package/src/agent-profiles.ts +379 -0
  64. package/src/children.ts +168 -27
  65. package/src/claude-provider.ts +2 -189
  66. package/src/cli-help.ts +107 -6
  67. package/src/cli.ts +602 -12
  68. package/src/config.ts +34 -3
  69. package/src/credentials.ts +9 -2
  70. package/src/external-runtime.ts +54 -0
  71. package/src/external-work.ts +663 -0
  72. package/src/harness-backend.ts +557 -0
  73. package/src/harness-container.ts +543 -0
  74. package/src/harness-model-gateway.ts +138 -0
  75. package/src/harness-prepare.ts +113 -0
  76. package/src/harness-probe.ts +150 -0
  77. package/src/harness-protocol.ts +388 -0
  78. package/src/harness-readiness.ts +71 -0
  79. package/src/harness-rpc.ts +244 -0
  80. package/src/harness-tool-identity.ts +91 -0
  81. package/src/harness-wire-bridge.ts +418 -0
  82. package/src/host.ts +11 -6
  83. package/src/house-extension.ts +165 -66
  84. package/src/live-provider.ts +347 -0
  85. package/src/managed.ts +43 -4
  86. package/src/protocol.ts +36 -8
  87. package/src/provider-auth.ts +336 -0
  88. package/src/provider-config.ts +89 -18
  89. package/src/provider-model.ts +86 -0
  90. package/src/provider-payload.ts +263 -0
  91. package/src/provider-transport.ts +51 -0
  92. package/src/provider-usage.ts +64 -0
  93. package/src/repository-snapshots.ts +259 -0
  94. package/src/service.ts +446 -11
  95. package/src/store.ts +60 -4
  96. package/src/supervisor.ts +294 -41
  97. package/src/task-budget.ts +24 -11
  98. package/src/task-context.ts +17 -0
  99. package/src/task-driver.ts +51 -15
  100. package/src/task-policy.ts +23 -3
  101. package/src/task-verification.ts +2 -0
  102. package/src/task-work.ts +46 -1
  103. package/src/team-work.ts +554 -0
  104. package/src/tui-client.ts +5 -0
  105. package/src/tui-view.ts +164 -12
  106. package/src/tui.ts +5 -2
  107. package/src/usage.ts +17 -0
  108. package/src/worker-boundary.ts +395 -0
  109. package/src/worker-workspaces.ts +390 -0
  110. package/src/workspaces.ts +4 -2
@@ -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.supervisor.assertActive(id, epoch, attempt),
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.supervisor.failControl(id, error),
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.children = new ChildWork(this.store, this.artifacts, this.supervisor);
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: true,
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
- void this.children
697
- .cancelOwned(attempt)
698
- .then(() => this.supervisor.cancelAttempt(attempt))
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 > 11) {
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) {