@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
@@ -38,6 +38,7 @@ export class TaskDriver {
38
38
  this.active = this.tick()
39
39
  .catch((error) => {
40
40
  // A persistence failure cannot authorize more work. Fence all affected task workers.
41
+ this.children.external?.failAll(error);
41
42
  for (const row of this.store.db.prepare('SELECT session_id FROM durable_tasks').all())
42
43
  this.supervisor.failControl(String(row.session_id), error);
43
44
  })
@@ -119,7 +120,7 @@ export class TaskDriver {
119
120
  }
120
121
  async stopActive(task) {
121
122
  const attempts = this.store.db
122
- .prepare("SELECT id,state,submission_id FROM attempts WHERE session_id=? AND state IN ('accepted','dispatching','running')")
123
+ .prepare("SELECT id,state,submission_id FROM attempts WHERE session_id=? AND state IN ('accepted','dispatching','running','delegated')")
123
124
  .all(task.session_id);
124
125
  this.store.transaction(() => {
125
126
  for (const attempt of attempts) {
@@ -144,6 +145,7 @@ export class TaskDriver {
144
145
  await this.children.cancelOwned(attempt.id);
145
146
  await this.supervisor.cancelAttempt(attempt.id);
146
147
  }
148
+ await this.children.external?.cancelRoot(task.session_id);
147
149
  const compactions = this.store.db
148
150
  .prepare("SELECT id FROM compactions WHERE session_id=? AND state='running'")
149
151
  .all(task.session_id);
@@ -164,7 +166,7 @@ export class TaskDriver {
164
166
  return this.tasks.get(id);
165
167
  }
166
168
  /** Revocation is durable before any process abort, including a job cancellation. */
167
- cancelInTransaction(id, expected, authorize) {
169
+ cancelInTransaction(id, expected, authorize, peer) {
168
170
  if (!this.store.db.isTransaction)
169
171
  throw new Error('Task cancellation requires a transaction');
170
172
  authorize();
@@ -174,10 +176,16 @@ export class TaskDriver {
174
176
  if (['succeeded', 'succeeded_with_exception', 'cancelled'].includes(task.state))
175
177
  throw new ServiceError('task_settled', 'Task already settled');
176
178
  this.store.db.prepare('UPDATE task_runtime SET auto_enabled=0 WHERE task_id=?').run(id);
179
+ this.children.cancelSessionInTransaction(task.session_id);
177
180
  this.store.db
178
- .prepare("UPDATE durable_tasks SET state='cancelled',reason='Operator cancelled the task; inspect any uncertain effects',revision=revision+1 WHERE id=?")
179
- .run(id);
180
- this.store.event(task.session_id, 'work_task.cancelled', { task_id: id });
181
+ .prepare("UPDATE durable_tasks SET state='cancelled',reason=?,revision=revision+1 WHERE id=?")
182
+ .run(peer
183
+ ? 'Authenticated peer cancelled its dedicated task; inspect any uncertain effects'
184
+ : 'Operator cancelled the task; inspect any uncertain effects', id);
185
+ this.store.event(task.session_id, 'work_task.cancelled', {
186
+ task_id: id,
187
+ ...(peer ? { peer } : {}),
188
+ });
181
189
  return { id };
182
190
  }
183
191
  async stopCancelled(id) {
@@ -288,15 +296,24 @@ export class TaskDriver {
288
296
  this.stop(task, 'An interrupted or failed step requires explicit operator review');
289
297
  return;
290
298
  }
291
- if (this.budget.active(task) > 0) {
292
- const accepted = task.steps.some((s) => s.state === 'queued');
299
+ const active = this.budget.active(task);
300
+ if (active > 0) {
301
+ const accepted = task.steps.some((s) => s.state === 'queued' && !this.tasks.delegatedStep?.(task.id, s.id));
293
302
  if (accepted &&
294
303
  !['ready', 'starting'].includes(this.supervisor.inspect(task.session_id).state))
295
304
  await this.worker(task);
296
- return;
305
+ const rootActive = this.store.db
306
+ .prepare("SELECT 1 FROM attempts WHERE session_id=? AND state IN ('accepted','dispatching','running')")
307
+ .get(task.session_id);
308
+ if (rootActive ||
309
+ active >= task.limits.concurrency ||
310
+ this.children.activeCount(task.session_id) >= 2 ||
311
+ this.children.activeCount() >= 8)
312
+ return;
297
313
  }
298
314
  const reviewing = task.steps.find((s) => s.state === 'awaiting_review');
299
- if (reviewing) {
315
+ if (reviewing && !active) {
316
+ this.tasks.assertStepReview?.(task.id, reviewing.id);
300
317
  if (!reviewing.checks?.length) {
301
318
  this.stop(task, 'Step requires operator review');
302
319
  return;
@@ -322,19 +339,26 @@ export class TaskDriver {
322
339
  return;
323
340
  }
324
341
  if (Number(r.remaining_steps) < 1) {
342
+ if (active)
343
+ return;
325
344
  this.stop(task, 'Bounded progression step limit reached');
326
345
  return;
327
346
  }
328
347
  this.tasks.assertProgressionBinding?.(task, String(r.auto_binding));
329
348
  const next = task.steps.find((s) => s.state === 'pending' &&
349
+ (!active || this.tasks.delegatedStep?.(task.id, s.id)) &&
330
350
  s.depends_on.every((k) => task.steps.find((p) => p.key === k)?.state === 'completed'));
331
351
  if (!next) {
352
+ if (active)
353
+ return;
332
354
  this.stop(task, 'No eligible step; inspect the plan');
333
355
  return;
334
356
  }
335
- if (!(await this.worker(task)) || this.stopped)
357
+ const assigned = this.tasks.delegatedStep?.(task.id, next.id);
358
+ if ((!assigned && !(await this.worker(task))) || this.stopped)
336
359
  return;
337
- if (r.auto_compact &&
360
+ if (!assigned &&
361
+ r.auto_compact &&
338
362
  task.steps.some((s) => s.state === 'completed') &&
339
363
  !this.compact(task, next.id))
340
364
  return;
@@ -377,8 +401,8 @@ export class TaskDriver {
377
401
  if (task.steps.some((s) => ['queued', 'running'].includes(s.state)) && !r.blocked_reason) {
378
402
  let state = 'running', reason = 'Explicit step attempt in progress';
379
403
  if (this.store.db
380
- .prepare("SELECT 1 FROM approvals WHERE session_id=? AND state='pending'")
381
- .get(task.session_id)) {
404
+ .prepare("SELECT 1 FROM approvals WHERE state='pending' AND (session_id=? OR session_id IN (SELECT child_session FROM child_tasks WHERE parent_session=?))")
405
+ .get(task.session_id, task.session_id)) {
382
406
  state = 'waiting_for_person';
383
407
  reason = 'An operation-bound human approval is pending';
384
408
  }
@@ -386,7 +410,7 @@ export class TaskDriver {
386
410
  .prepare("SELECT 1 FROM child_tasks WHERE parent_session=? AND state='accepted'")
387
411
  .get(task.session_id)) {
388
412
  state = 'waiting_for_child';
389
- reason = 'Read-only child work is pending';
413
+ reason = 'A child worker is pending';
390
414
  }
391
415
  else if (this.supervisor.inspect(task.session_id).state !== 'ready') {
392
416
  state = 'waiting_for_infrastructure';
@@ -41,11 +41,30 @@ export function taskPolicy(value) {
41
41
  output_token_bound: 8192,
42
42
  verification_runs: 256,
43
43
  };
44
+ // Explicit opt-out; omitted limits retain the existing capped defaults.
45
+ if (input.token_units === null) {
46
+ limits.token_units = null;
47
+ if (input.output_token_bound === undefined)
48
+ limits.output_token_bound = null;
49
+ }
44
50
  for (const key of Object.keys(limits)) {
51
+ if ((key === 'token_units' || key === 'output_token_bound') && input[key] === null) {
52
+ limits[key] = null;
53
+ continue;
54
+ }
45
55
  if (input[key] !== undefined)
46
56
  limits[key] = integer(input[key], key);
47
- if (limits[key] < 1 || limits[key] > max[key])
57
+ const bound = limits[key];
58
+ if (bound !== null && bound !== undefined && (bound < 1 || bound > max[key]))
48
59
  throw new ServiceError('invalid_budget', 'Task limit out of range: ' + key, 400);
49
60
  }
61
+ if (limits.output_token_bound === null && limits.token_units !== null)
62
+ throw new ServiceError('invalid_budget', 'A token budget requires an output token bound', 400);
63
+ if (input.invocations !== undefined) {
64
+ const n = integer(input.invocations, 'invocations');
65
+ if (n < 1 || n > 64)
66
+ throw new ServiceError('invalid_budget', 'Invocation limit must be 1–64', 400);
67
+ limits.invocations = n;
68
+ }
50
69
  return { acceptance, deliverables, limits };
51
70
  }
@@ -78,6 +78,8 @@ export class TaskVerification {
78
78
  !task.steps.some((s) => s.id === step && s.state === 'awaiting_review' && s.checks?.includes(key)))
79
79
  throw new ServiceError('invalid_check', 'Step does not await this configured check');
80
80
  const w = this.workspaces.verify(task.session_id);
81
+ if (step)
82
+ this.tasks.assertStepReview?.(task.id, step);
81
83
  if (!this.coding.executor)
82
84
  throw new ServiceError('verification_unavailable', 'No proven verification executor');
83
85
  this.budget.verification(task);
@@ -270,6 +272,7 @@ export class TaskVerification {
270
272
  return;
271
273
  if (step?.state !== 'awaiting_review' || !step.checks?.length)
272
274
  throw new ServiceError('verification_required', 'Step requires configured checks');
275
+ this.tasks.assertStepReview?.(current.id, stepId);
273
276
  this.assertIdle(current);
274
277
  const receipts = step.checks.map((key) => this.latest(current, key, stepId));
275
278
  if (receipts.some((r) => !r || !this.fresh(r)))
@@ -5,8 +5,12 @@ import { canonical, object, ServiceError, string, } from './protocol.js';
5
5
  export class TaskWork {
6
6
  store;
7
7
  context;
8
+ providerCheck;
8
9
  assertLimits;
9
10
  assertProgressionBinding;
11
+ delegatedStep;
12
+ advanceDelegate;
13
+ assertStepReview;
10
14
  authorizeGrant(task, step, grant) {
11
15
  const row = this.store.db
12
16
  .prepare('SELECT * FROM task_grants WHERE id=? AND task_id=? AND step_id=? AND plan_revision=?')
@@ -34,6 +38,8 @@ export class TaskWork {
34
38
  const grant = this.store.db.prepare('SELECT * FROM task_grants WHERE id=?').get(grantId);
35
39
  if (!grant)
36
40
  throw new ServiceError('not_found', 'Task grant not found', 404);
41
+ if (this.delegatedStep?.(grant.task_id, grant.step_id))
42
+ return this.advanceDelegate(grantId);
37
43
  return this.run(grant.task_id, grant.step_id, 'task-grant:' + grantId, grant.expected_revision, null, null, null, grantId);
38
44
  }
39
45
  constructor(store, context) {
@@ -184,6 +190,7 @@ export class TaskWork {
184
190
  throw new Error('Task admission requires a transaction');
185
191
  const plan = this.parse(input);
186
192
  authorize();
193
+ this.providerCheck?.(plan.limits);
187
194
  if (expected !== 0)
188
195
  throw new ServiceError('task_revision_conflict', 'New tasks require revision zero');
189
196
  this.idle(session);
@@ -225,7 +232,22 @@ export class TaskWork {
225
232
  const task = this.get(id);
226
233
  this.store.authorizeControl(task.session_id, controller);
227
234
  this.expect(task, expected);
235
+ this.providerCheck?.(plan.limits);
228
236
  this.idle(task.session_id);
237
+ const runtime = this.store.db
238
+ .prepare('SELECT * FROM task_runtime WHERE task_id=?')
239
+ .get(id);
240
+ const invocations = Number(this.store.db
241
+ .prepare('SELECT count(*) AS n FROM task_invocations WHERE task_id=?')
242
+ .get(id).n);
243
+ if (plan.limits.invocations !== undefined && plan.limits.invocations < invocations)
244
+ throw new ServiceError('invalid_budget', 'Invocation limits cannot discard admitted work');
245
+ if (task.limits.token_units === null &&
246
+ plan.limits.token_units !== null &&
247
+ (Number(runtime.requests_spent) > 0 ||
248
+ Number(runtime.reserved_requests) > 0 ||
249
+ invocations > 0))
250
+ throw new ServiceError('invalid_budget', 'Cannot enable token budgeting after uncapped model work is admitted');
229
251
  for (const [index, step] of task.steps.entries()) {
230
252
  const next = plan.steps[index];
231
253
  if (!next || next.key !== step.key)
@@ -277,9 +299,12 @@ export class TaskWork {
277
299
  else
278
300
  this.authorizeGrant(task, stepId, grantId);
279
301
  this.expect(task, expected);
302
+ this.providerCheck?.(task.limits);
280
303
  if (['cancelled', 'succeeded', 'succeeded_with_exception'].includes(task.state))
281
304
  throw new ServiceError('task_settled', 'Revise a settled task before another step');
282
305
  const step = task.steps.find((s) => s.id === stepId);
306
+ if (this.delegatedStep?.(id, stepId))
307
+ throw new ServiceError('team_step_required', 'Run assigned steps through their durable team assignment');
283
308
  if (!step)
284
309
  throw new ServiceError('invalid_step', 'Step does not belong to this task');
285
310
  if (!['pending', 'failed', 'interrupted', 'cancelled'].includes(step.state))
@@ -327,6 +352,7 @@ export class TaskWork {
327
352
  !step.attempt_id ||
328
353
  this.store.attempt(step.attempt_id).state !== 'completed')
329
354
  throw new ServiceError('invalid_step_state', 'Only a settled step awaiting operator review can be completed');
355
+ this.assertStepReview?.(id, step.id);
330
356
  this.store.db
331
357
  .prepare("UPDATE task_steps SET state='completed',review=? WHERE id=?")
332
358
  .run(review, step.id);
@@ -354,8 +380,13 @@ export class TaskWork {
354
380
  .get(attempt.id);
355
381
  if (!row || row.state === 'completed')
356
382
  return;
383
+ if (this.store.db
384
+ .prepare('SELECT 1 FROM team_rejections WHERE step_id=? AND attempt_id=?')
385
+ .get(row.id, attempt.id))
386
+ return;
357
387
  const state = {
358
388
  accepted: 'queued',
389
+ delegated: 'queued',
359
390
  dispatching: 'running',
360
391
  running: 'running',
361
392
  completed: 'awaiting_review',
@@ -0,0 +1,378 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { digest, ServiceError } from './protocol.js';
3
+ /** Assignment claims extend the task ledger. They are never an independent scheduler. */
4
+ export class TeamWork {
5
+ store;
6
+ tasks;
7
+ budget;
8
+ children;
9
+ profiles;
10
+ constructor(store, tasks, budget, children, profiles) {
11
+ this.store = store;
12
+ this.tasks = tasks;
13
+ this.budget = budget;
14
+ this.children = children;
15
+ this.profiles = profiles;
16
+ store.workerBoundary.assertAssignment = (binding) => this.assert(binding);
17
+ store.teamSnapshot = (session) => {
18
+ const task = tasks.forSession(session);
19
+ return task ? this.view(task.id) : null;
20
+ };
21
+ tasks.delegatedStep = (task, step) => !!this.forStep(task, step);
22
+ tasks.advanceDelegate = (id) => {
23
+ const grant = store.db.prepare('SELECT * FROM task_grants WHERE id=?').get(id);
24
+ const assignment = this.forStep(String(grant.task_id), String(grant.step_id));
25
+ if (!assignment)
26
+ throw new ServiceError('team_assignment_missing', 'Step assignment disappeared');
27
+ return this.run(assignment.id, 'task-grant:' + id, Number(grant.expected_revision), assignment.generation, null, null, null, id);
28
+ };
29
+ }
30
+ forStep(task, step) {
31
+ return this.store.db
32
+ .prepare('SELECT * FROM team_assignments WHERE task_id=? AND step_id=? AND superseded=0')
33
+ .get(task, step);
34
+ }
35
+ get(id) {
36
+ const row = this.store.db
37
+ .prepare('SELECT * FROM team_assignments WHERE id=?')
38
+ .get(id);
39
+ if (!row)
40
+ throw new ServiceError('not_found', 'Team assignment not found', 404);
41
+ return row;
42
+ }
43
+ expect(task, revision) {
44
+ if (this.tasks.get(task).revision !== revision)
45
+ throw new ServiceError('task_revision_conflict', 'Task revision changed');
46
+ }
47
+ assign(taskId, stepId, profile, requests, request, expected, controller, runtime) {
48
+ return this.store.request('local-operator', request, {
49
+ kind: 'team-assign',
50
+ taskId,
51
+ stepId,
52
+ profile,
53
+ requests,
54
+ expected,
55
+ ...(runtime ? { runtime } : {}),
56
+ }, () => {
57
+ const task = this.tasks.get(taskId);
58
+ this.store.authorizeControl(task.session_id, controller);
59
+ this.expect(taskId, expected);
60
+ if (['cancelled', 'succeeded', 'succeeded_with_exception'].includes(task.state))
61
+ throw new ServiceError('task_settled', 'Revise the task before assigning workers');
62
+ const step = task.steps.find((s) => s.id === stepId);
63
+ if (!step || step.attempt_id || step.state !== 'pending')
64
+ throw new ServiceError('invalid_step_state', 'Assign an unattempted pending step');
65
+ const old = this.forStep(taskId, stepId);
66
+ if (old) {
67
+ if (old.plan_revision === task.plan_revision || old.generation !== 0)
68
+ throw new ServiceError('team_assignment_exists', 'An immutable assignment already owns this step');
69
+ this.store.db.prepare('UPDATE team_assignments SET superseded=1 WHERE id=?').run(old.id);
70
+ }
71
+ const grant = this.profiles.resolve(task.session_id, profile);
72
+ if (!Number.isSafeInteger(requests) ||
73
+ requests < 1 ||
74
+ requests > (grant.profile.max_requests ?? 8))
75
+ throw new ServiceError('invalid_budget', 'Assignment exceeds profile request bounds');
76
+ const id = randomUUID();
77
+ this.store.db
78
+ .prepare('INSERT INTO team_assignments VALUES (?,?,?,?,?,?,?,0,NULL,0)')
79
+ .run(id, taskId, stepId, task.plan_revision, profile, grant.id, requests);
80
+ if (runtime) {
81
+ if (!this.children.external || requests !== 1)
82
+ throw new ServiceError('runtime_unavailable', 'External assignments require one invocation and a configured runtime');
83
+ this.children.external.assign(id, task, runtime, grant.profile.tools);
84
+ }
85
+ this.changed(taskId, 'assigned', {
86
+ assignment_id: id,
87
+ step_id: stepId,
88
+ profile_id: profile,
89
+ });
90
+ return this.get(id);
91
+ });
92
+ }
93
+ run(id, request, expected, generation, instruction, continuation, controller, grantId, peerAdmission) {
94
+ let launch;
95
+ const result = this.store.request(peerAdmission?.scope ?? 'local-operator', request, {
96
+ kind: 'team-run',
97
+ id,
98
+ expected,
99
+ ...(!grantId ? { generation } : {}),
100
+ instruction,
101
+ continuation,
102
+ ...(grantId ? { grantId } : {}),
103
+ ...(peerAdmission ? { peer_admission: peerAdmission.digest } : {}),
104
+ }, () => {
105
+ const row = this.get(id), task = this.tasks.get(row.task_id);
106
+ if (peerAdmission)
107
+ peerAdmission.authorize();
108
+ else if (controller)
109
+ this.store.authorizeControl(task.session_id, controller);
110
+ else
111
+ this.tasks.authorizeGrant(task, row.step_id, grantId);
112
+ this.expect(task.id, expected);
113
+ if (row.superseded ||
114
+ row.plan_revision !== task.plan_revision ||
115
+ row.generation !== generation)
116
+ throw new ServiceError('team_claim_stale', 'Assignment plan or generation changed');
117
+ if (generation >= 8)
118
+ throw new ServiceError('team_claim_limit', 'At most eight explicitly admitted generations per assignment');
119
+ if (['cancelled', 'succeeded', 'succeeded_with_exception'].includes(task.state))
120
+ throw new ServiceError('task_settled', 'Task no longer admits work');
121
+ const step = task.steps.find((s) => s.id === row.step_id);
122
+ if (!['pending', 'interrupted', 'failed', 'cancelled'].includes(step.state))
123
+ throw new ServiceError('invalid_step_state', 'Assignment is active or already awaiting review');
124
+ if (step.depends_on.some((key) => task.steps.find((s) => s.key === key)?.state !== 'completed'))
125
+ throw new ServiceError('dependency_pending', 'Complete predecessor steps first');
126
+ const interrupted = step.attempt_id && this.store.attempt(step.attempt_id).state === 'interrupted'
127
+ ? step.attempt_id
128
+ : null;
129
+ if (continuation !== interrupted || (row.child_id && !instruction))
130
+ throw new ServiceError('invalid_continuation', 'Follow-up requires a new instruction and the exact interrupted attempt, if any');
131
+ if (instruction !== null && (!instruction.trim() || instruction.length > 4096))
132
+ throw new ServiceError('invalid_field', 'Supply a bounded follow-up instruction', 400);
133
+ if (row.child_id) {
134
+ const prior = this.children.inspect(task.session_id, row.child_id);
135
+ if (prior.state === 'accepted' ||
136
+ (prior.worker?.binding.external && !this.children.external?.stopped(prior.id)) ||
137
+ this.store.db
138
+ .prepare("SELECT 1 FROM workers WHERE session_id=? AND state!='exited'")
139
+ .get(prior.child_session))
140
+ throw new ServiceError('team_worker_uncertain', 'Confirm the previous process stopped before claiming a new generation');
141
+ }
142
+ const agent = this.profiles.resolve(task.session_id, row.profile_id);
143
+ if (agent.id !== row.grant_id)
144
+ throw new ServiceError('team_claim_stale', 'Assignment requires its original profile admission');
145
+ this.budget.check(task.session_id);
146
+ this.children.external?.validateAssignment(id, task);
147
+ if (this.budget.active(task) >= task.limits.concurrency)
148
+ throw new ServiceError('task_busy', 'Root task concurrency is full');
149
+ if (this.store.db
150
+ .prepare("SELECT 1 FROM attempts WHERE session_id=? AND state IN ('accepted','dispatching','running','interrupted') AND resolved_by IS NULL")
151
+ .get(task.session_id))
152
+ throw new ServiceError('task_busy', 'Root session has pending work');
153
+ const envelope = randomUUID(), submission = randomUUID();
154
+ // A delegated envelope has never been dispatched to a model. Dispatcher excludes this state.
155
+ this.store.db
156
+ .prepare("INSERT INTO submissions(id,session_id,text,state,attempt_id) VALUES (?,?,?,'delegated',?)")
157
+ .run(submission, task.session_id, step.instruction, envelope);
158
+ this.store.db
159
+ .prepare("INSERT INTO attempts(id,session_id,submission_id,state) VALUES (?,?,?,'delegated')")
160
+ .run(envelope, task.session_id, submission);
161
+ const next = generation + 1;
162
+ const child = this.children.spawn(task.session_id, 0, envelope, `team:${id}:${next}`, instruction ?? step.instruction, row.budget, [], row.profile_id, {
163
+ authorize: () => { },
164
+ defer: true,
165
+ assignment: { id, generation: next },
166
+ runtime: this.children.external?.assignment(id),
167
+ });
168
+ this.store.db
169
+ .prepare('UPDATE team_assignments SET generation=?,child_id=? WHERE id=?')
170
+ .run(next, child.id, id);
171
+ this.store.db
172
+ .prepare('INSERT INTO task_step_attempts VALUES (?,?,?,?)')
173
+ .run(child.child_attempt, step.id, task.plan_revision, instruction ?? step.instruction);
174
+ this.store.db
175
+ .prepare("UPDATE task_steps SET attempt_id=?,state='queued',review=NULL WHERE id=?")
176
+ .run(child.child_attempt, step.id);
177
+ if (interrupted)
178
+ this.store.db
179
+ .prepare('UPDATE attempts SET resolved_by=? WHERE id=?')
180
+ .run(child.child_attempt, interrupted);
181
+ this.store.db
182
+ .prepare('UPDATE task_runtime SET started_at=coalesce(started_at,?) WHERE task_id=?')
183
+ .run(Date.now(), task.id);
184
+ this.store.db
185
+ .prepare("UPDATE durable_tasks SET state='running',reason='Team assignment claimed' WHERE id=?")
186
+ .run(task.id);
187
+ if (grantId)
188
+ this.store.db.prepare("UPDATE task_grants SET state='accepted' WHERE id=?").run(grantId);
189
+ this.changed(task.id, 'claimed', {
190
+ assignment_id: id,
191
+ generation: next,
192
+ child_id: child.id,
193
+ attempt_id: child.child_attempt,
194
+ });
195
+ peerAdmission?.committed(child);
196
+ launch = child;
197
+ return this.get(id);
198
+ });
199
+ if (launch)
200
+ this.children.launchPrepared(launch);
201
+ return result;
202
+ }
203
+ reject(id, request, expected, generation, reason, controller) {
204
+ return this.store.request('local-operator', request, { kind: 'team-reject', id, expected, generation, reason }, () => {
205
+ const assignment = this.get(id), task = this.tasks.get(assignment.task_id), step = task.steps.find((s) => s.id === assignment.step_id);
206
+ this.store.authorizeControl(task.session_id, controller);
207
+ this.expect(task.id, expected);
208
+ if (task.state === 'cancelled' ||
209
+ assignment.generation !== generation ||
210
+ !step.attempt_id ||
211
+ !['awaiting_review', 'failed', 'interrupted'].includes(step.state))
212
+ throw new ServiceError('team_claim_stale', 'Reject the current settled claim only');
213
+ if (!reason.trim() || reason.length > 4096)
214
+ throw new ServiceError('invalid_field', 'Record a bounded rejection reason', 400);
215
+ this.store.db
216
+ .prepare('INSERT INTO team_rejections VALUES (?,?,?,?)')
217
+ .run(step.id, step.attempt_id, request, reason);
218
+ this.store.db
219
+ .prepare("UPDATE task_steps SET state='failed',review=? WHERE id=?")
220
+ .run(reason, step.id);
221
+ this.store.db
222
+ .prepare('UPDATE task_runtime SET auto_enabled=0 WHERE task_id=?')
223
+ .run(task.id);
224
+ this.store.db
225
+ .prepare("UPDATE durable_tasks SET state='blocked',reason='Operator rejected worker proposal; explicit follow-up required' WHERE id=?")
226
+ .run(task.id);
227
+ this.changed(task.id, 'proposal_rejected', {
228
+ assignment_id: id,
229
+ generation,
230
+ attempt_id: step.attempt_id,
231
+ reason,
232
+ });
233
+ return this.get(id);
234
+ });
235
+ }
236
+ assert(binding) {
237
+ const row = this.get(binding.assignment.id), task = this.tasks.get(row.task_id), step = task.steps.find((s) => s.id === row.step_id);
238
+ if (task.state === 'cancelled' ||
239
+ row.plan_revision !== task.plan_revision ||
240
+ row.generation !== binding.assignment.generation ||
241
+ row.child_id === null ||
242
+ this.children.inspect(task.session_id, row.child_id).child_attempt !== binding.attempt_id ||
243
+ step?.attempt_id !== binding.attempt_id ||
244
+ binding.owner_task !== task.id ||
245
+ binding.owner_session !== task.session_id ||
246
+ binding.agent?.id !== row.grant_id)
247
+ throw new ServiceError('team_claim_stale', 'Worker no longer owns the current task-step claim');
248
+ return row;
249
+ }
250
+ changed(task, kind, detail) {
251
+ this.store.db.prepare('UPDATE durable_tasks SET revision=revision+1 WHERE id=?').run(task);
252
+ this.store.event(this.tasks.get(task).session_id, 'team.' + kind, {
253
+ task_id: task,
254
+ ...detail,
255
+ });
256
+ }
257
+ view(task) {
258
+ const current = this.tasks.get(task);
259
+ const assignments = this.store.db
260
+ .prepare('SELECT * FROM team_assignments WHERE task_id=? ORDER BY rowid')
261
+ .all(task).map((a) => ({
262
+ ...a,
263
+ stale: a.plan_revision !== current.plan_revision,
264
+ step_state: current.steps.find((s) => s.id === a.step_id)?.state,
265
+ worker: a.child_id ? this.children.inspect(current.session_id, a.child_id).worker : null,
266
+ runtime: this.children.external?.assignment(a.id) ?? null,
267
+ execution: a.child_id && this.children.inspect(current.session_id, a.child_id).worker?.binding.external
268
+ ? {
269
+ state: this.children.external.row(a.child_id).state,
270
+ stopped: this.children.external.stopped(a.child_id),
271
+ stop_scope: this.children.external.assignment(a.id)?.kind === 'a2a'
272
+ ? (() => {
273
+ const receipt = this.children.external.row(a.child_id).receipt;
274
+ return receipt
275
+ ? (JSON.parse(receipt).scope ?? 'remote-terminal-receipt')
276
+ : 'remote-terminal-receipt';
277
+ })()
278
+ : 'owned-processes',
279
+ peer: (() => {
280
+ const row = this.children.external.row(a.child_id);
281
+ const cp = row.checkpoint ? JSON.parse(row.checkpoint) : null;
282
+ return cp?.dialect === 'a2a'
283
+ ? {
284
+ peer_id: cp.peer_id,
285
+ task_id: cp.task_id,
286
+ context_id: cp.context_id,
287
+ cancel_state: cp.cancel_state ?? null,
288
+ remote_state: cp.result?.status?.state ?? null,
289
+ }
290
+ : null;
291
+ })(),
292
+ }
293
+ : null,
294
+ }));
295
+ const messages = this.store.db
296
+ .prepare('SELECT id,sender,recipient,created_at,delivered_at,acknowledged_at FROM team_messages WHERE task_id=? ORDER BY rowid DESC LIMIT 128')
297
+ .all(task);
298
+ return {
299
+ task_id: task,
300
+ assignments,
301
+ messages,
302
+ bounds: {
303
+ root_concurrency: current.limits.concurrency,
304
+ session_workers: 2,
305
+ daemon_workers: 8,
306
+ wake_on_message: false,
307
+ },
308
+ };
309
+ }
310
+ send(taskId, sender, recipient, text, messageId, authorize) {
311
+ return this.store.request(`team-message:${taskId}:${sender}`, messageId, { recipient, text }, () => {
312
+ authorize();
313
+ const task = this.tasks.get(taskId);
314
+ if (task.state === 'cancelled')
315
+ throw new ServiceError('task_settled', 'Task is cancelled');
316
+ if (this.get(recipient).task_id !== taskId ||
317
+ (sender !== 'operator' && this.get(sender).task_id !== taskId))
318
+ throw new ServiceError('team_message_scope', 'Messages stay within their owning task');
319
+ if (!text.trim() || Buffer.byteLength(text) > 4096 || !messageId || messageId.length > 128)
320
+ throw new ServiceError('invalid_field', 'Bound messages to 4 KiB and IDs to 128 characters', 400);
321
+ const n = Number(this.store.db
322
+ .prepare('SELECT count(*) AS n FROM team_messages WHERE task_id=?')
323
+ .get(taskId).n);
324
+ if (n >= 256 ||
325
+ Number(this.store.db
326
+ .prepare('SELECT count(*) AS n FROM team_messages WHERE recipient=? AND acknowledged_at IS NULL')
327
+ .get(recipient).n) >= 32)
328
+ throw new ServiceError('team_backpressure', 'Team message quota reached');
329
+ const id = randomUUID();
330
+ this.store.db
331
+ .prepare('INSERT INTO team_messages VALUES (?,?,?,?,?,?,?,NULL,NULL)')
332
+ .run(id, taskId, sender, recipient, text, digest({ sender, recipient, text }), Date.now());
333
+ this.store.event(task.session_id, 'team.message_saved', {
334
+ message_id: id,
335
+ sender,
336
+ recipient,
337
+ wakes_worker: false,
338
+ });
339
+ return { id, stored: true, delivered: false, acknowledged: false, wakes_worker: false };
340
+ });
341
+ }
342
+ receive(recipient, request, authorize) {
343
+ return this.store.request('team-receive:' + recipient, request, {}, () => {
344
+ authorize();
345
+ const row = this.get(recipient), now = Date.now();
346
+ const messages = this.store.db
347
+ .prepare('SELECT * FROM team_messages WHERE recipient=? AND acknowledged_at IS NULL ORDER BY rowid LIMIT 32')
348
+ .all(recipient);
349
+ for (const m of messages)
350
+ this.store.db
351
+ .prepare('UPDATE team_messages SET delivered_at=coalesce(delivered_at,?) WHERE id=?')
352
+ .run(now, String(m.id));
353
+ this.store.event(this.tasks.get(row.task_id).session_id, 'team.messages_delivered', {
354
+ recipient,
355
+ message_ids: messages.map((m) => m.id),
356
+ });
357
+ return messages.map((m) => ({ ...m, delivered_at: m.delivered_at ?? now }));
358
+ });
359
+ }
360
+ acknowledge(recipient, id, request, authorize) {
361
+ return this.store.request('team-ack:' + recipient, request, { id }, () => {
362
+ authorize();
363
+ const row = this.get(recipient), message = this.store.db
364
+ .prepare('SELECT * FROM team_messages WHERE id=? AND recipient=?')
365
+ .get(id, recipient);
366
+ if (!message?.delivered_at)
367
+ throw new ServiceError('message_not_delivered', 'Receive the message before acknowledging');
368
+ this.store.db
369
+ .prepare('UPDATE team_messages SET acknowledged_at=coalesce(acknowledged_at,?) WHERE id=?')
370
+ .run(Date.now(), id);
371
+ this.store.event(this.tasks.get(row.task_id).session_id, 'team.message_acknowledged', {
372
+ recipient,
373
+ message_id: id,
374
+ });
375
+ return { id, acknowledged: true };
376
+ });
377
+ }
378
+ }