@ours.network/fleet 2.0.4 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/application/room-layout-definitions.js +5 -0
- package/dist/application/task-layouts.d.ts +10 -1
- package/dist/application/task-layouts.js +53 -5
- package/dist/application/task-room-service.d.ts +5 -1
- package/dist/application/task-room-service.js +15 -6
- package/dist/build-info.json +4 -4
- package/dist/rooms-tasks/add-member.d.ts +3 -1
- package/dist/rooms-tasks/add-member.js +40 -0
- package/dist/rooms-tasks/cli.js +2 -2
- package/dist/rooms-tasks/empty-layout.d.ts +5 -0
- package/dist/rooms-tasks/empty-layout.js +8 -0
- package/dist/rooms-tasks/layout-config.js +2 -2
- package/dist/rooms-tasks/layout-service.d.ts +1 -1
- package/dist/rooms-tasks/layout-service.js +5 -2
- package/dist/rooms-tasks/layout.d.ts +18 -1
- package/dist/rooms-tasks/layout.js +40 -6
- package/dist/web/server.js +7 -5
- package/docs/room-layouts.md +34 -0
- package/package.json +1 -1
|
@@ -14,6 +14,7 @@ import { loadConfig, splitRootFor } from '../config.js';
|
|
|
14
14
|
import { assertLayoutFile, validLayoutKey, validateRoomLayout } from '../rooms-tasks/layout-config.js';
|
|
15
15
|
import { FleetError } from './errors.js';
|
|
16
16
|
import { layoutDefinitionHash } from './task-layouts.js';
|
|
17
|
+
import { EMPTY_TASK_LAYOUT } from '../rooms-tasks/empty-layout.js';
|
|
17
18
|
export const ABSENT_REVISION = 'absent';
|
|
18
19
|
const revisionOf = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
19
20
|
export class RoomLayoutDefinitions {
|
|
@@ -25,6 +26,8 @@ export class RoomLayoutDefinitions {
|
|
|
25
26
|
}
|
|
26
27
|
root() { return join(splitRootFor(this.configPath ?? defaultConfigPath()), 'room_layouts'); }
|
|
27
28
|
pathFor(name) {
|
|
29
|
+
if (name === EMPTY_TASK_LAYOUT)
|
|
30
|
+
throw new FleetError('invalid_request', 'the built-in empty layout cannot be edited or deleted');
|
|
28
31
|
if (!validLayoutKey(name))
|
|
29
32
|
throw new FleetError('invalid_request', 'layout name must start with a letter and use letters, digits, - or _');
|
|
30
33
|
return join(this.root(), `${name}.yaml`);
|
|
@@ -68,6 +71,8 @@ export class RoomLayoutDefinitions {
|
|
|
68
71
|
return readdirSync(root).filter(file => /\.ya?ml$/.test(file)).sort().map(file => {
|
|
69
72
|
const name = basename(file, extname(file)), path = join(root, file);
|
|
70
73
|
try {
|
|
74
|
+
if (name === EMPTY_TASK_LAYOUT)
|
|
75
|
+
throw Error('reserved built-in layout name');
|
|
71
76
|
if (!validLayoutKey(name))
|
|
72
77
|
throw Error(`invalid room layout id: ${name}`);
|
|
73
78
|
assertLayoutFile(path);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type RoomLayoutDefinition } from '../rooms-tasks/layout-config.js';
|
|
2
2
|
import { RoomLayoutService } from '../rooms-tasks/layout-service.js';
|
|
3
3
|
import type { TaskLayoutLink, TaskRecord } from '../rooms-tasks/types.js';
|
|
4
|
-
export type TaskLayoutOperation = 'open' | 'close-room' | 'close';
|
|
4
|
+
export type TaskLayoutOperation = 'start' | 'open' | 'close-room' | 'close';
|
|
5
5
|
export interface TaskLayoutOperationRecord {
|
|
6
6
|
/** Only the worker holding the current ID may update the record. */
|
|
7
7
|
id: string;
|
|
@@ -73,6 +73,15 @@ export declare class TaskLayouts {
|
|
|
73
73
|
runExists(link: TaskLayoutLink): boolean;
|
|
74
74
|
/** Idempotent: reuse a run whose provenance matches this task, or snapshot one from the pinned source. */
|
|
75
75
|
ensureRun(task: TaskRecord, precondition?: (current: TaskRecord) => void): Promise<void>;
|
|
76
|
+
/** Starting a mission initializes every room and its required agents before Active. */
|
|
77
|
+
initializeRooms(task: TaskRecord): Promise<void>;
|
|
78
|
+
addMember(task: TaskRecord, input: {
|
|
79
|
+
key: string;
|
|
80
|
+
role: string;
|
|
81
|
+
requestId: string;
|
|
82
|
+
hash: string;
|
|
83
|
+
definition: import('../config.js').AgentTemplateDefinition;
|
|
84
|
+
}): Promise<void>;
|
|
76
85
|
private verifyProvenance;
|
|
77
86
|
readOperation(runId: string): TaskLayoutOperationRecord | undefined;
|
|
78
87
|
/** Update only the still-current operation; a superseded or forgotten one is never resurrected. */
|
|
@@ -11,7 +11,7 @@ import { join } from 'node:path';
|
|
|
11
11
|
import { canonicalJson } from '../canonical-json.js';
|
|
12
12
|
import { replaceFileAtomically, withFileLock } from '../atomic-file.js';
|
|
13
13
|
import { assertLayoutFile } from '../rooms-tasks/layout-config.js';
|
|
14
|
-
import { getTask as readTask } from '../rooms-tasks/task-state.js';
|
|
14
|
+
import { getTask as readTask, activateTask, blockTask } from '../rooms-tasks/task-state.js';
|
|
15
15
|
import { stateRoot } from '../paths.js';
|
|
16
16
|
import { launchFleetWorker } from '../rooms-tasks/external-worker.js';
|
|
17
17
|
import { layoutSupervisorId } from '../rooms-tasks/layout-control.js';
|
|
@@ -20,6 +20,7 @@ import { FleetError } from './errors.js';
|
|
|
20
20
|
import { ownedLayoutRetirementSeats } from '../rooms-tasks/layout-member-retirement.js';
|
|
21
21
|
import { eraseMemberArtifacts } from '../rooms-tasks/erasure.js';
|
|
22
22
|
import { assertTaskRegistrationsAbsent } from '../supervisor/catalog.js';
|
|
23
|
+
import { EMPTY_TASK_LAYOUT, EMPTY_TASK_ROOM } from '../rooms-tasks/empty-layout.js';
|
|
23
24
|
/** Hash of the authorable definition only (never the source file path). */
|
|
24
25
|
export function layoutDefinitionHash(definition) {
|
|
25
26
|
const { version, description, participants, rooms } = definition;
|
|
@@ -95,12 +96,35 @@ export class TaskLayouts {
|
|
|
95
96
|
if (layoutDefinitionHash(source) !== link.definition_hash)
|
|
96
97
|
throw Error(`room layout ${link.name} changed before this task's layout run was created; create a new task to use the edited layout`);
|
|
97
98
|
const scoped = taskScopedDefinition(structuredClone(source), current);
|
|
99
|
+
if (link.name === EMPTY_TASK_LAYOUT)
|
|
100
|
+
scoped.rooms[EMPTY_TASK_ROOM].goal = current.brief?.trim() || current.title;
|
|
98
101
|
const provenance = { task_id: current.task_id, source_hash: link.definition_hash, scoped_hash: layoutDefinitionHash(scoped) };
|
|
99
102
|
// Provenance precedes the run: a crash in between is re-derived from the same pinned source.
|
|
100
103
|
replaceFileAtomically(this.provenancePath(link.run_id), JSON.stringify(provenance), 0o600);
|
|
101
104
|
await service.create(link.name, {}, link.run_id, { source: scoped, cwd: current.workspace?.path });
|
|
102
105
|
});
|
|
103
106
|
}
|
|
107
|
+
/** Starting a mission initializes every room and its required agents before Active. */
|
|
108
|
+
async initializeRooms(task) {
|
|
109
|
+
await this.withTaskLock(task.layout.run_id, async () => {
|
|
110
|
+
const current = readTask(task.task_id);
|
|
111
|
+
if (current.deletion?.status === 'pending' || current.terminal_intent || !['provisioning', 'active'].includes(current.state))
|
|
112
|
+
throw new FleetError('conflict', 'task is no longer starting; its room cannot be opened');
|
|
113
|
+
this.verifyProvenance(current);
|
|
114
|
+
const engine = this.service().open(task.layout.run_id);
|
|
115
|
+
for (const key of Object.keys(engine.snapshot().definition.rooms))
|
|
116
|
+
await engine.activate(layoutSupervisorId(), key);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
async addMember(task, input) {
|
|
120
|
+
const link = task.layout;
|
|
121
|
+
await this.withTaskLock(link.run_id, async () => {
|
|
122
|
+
const current = readTask(task.task_id);
|
|
123
|
+
assertLayoutOpenable(current);
|
|
124
|
+
this.verifyProvenance(current);
|
|
125
|
+
await this.service().open(link.run_id).addMember(layoutSupervisorId(), { ...input, room: EMPTY_TASK_ROOM });
|
|
126
|
+
});
|
|
127
|
+
}
|
|
104
128
|
verifyProvenance(task) {
|
|
105
129
|
const link = task.layout, path = this.provenancePath(link.run_id);
|
|
106
130
|
let provenance;
|
|
@@ -151,15 +175,18 @@ export class TaskLayouts {
|
|
|
151
175
|
...(state.uncertain ? { uncertain: state.uncertain } : {}),
|
|
152
176
|
rooms: Object.entries(state.definition.rooms).map(([key, spec]) => {
|
|
153
177
|
const room = state.rooms[key];
|
|
178
|
+
const additions = Object.entries(state.additional_members ?? {}).filter(([, member]) => member.room === key);
|
|
179
|
+
const roles = { ...spec.roles, ...Object.fromEntries(additions.map(([name, member]) => [name, member.role])) };
|
|
154
180
|
return {
|
|
155
|
-
key, goal: spec.goal, members: [...spec.members
|
|
181
|
+
key, goal: spec.goal, members: [...spec.members, ...additions.map(([name]) => name)],
|
|
182
|
+
...(Object.keys(roles).length ? { roles } : {}),
|
|
156
183
|
state: room?.state ?? 'declared', ready: [...(room?.ready ?? [])],
|
|
157
184
|
...(room?.native ? { room_id: room.native.room_id, identity_cid: room.native.identity_cid } : {}),
|
|
158
185
|
};
|
|
159
186
|
}),
|
|
160
187
|
participants: Object.entries(state.participants).map(([key, p]) => ({
|
|
161
188
|
key, ...(p.participant ? { participant: p.participant, room: p.room } : {}),
|
|
162
|
-
agent_template: state.definition.participants[p.participant ?? key]?.agent_template, owned: p.owned, retired: Boolean(p.retired),
|
|
189
|
+
agent_template: state.definition.participants[p.participant ?? key]?.agent_template ?? state.additional_members?.[key]?.template, owned: p.owned, retired: Boolean(p.retired),
|
|
163
190
|
...(p.instance?.agent ? { agent: p.instance.agent } : {}), ...(p.instance?.cid ? { cid: p.instance.cid } : {}),
|
|
164
191
|
})),
|
|
165
192
|
};
|
|
@@ -169,7 +196,7 @@ export class TaskLayouts {
|
|
|
169
196
|
const link = task.layout;
|
|
170
197
|
if (!link)
|
|
171
198
|
throw Error(`task ${task.task_id} has no layout`);
|
|
172
|
-
if (
|
|
199
|
+
if (!['start', 'close'].includes(operation) && (!room || !ROOM_KEY.test(room)))
|
|
173
200
|
throw new FleetError('invalid_request', 'a valid room key is required');
|
|
174
201
|
if (room) {
|
|
175
202
|
const definition = this.runExists(link) ? this.service().open(link.run_id, true).snapshot().definition : undefined;
|
|
@@ -177,13 +204,20 @@ export class TaskLayouts {
|
|
|
177
204
|
throw new FleetError('resource_not_found', `unknown layout room: ${room}`);
|
|
178
205
|
}
|
|
179
206
|
const record = { id: randomUUID(), operation, ...(room ? { room } : {}), status: 'launching' };
|
|
180
|
-
|
|
207
|
+
let accepted = false;
|
|
208
|
+
const selected = await withFileLock(this.operationPath(link.run_id) + '.lock', () => {
|
|
181
209
|
const current = this.readOperation(link.run_id);
|
|
182
210
|
// Full close always proceeds: terminal cleanup must not wait behind an open; the engine lock serializes them.
|
|
211
|
+
if (operation === 'start' && current?.operation === 'start' && ['launching', 'running'].includes(current.status) && Date.now() - Date.parse(current.updated_at) < 10 * 60_000)
|
|
212
|
+
return current;
|
|
183
213
|
if (operation !== 'close' && current && ['launching', 'running'].includes(current.status) && Date.now() - Date.parse(current.updated_at) < 10 * 60_000)
|
|
184
214
|
throw new FleetError('conflict', `layout operation already in progress: ${current.operation}${current.room ? ' ' + current.room : ''}`);
|
|
185
215
|
this.writeOperation(link.run_id, record);
|
|
216
|
+
accepted = true;
|
|
217
|
+
return record;
|
|
186
218
|
});
|
|
219
|
+
if (!accepted)
|
|
220
|
+
return selected;
|
|
187
221
|
const args = ['task', '_layout', task.task_id, operation, ...(room ? [room] : []), '--operation-id', record.id];
|
|
188
222
|
const launch = this.deps.launch
|
|
189
223
|
?? ((cli, name) => launchFleetWorker(cli, name, this.configPath, this.deps.binPath?.()));
|
|
@@ -218,6 +252,20 @@ export class TaskLayouts {
|
|
|
218
252
|
}
|
|
219
253
|
async perform(task, operation, room) {
|
|
220
254
|
const link = task.layout;
|
|
255
|
+
if (operation === 'start') {
|
|
256
|
+
try {
|
|
257
|
+
await this.initializeRooms(task);
|
|
258
|
+
if (readTask(task.task_id).state !== 'active')
|
|
259
|
+
activateTask(task.task_id);
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
const current = readTask(task.task_id);
|
|
263
|
+
if (current.state === 'provisioning' && !current.terminal_intent && current.deletion?.status !== 'pending')
|
|
264
|
+
blockTask(task.task_id, `layout startup failed: ${error.message}`);
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
221
269
|
if (operation === 'open') {
|
|
222
270
|
// Re-read under the task-layout lock: a deletion or terminal action accepted after HTTP admission wins.
|
|
223
271
|
await this.ensureRun(task, assertLayoutOpenable);
|
|
@@ -79,6 +79,8 @@ export interface CreateTaskRequest {
|
|
|
79
79
|
anonymous?: boolean;
|
|
80
80
|
/** Room Layout name: the task's rooms and participants come from a layout run. */
|
|
81
81
|
layout?: string;
|
|
82
|
+
/** HTTP acceptance returns while a detached worker initializes the layout. */
|
|
83
|
+
deferLayoutStart?: boolean;
|
|
82
84
|
}
|
|
83
85
|
export interface CreateRoomRequest {
|
|
84
86
|
actor: TaskRoomActor;
|
|
@@ -167,7 +169,7 @@ export declare class TaskRoomApplicationService {
|
|
|
167
169
|
runTaskLayoutOperation(input: {
|
|
168
170
|
actor: TaskRoomActor;
|
|
169
171
|
taskId: string;
|
|
170
|
-
operation: 'open' | 'close-room' | 'close';
|
|
172
|
+
operation: 'start' | 'open' | 'close-room' | 'close';
|
|
171
173
|
room?: string;
|
|
172
174
|
operationId?: string;
|
|
173
175
|
}): Promise<import("./task-layouts.js").TaskLayoutView>;
|
|
@@ -179,6 +181,7 @@ export declare class TaskRoomApplicationService {
|
|
|
179
181
|
template?: string;
|
|
180
182
|
members?: MemberOverrides;
|
|
181
183
|
anonymous?: boolean;
|
|
184
|
+
deferLayoutStart?: boolean;
|
|
182
185
|
}): Promise<TaskRecord>;
|
|
183
186
|
listTasks(filter?: {
|
|
184
187
|
state?: TaskState | TaskState[];
|
|
@@ -404,6 +407,7 @@ export declare class TaskRoomApplicationService {
|
|
|
404
407
|
template?: string;
|
|
405
408
|
members?: MemberOverrides;
|
|
406
409
|
anonymous?: boolean;
|
|
410
|
+
deferLayoutStart?: boolean;
|
|
407
411
|
}): Promise<{
|
|
408
412
|
task: TaskRecord;
|
|
409
413
|
status: 'ready' | 'already_active' | 'in_progress';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { TaskLayouts, assertLayoutOpenable, writeTaskBrief } from './task-layouts.js';
|
|
2
|
+
import { EMPTY_TASK_LAYOUT } from '../rooms-tasks/empty-layout.js';
|
|
2
3
|
import { FleetError } from './errors.js';
|
|
3
4
|
import { readFileSync } from 'node:fs';
|
|
4
5
|
import { taskLiveReadiness } from '../rooms-tasks/live-readiness.js';
|
|
@@ -78,7 +79,7 @@ export class TaskRoomApplicationService {
|
|
|
78
79
|
const { requestMemberAddition } = await import('../rooms-tasks/add-member.js');
|
|
79
80
|
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
80
81
|
return requestMemberAddition({ taskId, request, cfg, configPath: this.configurationPath,
|
|
81
|
-
cowork: (this.deps.cowork ?? createCoworkAdapter)(cfg), binPath: (this.deps.binPath ?? getBinPath)(), provision: this.deps.provisionMembers });
|
|
82
|
+
cowork: readTask(taskId).layout ? undefined : (this.deps.cowork ?? createCoworkAdapter)(cfg), binPath: (this.deps.binPath ?? getBinPath)(), provision: this.deps.provisionMembers, layouts: this.layouts() });
|
|
82
83
|
}
|
|
83
84
|
async memberAddition(taskId, requestId) { const { memberAddition } = await import('../rooms-tasks/add-member.js'); return memberAddition(taskId, requestId); }
|
|
84
85
|
layouts() {
|
|
@@ -103,10 +104,10 @@ export class TaskRoomApplicationService {
|
|
|
103
104
|
recordFleetAuditPresentation({ kind: 'task', operation: 'create', id: task.task_id,
|
|
104
105
|
title: task.title, previousState: 'none', newState: task.state, revision: task.created_at,
|
|
105
106
|
list: task.list_name ?? 'default', roomId: undefined, template: `layout:${layout.name}`, agents: [] });
|
|
106
|
-
return task.state === 'provisioning' ? (await this.ensureLayoutWork(task)).task : task;
|
|
107
|
+
return task.state === 'provisioning' ? (await this.ensureLayoutWork(task, request.deferLayoutStart)).task : task;
|
|
107
108
|
}
|
|
108
109
|
/** Layout tasks never provision the legacy single room; their run is a local snapshot. */
|
|
109
|
-
async ensureLayoutWork(task) {
|
|
110
|
+
async ensureLayoutWork(task, defer = false) {
|
|
110
111
|
if (TASK_TERMINAL_STATES.includes(task.state))
|
|
111
112
|
throw new TaskRoomApplicationError('task_terminal', 'task terminal', { task: task.task_id, state: task.state });
|
|
112
113
|
if (task.state === 'active' || task.state === 'review')
|
|
@@ -119,6 +120,11 @@ export class TaskRoomApplicationService {
|
|
|
119
120
|
}
|
|
120
121
|
try {
|
|
121
122
|
await this.layouts().ensureRun(task);
|
|
123
|
+
if (defer) {
|
|
124
|
+
await this.layouts().launch(task, 'start');
|
|
125
|
+
return { task: readTask(task.task_id), status: 'in_progress' };
|
|
126
|
+
}
|
|
127
|
+
await this.layouts().initializeRooms(task);
|
|
122
128
|
}
|
|
123
129
|
catch (error) {
|
|
124
130
|
const reason = `layout run could not be created: ${error.message}`;
|
|
@@ -362,10 +368,11 @@ export class TaskRoomApplicationService {
|
|
|
362
368
|
const blocker = task.outcome?.summary ?? task.blocked?.reason;
|
|
363
369
|
return {
|
|
364
370
|
kind: failed ? 'failed' : ready ? 'ready' : 'in_progress', task, room: undefined,
|
|
365
|
-
launch: { template: `layout:${task.layout.name}`, anonymous: false,
|
|
371
|
+
launch: { template: `layout:${task.layout.name}`, anonymous: false,
|
|
372
|
+
owner_attached: task.layout.name === EMPTY_TASK_LAYOUT && this.layouts().view(task)?.rooms.some(room => room.state === 'active') === true },
|
|
366
373
|
members: { expected: 0, active: 0, launched: 0 },
|
|
367
374
|
...(blocker ? { blocker } : {}),
|
|
368
|
-
...(!ready && !failed ? { next_action: `Correct the blocker, then run ours-fleet task start ${task.task_id}.` } : {}),
|
|
375
|
+
...(!ready && !failed && blocker ? { next_action: `Correct the blocker, then run ours-fleet task start ${task.task_id}.` } : {}),
|
|
369
376
|
};
|
|
370
377
|
}
|
|
371
378
|
const room = task.room_id ? getRoomRecord(task.room_id) : undefined;
|
|
@@ -584,6 +591,8 @@ export class TaskRoomApplicationService {
|
|
|
584
591
|
}
|
|
585
592
|
async launchTaskProvisioning(taskId) {
|
|
586
593
|
const outcome = this.taskProvisioningOutcome(taskId);
|
|
594
|
+
if (outcome.task.layout)
|
|
595
|
+
return outcome; // layout startup already has its own durable operation/worker
|
|
587
596
|
if (outcome.kind === 'in_progress' && !outcome.next_action) {
|
|
588
597
|
await launchFleetWorker(['task', '_provision', taskId], `task-provision-${taskId}`, this.configurationPath, this.deps.binPath?.());
|
|
589
598
|
}
|
|
@@ -882,7 +891,7 @@ export class TaskRoomApplicationService {
|
|
|
882
891
|
if (task.layout) {
|
|
883
892
|
if (input.template || input.members || input.anonymous !== undefined)
|
|
884
893
|
throw new FleetError('invalid_request', 'a layout task cannot take a room template or member overrides');
|
|
885
|
-
return this.ensureLayoutWork(task);
|
|
894
|
+
return this.ensureLayoutWork(task, input.deferLayoutStart);
|
|
886
895
|
}
|
|
887
896
|
const recordedRoom = task.room_id ? getRoomRecord(task.room_id) : undefined;
|
|
888
897
|
const recordedPolicy = storedRoomLaunchPolicy(recordedRoom?.room_policy ?? task.execution_plan?.room_policy);
|
package/dist/build-info.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "2.0
|
|
3
|
-
"buildId": "
|
|
4
|
-
"commit": "
|
|
2
|
+
"version": "2.1.0",
|
|
3
|
+
"buildId": "697e8bf9e53c",
|
|
4
|
+
"commit": "2805e0e84c2a3bad05efdbe895fd8fb6e9d2aff4",
|
|
5
5
|
"dirty": false,
|
|
6
|
-
"builtAt": "2026-10-
|
|
6
|
+
"builtAt": "2026-10-27T12:22:22.859Z",
|
|
7
7
|
"capabilities": [
|
|
8
8
|
"service.refresh-v1",
|
|
9
9
|
"managed-cli.codex-reasoning-effort-v1",
|
|
@@ -2,6 +2,7 @@ import type { FleetConfig } from '../config.js';
|
|
|
2
2
|
import { TaskStateError } from './task-state.js';
|
|
3
3
|
import { provisionMembers } from './provision.js';
|
|
4
4
|
import type { CoworkAdapter } from './cowork-adapter.js';
|
|
5
|
+
import type { TaskLayouts } from '../application/task-layouts.js';
|
|
5
6
|
export declare class MemberAdditionRejected extends TaskStateError {
|
|
6
7
|
readonly accepted = false;
|
|
7
8
|
}
|
|
@@ -29,10 +30,11 @@ export declare function requestMemberAddition(input: {
|
|
|
29
30
|
taskId: string;
|
|
30
31
|
request: AddMemberRequest;
|
|
31
32
|
cfg: FleetConfig;
|
|
32
|
-
cowork
|
|
33
|
+
cowork?: CoworkAdapter;
|
|
33
34
|
configPath?: string;
|
|
34
35
|
binPath: string;
|
|
35
36
|
provision?: typeof provisionMembers;
|
|
37
|
+
layouts?: TaskLayouts;
|
|
36
38
|
}): Promise<{
|
|
37
39
|
requestId: string;
|
|
38
40
|
taskId: string;
|
|
@@ -10,6 +10,9 @@ import { getRoomRecord } from './room-state.js';
|
|
|
10
10
|
import { prepareExecutionPlan } from './member-overrides.js';
|
|
11
11
|
import { sealTemplateSnapshot } from './templates.js';
|
|
12
12
|
import { provisionMembers } from './provision.js';
|
|
13
|
+
import { EMPTY_TASK_LAYOUT, EMPTY_TASK_ROOM } from './empty-layout.js';
|
|
14
|
+
import { layoutOwnedMemberName } from './layout-member-retirement.js';
|
|
15
|
+
import { validLayoutKey } from './layout-config.js';
|
|
13
16
|
export class MemberAdditionRejected extends TaskStateError {
|
|
14
17
|
accepted = false;
|
|
15
18
|
}
|
|
@@ -46,6 +49,43 @@ export async function requestMemberAddition(input) {
|
|
|
46
49
|
}
|
|
47
50
|
const task = getTask(taskId);
|
|
48
51
|
const room = task.room_id && getRoomRecord(task.room_id);
|
|
52
|
+
if (task.layout?.name === EMPTY_TASK_LAYOUT && input.layouts) {
|
|
53
|
+
if (!validLayoutKey(r.slot))
|
|
54
|
+
throw new MemberAdditionRejected('Invalid member name');
|
|
55
|
+
const view = input.layouts.view(task);
|
|
56
|
+
if (!['active', 'review'].includes(task.state) || task.terminal_intent || task.deletion?.status === 'pending'
|
|
57
|
+
|| view?.rooms.find(room => room.key === EMPTY_TASK_ROOM)?.state !== 'active' || view.uncertain || view.closing)
|
|
58
|
+
throw new MemberAdditionRejected('Adding an agent requires an active task room');
|
|
59
|
+
if (view.participants.some(p => p.key === r.slot))
|
|
60
|
+
throw new MemberAdditionRejected('This member slot already exists');
|
|
61
|
+
if (r.slot === (input.cfg.rooms?.owner.role ?? 'Owner'))
|
|
62
|
+
throw new MemberAdditionRejected('A member cannot use the Owner role');
|
|
63
|
+
const definition = (() => {
|
|
64
|
+
try {
|
|
65
|
+
const plan = prepareExecutionPlan({ name: 'additional-member', version: 1, description: 'Additional task participant',
|
|
66
|
+
members: [{ slot: r.slot, role: r.slot, count: 1, agent_template: r.agentTemplate }] }, input.cfg, { [r.slot]: { role: r.role, brain: r.brain, ...(task.workspace ? { cwd: task.workspace.path } : {}) } });
|
|
67
|
+
return Object.values(plan.launchDefinitions)[0];
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
throw new MemberAdditionRejected(error instanceof Error ? error.message : 'Invalid member configuration');
|
|
71
|
+
}
|
|
72
|
+
})();
|
|
73
|
+
const receipt = { taskId, requestId: r.requestId,
|
|
74
|
+
roleId: layoutOwnedMemberName(task.layout.run_id, r.slot), hash, state: 'running' };
|
|
75
|
+
const save = () => { replaceFileAtomically(path, JSON.stringify(receipt) + '\n', 0o600); publishFleetResourceCommit(path); };
|
|
76
|
+
save();
|
|
77
|
+
running.add(path);
|
|
78
|
+
void input.layouts.addMember(task, { key: r.slot, role: r.slot, requestId: r.requestId, hash, definition }).then(() => {
|
|
79
|
+
receipt.state = 'succeeded';
|
|
80
|
+
}).catch(() => { receipt.state = 'attention'; receipt.error = 'Member launch is not confirmed. Inspect the task layout and agent before recovery.'; })
|
|
81
|
+
.finally(() => { try {
|
|
82
|
+
save();
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
running.delete(path);
|
|
86
|
+
} }).catch(() => { running.delete(path); });
|
|
87
|
+
return memberAddition(taskId, r.requestId);
|
|
88
|
+
}
|
|
49
89
|
if (!room || !['active', 'review'].includes(task.state) || task.terminal_intent || room.state !== 'active'
|
|
50
90
|
|| room.task_id !== taskId || room.room_identity_cid !== task.room_identity_cid)
|
|
51
91
|
throw new MemberAdditionRejected('Adding an agent requires an active task room');
|
package/dist/rooms-tasks/cli.js
CHANGED
|
@@ -1334,12 +1334,12 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
1334
1334
|
}
|
|
1335
1335
|
});
|
|
1336
1336
|
cOpt(taskCmd.command('_layout <id> <operation> [room]', { hidden: true }))
|
|
1337
|
-
.description('internal: run an accepted layout
|
|
1337
|
+
.description('internal: run an accepted layout operation (start, open, close-room, close)')
|
|
1338
1338
|
.option('--json', 'JSON output')
|
|
1339
1339
|
.option('--operation-id <id>', 'accepted operation ID; status updates apply only while it is current')
|
|
1340
1340
|
.action(async (id, operation, room, opts) => {
|
|
1341
1341
|
try {
|
|
1342
|
-
if (!['open', 'close-room', 'close'].includes(operation))
|
|
1342
|
+
if (!['start', 'open', 'close-room', 'close'].includes(operation))
|
|
1343
1343
|
throw new Error(`unknown layout operation: ${operation}`);
|
|
1344
1344
|
const view = await taskRoomService(opts.configuration).runTaskLayoutOperation({
|
|
1345
1345
|
actor: { kind: 'internal_worker', surface: 'cli' }, taskId: id,
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { RoomLayoutDefinition } from './layout-config.js';
|
|
2
|
+
/** Reserved built-in source; task runs snapshot it just like a saved layout. */
|
|
3
|
+
export declare const EMPTY_TASK_LAYOUT = "builtin-empty";
|
|
4
|
+
export declare const EMPTY_TASK_ROOM = "mission";
|
|
5
|
+
export declare function emptyTaskLayout(): RoomLayoutDefinition;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Reserved built-in source; task runs snapshot it just like a saved layout. */
|
|
2
|
+
export const EMPTY_TASK_LAYOUT = 'builtin-empty';
|
|
3
|
+
export const EMPTY_TASK_ROOM = 'mission';
|
|
4
|
+
export function emptyTaskLayout() {
|
|
5
|
+
return { version: 1, participants: {}, rooms: {
|
|
6
|
+
[EMPTY_TASK_ROOM]: { goal: 'Work on the mission', members: [] },
|
|
7
|
+
} };
|
|
8
|
+
}
|
|
@@ -24,8 +24,8 @@ export function validateRoomLayout(raw, label = 'room layout') {
|
|
|
24
24
|
text(doc.description, `${label}.description`);
|
|
25
25
|
for (const section of ['participants', 'rooms']) {
|
|
26
26
|
const value = doc[section];
|
|
27
|
-
if (!value || typeof value !== 'object' || Array.isArray(value) || !Object.keys(value).length)
|
|
28
|
-
throw Error(`${label}.${section}: non-empty mapping required`);
|
|
27
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || (section === 'rooms' && !Object.keys(value).length))
|
|
28
|
+
throw Error(`${label}.${section}: ${section === 'rooms' ? 'non-empty ' : ''}mapping required`);
|
|
29
29
|
for (const key of Object.keys(value))
|
|
30
30
|
if (!validLayoutKey(key))
|
|
31
31
|
throw Error(`${label}: invalid key ${key}`);
|
|
@@ -12,7 +12,7 @@ export declare class NativeLayoutSupervisor implements LayoutSupervisor {
|
|
|
12
12
|
inspect(agent: string, temporary: boolean): Promise<LayoutInstance>;
|
|
13
13
|
private call;
|
|
14
14
|
verify(i: LayoutInstance): Promise<void>;
|
|
15
|
-
spawn(key: string, template: string): Promise<LayoutInstance>;
|
|
15
|
+
spawn(key: string, template: string, snapshot?: AgentTemplateDefinition): Promise<LayoutInstance>;
|
|
16
16
|
join(i: LayoutInstance, invite: string, roomCid: string, membership?: {
|
|
17
17
|
roomId: string;
|
|
18
18
|
role: string;
|
|
@@ -14,6 +14,7 @@ import { callRemoteLayoutBinding } from './layout-binding-client.js';
|
|
|
14
14
|
import { readClientProfile } from '../client-profile.js';
|
|
15
15
|
import { tempSupervisorLiveness } from '../temp-lifecycle.js';
|
|
16
16
|
import { layoutOwnedMemberName, retireOwnedLayoutMember } from './layout-member-retirement.js';
|
|
17
|
+
import { EMPTY_TASK_LAYOUT, emptyTaskLayout } from './empty-layout.js';
|
|
17
18
|
export class NativeLayoutSupervisor {
|
|
18
19
|
configPath;
|
|
19
20
|
runId;
|
|
@@ -47,8 +48,8 @@ export class NativeLayoutSupervisor {
|
|
|
47
48
|
return this.request(i.agent, i.temporary, { ...request, instance: i });
|
|
48
49
|
}
|
|
49
50
|
async verify(i) { await this.call(i, { action: 'verify' }); }
|
|
50
|
-
async spawn(key, template) {
|
|
51
|
-
const definition = this.templates[template];
|
|
51
|
+
async spawn(key, template, snapshot) {
|
|
52
|
+
const definition = snapshot ?? this.templates[template];
|
|
52
53
|
if (!definition)
|
|
53
54
|
throw Error(`agent template not found in run snapshot: ${template}`);
|
|
54
55
|
const name = layoutOwnedMemberName(this.runId, key);
|
|
@@ -145,6 +146,8 @@ export class RoomLayoutService {
|
|
|
145
146
|
}
|
|
146
147
|
/** Reads only the named file, so an unrelated broken layout cannot block this one. */
|
|
147
148
|
definition(name) {
|
|
149
|
+
if (name === EMPTY_TASK_LAYOUT)
|
|
150
|
+
return emptyTaskLayout();
|
|
148
151
|
if (!validLayoutKey(name))
|
|
149
152
|
throw Error(`room layout not found: ${name}`);
|
|
150
153
|
const root = join(splitRootFor(this.configPath ?? defaultConfigPath()), 'room_layouts');
|
|
@@ -53,7 +53,7 @@ export interface LayoutSupervisor {
|
|
|
53
53
|
id: string;
|
|
54
54
|
/** Must verify current launch, CID, live session and standalone ownership. */
|
|
55
55
|
verify(instance: LayoutInstance): Promise<void>;
|
|
56
|
-
spawn(key: string, template: string): Promise<LayoutInstance>;
|
|
56
|
+
spawn(key: string, template: string, definition?: import('../config.js').AgentTemplateDefinition): Promise<LayoutInstance>;
|
|
57
57
|
join(instance: LayoutInstance, invite: string, roomCid: string, membership?: {
|
|
58
58
|
roomId: string;
|
|
59
59
|
role: string;
|
|
@@ -86,6 +86,14 @@ export interface RoomLayoutState {
|
|
|
86
86
|
controller: string;
|
|
87
87
|
participants: Record<string, ParticipantState>;
|
|
88
88
|
rooms: Record<string, RoomState>;
|
|
89
|
+
/** Runtime additions leave the pinned source definition unchanged. */
|
|
90
|
+
additional_members?: Record<string, {
|
|
91
|
+
room: string;
|
|
92
|
+
role: string;
|
|
93
|
+
template: string;
|
|
94
|
+
request_id: string;
|
|
95
|
+
hash: string;
|
|
96
|
+
}>;
|
|
89
97
|
order: string[];
|
|
90
98
|
closed: boolean;
|
|
91
99
|
closing?: boolean;
|
|
@@ -109,6 +117,15 @@ export declare class RoomLayout {
|
|
|
109
117
|
private mutation;
|
|
110
118
|
private activateRoom;
|
|
111
119
|
activate(actor: string, key: string): Promise<void>;
|
|
120
|
+
/** Append an explicitly requested agent to an active owner-only room, with durable retry identity. */
|
|
121
|
+
addMember(actor: string, input: {
|
|
122
|
+
key: string;
|
|
123
|
+
room: string;
|
|
124
|
+
role: string;
|
|
125
|
+
requestId: string;
|
|
126
|
+
hash: string;
|
|
127
|
+
definition: import('../config.js').AgentTemplateDefinition;
|
|
128
|
+
}): Promise<void>;
|
|
112
129
|
private assertRoomActive;
|
|
113
130
|
private archiveRoom;
|
|
114
131
|
closeRoom(actor: string, key: string): Promise<void>;
|
|
@@ -14,17 +14,20 @@ function need(ok, message) { if (!ok)
|
|
|
14
14
|
throw Error(message); }
|
|
15
15
|
/** Room/participant keys cannot contain ':', so scoped instance keys cannot collide. */
|
|
16
16
|
function instanceKey(definition, participant, room) {
|
|
17
|
-
return definition.participants[participant]
|
|
17
|
+
return definition.participants[participant]?.instance_scope === 'room' ? `${room}:${participant}` : participant;
|
|
18
18
|
}
|
|
19
19
|
export function validateLayout(def) {
|
|
20
|
-
need(Object.keys(def.rooms).length
|
|
20
|
+
need(Object.keys(def.rooms).length, 'rooms required');
|
|
21
|
+
const empty = Object.keys(def.participants).length === 0;
|
|
22
|
+
if (empty)
|
|
23
|
+
need(Object.keys(def.rooms).length === 1, 'an empty layout requires exactly one room');
|
|
21
24
|
for (const [key, participant] of Object.entries(def.participants)) {
|
|
22
25
|
need(participant.instance_scope === undefined || ['layout', 'room'].includes(participant.instance_scope), `invalid instance_scope for ${key}`);
|
|
23
26
|
if (participant.instance_scope === 'room')
|
|
24
27
|
need(typeof participant.agent_template === 'string' && participant.agent_template.trim(), `room-scoped participant ${key} requires agent_template`);
|
|
25
28
|
}
|
|
26
29
|
for (const [key, room] of Object.entries(def.rooms)) {
|
|
27
|
-
need(key.length && room.goal.trim() && room.members.length, 'room goal and members required');
|
|
30
|
+
need(key.length && room.goal.trim() && (empty || room.members.length), 'room goal and members required');
|
|
28
31
|
need(new Set(room.members).size === room.members.length, 'duplicate participant in room');
|
|
29
32
|
for (const member of room.members)
|
|
30
33
|
need(own(def.participants, member), `unknown participant ${member}`);
|
|
@@ -87,6 +90,8 @@ export class RoomLayout {
|
|
|
87
90
|
});
|
|
88
91
|
}
|
|
89
92
|
checkOwnerRoles(definition) {
|
|
93
|
+
if (!Object.keys(definition.participants).length)
|
|
94
|
+
need(this.owner, 'an empty layout requires an attached room owner');
|
|
90
95
|
if (!this.owner)
|
|
91
96
|
return;
|
|
92
97
|
for (const room of Object.values(definition.rooms))
|
|
@@ -113,6 +118,9 @@ export class RoomLayout {
|
|
|
113
118
|
this.save(s);
|
|
114
119
|
}
|
|
115
120
|
async activateRoom(s, key, spec) {
|
|
121
|
+
const additions = Object.entries(s.additional_members ?? {}).filter(([, member]) => member.room === key);
|
|
122
|
+
spec = { ...spec, members: [...spec.members, ...additions.map(([name]) => name)],
|
|
123
|
+
roles: { ...spec.roles, ...Object.fromEntries(additions.map(([name, member]) => [name, member.role])) } };
|
|
116
124
|
if (s.owner)
|
|
117
125
|
need(this.owner && canonicalJson(s.owner) === canonicalJson(ownerReference(this.owner)), 'room owner configuration changed');
|
|
118
126
|
this.checkOwnerRoles(s.definition);
|
|
@@ -129,15 +137,18 @@ export class RoomLayout {
|
|
|
129
137
|
return;
|
|
130
138
|
}
|
|
131
139
|
const room = s.rooms[key] ??= { spec, state: 'provisioning', ready: [] };
|
|
140
|
+
room.spec = spec;
|
|
132
141
|
this.save(s);
|
|
133
142
|
for (const member of spec.members) {
|
|
134
143
|
const resolvedKey = instanceKey(s.definition, member, key), p = s.participants[resolvedKey];
|
|
135
144
|
if (!p.instance) {
|
|
136
|
-
const template = s.definition.participants[member]?.agent_template;
|
|
145
|
+
const template = s.definition.participants[member]?.agent_template ?? s.additional_members?.[member]?.template;
|
|
137
146
|
need(template, `no factory for ${member}`);
|
|
138
147
|
if (this.supervisor.taskSupervised)
|
|
139
148
|
p.task_supervised = true;
|
|
140
|
-
await this.mutation(s, `spawn:${resolvedKey}`, () =>
|
|
149
|
+
await this.mutation(s, `spawn:${resolvedKey}`, () => s.additional_members?.[member]
|
|
150
|
+
? this.supervisor.spawn(resolvedKey, template, s.agent_templates?.[template])
|
|
151
|
+
: this.supervisor.spawn(resolvedKey, template), i => { p.instance = i; });
|
|
141
152
|
await this.verify(p.instance);
|
|
142
153
|
}
|
|
143
154
|
}
|
|
@@ -147,7 +158,8 @@ export class RoomLayout {
|
|
|
147
158
|
room_name: `${key.slice(0, 24)}-${createHash('sha256').update(this.file + '\0' + key).digest('hex').slice(0, 24)}`,
|
|
148
159
|
goal: spec.goal, briefing: JSON.stringify({ goal: spec.goal, contract: spec.contract }),
|
|
149
160
|
quiet_membership: spec.quiet_membership ?? true, anonymous: spec.anonymous ?? false,
|
|
150
|
-
activation_requirements:
|
|
161
|
+
activation_requirements: !spec.members.length && s.owner ? [{ role: s.owner.role, count: 1 }]
|
|
162
|
+
: [...new Set(spec.members.map(roleFor))].map(role => ({ role, count: spec.members.filter(m => roleFor(m) === role).length })),
|
|
151
163
|
}), native => { room.native = native; s.order.push(key); });
|
|
152
164
|
if (s.owner && !room.owner_ready)
|
|
153
165
|
await this.mutation(s, `owner:${key}`, async () => {
|
|
@@ -188,6 +200,28 @@ export class RoomLayout {
|
|
|
188
200
|
await this.activateRoom(s, key, s.definition.rooms[key]);
|
|
189
201
|
});
|
|
190
202
|
}
|
|
203
|
+
/** Append an explicitly requested agent to an active owner-only room, with durable retry identity. */
|
|
204
|
+
addMember(actor, input) {
|
|
205
|
+
return this.operation(actor, async (s) => {
|
|
206
|
+
need(!Object.keys(s.definition.participants).length, 'manual layout additions require an empty source layout');
|
|
207
|
+
need(/^[A-Za-z][A-Za-z0-9_-]{0,31}$/.test(input.key) && !['constructor', 'prototype', '__proto__'].includes(input.key), 'invalid member key');
|
|
208
|
+
need(s.owner && input.role !== s.owner.role, 'participant cannot use the owner role');
|
|
209
|
+
const existing = s.additional_members?.[input.key];
|
|
210
|
+
if (existing)
|
|
211
|
+
need(existing.request_id === input.requestId && existing.hash === input.hash, 'this member slot already exists');
|
|
212
|
+
else {
|
|
213
|
+
need(s.rooms[input.room]?.state === 'active', 'adding an agent requires an active layout room');
|
|
214
|
+
await this.assertRoomActive(s, input.room);
|
|
215
|
+
const template = `added-${input.key}`;
|
|
216
|
+
(s.additional_members ??= {})[input.key] = { room: input.room, role: input.role, template, request_id: input.requestId, hash: input.hash };
|
|
217
|
+
(s.agent_templates ??= {})[template] = structuredClone(input.definition);
|
|
218
|
+
s.participants[input.key] = { owned: true };
|
|
219
|
+
s.rooms[input.room].state = 'provisioning';
|
|
220
|
+
this.save(s);
|
|
221
|
+
}
|
|
222
|
+
await this.activateRoom(s, input.room, s.definition.rooms[input.room]);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
191
225
|
async assertRoomActive(s, key) {
|
|
192
226
|
const room = s.rooms[key], native = await this.cowork.getRoom(room.native.room_id);
|
|
193
227
|
need(native?.identity_cid === room.native.identity_cid && native.state === 'active', 'native room missing, changed or inactive');
|
package/dist/web/server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { FleetResourceViews, ResourceBindingError, canonicalResourceDescriptor } from './resource-views.js';
|
|
2
2
|
import { publishFleetResourceCommit } from '../application/resource-observer.js';
|
|
3
3
|
import { stateRoot, defaultConfigPath } from '../paths.js';
|
|
4
|
+
import { EMPTY_TASK_LAYOUT } from '../rooms-tasks/empty-layout.js';
|
|
4
5
|
import { conversationPreview } from '../application/conversation-history.js';
|
|
5
6
|
import { registerConversationStream } from './conversation-stream.js';
|
|
6
7
|
import { conversationHasAttachment } from '../application/conversation-history.js';
|
|
@@ -561,9 +562,9 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
561
562
|
const body = request.body;
|
|
562
563
|
if (typeof body?.title !== 'string' || !body.title)
|
|
563
564
|
throw new FleetError('invalid_request', 'title is required');
|
|
564
|
-
//
|
|
565
|
-
if (typeof body.layout !== 'string' || !body.layout)
|
|
566
|
-
throw new FleetError('invalid_request', 'layout
|
|
565
|
+
// Omission selects a real built-in layout, never a legacy no-room task.
|
|
566
|
+
if (body.layout !== undefined && (typeof body.layout !== 'string' || !body.layout))
|
|
567
|
+
throw new FleetError('invalid_request', 'layout must be a non-empty room layout name, or omitted for the empty layout');
|
|
567
568
|
if (body.template !== undefined || body.noRoom !== undefined)
|
|
568
569
|
throw new FleetError('invalid_request', 'tasks use room layouts; template and noRoom are not accepted');
|
|
569
570
|
const task = await taskApi(() => requireTaskRooms().createTask({
|
|
@@ -571,7 +572,8 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
571
572
|
brief: typeof body.brief === 'string' ? body.brief : undefined,
|
|
572
573
|
backlog: body.backlog === true,
|
|
573
574
|
list: typeof body.list === 'string' ? body.list : undefined,
|
|
574
|
-
layout: body.layout,
|
|
575
|
+
layout: body.layout === undefined ? EMPTY_TASK_LAYOUT : body.layout,
|
|
576
|
+
deferLayoutStart: true,
|
|
575
577
|
idempotencyKey: typeof request.headers['idempotency-key'] === 'string'
|
|
576
578
|
? request.headers['idempotency-key'] : undefined,
|
|
577
579
|
origin: { type: 'web' },
|
|
@@ -748,7 +750,7 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
748
750
|
// A task's layout can be chosen or changed but never removed, so this check cannot go stale.
|
|
749
751
|
if (!api.getTask(taskId).task.layout)
|
|
750
752
|
throw new FleetError('conflict', 'choose a room layout for this task before starting it');
|
|
751
|
-
const task = await api.startTask(input);
|
|
753
|
+
const task = await api.startTask({ ...input, deferLayoutStart: true });
|
|
752
754
|
const provisioning = await api.launchTaskProvisioning(taskId);
|
|
753
755
|
if (provisioning.kind !== 'ready')
|
|
754
756
|
reply.code(202);
|
package/docs/room-layouts.md
CHANGED
|
@@ -3,6 +3,40 @@
|
|
|
3
3
|
Room Layout describes room goals, participants and their shared or independent
|
|
4
4
|
sessions. Rooms and agents have independent lifecycles.
|
|
5
5
|
|
|
6
|
+
## Empty mission layout
|
|
7
|
+
|
|
8
|
+
`POST /api/v1/tasks` may omit `layout`. Fleet then assigns the reserved built-in
|
|
9
|
+
`builtin-empty` layout: one `mission` room and no agent participants. It is a real
|
|
10
|
+
task layout with the usual pinned definition, private run snapshot, room ownership,
|
|
11
|
+
and finish/cancel/delete cleanup. The built-in source cannot be edited or deleted;
|
|
12
|
+
explicit saved-layout names select their configured rooms and participants.
|
|
13
|
+
|
|
14
|
+
Starting this default mission creates its native room and admits the configured
|
|
15
|
+
Owner CID before reporting Active. Starting a saved layout likewise initializes all
|
|
16
|
+
its rooms and required agents automatically, in definition order. Shared
|
|
17
|
+
participants are reused across their rooms. The HTTP request accepts startup
|
|
18
|
+
promptly and a detached worker completes it; the task remains Provisioning until
|
|
19
|
+
all rooms are ready. No agents are spawned. Owner attachment must be
|
|
20
|
+
enabled and an Owner invite configured; an empty ownerless room is rejected.
|
|
21
|
+
Saving with `backlog: true` creates no native room or agents until Start. Unknown
|
|
22
|
+
native mutation outcomes retain the existing reconciliation marker and are not
|
|
23
|
+
blindly replayed. Repeated requests with the same idempotency key reuse the task.
|
|
24
|
+
|
|
25
|
+
An active built-in empty mission accepts agents later through
|
|
26
|
+
`POST /api/v1/tasks/:id/members` and the existing durable member-addition receipt
|
|
27
|
+
API. These explicit additions retain their own launch snapshots alongside the
|
|
28
|
+
original empty definition. They use the mission workspace and existing agent
|
|
29
|
+
ownership, admission, assignment and retirement checks. A repeated request ID
|
|
30
|
+
observes the original receipt; an uncertain launch requires inspection, not a
|
|
31
|
+
second automatic launch. The retained room-opening endpoint remains available for recovery of older
|
|
32
|
+
active tasks whose rooms were declared but never initialized. New missions do not
|
|
33
|
+
require a separate room-opening action.
|
|
34
|
+
|
|
35
|
+
Layout YAML may also describe a single Owner room using `participants: {}` and
|
|
36
|
+
`members: []`. Other layouts continue to require valid participant references.
|
|
37
|
+
|
|
38
|
+
## Saved layouts
|
|
39
|
+
|
|
6
40
|
Place a layout at `fleet/room_layouts/<name>.yaml` beside the split configuration
|
|
7
41
|
for `fleet.yaml`. Agent Templates remain in `fleet/agent_templates/`.
|
|
8
42
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ours.network/fleet",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Harness-agnostic fleet of persistent, identity-bound AI agents. Declarative fleet.yaml, managed native/ACP sessions, supervision, and ours.network messaging.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "FSL-1.1-Apache-2.0",
|