@ours.network/fleet 1.2.0-nightly.2 → 1.2.0-nightly.20
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/README.md +152 -29
- package/dist/agent-ours/bridge.d.ts +3 -0
- package/dist/agent-ours/bridge.js +178 -0
- package/dist/agent-ours/controller.d.ts +14 -0
- package/dist/agent-ours/controller.js +60 -0
- package/dist/agent-ours/correspondence.d.ts +10 -0
- package/dist/agent-ours/correspondence.js +1 -0
- package/dist/agent-ours/file-rpc.d.ts +23 -0
- package/dist/agent-ours/file-rpc.js +123 -0
- package/dist/agent-ours/harness.d.ts +14 -0
- package/dist/agent-ours/harness.js +72 -0
- package/dist/agent-ours/mcp-endpoint.d.ts +15 -0
- package/dist/agent-ours/mcp-endpoint.js +111 -0
- package/dist/agent-ours/runtime.d.ts +59 -0
- package/dist/agent-ours/runtime.js +283 -0
- package/dist/agent-ours/service.d.ts +23 -0
- package/dist/agent-ours/service.js +372 -0
- package/dist/agent-ours/state.d.ts +45 -0
- package/dist/agent-ours/state.js +95 -0
- package/dist/agent-ours/wire.d.ts +14 -0
- package/dist/agent-ours/wire.js +55 -0
- package/dist/agent-recovery-gate.js +1 -1
- package/dist/application/conversation-history.d.ts +6 -0
- package/dist/application/conversation-history.js +65 -0
- package/dist/application/fleet-query-service.d.ts +1 -1
- package/dist/application/fleet-query-service.js +2 -2
- package/dist/application/legacy-supervisor-identity.d.ts +7 -0
- package/dist/application/legacy-supervisor-identity.js +43 -0
- package/dist/application/model-catalog.d.ts +1 -1
- package/dist/application/model-catalog.js +5 -2
- package/dist/application/preset-provenance.d.ts +19 -0
- package/dist/application/preset-provenance.js +59 -0
- package/dist/application/room-layout-definitions.d.ts +45 -0
- package/dist/application/room-layout-definitions.js +130 -0
- package/dist/application/session-control.d.ts +8 -0
- package/dist/application/session-control.js +21 -3
- package/dist/application/supervisor-ours-tools.d.ts +159 -0
- package/dist/application/supervisor-ours-tools.js +113 -0
- package/dist/application/task-layouts.d.ts +89 -0
- package/dist/application/task-layouts.js +248 -0
- package/dist/application/task-room-service.d.ts +99 -3
- package/dist/application/task-room-service.js +296 -31
- package/dist/atomic-file.d.ts +5 -0
- package/dist/atomic-file.js +62 -1
- package/dist/briefing.js +10 -59
- package/dist/build-info.json +5 -4
- package/dist/capabilities.d.ts +2 -1
- package/dist/capabilities.js +2 -0
- package/dist/cli.js +65 -2
- package/dist/client-profile.d.ts +5 -16
- package/dist/client-profile.js +4 -112
- package/dist/config.d.ts +4 -0
- package/dist/config.js +7 -0
- package/dist/creation.d.ts +1 -1
- package/dist/creation.js +5 -16
- package/dist/daemon-recovery.d.ts +1 -8
- package/dist/daemon-recovery.js +8 -113
- package/dist/docs.d.ts +3 -3
- package/dist/docs.js +43 -23
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +46 -59
- package/dist/erased-resources.d.ts +9 -0
- package/dist/erased-resources.js +87 -0
- package/dist/fleet-command-audit.d.ts +1 -0
- package/dist/fleet-command-audit.js +12 -7
- package/dist/harness/agent-session.d.ts +2 -0
- package/dist/harness/claude-code-session.js +4 -2
- package/dist/harness/codex-app-server-proxy.d.ts +4 -1
- package/dist/harness/codex-app-server-proxy.js +41 -5
- package/dist/harness/codex-session.js +9 -2
- package/dist/harness/codex.d.ts +3 -2
- package/dist/harness/codex.js +4 -3
- package/dist/harness/hermes-session.js +7 -1
- package/dist/init-wizard.js +19 -9
- package/dist/monitor.d.ts +1 -34
- package/dist/monitor.js +24 -151
- package/dist/owner-channel/channel.d.ts +3 -0
- package/dist/owner-channel/channel.js +34 -6
- package/dist/owner-channel/commands.d.ts +4 -0
- package/dist/owner-channel/commands.js +8 -0
- package/dist/owner-channel/lifecycle-outbox.js +4 -3
- package/dist/owner-channel/ours-client.js +2 -17
- package/dist/preset-migration.js +15 -0
- package/dist/rooms-tasks/add-member.d.ts +48 -0
- package/dist/rooms-tasks/add-member.js +83 -0
- package/dist/rooms-tasks/archived-absence.d.ts +8 -0
- package/dist/rooms-tasks/archived-absence.js +48 -0
- package/dist/rooms-tasks/cli.js +34 -5
- package/dist/rooms-tasks/close.d.ts +5 -2
- package/dist/rooms-tasks/close.js +208 -28
- package/dist/rooms-tasks/config.js +3 -1
- package/dist/rooms-tasks/cowork-adapter.d.ts +6 -4
- package/dist/rooms-tasks/cowork-adapter.js +11 -117
- package/dist/rooms-tasks/cowork-http.d.ts +3 -0
- package/dist/rooms-tasks/cowork-http.js +70 -0
- package/dist/rooms-tasks/deletion.d.ts +1 -1
- package/dist/rooms-tasks/deletion.js +74 -5
- package/dist/rooms-tasks/erasure.d.ts +6 -0
- package/dist/rooms-tasks/erasure.js +169 -0
- package/dist/rooms-tasks/external-worker.d.ts +1 -1
- package/dist/rooms-tasks/external-worker.js +3 -4
- package/dist/rooms-tasks/layout-cli.d.ts +2 -0
- package/dist/rooms-tasks/layout-cli.js +81 -0
- package/dist/rooms-tasks/layout-config.d.ts +11 -0
- package/dist/rooms-tasks/layout-config.js +89 -0
- package/dist/rooms-tasks/layout-control.d.ts +21 -0
- package/dist/rooms-tasks/layout-control.js +52 -0
- package/dist/rooms-tasks/layout-service.d.ts +39 -0
- package/dist/rooms-tasks/layout-service.js +173 -0
- package/dist/rooms-tasks/layout.d.ts +102 -0
- package/dist/rooms-tasks/layout.js +227 -0
- package/dist/rooms-tasks/live-readiness.d.ts +18 -0
- package/dist/rooms-tasks/live-readiness.js +85 -0
- package/dist/rooms-tasks/member-startup.d.ts +1 -0
- package/dist/rooms-tasks/member-startup.js +2 -0
- package/dist/rooms-tasks/provision.d.ts +14 -0
- package/dist/rooms-tasks/provision.js +182 -48
- package/dist/rooms-tasks/room-state.d.ts +4 -2
- package/dist/rooms-tasks/room-state.js +53 -8
- package/dist/rooms-tasks/task-state.d.ts +42 -7
- package/dist/rooms-tasks/task-state.js +153 -23
- package/dist/rooms-tasks/types.d.ts +28 -0
- package/dist/rooms-tasks/workspace-artifacts.d.ts +6 -0
- package/dist/rooms-tasks/workspace-artifacts.js +93 -0
- package/dist/rooms-tasks/workspace.d.ts +26 -0
- package/dist/rooms-tasks/workspace.js +195 -0
- package/dist/runner.d.ts +23 -3
- package/dist/runner.js +574 -494
- package/dist/session/acp.d.ts +3 -0
- package/dist/session/acp.js +7 -0
- package/dist/session/arbiter.d.ts +3 -0
- package/dist/session/arbiter.js +20 -2
- package/dist/session/codex-app-server-transport.js +1 -1
- package/dist/session/codex-app-server.d.ts +3 -0
- package/dist/session/codex-app-server.js +25 -1
- package/dist/session/control.d.ts +10 -1
- package/dist/session/control.js +36 -2
- package/dist/session/conversation-normalizer.d.ts +2 -1
- package/dist/session/conversation-normalizer.js +1 -1
- package/dist/session/conversation-types.d.ts +5 -3
- package/dist/session/task-notice.d.ts +14 -0
- package/dist/session/task-notice.js +13 -0
- package/dist/session/types.d.ts +3 -0
- package/dist/socket-path.d.ts +2 -0
- package/dist/socket-path.js +22 -0
- package/dist/spawn.js +19 -16
- package/dist/startup-readiness.d.ts +13 -0
- package/dist/startup-readiness.js +38 -0
- package/dist/supervisor/launchd.js +2 -1
- package/dist/supervisor/systemd.js +9 -23
- package/dist/temp-idle.d.ts +17 -0
- package/dist/temp-idle.js +42 -0
- package/dist/temp-lifecycle.d.ts +6 -1
- package/dist/temp-lifecycle.js +29 -13
- package/dist/temp-supervisor-recovery.d.ts +3 -0
- package/dist/temp-supervisor-recovery.js +46 -0
- package/dist/watchdog/briefing.js +3 -16
- package/dist/watchdog/run.js +19 -5
- package/dist/web/agent-attachments.d.ts +29 -0
- package/dist/web/agent-attachments.js +162 -0
- package/dist/web/auth.d.ts +4 -1
- package/dist/web/auth.js +23 -2
- package/dist/web/control.js +2 -1
- package/dist/web/fleet-config-service.d.ts +4 -1
- package/dist/web/fleet-config-service.js +46 -15
- package/dist/web/prefix-gateway.d.ts +20 -0
- package/dist/web/prefix-gateway.js +150 -0
- package/dist/web/runtime.d.ts +4 -0
- package/dist/web/runtime.js +19 -14
- package/dist/web/server.d.ts +7 -0
- package/dist/web/server.js +293 -25
- package/docs/room-layouts.md +92 -0
- package/docs/task-workspaces.md +130 -0
- package/examples/room-layouts/fresh.yaml +27 -0
- package/examples/room-layouts/shared.yaml +19 -0
- package/package.json +17 -13
- package/presets/README.md +66 -0
- package/presets/brain-catalog.json +7 -2
- package/presets/fleet/agent_templates/Engineer.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-1-high.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-1-low.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-1-max.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-1-medium.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-1-xhigh.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-high.yaml +1 -1
- package/presets/fleet/brains/claude-fable-5-low.yaml +1 -1
- package/presets/fleet/brains/claude-fable-5-max.yaml +1 -1
- package/presets/fleet/brains/claude-fable-5-medium.yaml +1 -1
- package/presets/fleet/brains/claude-fable-5-xhigh.yaml +1 -1
- package/presets/fleet/brains/claude-haiku-4-5-20251001-default.yaml +4 -0
- package/presets/fleet/brains/claude-opus-5-5-high.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-5-low.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-5-max.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-5-medium.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-high.yaml +1 -1
- package/presets/fleet/brains/claude-opus-5-low.yaml +1 -1
- package/presets/fleet/brains/claude-opus-5-max.yaml +1 -1
- package/presets/fleet/brains/claude-opus-5-medium.yaml +1 -1
- package/presets/fleet/brains/claude-opus-5-xhigh.yaml +1 -1
- package/presets/fleet/brains/claude-sonnet-5-high.yaml +1 -1
- package/presets/fleet/brains/claude-sonnet-5-low.yaml +1 -1
- package/presets/fleet/brains/claude-sonnet-5-max.yaml +1 -1
- package/presets/fleet/brains/claude-sonnet-5-medium.yaml +1 -1
- package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-5-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-5-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-astra-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-astra-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-astra-max.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-astra-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-astra-ultra.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-astra-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-luna-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-luna-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-luna-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-luna-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-luna-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-sol-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-sol-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-sol-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-sol-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-sol-ultra.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-sol-xhigh.yaml +5 -0
- package/presets/fleet/roles/Coordinator.yaml +22 -0
- package/presets/fleet/roles/Engineer.yaml +40 -0
- package/presets/fleet/room_layouts/engineering.yaml +25 -0
- package/presets/fleet/room_layouts/pair.yaml +24 -0
- package/presets/fleet/room_layouts/single.yaml +17 -0
- package/presets/fleet/room_layouts/team.yaml +30 -0
- package/presets/fleet/room_templates/engineering.yaml +19 -0
- package/presets/manifest.json +2 -2
- package/dist/web-app/assets/index-59GF-gsZ.css +0 -1
- package/dist/web-app/assets/index-C0h3ALvs.js +0 -10
- package/dist/web-app/icons/ours-fleet-maskable.svg +0 -4
- package/dist/web-app/icons/ours-fleet.svg +0 -4
- package/dist/web-app/index.html +0 -17
- package/dist/web-app/manifest.webmanifest +0 -15
- package/dist/web-app/offline.html +0 -18
- package/dist/web-app/sw.js +0 -51
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
export interface SupervisorToolRequest {
|
|
2
|
+
tool: string;
|
|
3
|
+
arguments?: Record<string, unknown>;
|
|
4
|
+
}
|
|
5
|
+
/** CLI and REST use the same fixed-identity MCP server as the agent harness. */
|
|
6
|
+
export declare class SupervisorOursTools {
|
|
7
|
+
private withClient;
|
|
8
|
+
list(role: string): Promise<{
|
|
9
|
+
tools: {
|
|
10
|
+
inputSchema: {
|
|
11
|
+
[x: string]: unknown;
|
|
12
|
+
type: "object";
|
|
13
|
+
properties?: Record<string, object> | undefined;
|
|
14
|
+
required?: string[] | undefined;
|
|
15
|
+
};
|
|
16
|
+
name: string;
|
|
17
|
+
description?: string | undefined;
|
|
18
|
+
outputSchema?: {
|
|
19
|
+
[x: string]: unknown;
|
|
20
|
+
type: "object";
|
|
21
|
+
properties?: Record<string, object> | undefined;
|
|
22
|
+
required?: string[] | undefined;
|
|
23
|
+
} | undefined;
|
|
24
|
+
annotations?: {
|
|
25
|
+
title?: string | undefined;
|
|
26
|
+
readOnlyHint?: boolean | undefined;
|
|
27
|
+
destructiveHint?: boolean | undefined;
|
|
28
|
+
idempotentHint?: boolean | undefined;
|
|
29
|
+
openWorldHint?: boolean | undefined;
|
|
30
|
+
} | undefined;
|
|
31
|
+
execution?: {
|
|
32
|
+
taskSupport?: "optional" | "required" | "forbidden" | undefined;
|
|
33
|
+
} | undefined;
|
|
34
|
+
_meta?: Record<string, unknown> | undefined;
|
|
35
|
+
icons?: {
|
|
36
|
+
src: string;
|
|
37
|
+
mimeType?: string | undefined;
|
|
38
|
+
sizes?: string[] | undefined;
|
|
39
|
+
theme?: "light" | "dark" | undefined;
|
|
40
|
+
}[] | undefined;
|
|
41
|
+
title?: string | undefined;
|
|
42
|
+
}[];
|
|
43
|
+
_meta?: {
|
|
44
|
+
[x: string]: unknown;
|
|
45
|
+
progressToken?: string | number | undefined;
|
|
46
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
47
|
+
taskId: string;
|
|
48
|
+
} | undefined;
|
|
49
|
+
} | undefined;
|
|
50
|
+
nextCursor?: string | undefined;
|
|
51
|
+
agent: string;
|
|
52
|
+
identity: {
|
|
53
|
+
name: string;
|
|
54
|
+
cid: string;
|
|
55
|
+
generation: number;
|
|
56
|
+
};
|
|
57
|
+
}>;
|
|
58
|
+
call(role: string, request: SupervisorToolRequest): Promise<{
|
|
59
|
+
agent: string;
|
|
60
|
+
identity: {
|
|
61
|
+
name: string;
|
|
62
|
+
cid: string;
|
|
63
|
+
generation: number;
|
|
64
|
+
};
|
|
65
|
+
result: {
|
|
66
|
+
[x: string]: unknown;
|
|
67
|
+
content: ({
|
|
68
|
+
type: "text";
|
|
69
|
+
text: string;
|
|
70
|
+
annotations?: {
|
|
71
|
+
audience?: ("user" | "assistant")[] | undefined;
|
|
72
|
+
priority?: number | undefined;
|
|
73
|
+
lastModified?: string | undefined;
|
|
74
|
+
} | undefined;
|
|
75
|
+
_meta?: Record<string, unknown> | undefined;
|
|
76
|
+
} | {
|
|
77
|
+
type: "image";
|
|
78
|
+
data: string;
|
|
79
|
+
mimeType: string;
|
|
80
|
+
annotations?: {
|
|
81
|
+
audience?: ("user" | "assistant")[] | undefined;
|
|
82
|
+
priority?: number | undefined;
|
|
83
|
+
lastModified?: string | undefined;
|
|
84
|
+
} | undefined;
|
|
85
|
+
_meta?: Record<string, unknown> | undefined;
|
|
86
|
+
} | {
|
|
87
|
+
type: "audio";
|
|
88
|
+
data: string;
|
|
89
|
+
mimeType: string;
|
|
90
|
+
annotations?: {
|
|
91
|
+
audience?: ("user" | "assistant")[] | undefined;
|
|
92
|
+
priority?: number | undefined;
|
|
93
|
+
lastModified?: string | undefined;
|
|
94
|
+
} | undefined;
|
|
95
|
+
_meta?: Record<string, unknown> | undefined;
|
|
96
|
+
} | {
|
|
97
|
+
type: "resource";
|
|
98
|
+
resource: {
|
|
99
|
+
uri: string;
|
|
100
|
+
text: string;
|
|
101
|
+
mimeType?: string | undefined;
|
|
102
|
+
_meta?: Record<string, unknown> | undefined;
|
|
103
|
+
} | {
|
|
104
|
+
uri: string;
|
|
105
|
+
blob: string;
|
|
106
|
+
mimeType?: string | undefined;
|
|
107
|
+
_meta?: Record<string, unknown> | undefined;
|
|
108
|
+
};
|
|
109
|
+
annotations?: {
|
|
110
|
+
audience?: ("user" | "assistant")[] | undefined;
|
|
111
|
+
priority?: number | undefined;
|
|
112
|
+
lastModified?: string | undefined;
|
|
113
|
+
} | undefined;
|
|
114
|
+
_meta?: Record<string, unknown> | undefined;
|
|
115
|
+
} | {
|
|
116
|
+
uri: string;
|
|
117
|
+
name: string;
|
|
118
|
+
type: "resource_link";
|
|
119
|
+
description?: string | undefined;
|
|
120
|
+
mimeType?: string | undefined;
|
|
121
|
+
size?: number | undefined;
|
|
122
|
+
annotations?: {
|
|
123
|
+
audience?: ("user" | "assistant")[] | undefined;
|
|
124
|
+
priority?: number | undefined;
|
|
125
|
+
lastModified?: string | undefined;
|
|
126
|
+
} | undefined;
|
|
127
|
+
_meta?: {
|
|
128
|
+
[x: string]: unknown;
|
|
129
|
+
} | undefined;
|
|
130
|
+
icons?: {
|
|
131
|
+
src: string;
|
|
132
|
+
mimeType?: string | undefined;
|
|
133
|
+
sizes?: string[] | undefined;
|
|
134
|
+
theme?: "light" | "dark" | undefined;
|
|
135
|
+
}[] | undefined;
|
|
136
|
+
title?: string | undefined;
|
|
137
|
+
})[];
|
|
138
|
+
_meta?: {
|
|
139
|
+
[x: string]: unknown;
|
|
140
|
+
progressToken?: string | number | undefined;
|
|
141
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
142
|
+
taskId: string;
|
|
143
|
+
} | undefined;
|
|
144
|
+
} | undefined;
|
|
145
|
+
structuredContent?: Record<string, unknown> | undefined;
|
|
146
|
+
isError?: boolean | undefined;
|
|
147
|
+
} | {
|
|
148
|
+
[x: string]: unknown;
|
|
149
|
+
toolResult: unknown;
|
|
150
|
+
_meta?: {
|
|
151
|
+
[x: string]: unknown;
|
|
152
|
+
progressToken?: string | number | undefined;
|
|
153
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
154
|
+
taskId: string;
|
|
155
|
+
} | undefined;
|
|
156
|
+
} | undefined;
|
|
157
|
+
};
|
|
158
|
+
}>;
|
|
159
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
6
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
7
|
+
import { agentDir } from '../paths.js';
|
|
8
|
+
import { ROLE_NAME_RE } from '../config.js';
|
|
9
|
+
import { legacySupervisorIdentity } from './legacy-supervisor-identity.js';
|
|
10
|
+
import { FleetError } from './errors.js';
|
|
11
|
+
/** CLI and REST use the same fixed-identity MCP server as the agent harness. */
|
|
12
|
+
export class SupervisorOursTools {
|
|
13
|
+
async withClient(role, work) {
|
|
14
|
+
if (!ROLE_NAME_RE.test(role))
|
|
15
|
+
throw new FleetError('invalid_request', 'invalid agent name');
|
|
16
|
+
const findCandidates = () => [false, true].map(temporary => ({ temporary, dir: agentDir(role, temporary) }))
|
|
17
|
+
.map(row => ({ ...row, path: join(row.dir, '.ours-bridge', 'descriptor.json') }))
|
|
18
|
+
.filter(row => existsSync(row.path));
|
|
19
|
+
const candidates = findCandidates();
|
|
20
|
+
if (candidates.length !== 1)
|
|
21
|
+
throw new FleetError('capability_unavailable', 'agent supervisor endpoint is missing or ambiguous');
|
|
22
|
+
const selected = candidates[0];
|
|
23
|
+
let descriptor;
|
|
24
|
+
let identityName;
|
|
25
|
+
try {
|
|
26
|
+
descriptor = JSON.parse(readFileSync(selected.path, 'utf8'));
|
|
27
|
+
identityName = readFileSync(join(selected.dir, '.identity'), 'utf8').trim();
|
|
28
|
+
if (!descriptor || typeof descriptor !== 'object')
|
|
29
|
+
throw new Error();
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
throw new FleetError('capability_unavailable', 'supervisor identity metadata cannot be read');
|
|
33
|
+
}
|
|
34
|
+
const legacy = ['role', 'identity', 'cid'].every(key => descriptor[key] === undefined);
|
|
35
|
+
const identity = legacy
|
|
36
|
+
? legacySupervisorIdentity(role, identityName, selected.temporary, descriptor.generation)
|
|
37
|
+
: { name: identityName, cid: descriptor.cid, generation: descriptor.generation };
|
|
38
|
+
if (legacy && descriptor.socket !== join(selected.dir, '.ours-bridge', `g${descriptor.generation}.sock`))
|
|
39
|
+
throw new FleetError('capability_unavailable', 'legacy supervisor socket does not match the selected agent');
|
|
40
|
+
if ((!legacy && (descriptor.role !== role || descriptor.identity !== identityName))
|
|
41
|
+
|| !/^[a-f0-9]{64}$/i.test(identity.cid ?? '') || !Number.isSafeInteger(descriptor.generation) || descriptor.generation < 1)
|
|
42
|
+
throw new FleetError('capability_unavailable', 'supervisor identity metadata is unavailable or mismatched');
|
|
43
|
+
const verifySelection = () => {
|
|
44
|
+
try {
|
|
45
|
+
const current = findCandidates();
|
|
46
|
+
const freshDescriptor = JSON.parse(readFileSync(selected.path, 'utf8'));
|
|
47
|
+
if (current.length !== 1 || current[0].path !== selected.path
|
|
48
|
+
|| readFileSync(join(selected.dir, '.identity'), 'utf8').trim() !== identityName
|
|
49
|
+
|| ['role', 'identity', 'cid', 'socket', 'capability', 'generation']
|
|
50
|
+
.some(key => freshDescriptor[key] !== descriptor[key]))
|
|
51
|
+
throw new Error();
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
throw new FleetError('capability_unavailable', 'selected supervisor assignment changed');
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const client = new Client({ name: 'ours-fleet-supervisor-tools', version: '1' });
|
|
58
|
+
const transport = new StdioClientTransport({
|
|
59
|
+
command: process.execPath,
|
|
60
|
+
args: [fileURLToPath(new URL('../agent-ours/bridge.js', import.meta.url))],
|
|
61
|
+
env: { ...Object.fromEntries(Object.entries(process.env).filter((entry) => entry[1] !== undefined)),
|
|
62
|
+
FLEET_OURS_BRIDGE_DESCRIPTOR: selected.path,
|
|
63
|
+
FLEET_OURS_BRIDGE_EXPECTED: JSON.stringify({ role: descriptor.role, identity: descriptor.identity, cid: descriptor.cid, generation: descriptor.generation,
|
|
64
|
+
transportDigest: createHash('sha256').update(JSON.stringify([descriptor.socket, descriptor.capability, descriptor.generation])).digest('hex') }) },
|
|
65
|
+
stderr: 'pipe',
|
|
66
|
+
});
|
|
67
|
+
try {
|
|
68
|
+
await client.connect(transport);
|
|
69
|
+
const verify = async () => {
|
|
70
|
+
if (!legacy) {
|
|
71
|
+
verifySelection();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
// The supervisor owns the binding. The authenticated bridge and pinned local
|
|
75
|
+
// assignment are authoritative; current_identity is display text, not a protocol.
|
|
76
|
+
const fresh = legacySupervisorIdentity(role, identityName, selected.temporary, descriptor.generation);
|
|
77
|
+
if (!('proof' in identity) || fresh.proof !== identity.proof)
|
|
78
|
+
throw new FleetError('capability_unavailable', 'legacy supervisor identity changed');
|
|
79
|
+
verifySelection();
|
|
80
|
+
};
|
|
81
|
+
await verify();
|
|
82
|
+
return await work(client, { name: identity.name, cid: identity.cid, generation: identity.generation }, verify);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
if (error instanceof FleetError)
|
|
86
|
+
throw error;
|
|
87
|
+
// Do not leak arguments, invites, capabilities, or raw transport errors.
|
|
88
|
+
throw new FleetError('control_unavailable', 'supervisor tool request failed; its outcome may be unknown, do not retry a mutation automatically', { retryable: false });
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
await client.close().catch(() => { });
|
|
92
|
+
await transport.close().catch(() => { });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
list(role) {
|
|
96
|
+
return this.withClient(role, async (client, identity) => ({
|
|
97
|
+
agent: role, identity, ...(await client.listTools()),
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
call(role, request) {
|
|
101
|
+
if (!request || typeof request.tool !== 'string'
|
|
102
|
+
|| (request.arguments !== undefined && (!request.arguments || typeof request.arguments !== 'object' || Array.isArray(request.arguments))))
|
|
103
|
+
throw new FleetError('invalid_request', 'tool and object arguments are required');
|
|
104
|
+
return this.withClient(role, async (client, identity, verify) => {
|
|
105
|
+
const { tools } = await client.listTools();
|
|
106
|
+
if (!tools.some(tool => tool.name === request.tool))
|
|
107
|
+
throw new FleetError('forbidden', 'tool is not exposed by the fixed-identity supervisor');
|
|
108
|
+
await verify();
|
|
109
|
+
const result = await client.callTool({ name: request.tool, arguments: request.arguments ?? {} });
|
|
110
|
+
return { agent: role, identity, result };
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { type RoomLayoutDefinition } from '../rooms-tasks/layout-config.js';
|
|
2
|
+
import { RoomLayoutService } from '../rooms-tasks/layout-service.js';
|
|
3
|
+
import type { TaskLayoutLink, TaskRecord } from '../rooms-tasks/types.js';
|
|
4
|
+
export type TaskLayoutOperation = 'open' | 'close-room' | 'close';
|
|
5
|
+
export interface TaskLayoutOperationRecord {
|
|
6
|
+
/** Only the worker holding the current ID may update the record. */
|
|
7
|
+
id: string;
|
|
8
|
+
operation: TaskLayoutOperation;
|
|
9
|
+
room?: string;
|
|
10
|
+
status: 'launching' | 'running' | 'succeeded' | 'failed';
|
|
11
|
+
error?: string;
|
|
12
|
+
updated_at: string;
|
|
13
|
+
}
|
|
14
|
+
export interface TaskLayoutRoomView {
|
|
15
|
+
key: string;
|
|
16
|
+
goal: string;
|
|
17
|
+
members: string[];
|
|
18
|
+
roles?: Record<string, string>;
|
|
19
|
+
state: 'declared' | 'provisioning' | 'active' | 'closed';
|
|
20
|
+
room_id?: string;
|
|
21
|
+
identity_cid?: string;
|
|
22
|
+
ready: string[];
|
|
23
|
+
}
|
|
24
|
+
export interface TaskLayoutView {
|
|
25
|
+
name: string;
|
|
26
|
+
run_id: string;
|
|
27
|
+
created: boolean;
|
|
28
|
+
closed: boolean;
|
|
29
|
+
closing: boolean;
|
|
30
|
+
uncertain?: string;
|
|
31
|
+
rooms: TaskLayoutRoomView[];
|
|
32
|
+
participants: Array<{
|
|
33
|
+
key: string;
|
|
34
|
+
agent_template?: string;
|
|
35
|
+
owned: boolean;
|
|
36
|
+
retired: boolean;
|
|
37
|
+
agent?: string;
|
|
38
|
+
cid?: string;
|
|
39
|
+
}>;
|
|
40
|
+
operation?: TaskLayoutOperationRecord;
|
|
41
|
+
}
|
|
42
|
+
/** Hash of the authorable definition only (never the source file path). */
|
|
43
|
+
export declare function layoutDefinitionHash(definition: RoomLayoutDefinition): string;
|
|
44
|
+
/** Always-current brief for layout participants; rooms may open long after the run snapshot.
|
|
45
|
+
* It lives in Fleet's private layout state, never in the agent-writable task workspace. */
|
|
46
|
+
export declare function taskBriefPath(task: Pick<TaskRecord, 'task_id'>): string;
|
|
47
|
+
export declare function writeTaskBrief(task: Pick<TaskRecord, 'task_id' | 'title' | 'brief'>): void;
|
|
48
|
+
/** Task context reaches every participant through the room contract (briefing text).
|
|
49
|
+
* The contract references the brief file instead of copying the brief, so later edits reach later rooms. */
|
|
50
|
+
export declare function taskScopedDefinition(definition: RoomLayoutDefinition, task: Pick<TaskRecord, 'task_id' | 'title' | 'brief' | 'workspace'>): RoomLayoutDefinition;
|
|
51
|
+
/** Opening rooms requires a started, live task; checked again by the worker before any side effect. */
|
|
52
|
+
export declare function assertLayoutOpenable(task: TaskRecord): void;
|
|
53
|
+
export declare class TaskLayouts {
|
|
54
|
+
private configPath?;
|
|
55
|
+
private deps;
|
|
56
|
+
constructor(configPath?: string | undefined, deps?: {
|
|
57
|
+
service?: RoomLayoutService;
|
|
58
|
+
launch?: (args: string[], operation: string) => Promise<void>;
|
|
59
|
+
binPath?: () => string | undefined;
|
|
60
|
+
});
|
|
61
|
+
private service;
|
|
62
|
+
private operationPath;
|
|
63
|
+
private provenancePath;
|
|
64
|
+
/** Serializes run creation, worker preconditions and deletion's removal of the run. */
|
|
65
|
+
private withTaskLock;
|
|
66
|
+
/** Resolve a layout for task creation: it must exist, parse and reference known Agent Templates. */
|
|
67
|
+
resolve(name: string): {
|
|
68
|
+
name: string;
|
|
69
|
+
definition_hash: string;
|
|
70
|
+
};
|
|
71
|
+
runExists(link: TaskLayoutLink): boolean;
|
|
72
|
+
/** Idempotent: reuse a run whose provenance matches this task, or snapshot one from the pinned source. */
|
|
73
|
+
ensureRun(task: TaskRecord, precondition?: (current: TaskRecord) => void): Promise<void>;
|
|
74
|
+
private verifyProvenance;
|
|
75
|
+
readOperation(runId: string): TaskLayoutOperationRecord | undefined;
|
|
76
|
+
/** Update only the still-current operation; a superseded or forgotten one is never resurrected. */
|
|
77
|
+
private updateOperation;
|
|
78
|
+
private writeOperation;
|
|
79
|
+
/** Sanitized read model: no Agent Template snapshots, owner fingerprints or session internals. */
|
|
80
|
+
view(task: TaskRecord): TaskLayoutView | undefined;
|
|
81
|
+
/** Accept an operation and run it in a detached worker; the outcome is recorded for polling. */
|
|
82
|
+
launch(task: TaskRecord, operation: TaskLayoutOperation, room?: string): Promise<TaskLayoutOperationRecord>;
|
|
83
|
+
/** Worker body. Errors are recorded and rethrown; unknown engine outcomes stay in run state for inspection. */
|
|
84
|
+
/** Without an ID (direct CLI use) the worker claims a new current operation. */
|
|
85
|
+
run(task: TaskRecord, operation: TaskLayoutOperation, room?: string, operationId?: string): Promise<void>;
|
|
86
|
+
private perform;
|
|
87
|
+
/** Deletion: close every layout resource, then remove the run and its operation record. */
|
|
88
|
+
closeAndForget(task: Pick<TaskRecord, 'task_id' | 'layout'>): Promise<void>;
|
|
89
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/** Task ↔ Room Layout bridge.
|
|
2
|
+
* A layout task owns exactly one Room Layout run, `task-<task_id>`, snapshotted
|
|
3
|
+
* from the named source layout with task context added to every room contract.
|
|
4
|
+
* Room opening and closing reuse the #199 engine in detached workers; each
|
|
5
|
+
* operation's outcome is recorded beside the run so HTTP callers can observe it.
|
|
6
|
+
*/
|
|
7
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
8
|
+
import { existsSync, readFileSync, rmSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { canonicalJson } from '../canonical-json.js';
|
|
11
|
+
import { replaceFileAtomically, withFileLock } from '../atomic-file.js';
|
|
12
|
+
import { assertLayoutFile } from '../rooms-tasks/layout-config.js';
|
|
13
|
+
import { getTask as readTask } from '../rooms-tasks/task-state.js';
|
|
14
|
+
import { stateRoot } from '../paths.js';
|
|
15
|
+
import { launchFleetWorker } from '../rooms-tasks/external-worker.js';
|
|
16
|
+
import { layoutSupervisorId } from '../rooms-tasks/layout-control.js';
|
|
17
|
+
import { layoutRunPath, RoomLayoutService } from '../rooms-tasks/layout-service.js';
|
|
18
|
+
import { FleetError } from './errors.js';
|
|
19
|
+
/** Hash of the authorable definition only (never the source file path). */
|
|
20
|
+
export function layoutDefinitionHash(definition) {
|
|
21
|
+
const { version, description, participants, rooms } = definition;
|
|
22
|
+
return createHash('sha256').update(canonicalJson({ version, description: description ?? null, participants, rooms })).digest('hex');
|
|
23
|
+
}
|
|
24
|
+
/** Always-current brief for layout participants; rooms may open long after the run snapshot.
|
|
25
|
+
* It lives in Fleet's private layout state, never in the agent-writable task workspace. */
|
|
26
|
+
export function taskBriefPath(task) {
|
|
27
|
+
return layoutRunPath(`task-${task.task_id}`).replace(/\.json$/, '.brief.md');
|
|
28
|
+
}
|
|
29
|
+
export function writeTaskBrief(task) {
|
|
30
|
+
const path = taskBriefPath(task); // layoutRunPath verified the private directory
|
|
31
|
+
if (existsSync(path))
|
|
32
|
+
assertLayoutFile(path);
|
|
33
|
+
replaceFileAtomically(path, `# ${task.title}\n\nTask ${task.task_id}\n\n${task.brief?.trim() ?? ''}\n`, 0o600);
|
|
34
|
+
}
|
|
35
|
+
/** Task context reaches every participant through the room contract (briefing text).
|
|
36
|
+
* The contract references the brief file instead of copying the brief, so later edits reach later rooms. */
|
|
37
|
+
export function taskScopedDefinition(definition, task) {
|
|
38
|
+
const preamble = [
|
|
39
|
+
`Task ${task.task_id}: ${task.title}`,
|
|
40
|
+
...(task.workspace ? [`Task workspace: ${task.workspace.path}`] : []),
|
|
41
|
+
`Current task brief: ${taskBriefPath(task)} (kept up to date by Fleet; read it before starting work)`,
|
|
42
|
+
].join('\n');
|
|
43
|
+
for (const room of Object.values(definition.rooms))
|
|
44
|
+
room.contract = room.contract ? `${preamble}\n\nRoom contract:\n${room.contract}` : preamble;
|
|
45
|
+
return definition;
|
|
46
|
+
}
|
|
47
|
+
const ROOM_KEY = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
48
|
+
/** Opening rooms requires a started, live task; checked again by the worker before any side effect. */
|
|
49
|
+
export function assertLayoutOpenable(task) {
|
|
50
|
+
if (task.deletion?.status === 'pending')
|
|
51
|
+
throw new FleetError('conflict', 'task is being deleted; its layout rooms cannot be opened');
|
|
52
|
+
if (task.terminal_intent || ['done', 'cancelled', 'failed'].includes(task.state))
|
|
53
|
+
throw new FleetError('conflict', 'task is closing or closed; its layout rooms cannot be opened');
|
|
54
|
+
if (task.state !== 'active' && task.state !== 'review')
|
|
55
|
+
throw new FleetError('conflict', `start the task before opening rooms (state '${task.state}')`);
|
|
56
|
+
}
|
|
57
|
+
export class TaskLayouts {
|
|
58
|
+
configPath;
|
|
59
|
+
deps;
|
|
60
|
+
constructor(configPath, deps = {}) {
|
|
61
|
+
this.configPath = configPath;
|
|
62
|
+
this.deps = deps;
|
|
63
|
+
}
|
|
64
|
+
service() { return this.deps.service ?? new RoomLayoutService(this.configPath); }
|
|
65
|
+
operationPath(runId) { return join(stateRoot(), 'layouts', `${runId}.op.json`); }
|
|
66
|
+
provenancePath(runId) { return layoutRunPath(runId).replace(/\.json$/, '.provenance.json'); }
|
|
67
|
+
/** Serializes run creation, worker preconditions and deletion's removal of the run. */
|
|
68
|
+
withTaskLock(runId, fn) {
|
|
69
|
+
return withFileLock(layoutRunPath(runId).replace(/\.json$/, '.task.lock'), fn);
|
|
70
|
+
}
|
|
71
|
+
/** Resolve a layout for task creation: it must exist, parse and reference known Agent Templates. */
|
|
72
|
+
resolve(name) {
|
|
73
|
+
return { name, definition_hash: layoutDefinitionHash(this.service().definition(name)) };
|
|
74
|
+
}
|
|
75
|
+
runExists(link) { return existsSync(layoutRunPath(link.run_id)); }
|
|
76
|
+
/** Idempotent: reuse a run whose provenance matches this task, or snapshot one from the pinned source. */
|
|
77
|
+
async ensureRun(task, precondition) {
|
|
78
|
+
const link = task.layout;
|
|
79
|
+
if (!link)
|
|
80
|
+
throw Error(`task ${task.task_id} has no layout`);
|
|
81
|
+
await this.withTaskLock(link.run_id, async () => {
|
|
82
|
+
const current = readTask(task.task_id);
|
|
83
|
+
precondition?.(current);
|
|
84
|
+
writeTaskBrief(current);
|
|
85
|
+
if (this.runExists(link)) {
|
|
86
|
+
this.verifyProvenance(current);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const service = this.service();
|
|
90
|
+
const source = service.definition(link.name);
|
|
91
|
+
if (layoutDefinitionHash(source) !== link.definition_hash)
|
|
92
|
+
throw Error(`room layout ${link.name} changed before this task's layout run was created; create a new task to use the edited layout`);
|
|
93
|
+
const scoped = taskScopedDefinition(structuredClone(source), current);
|
|
94
|
+
const provenance = { task_id: current.task_id, source_hash: link.definition_hash, scoped_hash: layoutDefinitionHash(scoped) };
|
|
95
|
+
// Provenance precedes the run: a crash in between is re-derived from the same pinned source.
|
|
96
|
+
replaceFileAtomically(this.provenancePath(link.run_id), JSON.stringify(provenance), 0o600);
|
|
97
|
+
await service.create(link.name, {}, link.run_id, { source: scoped, cwd: current.workspace?.path });
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
verifyProvenance(task) {
|
|
101
|
+
const link = task.layout, path = this.provenancePath(link.run_id);
|
|
102
|
+
let provenance;
|
|
103
|
+
try {
|
|
104
|
+
assertLayoutFile(path);
|
|
105
|
+
provenance = JSON.parse(readFileSync(path, 'utf8'));
|
|
106
|
+
}
|
|
107
|
+
catch { /* missing or untrusted */ }
|
|
108
|
+
const snapshot = this.service().open(link.run_id, true).snapshot();
|
|
109
|
+
if (!provenance || provenance.task_id !== task.task_id || provenance.source_hash !== link.definition_hash
|
|
110
|
+
|| provenance.scoped_hash !== layoutDefinitionHash({ version: 1, ...snapshot.definition }))
|
|
111
|
+
throw Error(`layout run ${link.run_id} does not match this task's pinned layout; inspect it before continuing`);
|
|
112
|
+
}
|
|
113
|
+
readOperation(runId) {
|
|
114
|
+
const path = this.operationPath(runId);
|
|
115
|
+
try {
|
|
116
|
+
return existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : undefined;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/** Update only the still-current operation; a superseded or forgotten one is never resurrected. */
|
|
123
|
+
updateOperation(runId, record) {
|
|
124
|
+
return withFileLock(this.operationPath(runId) + '.lock', () => {
|
|
125
|
+
if (this.readOperation(runId)?.id !== record.id)
|
|
126
|
+
return false;
|
|
127
|
+
this.writeOperation(runId, record);
|
|
128
|
+
return true;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
writeOperation(runId, record) {
|
|
132
|
+
layoutRunPath(runId); // validates the ID and creates the private layouts directory
|
|
133
|
+
replaceFileAtomically(this.operationPath(runId), JSON.stringify({ ...record, updated_at: new Date().toISOString() }), 0o600);
|
|
134
|
+
}
|
|
135
|
+
/** Sanitized read model: no Agent Template snapshots, owner fingerprints or session internals. */
|
|
136
|
+
view(task) {
|
|
137
|
+
const link = task.layout;
|
|
138
|
+
if (!link)
|
|
139
|
+
return undefined;
|
|
140
|
+
const base = { name: link.name, run_id: link.run_id, operation: this.readOperation(link.run_id) };
|
|
141
|
+
if (!this.runExists(link))
|
|
142
|
+
return { ...base, created: false, closed: false, closing: false, rooms: [], participants: [] };
|
|
143
|
+
const state = this.service().open(link.run_id, true).snapshot();
|
|
144
|
+
return {
|
|
145
|
+
...base, created: true, closed: state.closed, closing: Boolean(state.closing),
|
|
146
|
+
...(state.uncertain ? { uncertain: state.uncertain } : {}),
|
|
147
|
+
rooms: Object.entries(state.definition.rooms).map(([key, spec]) => {
|
|
148
|
+
const room = state.rooms[key];
|
|
149
|
+
return {
|
|
150
|
+
key, goal: spec.goal, members: [...spec.members], ...(spec.roles ? { roles: { ...spec.roles } } : {}),
|
|
151
|
+
state: room?.state ?? 'declared', ready: [...(room?.ready ?? [])],
|
|
152
|
+
...(room?.native ? { room_id: room.native.room_id, identity_cid: room.native.identity_cid } : {}),
|
|
153
|
+
};
|
|
154
|
+
}),
|
|
155
|
+
participants: Object.entries(state.participants).map(([key, p]) => ({
|
|
156
|
+
key, agent_template: state.definition.participants[key]?.agent_template, owned: p.owned, retired: Boolean(p.retired),
|
|
157
|
+
...(p.instance?.agent ? { agent: p.instance.agent } : {}), ...(p.instance?.cid ? { cid: p.instance.cid } : {}),
|
|
158
|
+
})),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
/** Accept an operation and run it in a detached worker; the outcome is recorded for polling. */
|
|
162
|
+
async launch(task, operation, room) {
|
|
163
|
+
const link = task.layout;
|
|
164
|
+
if (!link)
|
|
165
|
+
throw Error(`task ${task.task_id} has no layout`);
|
|
166
|
+
if (operation !== 'close' && (!room || !ROOM_KEY.test(room)))
|
|
167
|
+
throw new FleetError('invalid_request', 'a valid room key is required');
|
|
168
|
+
if (room) {
|
|
169
|
+
const definition = this.runExists(link) ? this.service().open(link.run_id, true).snapshot().definition : undefined;
|
|
170
|
+
if (definition && !Object.hasOwn(definition.rooms, room))
|
|
171
|
+
throw new FleetError('resource_not_found', `unknown layout room: ${room}`);
|
|
172
|
+
}
|
|
173
|
+
const record = { id: randomUUID(), operation, ...(room ? { room } : {}), status: 'launching' };
|
|
174
|
+
await withFileLock(this.operationPath(link.run_id) + '.lock', () => {
|
|
175
|
+
const current = this.readOperation(link.run_id);
|
|
176
|
+
// Full close always proceeds: terminal cleanup must not wait behind an open; the engine lock serializes them.
|
|
177
|
+
if (operation !== 'close' && current && ['launching', 'running'].includes(current.status) && Date.now() - Date.parse(current.updated_at) < 10 * 60_000)
|
|
178
|
+
throw new FleetError('conflict', `layout operation already in progress: ${current.operation}${current.room ? ' ' + current.room : ''}`);
|
|
179
|
+
this.writeOperation(link.run_id, record);
|
|
180
|
+
});
|
|
181
|
+
const args = ['task', '_layout', task.task_id, operation, ...(room ? [room] : []), '--operation-id', record.id];
|
|
182
|
+
const launch = this.deps.launch
|
|
183
|
+
?? ((cli, name) => launchFleetWorker(cli, name, this.configPath, this.deps.binPath?.()));
|
|
184
|
+
try {
|
|
185
|
+
await launch(args, `task-layout-${task.task_id}`);
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
await this.updateOperation(link.run_id, { ...record, status: 'failed', error: `worker failed to start: ${String(error.message ?? error)}` });
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
return this.readOperation(link.run_id);
|
|
192
|
+
}
|
|
193
|
+
/** Worker body. Errors are recorded and rethrown; unknown engine outcomes stay in run state for inspection. */
|
|
194
|
+
/** Without an ID (direct CLI use) the worker claims a new current operation. */
|
|
195
|
+
async run(task, operation, room, operationId) {
|
|
196
|
+
const link = task.layout;
|
|
197
|
+
if (!link)
|
|
198
|
+
throw Error(`task ${task.task_id} has no layout`);
|
|
199
|
+
const record = { id: operationId ?? randomUUID(), operation, ...(room ? { room } : {}) };
|
|
200
|
+
if (!operationId)
|
|
201
|
+
await withFileLock(this.operationPath(link.run_id) + '.lock', () => this.writeOperation(link.run_id, { ...record, status: 'running' }));
|
|
202
|
+
else
|
|
203
|
+
await this.updateOperation(link.run_id, { ...record, status: 'running' });
|
|
204
|
+
try {
|
|
205
|
+
await this.perform(task, operation, room);
|
|
206
|
+
await this.updateOperation(link.run_id, { ...record, status: 'succeeded' });
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
await this.updateOperation(link.run_id, { ...record, status: 'failed', error: error.message ?? String(error) });
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
async perform(task, operation, room) {
|
|
214
|
+
const link = task.layout;
|
|
215
|
+
if (operation === 'open') {
|
|
216
|
+
// Re-read under the task-layout lock: a deletion or terminal action accepted after HTTP admission wins.
|
|
217
|
+
await this.ensureRun(task, assertLayoutOpenable);
|
|
218
|
+
// If deletion removed the run since, fail rather than recreate it.
|
|
219
|
+
if (!this.runExists(link))
|
|
220
|
+
throw new FleetError('conflict', 'task layout run was removed');
|
|
221
|
+
await this.service().open(link.run_id).activate(layoutSupervisorId(), room);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (!this.runExists(link))
|
|
225
|
+
return; // nothing was created, nothing to close
|
|
226
|
+
const engine = this.service().open(link.run_id, true);
|
|
227
|
+
if (operation === 'close-room') {
|
|
228
|
+
if (!engine.snapshot().rooms[room]?.native)
|
|
229
|
+
return; // declared but never opened
|
|
230
|
+
await engine.closeRoom(layoutSupervisorId(), room);
|
|
231
|
+
}
|
|
232
|
+
else
|
|
233
|
+
await engine.close(layoutSupervisorId());
|
|
234
|
+
}
|
|
235
|
+
/** Deletion: close every layout resource, then remove the run and its operation record. */
|
|
236
|
+
async closeAndForget(task) {
|
|
237
|
+
const link = task.layout;
|
|
238
|
+
if (!link)
|
|
239
|
+
return;
|
|
240
|
+
if (this.runExists(link))
|
|
241
|
+
await this.service().open(link.run_id, true).close(layoutSupervisorId());
|
|
242
|
+
await this.withTaskLock(link.run_id, async () => {
|
|
243
|
+
for (const path of [layoutRunPath(link.run_id), this.provenancePath(link.run_id), taskBriefPath(task)])
|
|
244
|
+
rmSync(path, { force: true });
|
|
245
|
+
await withFileLock(this.operationPath(link.run_id) + '.lock', () => rmSync(this.operationPath(link.run_id), { force: true }));
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|