clay-server 4.0.0-beta.13 → 4.0.0-beta.15
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/lib/capsule-display-floor.js +51 -0
- package/lib/capsule-frame-server.js +229 -0
- package/lib/capsule-pig-logic.js +268 -0
- package/lib/capsule-server-runtimes.js +41 -0
- package/lib/capsule-tictactoe-logic.js +260 -0
- package/lib/capsules/pig/display.js +190 -0
- package/lib/capsules/pig/manifest.json +9 -0
- package/lib/capsules/pig/ui.json +46 -0
- package/lib/capsules/tictactoe/manifest.json +9 -0
- package/lib/capsules/tictactoe/ui.json +203 -0
- package/lib/project-capsule-catalog.js +9 -1
- package/lib/project-connection.js +1 -1
- package/lib/project-log-feedback-delivery.js +76 -0
- package/lib/project-logs.js +4 -0
- package/lib/project-pair-lifecycle.js +7 -12
- package/lib/project-session-pair.js +29 -39
- package/lib/project-worker-proposal.js +203 -72
- package/lib/project.js +13 -0
- package/lib/public/css/capsule-ui.css +18 -0
- package/lib/public/css/home-session-actions.css +66 -0
- package/lib/public/css/home-sidebar.css +66 -0
- package/lib/public/css/mobile-nav.css +80 -0
- package/lib/public/css/sidebar.css +92 -0
- package/lib/public/css/worker-proposal.css +34 -1
- package/lib/public/modules/app-messages.js +14 -1
- package/lib/public/modules/home-conversations-sheet.js +102 -48
- package/lib/public/modules/home-session-actions.js +6 -0
- package/lib/public/modules/home-sidebar-chat-list.js +101 -41
- package/lib/public/modules/home-tool-frame.js +165 -0
- package/lib/public/modules/home-tools.js +129 -1
- package/lib/public/modules/session-hierarchy.js +54 -0
- package/lib/public/modules/sidebar-mobile.js +35 -6
- package/lib/public/modules/sidebar-session-hierarchy.js +206 -0
- package/lib/public/modules/sidebar-sessions.js +49 -7
- package/lib/public/modules/worker-proposal-state.js +16 -0
- package/lib/public/modules/worker-proposal.js +49 -12
- package/lib/sdk-bridge.js +16 -8
- package/lib/sdk-message-processor.js +34 -4
- package/lib/server-home-chat.js +12 -1
- package/lib/server-tools.js +97 -9
- package/lib/server.js +3 -0
- package/lib/session-driver-eligibility.js +20 -165
- package/lib/session-pair-factory.js +31 -28
- package/lib/session-pair-mcp-server.js +5 -9
- package/lib/session-pair-prompts.js +15 -15
- package/lib/session-provenance.js +119 -0
- package/lib/session-spawn-mcp-server.js +1 -1
- package/lib/sessions.js +40 -2
- package/lib/tools-registry.js +44 -10
- package/lib/ws-schema.js +8 -3
- package/lib/yoke/adapters/claude.js +12 -0
- package/package.json +1 -1
package/lib/server-home-chat.js
CHANGED
|
@@ -10,6 +10,7 @@ var attachHomeDebates = require("./server-home-debates").attachHomeDebates, atta
|
|
|
10
10
|
var homeChatEvents = require("./server-home-chat-events");
|
|
11
11
|
var homeCapsuleCreation = require("./server-home-capsule-creation");
|
|
12
12
|
var sessionTitlePolicy = require("./session-title-policy");
|
|
13
|
+
var sessionProvenance = require("./session-provenance");
|
|
13
14
|
var historyToHomeChat = homeChatEvents.historyToHomeChat;
|
|
14
15
|
var transformEvent = homeChatEvents.transformEvent;
|
|
15
16
|
function attachHomeChat(deps) {
|
|
@@ -83,8 +84,14 @@ function attachHomeChat(deps) {
|
|
|
83
84
|
var sessionManager = found.ctx.getSessionManager();
|
|
84
85
|
if (!sessionManager) return [];
|
|
85
86
|
var result = [];
|
|
87
|
+
var visibleSessions = [];
|
|
86
88
|
sessionManager.sessions.forEach(function (session) {
|
|
87
89
|
if (!ownsSession(session, userId)) return;
|
|
90
|
+
visibleSessions.push(session);
|
|
91
|
+
});
|
|
92
|
+
var hierarchy = sessionProvenance.hierarchyFor(visibleSessions);
|
|
93
|
+
for (var vi = 0; vi < visibleSessions.length; vi++) {
|
|
94
|
+
var session = visibleSessions[vi];
|
|
88
95
|
if (sessionTitlePolicy.repairLegacySearchTitle(session)
|
|
89
96
|
&& typeof sessionManager.saveSessionFile === "function") {
|
|
90
97
|
sessionManager.saveSessionFile(session);
|
|
@@ -102,12 +109,16 @@ function attachHomeChat(deps) {
|
|
|
102
109
|
createdAt: createdAt,
|
|
103
110
|
lastActivity: lastActivity,
|
|
104
111
|
isProcessing: !!session.isProcessing,
|
|
112
|
+
sessionRole: hierarchy[session.localId].role,
|
|
113
|
+
parentSessionId: hierarchy[session.localId].parentSessionId == null ? null : sessionReference(sessionManager.sessions.get(hierarchy[session.localId].parentSessionId)),
|
|
114
|
+
parentAvailable: hierarchy[session.localId].parentAvailable,
|
|
115
|
+
workerGeneration: hierarchy[session.localId].generation,
|
|
105
116
|
});
|
|
106
117
|
if (session.debateSetupMode === true) result[result.length - 1].debatePlanning = true;
|
|
107
118
|
if (session.homeDebatePhase) result[result.length - 1].debatePhase = session.homeDebatePhase;
|
|
108
119
|
if (session.mateCreationMode === true) result[result.length - 1].mateCreation = true;
|
|
109
120
|
if (session.homeMateCreationPhase) result[result.length - 1].mateCreationPhase = session.homeMateCreationPhase;
|
|
110
|
-
}
|
|
121
|
+
}
|
|
111
122
|
result.sort(function (a, b) { return b.lastActivity - a.lastActivity; });
|
|
112
123
|
return result;
|
|
113
124
|
}
|
package/lib/server-tools.js
CHANGED
|
@@ -2,11 +2,15 @@ var toolsRegistry = require("./tools-registry");
|
|
|
2
2
|
var toolStorage = require("./tool-storage");
|
|
3
3
|
var crypto = require("crypto");
|
|
4
4
|
var toolLlm = require("./tool-llm");
|
|
5
|
+
var capsuleFloor = require("./capsule-display-floor");
|
|
6
|
+
var capsuleServerRuntimes = require("./capsule-server-runtimes");
|
|
7
|
+
var capsuleFrameServer = require("./capsule-frame-server");
|
|
5
8
|
|
|
6
9
|
function attachTools(deps) {
|
|
7
10
|
var users = deps.users;
|
|
8
11
|
var projects = deps.projects;
|
|
9
|
-
var
|
|
12
|
+
var serverRuntimes = deps.serverRuntimes || capsuleServerRuntimes;
|
|
13
|
+
var frameServer = deps.frameServer || capsuleFrameServer.createCapsuleFrameServer({ tlsOptions: deps.tlsOptions || null });
|
|
10
14
|
var controlTimeoutMs = deps.controlTimeoutMs || 15000;
|
|
11
15
|
var llmControlTimeoutMs = deps.llmControlTimeoutMs || 90000;
|
|
12
16
|
var pendingControl = Object.create(null);
|
|
@@ -137,20 +141,47 @@ function attachTools(deps) {
|
|
|
137
141
|
});
|
|
138
142
|
}
|
|
139
143
|
|
|
144
|
+
// One act pipeline for server-runtime Capsules. The human's button click and
|
|
145
|
+
// the Mate's clay_tool_act call both arrive here, so the same Logic enforces
|
|
146
|
+
// the same rules for both and neither gets a private path. The seat is set
|
|
147
|
+
// from the server-resolved actor, never from caller-supplied text.
|
|
148
|
+
//
|
|
149
|
+
// The caller gets state and nothing else: a Mate never sees Display. Every
|
|
150
|
+
// successful act additionally pushes its causal {seq, actor, action,
|
|
151
|
+
// previous, next} event to the user's own connected clients, so a watching
|
|
152
|
+
// Display re-renders on any Logic change no matter who caused it.
|
|
153
|
+
async function controlServerCapsule(userId, installed, actor, callerId, kind, payload) {
|
|
154
|
+
var runtime = serverRuntimes.createRuntime(installed.manifest.id, contextFor(userId));
|
|
155
|
+
if (!runtime) throw new Error("No trusted server runtime is registered for this capsule.");
|
|
156
|
+
var context = { userId: userId, actor: actor, callerId: callerId };
|
|
157
|
+
if (kind === "snapshot") return runtime.snapshot(context);
|
|
158
|
+
if (kind === "act") {
|
|
159
|
+
var result = await runtime.act(context, payload.actionId, payload.args || {});
|
|
160
|
+
if (result && result.event) {
|
|
161
|
+
broadcastToUser(userId, { type: "tool_server_event", toolId: installed.manifest.id, event: result.event });
|
|
162
|
+
return result.state;
|
|
163
|
+
}
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
if (kind === "set" && runtime.set) return runtime.set(context, payload.controlId, payload.value);
|
|
167
|
+
throw new Error("The server-runtime capsule does not support '" + kind + "'.");
|
|
168
|
+
}
|
|
169
|
+
|
|
140
170
|
function controlForMate(userId, mateId, toolId, kind, payload) {
|
|
141
171
|
payload = payload || {};
|
|
142
172
|
var ctx = contextFor(userId);
|
|
143
173
|
toolsRegistry.listTools(ctx);
|
|
144
174
|
var installed = toolsRegistry.getTool(ctx, toolId);
|
|
145
175
|
if (!installed) return Promise.reject(new Error("Tool is not installed."));
|
|
176
|
+
// Skills are gated by Display: the Mate loses a Capsule at the same moment
|
|
177
|
+
// the human loses the declarative floor it is operated through.
|
|
178
|
+
if (!capsuleFloor.hasUsableFloor(installed.manifest, installed.uiTree)) {
|
|
179
|
+
return Promise.reject(new Error("This Capsule has no usable human Display, so it is unavailable to Mates."));
|
|
180
|
+
}
|
|
146
181
|
if (installed.manifest.runtime === "server") {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if (kind === "snapshot") return Promise.resolve(adapter.snapshot(context));
|
|
151
|
-
if (kind === "act") return Promise.resolve(adapter.act(context, payload.actionId, payload.args || {}));
|
|
152
|
-
if (kind === "set" && adapter.set) return Promise.resolve(adapter.set(context, payload.controlId, payload.value));
|
|
153
|
-
return Promise.reject(new Error("The server-runtime capsule does not support '" + kind + "'."));
|
|
182
|
+
return Promise.resolve().then(function () {
|
|
183
|
+
return controlServerCapsule(userId, installed, "mate", mateId, kind, payload);
|
|
184
|
+
});
|
|
154
185
|
}
|
|
155
186
|
var permissions = installed.manifest.permissions || [];
|
|
156
187
|
var timeoutMs = kind === "act" && permissions.indexOf("llm") !== -1 ? llmControlTimeoutMs : controlTimeoutMs;
|
|
@@ -262,8 +293,18 @@ function attachTools(deps) {
|
|
|
262
293
|
return { removed: true, toolId: toolId };
|
|
263
294
|
}
|
|
264
295
|
|
|
296
|
+
// The human half of the shared server-Capsule pipeline. Same Logic, same
|
|
297
|
+
// rules, no AI in the path.
|
|
298
|
+
function controlForUser(userId, toolId, kind, payload) {
|
|
299
|
+
var ctx = contextFor(userId);
|
|
300
|
+
var installed = toolsRegistry.getTool(ctx, toolId);
|
|
301
|
+
if (!installed) throw new Error("Tool is not installed.");
|
|
302
|
+
if (installed.manifest.runtime !== "server") throw new Error("This Capsule does not run on the server.");
|
|
303
|
+
return controlServerCapsule(userId, installed, "human", "user", kind, payload || {});
|
|
304
|
+
}
|
|
305
|
+
|
|
265
306
|
function handleMessage(ws, msg) {
|
|
266
|
-
var messageTypes = ["tools_list", "tool_get", "tool_install", "tool_remove", "tool_storage_op", "tool_llm_op", "tool_llm_config_get", "tool_control_response", "tool_source_get", "tool_mate_access_set"];
|
|
307
|
+
var messageTypes = ["tools_list", "tool_get", "tool_install", "tool_remove", "tool_storage_op", "tool_llm_op", "tool_llm_config_get", "tool_control_response", "tool_source_get", "tool_mate_access_set", "tool_server_control", "tool_frame_url"];
|
|
267
308
|
if (!msg || messageTypes.indexOf(msg.type) === -1) return false;
|
|
268
309
|
var userId;
|
|
269
310
|
if (users.isMultiUser()) {
|
|
@@ -312,6 +353,52 @@ function attachTools(deps) {
|
|
|
312
353
|
});
|
|
313
354
|
return true;
|
|
314
355
|
}
|
|
356
|
+
if (msg.type === "tool_frame_url") {
|
|
357
|
+
// The rich element renders on a separate anonymous origin, so the frame
|
|
358
|
+
// URL is minted here, on the authenticated socket, as a short-lived
|
|
359
|
+
// one-time token bound to this user's tools root. The rich element is
|
|
360
|
+
// additive only: it exists solely when the floor already validated at
|
|
361
|
+
// registration, and losing it never costs the human the floor.
|
|
362
|
+
Promise.resolve().then(function () {
|
|
363
|
+
var ctx = contextFor(userId);
|
|
364
|
+
var installed = toolsRegistry.getTool(ctx, msg.toolId);
|
|
365
|
+
if (!installed) throw new Error("Tool is not installed.");
|
|
366
|
+
if (!installed.hasRichDisplay) throw new Error("This Capsule ships no rich Display element.");
|
|
367
|
+
return frameServer.issueFrameUrl(ctx, msg.toolId);
|
|
368
|
+
}).then(function (frame) {
|
|
369
|
+
send(ws, { type: "tool_frame_url_state", toolId: msg.toolId, requestId: msg.requestId || null, ok: true, frame: frame });
|
|
370
|
+
}).catch(function (error) {
|
|
371
|
+
send(ws, {
|
|
372
|
+
type: "tool_frame_url_state",
|
|
373
|
+
toolId: msg.toolId,
|
|
374
|
+
requestId: msg.requestId || null,
|
|
375
|
+
ok: false,
|
|
376
|
+
error: error && error.message ? error.message : "The rich Display is unavailable.",
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
if (msg.type === "tool_server_control") {
|
|
382
|
+
Promise.resolve().then(function () {
|
|
383
|
+
return controlForUser(userId, msg.toolId, msg.kind, {
|
|
384
|
+
actionId: msg.actionId,
|
|
385
|
+
args: msg.args || {},
|
|
386
|
+
controlId: msg.controlId,
|
|
387
|
+
value: msg.value,
|
|
388
|
+
});
|
|
389
|
+
}).then(function (state) {
|
|
390
|
+
send(ws, { type: "tool_server_state", toolId: msg.toolId, requestId: msg.requestId || null, ok: true, state: state });
|
|
391
|
+
}).catch(function (error) {
|
|
392
|
+
send(ws, {
|
|
393
|
+
type: "tool_server_state",
|
|
394
|
+
toolId: msg.toolId,
|
|
395
|
+
requestId: msg.requestId || null,
|
|
396
|
+
ok: false,
|
|
397
|
+
error: error && error.message ? error.message : "The Capsule action failed.",
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
315
402
|
if (msg.type === "tool_install") {
|
|
316
403
|
run(ws, msg, function () {
|
|
317
404
|
installForMate(userId, {
|
|
@@ -366,6 +453,7 @@ function attachTools(deps) {
|
|
|
366
453
|
handleMessage: handleMessage,
|
|
367
454
|
installedManifests: installedManifests,
|
|
368
455
|
controlForMate: controlForMate,
|
|
456
|
+
controlForUser: controlForUser,
|
|
369
457
|
installForMate: installForMate,
|
|
370
458
|
sourceForMate: sourceForMate,
|
|
371
459
|
updateForMate: updateForMate,
|
package/lib/server.js
CHANGED
|
@@ -1428,6 +1428,9 @@ function createServer(opts) {
|
|
|
1428
1428
|
var toolsHandler = serverTools.attachTools({
|
|
1429
1429
|
users: users,
|
|
1430
1430
|
projects: projects,
|
|
1431
|
+
// The rich-Display frame listener must speak the same scheme as the app,
|
|
1432
|
+
// or the sandboxed frame would be blocked as mixed content under TLS.
|
|
1433
|
+
tlsOptions: tlsOptions,
|
|
1431
1434
|
});
|
|
1432
1435
|
|
|
1433
1436
|
// --- Mate handler ---
|
|
@@ -1,183 +1,38 @@
|
|
|
1
|
-
// Driver
|
|
2
|
-
// Driver/Split Worker pair and receive the autonomous lifecycle tools.
|
|
1
|
+
// Driver capability for a visible Driver/Split Worker pair.
|
|
3
2
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// project
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
// claude-opus-5, claude-fable-5
|
|
17
|
-
// Driver threshold: fable
|
|
18
|
-
//
|
|
19
|
-
// codex family gpt-5.2 1 < gpt-5.5 2 < luna 3 < sol 4 < terra 5
|
|
20
|
-
// real ids: the CODEX_MODELS catalog, strongest first
|
|
21
|
-
// Driver threshold: sol
|
|
22
|
-
//
|
|
23
|
-
// Matching is by token, not by exact id, so a later release in the same family
|
|
24
|
-
// (claude-fable-6, gpt-5.7-terra) stays eligible without a code change. A
|
|
25
|
-
// model in a known family whose tier token is unrecognized is NOT eligible:
|
|
26
|
-
// unknown capability fails closed rather than inheriting the threshold.
|
|
27
|
-
//
|
|
28
|
-
// Any other vendor is not eligible for the Driver role at all, because no
|
|
29
|
-
// equivalent tier metadata exists to compare against. Those vendors remain
|
|
30
|
-
// perfectly usable as Workers.
|
|
31
|
-
|
|
32
|
-
var models = require("./project-models");
|
|
33
|
-
|
|
34
|
-
var FAMILIES = {
|
|
35
|
-
claude: {
|
|
36
|
-
label: "Claude",
|
|
37
|
-
threshold: 4,
|
|
38
|
-
thresholdName: "Fable",
|
|
39
|
-
// Longest/most specific tokens first so "haiku" cannot shadow a future
|
|
40
|
-
// compound name. Each entry is a token searched in id and display name.
|
|
41
|
-
tiers: [
|
|
42
|
-
{ token: "fable", rank: 4, name: "Fable" },
|
|
43
|
-
{ token: "opus", rank: 3, name: "Opus" },
|
|
44
|
-
{ token: "sonnet", rank: 2, name: "Sonnet" },
|
|
45
|
-
{ token: "haiku", rank: 1, name: "Haiku" },
|
|
46
|
-
],
|
|
47
|
-
},
|
|
48
|
-
codex: {
|
|
49
|
-
label: "OpenAI",
|
|
50
|
-
threshold: 4,
|
|
51
|
-
thresholdName: "Sol",
|
|
52
|
-
tiers: [
|
|
53
|
-
{ token: "terra", rank: 5, name: "Terra" },
|
|
54
|
-
{ token: "sol", rank: 4, name: "Sol" },
|
|
55
|
-
{ token: "luna", rank: 3, name: "Luna" },
|
|
56
|
-
{ token: "gpt-5.5", rank: 2, name: "GPT-5.5" },
|
|
57
|
-
{ token: "gpt-5.2", rank: 1, name: "GPT-5.2" },
|
|
58
|
-
],
|
|
59
|
-
},
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
function familyFor(vendor) {
|
|
63
|
-
var key = String(vendor || "").toLowerCase();
|
|
64
|
-
return Object.prototype.hasOwnProperty.call(FAMILIES, key) ? FAMILIES[key] : null;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Every string the catalog knows this model by. The catalog entry is found with
|
|
68
|
-
// the repo's own matcher so an alias ("fable"), an id, or a resolvedModel all
|
|
69
|
-
// resolve to the same entry, and the display name participates in tier
|
|
70
|
-
// detection exactly as project-worker-proposal.js already relies on.
|
|
71
|
-
function searchTextFor(vendor, model, modelsByVendor) {
|
|
72
|
-
var parts = [String(model || "")];
|
|
73
|
-
var catalog = (modelsByVendor && modelsByVendor[vendor]) || [];
|
|
74
|
-
for (var i = 0; i < catalog.length; i++) {
|
|
75
|
-
if (!models.modelEntryMatches(catalog[i], model)) continue;
|
|
76
|
-
var entry = catalog[i];
|
|
77
|
-
if (typeof entry === "string") {
|
|
78
|
-
parts.push(entry);
|
|
79
|
-
} else {
|
|
80
|
-
parts.push(entry.value || "");
|
|
81
|
-
parts.push(entry.id || "");
|
|
82
|
-
parts.push(entry.resolvedModel || "");
|
|
83
|
-
parts.push(entry.displayName || "");
|
|
84
|
-
parts.push(entry.name || "");
|
|
85
|
-
}
|
|
86
|
-
break;
|
|
87
|
-
}
|
|
88
|
-
return parts.join(" ").toLowerCase();
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Resolve the tier of one model inside its vendor family. Returns null when
|
|
92
|
-
// the vendor has no tier metadata or the tier token is unrecognized.
|
|
93
|
-
function resolveTier(vendor, model, modelsByVendor) {
|
|
94
|
-
var family = familyFor(vendor);
|
|
95
|
-
if (!family || !model) return null;
|
|
96
|
-
var text = searchTextFor(vendor, model, modelsByVendor);
|
|
97
|
-
for (var i = 0; i < family.tiers.length; i++) {
|
|
98
|
-
if (text.indexOf(family.tiers[i].token) !== -1) {
|
|
99
|
-
return {
|
|
100
|
-
vendor: String(vendor).toLowerCase(),
|
|
101
|
-
family: family.label,
|
|
102
|
-
rank: family.tiers[i].rank,
|
|
103
|
-
name: family.tiers[i].name,
|
|
104
|
-
threshold: family.threshold,
|
|
105
|
-
thresholdName: family.thresholdName,
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
return null;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// The hard invariant. Returns { ok, tier, error } and never throws. `error` is
|
|
113
|
-
// a complete English sentence suitable for returning to a tool caller.
|
|
114
|
-
function evaluateDriverModel(vendor, model, modelsByVendor) {
|
|
115
|
-
var family = familyFor(vendor);
|
|
116
|
-
if (!family) {
|
|
117
|
-
return {
|
|
118
|
-
ok: false,
|
|
119
|
-
tier: null,
|
|
120
|
-
error: "The Driver role requires a Claude model of Fable tier or higher, or an OpenAI model of " +
|
|
121
|
-
"Sol tier or higher. Vendor \"" + String(vendor || "unknown") + "\" has no comparable tier " +
|
|
122
|
-
"metadata, so it cannot take the Driver role. It can still be used for the Split Worker.",
|
|
123
|
-
};
|
|
3
|
+
// Model and vendor choice belongs to the user. A session does not need to
|
|
4
|
+
// clear a model-tier threshold to coordinate a Split Worker; runtime catalog
|
|
5
|
+
// validation belongs to session-pair-factory.js and applies equally to every
|
|
6
|
+
// requested role. The only session-level exclusion here is structural: an
|
|
7
|
+
// embedded terminal has no project chat surface on which to host the Driver
|
|
8
|
+
// controls or the paired pane.
|
|
9
|
+
|
|
10
|
+
var sessionProvenance = require("./session-provenance");
|
|
11
|
+
|
|
12
|
+
function evaluateDriverSession(session) {
|
|
13
|
+
if (!session) {
|
|
14
|
+
return { ok: false, error: "No session was bound to this request." };
|
|
124
15
|
}
|
|
125
|
-
if (
|
|
126
|
-
return {
|
|
127
|
-
ok: false,
|
|
128
|
-
tier: null,
|
|
129
|
-
error: "This session has no resolved model yet, so its " + family.label +
|
|
130
|
-
" tier cannot be confirmed. The Driver role requires " + family.thresholdName +
|
|
131
|
-
" tier or higher.",
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
var tier = resolveTier(vendor, model, modelsByVendor);
|
|
135
|
-
if (!tier) {
|
|
136
|
-
return {
|
|
137
|
-
ok: false,
|
|
138
|
-
tier: null,
|
|
139
|
-
error: "Model \"" + model + "\" is not a recognized " + family.label +
|
|
140
|
-
" tier, so it cannot take the Driver role. The Driver role requires " +
|
|
141
|
-
family.thresholdName + " tier or higher.",
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
if (tier.rank < tier.threshold) {
|
|
16
|
+
if (session.mode === "tui") {
|
|
145
17
|
return {
|
|
146
18
|
ok: false,
|
|
147
|
-
|
|
148
|
-
error: "The Driver role requires a " + family.label + " model of " + family.thresholdName +
|
|
149
|
-
" tier or higher. This session is " + tier.name + " tier.",
|
|
19
|
+
error: "An embedded terminal session cannot take the Driver role.",
|
|
150
20
|
};
|
|
151
21
|
}
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// The session-level form. Resolves the session's effective model the same way
|
|
156
|
-
// the rest of the server does: the session's own model, else the vendor
|
|
157
|
-
// default. Identity is read from the session object, never from a caller.
|
|
158
|
-
function evaluateDriverSession(session, sm) {
|
|
159
|
-
if (!session) return { ok: false, tier: null, error: "No session was bound to this request." };
|
|
160
|
-
if (session.mode === "tui") {
|
|
22
|
+
if (sessionProvenance.isWorker(session)) {
|
|
161
23
|
return {
|
|
162
24
|
ok: false,
|
|
163
|
-
|
|
164
|
-
error: "An embedded terminal session cannot take the Driver role.",
|
|
25
|
+
error: "A Split Worker session cannot take the Driver role.",
|
|
165
26
|
};
|
|
166
27
|
}
|
|
167
|
-
|
|
168
|
-
var fallback = (sm && sm.defaultModelByVendor && sm.defaultModelByVendor[vendor]) || "";
|
|
169
|
-
var model = session.model || fallback;
|
|
170
|
-
return evaluateDriverModel(vendor, model, sm && sm.modelsByVendor);
|
|
28
|
+
return { ok: true, error: null };
|
|
171
29
|
}
|
|
172
30
|
|
|
173
|
-
function isEligibleDriverSession(session
|
|
174
|
-
return evaluateDriverSession(session
|
|
31
|
+
function isEligibleDriverSession(session) {
|
|
32
|
+
return evaluateDriverSession(session).ok;
|
|
175
33
|
}
|
|
176
34
|
|
|
177
35
|
module.exports = {
|
|
178
|
-
FAMILIES: FAMILIES,
|
|
179
|
-
evaluateDriverModel: evaluateDriverModel,
|
|
180
36
|
evaluateDriverSession: evaluateDriverSession,
|
|
181
37
|
isEligibleDriverSession: isEligibleDriverSession,
|
|
182
|
-
resolveTier: resolveTier,
|
|
183
38
|
};
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Extracted from project-session-pair.js, which operates a pair once it
|
|
4
4
|
// exists; this module is only about how one comes into being. Both the
|
|
5
|
-
// explicit "Add Split Worker"
|
|
6
|
-
//
|
|
7
|
-
//
|
|
5
|
+
// explicit "Add Split Worker", accepted proposal, and transactional replacement
|
|
6
|
+
// flows land here, so vendor validation, owner derivation and the group record
|
|
7
|
+
// are written in exactly one place.
|
|
8
8
|
//
|
|
9
9
|
// Ownership always comes from the connection or the Driver session, never from
|
|
10
10
|
// the incoming message.
|
|
@@ -22,11 +22,10 @@
|
|
|
22
22
|
//
|
|
23
23
|
// Two ordering rules matter and are load-bearing:
|
|
24
24
|
//
|
|
25
|
-
// 1. Nothing is created until everything is validated. Driver
|
|
25
|
+
// 1. Nothing is created until everything is validated. Driver capability and
|
|
26
26
|
// both runtime requests are settled first, so a rejected request leaves no
|
|
27
|
-
// orphan session behind.
|
|
28
|
-
//
|
|
29
|
-
// session exists.
|
|
27
|
+
// orphan session behind. Any installed, available model may be the Driver;
|
|
28
|
+
// model choice is the user's rather than a server-side tier policy.
|
|
30
29
|
// 2. `preflightRuntime` is side-effect free, so a caller that is about to
|
|
31
30
|
// destroy something (replacement dissolving a live pair) can validate the
|
|
32
31
|
// replacement first and abort while the old pair is still intact.
|
|
@@ -34,6 +33,7 @@
|
|
|
34
33
|
var driverEligibility = require("./session-driver-eligibility");
|
|
35
34
|
var models = require("./project-models");
|
|
36
35
|
var yoke = require("./yoke");
|
|
36
|
+
var sessionProvenance = require("./session-provenance");
|
|
37
37
|
|
|
38
38
|
function attachPairFactory(ctx) {
|
|
39
39
|
var sm = ctx.sm;
|
|
@@ -87,8 +87,8 @@ function attachPairFactory(ctx) {
|
|
|
87
87
|
};
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
// The
|
|
91
|
-
// different engine from the Driver's own, so the pair does not double up.
|
|
90
|
+
// The default Worker vendor for an accepted proposal or replacement prefers
|
|
91
|
+
// a different engine from the Driver's own, so the pair does not double up.
|
|
92
92
|
function defaultWorkerVendorFor(driver) {
|
|
93
93
|
var installed = sm.installedVendors || [];
|
|
94
94
|
if (installed.length === 0) throw new Error("no coding agent is installed for a Split Worker session");
|
|
@@ -140,14 +140,6 @@ function attachPairFactory(ctx) {
|
|
|
140
140
|
groupName = undefined; // auto name from member titles
|
|
141
141
|
} else {
|
|
142
142
|
driverRuntime = preflightRuntime(driverSpec, "claude");
|
|
143
|
-
// The tier is judged on the model this session would really resolve to:
|
|
144
|
-
// the explicit request if there is one, else that vendor's default.
|
|
145
|
-
var effectiveDriverModel = driverRuntime.model ||
|
|
146
|
-
(sm.defaultModelByVendor && sm.defaultModelByVendor[driverRuntime.vendor]) || "";
|
|
147
|
-
var newVerdict = driverEligibility.evaluateDriverModel(
|
|
148
|
-
driverRuntime.vendor, effectiveDriverModel, sm.modelsByVendor
|
|
149
|
-
);
|
|
150
|
-
if (!newVerdict.ok) throw new Error(newVerdict.error);
|
|
151
143
|
groupName = "Agent pair";
|
|
152
144
|
}
|
|
153
145
|
|
|
@@ -176,18 +168,29 @@ function attachPairFactory(ctx) {
|
|
|
176
168
|
worker.title = "Split Worker · " + ((yoke.getVendorInfo(workerRuntime.vendor) || {}).displayName || workerRuntime.vendor);
|
|
177
169
|
if (workerRuntime.explicitEffort) worker.loopSettings = { effort: workerRuntime.effort };
|
|
178
170
|
|
|
179
|
-
var result
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
171
|
+
var result;
|
|
172
|
+
try {
|
|
173
|
+
// Creation provenance is durable and independent from the active split
|
|
174
|
+
// group. Persist both ends before the group's broadcast so the Worker
|
|
175
|
+
// never flashes as unrelated and survives a restart with its parentage.
|
|
176
|
+
sessionProvenance.markWorker(driver, worker, sm.sessions);
|
|
177
|
+
if (typeof sm.saveSessionFile === "function") {
|
|
178
|
+
sm.saveSessionFile(driver);
|
|
179
|
+
sm.saveSessionFile(worker);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
result = store.create(ws, {
|
|
183
|
+
members: [driver.localId, worker.localId],
|
|
184
|
+
pair: { driverId: driver.localId, workerId: worker.localId },
|
|
185
|
+
name: groupName,
|
|
186
|
+
});
|
|
187
|
+
if (!result.ok) throw new Error(result.error);
|
|
188
|
+
} catch (e) {
|
|
189
|
+
// Any failure after session creation must remove exactly the sessions
|
|
190
|
+
// made by this call. An existing Driver is never removed.
|
|
188
191
|
removeCreated(worker);
|
|
189
192
|
removeCreated(createdDriver);
|
|
190
|
-
throw
|
|
193
|
+
throw e;
|
|
191
194
|
}
|
|
192
195
|
sm.broadcastSessionList();
|
|
193
196
|
return { driver: driver, worker: worker, group: result.group };
|
|
@@ -202,7 +205,7 @@ function attachPairFactory(ctx) {
|
|
|
202
205
|
}
|
|
203
206
|
|
|
204
207
|
function createWorkerForDriver(driver, args) {
|
|
205
|
-
// Validates the Driver's
|
|
208
|
+
// Validates the Driver's capability and the whole Worker runtime before
|
|
206
209
|
// any session is made, so a rejection creates nothing.
|
|
207
210
|
var runtime = preflightWorkerForDriver(driver, args);
|
|
208
211
|
var ws = {
|
|
@@ -9,14 +9,11 @@ function getToolDefs(handlers, options) {
|
|
|
9
9
|
var defs = [
|
|
10
10
|
{
|
|
11
11
|
name: "send_to_partner",
|
|
12
|
-
description: "Delegate one concrete task to the visible Split Worker.
|
|
12
|
+
description: "Delegate one concrete follow-up task to the existing visible Split Worker. This tool is exposed only after the user has configured and accepted an exact pair. A completed turn does not end the Split Worker session: reuse the same Split Worker for follow-up implementation and corrections instead of taking over its work. Detached completions are pushed back automatically. A delegated turn cannot delegate back, so keep orchestration one hop deep.",
|
|
13
13
|
inputSchema: buildShape({
|
|
14
14
|
message: { type: "string", description: "The complete task or question for the partner." },
|
|
15
15
|
wait: { type: "boolean", description: "Wait for the partner's turn to finish. Defaults to true." },
|
|
16
16
|
timeoutSeconds: { type: "number", description: "Maximum wait in seconds, from 1 to 900. Defaults to 300." },
|
|
17
|
-
workerVendor: { type: "string", description: "Optional Split Worker vendor to use only when creating a new pair. Defaults to a suitable installed vendor." },
|
|
18
|
-
workerModel: { type: "string", description: "Optional Split Worker model to use only when creating a new pair." },
|
|
19
|
-
workerEffort: { type: "string", description: "Optional Split Worker reasoning effort to use only when creating a new pair." },
|
|
20
17
|
operationId: { type: "string", description: "Optional stable id for this delegation. Reusing it within the same human turn returns the original operation instead of sending twice." },
|
|
21
18
|
}, ["message"]),
|
|
22
19
|
handler: function (args) { return handlers.send(args || {}); },
|
|
@@ -52,21 +49,20 @@ function getToolDefs(handlers, options) {
|
|
|
52
49
|
},
|
|
53
50
|
{
|
|
54
51
|
name: "replace_partner",
|
|
55
|
-
description: "
|
|
52
|
+
description: "Propose replacing the current Split Worker with a fresh runtime. This non-mutating call shows a user-controlled vendor, model, and effort card. Only acceptance performs the transactional replacement and delegates the supplied task once; decline keeps the existing pair and resumes the Driver. The previous Worker's conversation remains browsable. An active Worker requires interrupt true.",
|
|
56
53
|
inputSchema: buildShape({
|
|
57
54
|
interrupt: { type: "boolean", description: "Stop the Worker first if it is mid-turn or holding a delegated task. Required to replace an active Worker." },
|
|
58
|
-
message: { type: "string", description: "
|
|
59
|
-
wait: { type: "boolean", description: "When a message is given, wait for that turn to finish. Defaults to true." },
|
|
60
|
-
timeoutSeconds: { type: "number", description: "Maximum wait in seconds, from 1 to 900. Defaults to 300." },
|
|
55
|
+
message: { type: "string", description: "Task to deliver exactly once after the user accepts the replacement." },
|
|
61
56
|
workerVendor: { type: "string", description: "Optional vendor for the new Worker. Must be installed and available." },
|
|
62
57
|
workerModel: { type: "string", description: "Optional model for the new Worker. Must be offered by that vendor." },
|
|
63
58
|
workerEffort: { type: "string", description: "Optional reasoning effort for the new Worker." },
|
|
59
|
+
recommendationRationale: { type: "string", description: "Concise Driver-authored explanation of why the recommended replacement vendor, model, and effort fit the next task." },
|
|
64
60
|
evaluation: {
|
|
65
61
|
type: "object",
|
|
66
62
|
description: "Optional bounded assessment of the Worker being replaced, recorded against that exact generation.",
|
|
67
63
|
},
|
|
68
64
|
operationId: { type: "string", description: "Optional stable id for this replacement. Reusing it within the same human turn returns the original operation instead of replacing twice." },
|
|
69
|
-
}),
|
|
65
|
+
}, ["message", "recommendationRationale"]),
|
|
70
66
|
handler: function (args) { return handlers.replace(args || {}); },
|
|
71
67
|
},
|
|
72
68
|
{
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
// Driver-facing system prompt text for the visible Driver/Split Worker pair.
|
|
2
2
|
//
|
|
3
3
|
// Pure data, extracted from project-session-pair.js so that module stays under
|
|
4
|
-
// the size limit and so the guidance can be reviewed as prose.
|
|
5
|
-
// proposal or approval language here by design: a qualified Driver manages its
|
|
6
|
-
// Worker on its own authority, so telling the model to suggest or ask would
|
|
7
|
-
// contradict the tools it actually has.
|
|
4
|
+
// the size limit and so the guidance can be reviewed as prose.
|
|
8
5
|
|
|
9
6
|
var DRIVER = [
|
|
10
7
|
"You are the Driver of a visible Driver/Split Worker pair, and you manage that Split Worker yourself.",
|
|
@@ -17,18 +14,22 @@ var DRIVER = [
|
|
|
17
14
|
"working on something unrelated, replace it instead of carrying that cost forward. Call partner_status to",
|
|
18
15
|
"decide: it reports context tokens used and the ratio of its window, current activity, vendor/model/effort,",
|
|
19
16
|
"history size and idle time, whether replacing is safe right now, and the results you recorded for earlier",
|
|
20
|
-
"Worker generations. It returns no transcript, so it is cheap to consult.",
|
|
17
|
+
"Worker generations. It returns no transcript, so it is cheap to consult. Never tell the user that you will",
|
|
18
|
+
"reuse the current Worker before checking partner_status. If you previously expected reuse but the status leads",
|
|
19
|
+
"you to replace it, explicitly say that the decision changed and give the reason.",
|
|
21
20
|
"",
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"is lost. Replacing an actively running Worker requires
|
|
21
|
+
"Use send_to_partner seamlessly for follow-up work in the existing pair. Runtime creation and replacement are",
|
|
22
|
+
"recorded through a configuration card before anything mutates. replace_partner always posts that card. When",
|
|
23
|
+
"this Driver is in full-access mode, Clay may auto-accept your exact server-validated recommendation; otherwise",
|
|
24
|
+
"the selected vendor, model, and effort remain pending until the user explicitly accepts them.",
|
|
25
|
+
"The replaced Worker keeps its conversation, so nothing is lost. Replacing an actively running Worker requires",
|
|
26
|
+
"interrupt true, which stops it first only after acceptance. A human Stop",
|
|
27
27
|
"is authoritative: do not retry, send more work, or replace the Worker in the same turn. Clay blocks those",
|
|
28
28
|
"actions until the human sends a new Driver message. Use close_partner when they ask to close the pane.",
|
|
29
29
|
"",
|
|
30
|
-
"
|
|
31
|
-
"
|
|
30
|
+
"Recommend a Worker vendor, model, and effort from what is actually installed and offered, and include a concise",
|
|
31
|
+
"rationale explaining why all three fit the task. The card remains visible as an audit trail even when full access",
|
|
32
|
+
"auto-accepts it; an unavailable choice is never auto-accepted. After a Worker generation finishes or is",
|
|
32
33
|
"replaced, call record_partner_evaluation with succeeded, partial, failed, or abandoned and a short reason.",
|
|
33
34
|
"Clay stores that against that exact generation alongside what it measured itself, and partner_status hands it",
|
|
34
35
|
"back, so your next model choice can use observed results instead of guesswork. This is your own record for",
|
|
@@ -51,9 +52,8 @@ var UNPAIRED = [
|
|
|
51
52
|
"Split Worker and its partner tools. Internal Sub-agents are a distinct execution mechanism, not a lexical",
|
|
52
53
|
"category; use them only when the user clearly intends internal or background parallel delegation rather than a",
|
|
53
54
|
"visible paired session. When ambiguity would materially change where work runs, ask a concise clarification",
|
|
54
|
-
"instead of guessing. When a task is implementation-heavy, call
|
|
55
|
-
"
|
|
56
|
-
"ask the user for permission to start one, do not ask them to enable split mode, and do not use a background",
|
|
55
|
+
"instead of guessing. When a task is implementation-heavy, call propose_worker with an exact runtime recommendation",
|
|
56
|
+
"and concise rationale. Clay records and shows the card before the visible pair is accepted or auto-accepted. Do not use a background",
|
|
57
57
|
"Sub-agent as a substitute for a visible Split Worker.",
|
|
58
58
|
].join(" ").replace(/ {2,}/g, " ");
|
|
59
59
|
|