subharness 0.0.5 → 0.0.7

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 (148) hide show
  1. package/README.md +59 -3
  2. package/dist/adapters/claude-process.js +8 -1
  3. package/dist/adapters/claude-process.js.map +1 -1
  4. package/dist/adapters/claude-tools.d.ts +6 -2
  5. package/dist/adapters/claude-tools.js +14 -12
  6. package/dist/adapters/claude-tools.js.map +1 -1
  7. package/dist/adapters/claude-worker-client.d.ts +49 -0
  8. package/dist/adapters/claude-worker-client.js +359 -0
  9. package/dist/adapters/claude-worker-client.js.map +1 -0
  10. package/dist/adapters/claude-worker-process.d.ts +20 -0
  11. package/dist/adapters/claude-worker-process.js +76 -0
  12. package/dist/adapters/claude-worker-process.js.map +1 -0
  13. package/dist/adapters/claude-worker-protocol.d.ts +38 -0
  14. package/dist/adapters/claude-worker-protocol.js +2 -0
  15. package/dist/adapters/claude-worker-protocol.js.map +1 -0
  16. package/dist/adapters/claude-worker.d.ts +1 -0
  17. package/dist/adapters/claude-worker.js +126 -0
  18. package/dist/adapters/claude-worker.js.map +1 -0
  19. package/dist/adapters/claude.d.ts +16 -6
  20. package/dist/adapters/claude.js +138 -68
  21. package/dist/adapters/claude.js.map +1 -1
  22. package/dist/cli/catalog-worker.js.map +1 -1
  23. package/dist/cli/dashboard-client.js +19 -30
  24. package/dist/cli/dashboard-client.js.map +1 -1
  25. package/dist/cli/main.js +53 -45
  26. package/dist/cli/main.js.map +1 -1
  27. package/dist/errors.d.ts +2 -2
  28. package/dist/errors.js.map +1 -1
  29. package/dist/index.d.ts +6 -0
  30. package/dist/index.js +3 -0
  31. package/dist/index.js.map +1 -1
  32. package/dist/runtime/approval-registry.d.ts +9 -0
  33. package/dist/runtime/approval-registry.js +143 -4
  34. package/dist/runtime/approval-registry.js.map +1 -1
  35. package/dist/runtime/capture.d.ts +22 -0
  36. package/dist/runtime/capture.js +320 -0
  37. package/dist/runtime/capture.js.map +1 -0
  38. package/dist/runtime/catalog.d.ts +39 -0
  39. package/dist/runtime/catalog.js +84 -0
  40. package/dist/runtime/catalog.js.map +1 -0
  41. package/dist/runtime/client.d.ts +2 -1
  42. package/dist/runtime/client.js +52 -65
  43. package/dist/runtime/client.js.map +1 -1
  44. package/dist/runtime/coordinator.d.ts +44 -7
  45. package/dist/runtime/coordinator.js +265 -31
  46. package/dist/runtime/coordinator.js.map +1 -1
  47. package/dist/runtime/daemon.js +12 -6
  48. package/dist/runtime/daemon.js.map +1 -1
  49. package/dist/runtime/dashboard-workspace.js +5 -1
  50. package/dist/runtime/dashboard-workspace.js.map +1 -1
  51. package/dist/runtime/definition.d.ts +6 -1
  52. package/dist/runtime/definition.js +31 -1
  53. package/dist/runtime/definition.js.map +1 -1
  54. package/dist/runtime/native-owner.js +7 -1
  55. package/dist/runtime/native-owner.js.map +1 -1
  56. package/dist/runtime/pagination.d.ts +9 -0
  57. package/dist/runtime/pagination.js +36 -0
  58. package/dist/runtime/pagination.js.map +1 -0
  59. package/dist/runtime/sdk-observation.d.ts +8 -0
  60. package/dist/runtime/sdk-observation.js +75 -0
  61. package/dist/runtime/sdk-observation.js.map +1 -0
  62. package/dist/runtime/sdk-projection.d.ts +8 -0
  63. package/dist/runtime/sdk-projection.js +19 -0
  64. package/dist/runtime/sdk-projection.js.map +1 -0
  65. package/dist/runtime/sdk-service.d.ts +11 -0
  66. package/dist/runtime/sdk-service.js +109 -0
  67. package/dist/runtime/sdk-service.js.map +1 -0
  68. package/dist/runtime/select-native.d.ts +4 -1
  69. package/dist/runtime/select-native.js +14 -17
  70. package/dist/runtime/select-native.js.map +1 -1
  71. package/dist/runtime/service.d.ts +11 -1
  72. package/dist/runtime/service.js +178 -3
  73. package/dist/runtime/service.js.map +1 -1
  74. package/dist/runtime/session-launcher.js +1 -1
  75. package/dist/runtime/session-launcher.js.map +1 -1
  76. package/dist/runtime/snapshots.d.ts +103 -0
  77. package/dist/runtime/snapshots.js +124 -0
  78. package/dist/runtime/snapshots.js.map +1 -0
  79. package/dist/runtime/state.d.ts +26 -0
  80. package/dist/runtime/state.js +86 -17
  81. package/dist/runtime/state.js.map +1 -1
  82. package/dist/runtime/task-data.d.ts +38 -0
  83. package/dist/runtime/task-data.js +2 -0
  84. package/dist/runtime/task-data.js.map +1 -0
  85. package/dist/runtime/transport.d.ts +13 -0
  86. package/dist/runtime/transport.js +166 -0
  87. package/dist/runtime/transport.js.map +1 -0
  88. package/dist/runtime/types.d.ts +28 -9
  89. package/dist/runtime/types.js.map +1 -1
  90. package/dist/runtime/worker-client.js +2 -0
  91. package/dist/runtime/worker-client.js.map +1 -1
  92. package/dist/runtime/worker-server.js +105 -24
  93. package/dist/runtime/worker-server.js.map +1 -1
  94. package/dist/sdk/connected-types.d.ts +42 -0
  95. package/dist/sdk/connected-types.js +2 -0
  96. package/dist/sdk/connected-types.js.map +1 -0
  97. package/dist/sdk/connected.d.ts +3 -0
  98. package/dist/sdk/connected.js +232 -0
  99. package/dist/sdk/connected.js.map +1 -0
  100. package/dist/sdk/execution-definition.d.ts +3 -0
  101. package/dist/sdk/execution-definition.js +59 -0
  102. package/dist/sdk/execution-definition.js.map +1 -0
  103. package/dist/sdk/execution-driver.d.ts +6 -0
  104. package/dist/sdk/execution-driver.js +86 -0
  105. package/dist/sdk/execution-driver.js.map +1 -0
  106. package/dist/sdk/execution-observation.d.ts +3 -0
  107. package/dist/sdk/execution-observation.js +35 -0
  108. package/dist/sdk/execution-observation.js.map +1 -0
  109. package/dist/sdk/execution-options.d.ts +17 -0
  110. package/dist/sdk/execution-options.js +115 -0
  111. package/dist/sdk/execution-options.js.map +1 -0
  112. package/dist/sdk/execution-types.d.ts +72 -0
  113. package/dist/sdk/execution-types.js +2 -0
  114. package/dist/sdk/execution-types.js.map +1 -0
  115. package/dist/sdk/execution.d.ts +3 -0
  116. package/dist/sdk/execution.js +3 -0
  117. package/dist/sdk/execution.js.map +1 -0
  118. package/dist/sdk/hosted-error.d.ts +3 -0
  119. package/dist/sdk/hosted-error.js +14 -0
  120. package/dist/sdk/hosted-error.js.map +1 -0
  121. package/dist/sdk/hosted.d.ts +23 -0
  122. package/dist/sdk/hosted.js +204 -0
  123. package/dist/sdk/hosted.js.map +1 -0
  124. package/dist/sdk/runner.d.ts +5 -0
  125. package/dist/sdk/runner.js +107 -0
  126. package/dist/sdk/runner.js.map +1 -0
  127. package/dist/sdk/tools.js +4 -1
  128. package/dist/sdk/tools.js.map +1 -1
  129. package/package.json +3 -3
  130. package/sdk/access-config.md +2 -0
  131. package/sdk/adapter-contract.md +8 -0
  132. package/sdk/agent.md +1 -1
  133. package/sdk/approvals.md +2 -0
  134. package/sdk/completion-notifications.md +2 -0
  135. package/sdk/distribution.md +4 -2
  136. package/sdk/examples/chat-tool.ts +126 -0
  137. package/sdk/execution.md +743 -0
  138. package/sdk/index.md +45 -21
  139. package/sdk/message-delivery.md +2 -0
  140. package/sdk/permissions.md +2 -0
  141. package/sdk/plugins/sub-agents.md +4 -2
  142. package/sdk/project-team.md +23 -1
  143. package/sdk/sessions.md +2 -0
  144. package/sdk/tools.md +3 -1
  145. package/sdk/v1-runtime.md +7 -5
  146. package/dist/cli/catalog.d.ts +0 -26
  147. package/dist/cli/catalog.js +0 -41
  148. package/dist/cli/catalog.js.map +0 -1
@@ -5,32 +5,51 @@ import { terminal } from './types.js';
5
5
  import { dashboardHarness, dashboardTitle } from './dashboard.js';
6
6
  import { resolveDashboardWorkspace } from './dashboard-workspace.js';
7
7
  import { ApprovalRegistry } from './approval-registry.js';
8
+ import { createSnapshotWatch, detached } from './snapshots.js';
9
+ import { paginate } from './pagination.js';
10
+ import { responseReplay, terminalInterval } from './sdk-observation.js';
11
+ import { sdkSessionSummary as projectSessionSummary, sdkTaskSnapshot as projectTaskSnapshot, sdkTaskSummary as projectTaskSummary } from './sdk-projection.js';
8
12
  export class Coordinator {
9
13
  factory;
10
14
  now;
15
+ instanceId;
11
16
  sessions = new Map();
12
17
  tasks = new Map();
13
18
  dashboardHistory;
14
19
  closed = false;
20
+ closeComplete = false;
21
+ closePromise;
22
+ membership = 0;
15
23
  approvals = new ApprovalRegistry(id => this.approvalChanged(this.task(id)));
16
- constructor(factory, now = Date.now) {
24
+ constructor(factory, now = Date.now, instanceId = randomUUID()) {
17
25
  this.factory = factory;
18
26
  this.now = now;
27
+ this.instanceId = instanceId;
19
28
  }
20
29
  parentContext(token) {
21
30
  const session = [...this.sessions.values()].find(session => session.token === token);
22
31
  const task = session?.active ? this.task(session.active) : undefined;
23
32
  if (!session || !task || task.stopping || terminal(task.state))
24
33
  throw new AgentError('INVALID_PARENT', 'The parent context is no longer active.');
25
- return { taskId: task.id, reference: session.input.reference, cwd: session.input.cwd };
34
+ return { taskId: task.id, sessionId: session.id, reference: session.input.reference, cwd: session.input.cwd };
26
35
  }
27
- run(input) {
28
- this.validateAdmission(undefined, input.prompt, input.parentToken);
29
- const selected = 'kind' in input.reference ? input.reference.harness.kind : undefined;
36
+ createSession(input, prepared) {
37
+ if (this.closed)
38
+ throw new AgentError('COORDINATOR_UNAVAILABLE', 'The coordinator is shutting down.');
39
+ const selected = 'kind' in input.reference && input.reference.kind === 'harness'
40
+ ? input.reference.harness.kind : undefined;
30
41
  const session = { id: `ses_${randomUUID()}`, token: randomUUID(), input,
31
- tasks: [], paused: false, commands: Promise.resolve(), controls: 0, harness: dashboardHarness(selected),
32
- workspace: resolveDashboardWorkspace(input.cwd) };
42
+ tasks: [], paused: false, commands: Promise.resolve(), controls: 0, nextOrdinal: 1,
43
+ membership: ++this.membership, prepared,
44
+ harness: dashboardHarness(selected), workspace: 'kind' in input.reference && input.reference.kind === 'inline'
45
+ ? Promise.resolve({ directory: input.cwd }) : resolveDashboardWorkspace(input.cwd), state: 'open' };
33
46
  this.sessions.set(session.id, session);
47
+ return session.id;
48
+ }
49
+ run(input, prepared) {
50
+ this.validateAdmission(undefined, input.prompt, input.parentToken);
51
+ const { prompt, parentToken, ...creation } = input;
52
+ const session = this.session(this.createSession(creation, prepared));
34
53
  return this.admit(session, input.prompt, false, input.parentToken);
35
54
  }
36
55
  async send(id, prompt, delivery, parentToken) {
@@ -68,26 +87,30 @@ export class Coordinator {
68
87
  return this.command(session, operation);
69
88
  }
70
89
  async observe(id, after, readerToken, signal) {
90
+ if (signal?.aborted)
91
+ throw new AgentError('OBSERVATION_CLOSED', 'The observer disconnected.');
71
92
  const task = this.task(id);
72
- const position = after ? task.responses.findIndex(response => response.responseId === after) : -1;
73
- if (after && position === -1)
93
+ const hasCursor = after !== undefined;
94
+ const position = hasCursor ? task.responses.findIndex(response => response.responseId === after) : -1;
95
+ if (hasCursor && position === -1)
74
96
  throw new AgentError('UNKNOWN_RESPONSE', 'The response does not belong to this task.');
75
97
  for (;;) {
76
98
  const response = task.responses[position + 1];
77
99
  if (response) {
78
100
  if (terminal(response.state))
79
101
  this.acknowledge(task, readerToken);
80
- return response;
102
+ return detached(response);
81
103
  }
82
104
  if (terminal(task.state)) {
83
105
  this.acknowledge(task, readerToken);
84
- return { version: 1, type: 'terminal', taskId: id,
85
- sessionId: task.sessionId, state: task.state, ...(task.error ? { error: task.error } : {}) };
106
+ return detached({ version: 1, type: 'terminal', taskId: id,
107
+ sessionId: task.sessionId, state: task.state, ...(task.error ? { error: task.error } : {}),
108
+ stopUnconfirmed: task.stopUnconfirmed ?? false });
86
109
  }
87
110
  const requests = this.pendingApprovals(task);
88
111
  if (requests.length)
89
- return { version: 1, type: 'approval_required', taskId: id,
90
- sessionId: task.sessionId, state: task.state, requests };
112
+ return detached({ version: 1, type: 'approval_required', taskId: id,
113
+ sessionId: task.sessionId, state: task.state, requests });
91
114
  await waitForChange(task.changed, signal);
92
115
  }
93
116
  }
@@ -108,6 +131,94 @@ export class Coordinator {
108
131
  truncated: !full && response.text.length > 4000
109
132
  } } : {}) };
110
133
  }
134
+ snapshot(id) {
135
+ const task = this.task(id);
136
+ const response = task.responses.at(-1);
137
+ return detached({
138
+ ...this.summary(task),
139
+ revision: task.revision,
140
+ ...(task.parentId ? { parentTaskId: task.parentId } : {}),
141
+ children: [...task.children].map(childId => this.summary(this.task(childId))),
142
+ requests: this.pendingApprovals(task),
143
+ ...(response ? { latestResponse: { responseId: response.responseId, text: response.text, state: response.state } } : {}),
144
+ ...(task.error ? { error: task.error } : {}),
145
+ stopUnconfirmed: task.stopUnconfirmed ?? false,
146
+ });
147
+ }
148
+ sessionSnapshot(id) {
149
+ const session = this.session(id);
150
+ return detached({
151
+ sessionId: session.id,
152
+ state: session.state,
153
+ paused: session.paused,
154
+ ...(session.active ? { active: this.summary(this.task(session.active)) } : {}),
155
+ queued: session.tasks.map(taskId => this.summary(this.task(taskId))),
156
+ });
157
+ }
158
+ sdkTaskSnapshot(id) {
159
+ const task = this.task(id);
160
+ return detached(projectTaskSnapshot(task, candidate => this.sdkSummary(candidate), candidate => this.task(candidate), this.sdkPendingApprovals(task)));
161
+ }
162
+ async sdkSessionSnapshot(id) {
163
+ const session = this.session(id);
164
+ const workspace = await session.workspace;
165
+ return detached({ ...this.sdkSessionSummaryResolved(session, workspace),
166
+ queued: session.tasks.map(taskId => this.sdkSummary(this.task(taskId))) });
167
+ }
168
+ taskDetails(id) {
169
+ const task = this.task(id);
170
+ return detached({ ...this.sdkTaskSnapshot(id), prompt: task.prompt });
171
+ }
172
+ taskResult(id, signal) {
173
+ const task = this.task(id);
174
+ return terminalInterval(task, () => this.sdkTaskSnapshot(id), signal).then(({ snapshot, response }) => {
175
+ if (snapshot.state === 'completed') {
176
+ if (!response)
177
+ throw new AgentError('HARNESS_FAILED', 'The completed task has no final response.');
178
+ return detached({ taskId: task.id, sessionId: task.sessionId, responseId: response.responseId, text: response.text });
179
+ }
180
+ if (snapshot.state === 'cancelled')
181
+ throw new AgentError('CANCELLED', 'The task was cancelled.');
182
+ if (snapshot.state === 'interrupted')
183
+ throw new AgentError('INTERRUPTED', 'The task was interrupted.');
184
+ throw new AgentError(snapshot.error?.code ?? 'HARNESS_FAILED', snapshot.error?.message ?? 'Task execution failed.');
185
+ });
186
+ }
187
+ taskResponses(id, after, signal) {
188
+ const task = this.task(id);
189
+ const start = after === undefined ? 0 : task.responses.findIndex(response => response.responseId === after) + 1;
190
+ if (after !== undefined && start === 0)
191
+ throw new AgentError('UNKNOWN_RESPONSE', 'The response does not belong to this task.');
192
+ return responseReplay(task, start, signal);
193
+ }
194
+ watchTask(id, signal) {
195
+ const task = this.task(id);
196
+ return createSnapshotWatch(() => this.sdkTaskSnapshot(id), changed => {
197
+ task.watchers.add(changed);
198
+ return () => task.watchers.delete(changed);
199
+ }, signal);
200
+ }
201
+ async listSessions(options) {
202
+ const page = paginate([...this.sessions.values()], options, { instance: this.instanceId, collection: 'sessions', filter: '' }, value => value.membership);
203
+ return detached({ items: await Promise.all(page.items.map(session => this.sdkSessionSummary(session))),
204
+ ...(page.nextCursor ? { nextCursor: page.nextCursor } : {}) });
205
+ }
206
+ listTasks(options) {
207
+ const sessionId = options?.sessionId;
208
+ if (sessionId !== undefined)
209
+ this.session(sessionId);
210
+ const candidates = [...this.tasks.values()].filter(task => sessionId === undefined || task.sessionId === sessionId);
211
+ const page = paginate(candidates, options, { instance: this.instanceId, collection: 'tasks', filter: sessionId ?? '' }, value => value.membership);
212
+ return detached({ items: page.items.map(task => this.sdkSummary(task)), ...(page.nextCursor ? { nextCursor: page.nextCursor } : {}) });
213
+ }
214
+ respondSdk(id, response) { return this.approvals.respondSdk(id, response); }
215
+ watch(id, signal) {
216
+ const task = this.task(id);
217
+ return createSnapshotWatch(() => this.snapshot(task.id), changed => {
218
+ task.watchers.add(changed);
219
+ return () => task.watchers.delete(changed);
220
+ }, signal);
221
+ }
111
222
  respond(id, content, parentToken) {
112
223
  const request = this.approvals.request(id);
113
224
  if (parentToken !== undefined) {
@@ -245,13 +356,92 @@ export class Coordinator {
245
356
  return { version: 1, type: 'started', sessionId: id, taskId: task.id, state: task.state, resumed: true };
246
357
  });
247
358
  }
248
- async close() {
359
+ close() {
249
360
  this.closed = true;
250
- for (const task of this.tasks.values())
251
- if (!terminal(task.state))
252
- task.stopping = true;
253
361
  this.approvals.close();
254
- await Promise.allSettled([...this.sessions.values()].map(async (session) => (await session.driver)?.close()));
362
+ if (this.closeComplete)
363
+ return Promise.resolve();
364
+ if (this.closePromise)
365
+ return this.closePromise;
366
+ const attempt = (async () => {
367
+ const results = await Promise.allSettled([...this.sessions].map(([id]) => this.closeSession(id)));
368
+ const failures = results.flatMap(result => result.status === 'rejected'
369
+ ? this.cleanupErrors(result.reason, 'Runner cleanup could not be confirmed.') : []);
370
+ if (failures.length)
371
+ throw new AggregateError(failures, 'Coordinator cleanup failed.');
372
+ this.closeComplete = true;
373
+ })();
374
+ this.closePromise = attempt.finally(() => { this.closePromise = undefined; });
375
+ return this.closePromise;
376
+ }
377
+ closeSession(id) {
378
+ const session = this.session(id);
379
+ if (session.state === 'closed')
380
+ return Promise.resolve();
381
+ if (session.closePromise)
382
+ return session.closePromise;
383
+ session.state = 'closing';
384
+ const attempt = this.cleanupSession(session);
385
+ session.closePromise = attempt.finally(() => { session.closePromise = undefined; });
386
+ return session.closePromise;
387
+ }
388
+ async cleanupSession(session) {
389
+ const targets = [...this.tasks.values()].filter(task => task.sessionId === session.id &&
390
+ (!terminal(task.state) || task.stopUnconfirmed));
391
+ let preparedFailure = false;
392
+ const preparedClose = !session.preparedClosed && session.prepared
393
+ ? session.prepared.close().then(() => { session.preparedClosed = true; }, () => { preparedFailure = true; })
394
+ : Promise.resolve();
395
+ let stopped = await Promise.allSettled(targets.map(task => this.stop(task, 'cancelled')));
396
+ await session.commands.catch(() => undefined);
397
+ let driverClosedDuringAttempt = false;
398
+ const failures = [];
399
+ if (!session.driverClosed && session.driver) {
400
+ let driver;
401
+ try {
402
+ driver = await session.driver;
403
+ }
404
+ catch {
405
+ session.driverClosed = true;
406
+ driverClosedDuringAttempt = true;
407
+ }
408
+ if (driver) {
409
+ try {
410
+ await driver.close();
411
+ session.driverClosed = true;
412
+ driverClosedDuringAttempt = true;
413
+ for (const task of this.tasks.values())
414
+ if (task.sessionId === session.id)
415
+ task.nativeActive = false;
416
+ }
417
+ catch {
418
+ failures.push(new AgentError('HARNESS_FAILED', 'Native session cleanup could not be confirmed.'));
419
+ }
420
+ }
421
+ }
422
+ await preparedClose;
423
+ if (preparedFailure)
424
+ failures.push(new AgentError('HARNESS_FAILED', 'Prepared execution cleanup could not be confirmed.'));
425
+ if ((driverClosedDuringAttempt || session.preparedClosed) && stopped.some(result => result.status === 'rejected')) {
426
+ const retry = targets.filter(task => task.stopUnconfirmed);
427
+ stopped = await Promise.allSettled(retry.map(task => this.stop(task, 'cancelled')));
428
+ }
429
+ failures.push(...stopped.flatMap(result => result.status === 'rejected'
430
+ ? [new AgentError('CANCEL_FAILED', 'Task execution could not be confirmed stopped.')] : []));
431
+ if (failures.length)
432
+ throw new AggregateError(failures, 'Session cleanup failed.');
433
+ session.paused = false;
434
+ session.state = 'closed';
435
+ }
436
+ cleanupErrors(reason, message) {
437
+ if (reason instanceof AggregateError) {
438
+ const errors = [...reason.errors];
439
+ return errors.length ? errors.map(error => error instanceof AgentError
440
+ ? new AgentError(error.code, error.message) : new AgentError('HARNESS_FAILED', message)) : [];
441
+ }
442
+ return [reason instanceof AgentError
443
+ ? new AgentError(reason.code, reason.message)
444
+ : new AgentError('HARNESS_FAILED', message)];
255
445
  }
256
446
  async cancel(id) {
257
447
  const task = this.task(id);
@@ -297,7 +487,7 @@ export class Coordinator {
297
487
  stops.push(session.driver.then(async (driver) => {
298
488
  await driver.interrupt();
299
489
  task.nativeActive = false;
300
- }));
490
+ }, () => { task.nativeActive = false; }));
301
491
  const outcomes = await Promise.allSettled(stops);
302
492
  if (outcomes.some(outcome => outcome.status === 'rejected'))
303
493
  throw new Error('Cancellation unconfirmed');
@@ -308,7 +498,7 @@ export class Coordinator {
308
498
  task.error = { code: 'CANCEL_FAILED', message: 'Native execution could not be confirmed stopped.' };
309
499
  task.stopUnconfirmed = true;
310
500
  session.paused = true;
311
- this.notify(task);
501
+ this.publish(task);
312
502
  throw new AgentError('CANCEL_FAILED', task.error.message);
313
503
  }
314
504
  task.nativeActive = false;
@@ -319,7 +509,7 @@ export class Coordinator {
319
509
  session.active = undefined;
320
510
  session.paused = false;
321
511
  }
322
- this.notify(task);
512
+ this.publish(task);
323
513
  this.deliver(task);
324
514
  }
325
515
  admit(session, prompt, front = false, parentToken) {
@@ -327,9 +517,12 @@ export class Coordinator {
327
517
  const blockedByTaskId = session.paused ? session.active : undefined;
328
518
  const task = { id: `tsk_${randomUUID()}`, sessionId: session.id, prompt, title: dashboardTitle(prompt),
329
519
  parentId: parent?.id, state: 'queued', responses: [], children: new Set(), inbox: [], nativeActive: false,
330
- stopping: false, updatedAt: this.now(), changed: new Set() };
520
+ stopping: false, updatedAt: this.now(), revision: 1, ordinal: session.nextOrdinal++, membership: ++this.membership,
521
+ changed: new Set(), watchers: new Set() };
331
522
  this.tasks.set(task.id, task);
332
523
  parent?.children.add(task.id);
524
+ if (parent)
525
+ this.publish(parent);
333
526
  if (front)
334
527
  session.tasks.unshift(task.id);
335
528
  else
@@ -341,6 +534,8 @@ export class Coordinator {
341
534
  validateAdmission(session, prompt, parentToken, replacement = false) {
342
535
  if (this.closed)
343
536
  throw new AgentError('COORDINATOR_UNAVAILABLE', 'The coordinator is shutting down.');
537
+ if (session && session.state !== 'open')
538
+ throw new AgentError('SESSION_CLOSED', 'The session is closing or closed.');
344
539
  if (typeof prompt !== 'string' || !prompt.trim() || Buffer.byteLength(prompt) > 1024 * 1024)
345
540
  throw new AgentError('INVALID_ARGUMENT', 'Prompt must contain text and cannot exceed 1 MiB.');
346
541
  if (!replacement && session && session.tasks.length >= 100)
@@ -358,7 +553,7 @@ export class Coordinator {
358
553
  return parent;
359
554
  }
360
555
  dispatch(session) {
361
- if (this.closed || session.active || session.paused || session.controls)
556
+ if (this.closed || session.state !== 'open' || session.active || session.paused || session.controls)
362
557
  return;
363
558
  const id = session.tasks.shift();
364
559
  if (!id)
@@ -370,11 +565,13 @@ export class Coordinator {
370
565
  const session = this.session(task.sessionId);
371
566
  const dispatchedAt = this.now();
372
567
  task.dispatchedAt ??= dispatchedAt;
373
- this.touch(task, dispatchedAt);
374
568
  task.state = 'running';
375
569
  task.nativeActive = true;
570
+ this.touch(task, dispatchedAt);
571
+ this.publish(task);
376
572
  try {
377
- session.driver ??= this.factory({ ...session.input, sessionId: session.id, token: session.token,
573
+ const open = session.prepared ? session.prepared.open.bind(session.prepared) : this.factory;
574
+ session.driver ??= open({ ...session.input, sessionId: session.id, token: session.token,
378
575
  onApproval: async (request, signal) => {
379
576
  const active = session.active ? this.task(session.active) : undefined;
380
577
  if (this.closed || !active || active.stopping || !active.nativeActive || terminal(active.state))
@@ -384,9 +581,13 @@ export class Coordinator {
384
581
  const driver = await session.driver;
385
582
  if (driver.selectedHarness) {
386
583
  const harness = dashboardHarness(driver.selectedHarness);
387
- if (session.harness !== harness) {
584
+ if (session.selectedHarness !== driver.selectedHarness) {
585
+ session.selectedHarness = driver.selectedHarness;
388
586
  session.harness = harness;
389
587
  this.touch(task);
588
+ for (const candidate of this.tasks.values())
589
+ if (candidate.sessionId === session.id)
590
+ this.publish(candidate);
390
591
  }
391
592
  }
392
593
  if (task.stopping)
@@ -406,7 +607,7 @@ export class Coordinator {
406
607
  this.touch(task, respondedAt);
407
608
  task.responses.push({ version: 1, type: 'response', sessionId: session.id, taskId: task.id,
408
609
  responseId: `rsp_${randomUUID()}`, state: task.state, text });
409
- this.notify(task);
610
+ this.publish(task);
410
611
  if (task.state === 'completed') {
411
612
  session.active = undefined;
412
613
  this.deliver(task);
@@ -428,7 +629,7 @@ export class Coordinator {
428
629
  task.error = task.stopUnconfirmed
429
630
  ? { code: 'CANCEL_FAILED', message: 'Descendant execution could not be confirmed stopped.' } : errorInfo(error);
430
631
  session.paused = true;
431
- this.notify(task);
632
+ this.publish(task);
432
633
  this.deliver(task);
433
634
  }
434
635
  }
@@ -454,7 +655,34 @@ export class Coordinator {
454
655
  results.map(result => `${result.taskId} [${result.state}]:\n${result.text}`).join('\n\n');
455
656
  void this.execute(task, prompt);
456
657
  }
658
+ summary(task) {
659
+ const selectedHarness = this.session(task.sessionId).selectedHarness;
660
+ return { taskId: task.id, sessionId: task.sessionId, state: task.state,
661
+ ...(selectedHarness ? { selectedHarness } : {}) };
662
+ }
663
+ sdkSummary(task) {
664
+ return projectTaskSummary(task, this.session(task.sessionId).selectedHarness);
665
+ }
666
+ async sdkSessionSummary(session) {
667
+ const workspace = await session.workspace;
668
+ return this.sdkSessionSummaryResolved(session, workspace);
669
+ }
670
+ sdkSessionSummaryResolved(session, workspace) {
671
+ return projectSessionSummary(session, workspace, task => this.sdkSummary(task), id => this.task(id));
672
+ }
673
+ sdkPendingApprovals(task) {
674
+ return [...this.approvals.sdkPendingFor(task.id),
675
+ ...[...task.children].flatMap(id => this.sdkPendingApprovals(this.task(id)))];
676
+ }
457
677
  touch(task, at = this.now()) { task.updatedAt = at; }
678
+ publish(task) {
679
+ let current = task;
680
+ while (current) {
681
+ current.revision++;
682
+ this.notify(current);
683
+ current = current.parentId ? this.task(current.parentId) : undefined;
684
+ }
685
+ }
458
686
  pendingApprovals(task) {
459
687
  return [...this.approvals.pendingFor(task.id), ...[...task.children].flatMap(id => this.pendingApprovals(this.task(id)))];
460
688
  }
@@ -464,12 +692,18 @@ export class Coordinator {
464
692
  this.touch(task);
465
693
  let current = task;
466
694
  while (current) {
695
+ current.revision++;
467
696
  this.notify(current);
468
697
  current = current.parentId ? this.task(current.parentId) : undefined;
469
698
  }
470
699
  }
471
- notify(task) { for (const resolve of task.changed)
472
- resolve(); task.changed.clear(); }
700
+ notify(task) {
701
+ for (const resolve of task.changed)
702
+ resolve();
703
+ task.changed.clear();
704
+ for (const update of task.watchers)
705
+ update();
706
+ }
473
707
  task(id) { const task = this.tasks.get(id); if (!task)
474
708
  throw new AgentError('UNKNOWN_TASK', 'Task is unavailable in this coordinator.'); return task; }
475
709
  session(id) { const session = this.sessions.get(id); if (!session)