clay-server 3.8.0-beta.2 → 3.8.0-beta.4
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.
|
@@ -203,9 +203,13 @@ function attachSessionPair(ctx) {
|
|
|
203
203
|
async function sendToPartner(args, caller) {
|
|
204
204
|
try {
|
|
205
205
|
if (caller && caller._delegatedBy) throw new Error("delegated turns cannot delegate to another session");
|
|
206
|
-
var resolved = groupAndPartner(caller);
|
|
207
206
|
var message = typeof args.message === "string" ? args.message.trim() : "";
|
|
208
207
|
if (!message) throw new Error("message is required");
|
|
208
|
+
var created = null;
|
|
209
|
+
if (caller && !store.groupForMember(caller.localId)) {
|
|
210
|
+
created = createWorkerForDriver(caller, args);
|
|
211
|
+
}
|
|
212
|
+
var resolved = groupAndPartner(caller);
|
|
209
213
|
var wait = args.wait !== false;
|
|
210
214
|
var timeout = Number.isFinite(args.timeoutSeconds) ? Math.floor(args.timeoutSeconds) : 300;
|
|
211
215
|
timeout = Math.max(1, Math.min(900, timeout));
|
|
@@ -243,9 +247,14 @@ function attachSessionPair(ctx) {
|
|
|
243
247
|
if (!wait) {
|
|
244
248
|
token.detached = true;
|
|
245
249
|
monitorPartner(resolved.group, caller, partner, token);
|
|
246
|
-
return toolResult({ status: "running", partnerId: partner.localId, hint: "The completed result will be pushed back automatically. Use read_partner only for an interim status check." });
|
|
250
|
+
return toolResult({ status: "running", partnerId: partner.localId, workerCreated: !!created, hint: "The completed result will be pushed back automatically. Use read_partner only for an interim status check." });
|
|
251
|
+
}
|
|
252
|
+
var completed = await waitForPartner(resolved.group, caller, partner, token, timeout);
|
|
253
|
+
if (created) {
|
|
254
|
+
completed.workerCreated = true;
|
|
255
|
+
completed.partnerId = partner.localId;
|
|
247
256
|
}
|
|
248
|
-
return toolResult(
|
|
257
|
+
return toolResult(completed);
|
|
249
258
|
} catch (e) {
|
|
250
259
|
if (caller) {
|
|
251
260
|
var group = store.groupForMember(caller.localId);
|
|
@@ -277,10 +286,46 @@ function attachSessionPair(ctx) {
|
|
|
277
286
|
}
|
|
278
287
|
}
|
|
279
288
|
|
|
289
|
+
function interruptPartner(args, caller) {
|
|
290
|
+
try {
|
|
291
|
+
var resolved = groupAndPartner(caller);
|
|
292
|
+
var partner = resolved.partner;
|
|
293
|
+
if (!partner.isProcessing && !partner._queryStarting) {
|
|
294
|
+
return toolResult({ status: "idle", partnerId: partner.localId, title: partner.title || "New Session" });
|
|
295
|
+
}
|
|
296
|
+
partner.taskStopRequested = true;
|
|
297
|
+
if (partner.abortController) partner.abortController.abort();
|
|
298
|
+
return toolResult({ status: "interrupting", partnerId: partner.localId, title: partner.title || "New Session" });
|
|
299
|
+
} catch (e) {
|
|
300
|
+
return toolError(e);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function closePartner(args, caller) {
|
|
305
|
+
try {
|
|
306
|
+
var resolved = groupAndPartner(caller);
|
|
307
|
+
var partner = resolved.partner;
|
|
308
|
+
var interrupted = !!(partner.isProcessing || partner._queryStarting);
|
|
309
|
+
if (interrupted) {
|
|
310
|
+
partner.taskStopRequested = true;
|
|
311
|
+
if (partner.abortController) partner.abortController.abort();
|
|
312
|
+
}
|
|
313
|
+
if (partner._pairDelegation) finishDelegation(resolved.group, caller, partner, partner._pairDelegation);
|
|
314
|
+
var ws = { _clayUser: caller.ownerId ? { id: caller.ownerId } : null };
|
|
315
|
+
var result = store.dissolve(ws, { id: resolved.group.id });
|
|
316
|
+
if (!result.ok) throw new Error(result.error || "could not close the Worker pair");
|
|
317
|
+
return toolResult({ status: "closed", partnerId: partner.localId, interrupted: interrupted, historyPreserved: true });
|
|
318
|
+
} catch (e) {
|
|
319
|
+
return toolError(e);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
280
323
|
function getToolDefs(boundSession) {
|
|
281
324
|
if (!boundSession) return pairMcp.getToolDefs({
|
|
282
325
|
send: function () { return toolError(new Error("send_to_partner requires a session-bound tool server")); },
|
|
283
326
|
read: function () { return toolError(new Error("read_partner requires a session-bound tool server")); },
|
|
327
|
+
interrupt: function () { return toolError(new Error("interrupt_partner requires a session-bound tool server")); },
|
|
328
|
+
close: function () { return toolError(new Error("close_partner requires a session-bound tool server")); },
|
|
284
329
|
});
|
|
285
330
|
// Mount whenever we have a bound session: MCP servers are fixed for the
|
|
286
331
|
// lifetime of a query, and Claude queries live across turns, so gating on
|
|
@@ -295,6 +340,8 @@ function attachSessionPair(ctx) {
|
|
|
295
340
|
var tools = pairMcp.getToolDefs({
|
|
296
341
|
send: function (args) { return sendToPartner(args, boundSession); },
|
|
297
342
|
read: function (args) { return readPartner(args, boundSession); },
|
|
343
|
+
interrupt: function (args) { return interruptPartner(args, boundSession); },
|
|
344
|
+
close: function (args) { return closePartner(args, boundSession); },
|
|
298
345
|
});
|
|
299
346
|
return tools.concat(workerProposal.getToolDefs(boundSession));
|
|
300
347
|
}
|
|
@@ -343,6 +390,37 @@ function attachSessionPair(ctx) {
|
|
|
343
390
|
return { driver: driver, worker: worker, group: result.group };
|
|
344
391
|
}
|
|
345
392
|
|
|
393
|
+
function createWorkerForDriver(driver, args) {
|
|
394
|
+
if (ctx.isMate) throw new Error("Pair sessions are only available in projects");
|
|
395
|
+
var installed = sm.installedVendors || [];
|
|
396
|
+
if (installed.length === 0) throw new Error("no coding agent is installed for a Worker session");
|
|
397
|
+
var vendor = typeof args.workerVendor === "string" ? args.workerVendor.trim() : "";
|
|
398
|
+
if (vendor) validateVendor(vendor);
|
|
399
|
+
if (!vendor) {
|
|
400
|
+
if (installed.indexOf("codex") !== -1 && driver.vendor !== "codex") vendor = "codex";
|
|
401
|
+
else {
|
|
402
|
+
vendor = installed[0];
|
|
403
|
+
for (var i = 0; i < installed.length; i++) {
|
|
404
|
+
if (installed[i] !== driver.vendor) { vendor = installed[i]; break; }
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
var ws = {
|
|
409
|
+
_clayActiveSession: driver.localId,
|
|
410
|
+
_clayUser: driver.ownerId ? { id: driver.ownerId } : null,
|
|
411
|
+
};
|
|
412
|
+
var created = createPairRecord(ws, {
|
|
413
|
+
driver: { sessionId: driver.localId },
|
|
414
|
+
worker: {
|
|
415
|
+
vendor: vendor,
|
|
416
|
+
model: typeof args.workerModel === "string" ? args.workerModel : "",
|
|
417
|
+
effort: typeof args.workerEffort === "string" ? args.workerEffort : "",
|
|
418
|
+
},
|
|
419
|
+
});
|
|
420
|
+
sm.sendToSession(driver, { type: "pair_session_created", ok: true, group: created.group });
|
|
421
|
+
return created;
|
|
422
|
+
}
|
|
423
|
+
|
|
346
424
|
function createPair(ws, msg) {
|
|
347
425
|
try {
|
|
348
426
|
var created = createPairRecord(ws, msg);
|
|
@@ -390,7 +468,9 @@ function attachSessionPair(ctx) {
|
|
|
390
468
|
var group = store.groupForMember(session.localId);
|
|
391
469
|
var pairPrompt = "";
|
|
392
470
|
if (group && group.pair && group.pair.driverId === session.localId) {
|
|
393
|
-
pairPrompt = "You are the Driver in a two-agent pair. The tools send_to_partner and
|
|
471
|
+
pairPrompt = "You are the Driver in a two-agent pair. The tools send_to_partner, read_partner, interrupt_partner, and close_partner are provided directly to you. Use send_to_partner to delegate concrete bounded work to the Worker. The human may also message or stop the Worker directly; treat that intervention as authoritative. Use close_partner immediately when the human asks to close, dismiss, or remove the Worker pane; it preserves the session history. A completed Worker turn leaves the Worker session available for more work. Use interrupt_partner when execution must stop or change direction but the pair should remain open; its work is partial and unverified until you review it. If review or user feedback requires corrections to the Worker's implementation, delegate a follow-up turn to that Worker instead of editing the Worker-owned files yourself. If the Worker is no longer available, keep the work in the visible Worker flow and use send_to_partner again after the stale pair is removed; never substitute a background session. If a non-waiting delegation finishes later, Clay pushes the result back and resumes you automatically; use read_partner only when you need an interim status check. Integrate and verify the final outcome. Do not search the project for implementations of these tools, and do not delegate work that must be performed sequentially in this same session.";
|
|
472
|
+
} else if (!group && !ctx.isMate) {
|
|
473
|
+
pairPrompt = "When the user asks you to open or use a visible Worker, call send_to_partner directly. Clay will create the paired Worker session and open it in the right pane automatically before delivering the task. Do not ask the user to start split mode and do not use a background sub-agent as a substitute for a visible Worker.";
|
|
394
474
|
}
|
|
395
475
|
return [pairPrompt, workerProposal.getSystemPrompt(session)].filter(Boolean).join("\n\n");
|
|
396
476
|
}
|
|
@@ -247,7 +247,7 @@ function attachWorkerProposal(ctx) {
|
|
|
247
247
|
});
|
|
248
248
|
var followup;
|
|
249
249
|
if (result.status === "complete") {
|
|
250
|
-
followup = "[Worker execution completed]\nReview and verify the Worker's result. The Worker session remains available: if the implementation needs corrections or additional edits, send a follow-up with send_to_partner instead of taking over the Worker-owned files yourself. If that Worker is no longer available,
|
|
250
|
+
followup = "[Worker execution completed]\nReview and verify the Worker's result. The Worker session remains available: if the implementation needs corrections or additional edits, send a follow-up with send_to_partner instead of taking over the Worker-owned files yourself. If that Worker is no longer available, keep the work in the visible Worker flow and use send_to_partner again after the stale pair is removed; never substitute a background session.\n\n" + (result.response || "The Worker completed without a text summary.");
|
|
251
251
|
} else if (result.status === "interrupted") {
|
|
252
252
|
followup = "[Worker execution interrupted]\nThe user interrupted the Worker mid-turn. Its work is PARTIAL and unverified — do not treat it as finished. Review what was done and decide next steps with the user.";
|
|
253
253
|
} else if (result.status === "running") {
|
package/lib/public/css/pane.css
CHANGED
|
@@ -137,13 +137,7 @@ body.pane-mode #input-area {
|
|
|
137
137
|
.split-pair-role-driver { color: var(--accent); background: color-mix(in srgb, var(--accent) 13%, transparent); }
|
|
138
138
|
.split-pair-role-worker { color: var(--text-secondary); background: var(--bg-alt); }
|
|
139
139
|
|
|
140
|
-
/*
|
|
141
|
-
button.split-pair-role {
|
|
142
|
-
border: 0;
|
|
143
|
-
cursor: pointer;
|
|
144
|
-
font-family: inherit;
|
|
145
|
-
}
|
|
146
|
-
button.split-pair-role:hover { filter: brightness(1.25); }
|
|
140
|
+
/* Pair roles are status labels. Control remains with the Driver through MCP. */
|
|
147
141
|
|
|
148
142
|
/* Ad-hoc splits: role assignment in each pane header. ALWAYS fully
|
|
149
143
|
visible; hover/opacity gating made the control undiscoverable. */
|
|
@@ -265,16 +265,10 @@ export function syncPairChrome(host, split) {
|
|
|
265
265
|
(function (sessionId, header) {
|
|
266
266
|
var isDriver = sessionId === group.pair.driverId;
|
|
267
267
|
var role = isDriver ? "Driver" : "Worker";
|
|
268
|
-
var badge = document.createElement("
|
|
269
|
-
badge.type = "button";
|
|
268
|
+
var badge = document.createElement("span");
|
|
270
269
|
badge.className = "split-pair-role split-pair-role-" + role.toLowerCase();
|
|
271
270
|
badge.textContent = role;
|
|
272
|
-
badge.title = isDriver
|
|
273
|
-
? "Driver — click to clear the Driver/Worker roles"
|
|
274
|
-
: "Worker — click to make this session the Driver instead";
|
|
275
|
-
badge.addEventListener("click", function () {
|
|
276
|
-
sendSetPair(group.id, isDriver ? null : sessionId);
|
|
277
|
-
});
|
|
271
|
+
badge.title = isDriver ? "Driver controls this Worker" : "Worker controlled by the Driver";
|
|
278
272
|
var title = header.querySelector(".split-pane-title");
|
|
279
273
|
var access = header.querySelector(".split-pane-full-access");
|
|
280
274
|
header.insertBefore(badge, access ? access.nextSibling : (title ? title.nextSibling : null));
|
|
@@ -332,8 +332,11 @@ function activateTab(termId) {
|
|
|
332
332
|
if (tab.xterm) keyToolbar.bindXterm(tab.xterm);
|
|
333
333
|
}
|
|
334
334
|
|
|
335
|
-
// Mobile viewport handling
|
|
336
|
-
|
|
335
|
+
// Mobile viewport handling. Desktop browsers also expose visualViewport,
|
|
336
|
+
// but applying the full viewport height there makes this panel taller than
|
|
337
|
+
// #main-panels. Because the desktop panel is vertically centered, its top
|
|
338
|
+
// edge (including the header) is then clipped when the browser is resized.
|
|
339
|
+
if (isTouchDevice && window.visualViewport && !viewportHandler) {
|
|
337
340
|
viewportHandler = function () {
|
|
338
341
|
ctx.terminalContainerEl.style.height = window.visualViewport.height + "px";
|
|
339
342
|
fitTerminal();
|
|
@@ -6,11 +6,14 @@ function getToolDefs(handlers) {
|
|
|
6
6
|
return [
|
|
7
7
|
{
|
|
8
8
|
name: "send_to_partner",
|
|
9
|
-
description: "Delegate one concrete task to the
|
|
9
|
+
description: "Delegate one concrete task to the Worker. If this session has no pair yet, Clay creates a real Worker session and opens it visibly in the right pane before starting the task. A completed turn does not end the Worker session: reuse the same 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.",
|
|
10
10
|
inputSchema: buildShape({
|
|
11
11
|
message: { type: "string", description: "The complete task or question for the partner." },
|
|
12
12
|
wait: { type: "boolean", description: "Wait for the partner's turn to finish. Defaults to true." },
|
|
13
13
|
timeoutSeconds: { type: "number", description: "Maximum wait in seconds, from 1 to 900. Defaults to 300." },
|
|
14
|
+
workerVendor: { type: "string", description: "Optional Worker vendor to use only when creating a new pair. Defaults to a suitable installed vendor." },
|
|
15
|
+
workerModel: { type: "string", description: "Optional Worker model to use only when creating a new pair." },
|
|
16
|
+
workerEffort: { type: "string", description: "Optional Worker reasoning effort to use only when creating a new pair." },
|
|
14
17
|
}, ["message"]),
|
|
15
18
|
handler: function (args) { return handlers.send(args || {}); },
|
|
16
19
|
},
|
|
@@ -22,6 +25,18 @@ function getToolDefs(handlers) {
|
|
|
22
25
|
}),
|
|
23
26
|
handler: function (args) { return handlers.read(args || {}); },
|
|
24
27
|
},
|
|
28
|
+
{
|
|
29
|
+
name: "interrupt_partner",
|
|
30
|
+
description: "Interrupt the Worker's current task. Use this when the task is going in the wrong direction, needs to be reprioritized, or must stop before the next instruction. The Worker returns any partial result to you for review.",
|
|
31
|
+
inputSchema: buildShape({}),
|
|
32
|
+
handler: function (args) { return handlers.interrupt(args || {}); },
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "close_partner",
|
|
36
|
+
description: "Close the visible Worker pane and dissolve the Driver/Worker pair while preserving both session histories. If the Worker is still running, Clay interrupts it before closing the pair.",
|
|
37
|
+
inputSchema: buildShape({}),
|
|
38
|
+
handler: function (args) { return handlers.close(args || {}); },
|
|
39
|
+
},
|
|
25
40
|
];
|
|
26
41
|
}
|
|
27
42
|
|
package/package.json
CHANGED