clay-server 4.7.1-beta.2 → 4.7.1-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.
- package/lib/project-connection.js +1 -1
- package/lib/project-elicitation-response.js +42 -0
- package/lib/project-sessions.js +3 -15
- package/lib/public/app.js +1 -0
- package/lib/public/css/elicitation-form.css +33 -0
- package/lib/public/modules/app-connection.js +2 -0
- package/lib/public/modules/app-message-router.js +5 -0
- package/lib/public/modules/app-messages.js +19 -11
- package/lib/public/modules/chat-render-runtime.js +2 -2
- package/lib/public/modules/elicitation-form.js +260 -0
- package/lib/public/modules/elicitation-schema.js +139 -0
- package/lib/public/modules/history-replay-batch.js +113 -0
- package/lib/public/modules/tools.js +14 -179
- package/lib/public/style.css +1 -0
- package/lib/sdk-bridge.js +7 -4
- package/lib/sdk-worker.js +1 -1
- package/lib/ws-schema.js +1 -0
- package/lib/yoke/acp-agent-profiles.js +1 -1
- package/lib/yoke/acp-event-normalizer.js +17 -3
- package/lib/yoke/acp-query-handle.js +9 -9
- package/lib/yoke/acp-user-input.js +70 -0
- package/lib/yoke/adapters/acp.js +22 -5
- package/lib/yoke/adapters/antigravity.js +55 -6
- package/lib/yoke/adapters/claude-worker.js +1 -1
- package/lib/yoke/adapters/codex.js +5 -2
- package/lib/yoke/adapters/kiro.js +16 -17
- package/lib/yoke/claude-user-input.js +2 -4
- package/lib/yoke/codex-user-input.js +7 -4
- package/lib/yoke/elicitation-schema.js +204 -0
- package/lib/yoke/interface.js +12 -7
- package/lib/yoke/user-input.js +34 -25
- package/package.json +1 -1
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
|
|
3
|
+
// Recovery bounds for a replay whose history_done never arrives. Either limit
|
|
4
|
+
// restores everything received so far and lets later messages through, so a
|
|
5
|
+
// lost terminator cannot capture live output indefinitely.
|
|
6
|
+
export var HISTORY_REPLAY_TIMEOUT_MS = 20000;
|
|
7
|
+
export var HISTORY_REPLAY_MAX_EVENTS = 200000;
|
|
8
|
+
export var HISTORY_REPLAY_MAX_TEXT = 16 * 1024 * 1024;
|
|
9
|
+
var MAX_LOGGED_FAILURES = 5;
|
|
10
|
+
|
|
11
|
+
function cancelTimer(batch) {
|
|
12
|
+
if (batch && batch.timer) {
|
|
13
|
+
clearTimeout(batch.timer);
|
|
14
|
+
batch.timer = null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function resetHistoryReplayBatch() {
|
|
19
|
+
cancelTimer(store.get('historyReplayBatch'));
|
|
20
|
+
store.set({ historyReplayBatch: null, replayingHistory: false });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function canCombine(previous, message) {
|
|
24
|
+
if (!previous || previous.type !== message.type || typeof previous.text !== 'string' || typeof message.text !== 'string') return false;
|
|
25
|
+
if (message.type !== 'delta' && message.type !== 'thinking_delta') return false;
|
|
26
|
+
// Keep identity/metadata boundaries intact. Timestamps on individual tokens
|
|
27
|
+
// can differ; the first token still supplies the message's display time.
|
|
28
|
+
var keys = Object.keys(previous).concat(Object.keys(message));
|
|
29
|
+
return keys.every(function (key) {
|
|
30
|
+
return key === 'text' || key === '_ts' || previous[key] === message[key];
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function finishText(batch) {
|
|
35
|
+
if (!batch.textParts) return;
|
|
36
|
+
batch.items[batch.items.length - 1].text = batch.textParts.join('');
|
|
37
|
+
batch.textParts = null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Diagnostics name the entry type and error class only. Error messages and
|
|
41
|
+
// stacks can quote transcript content, so they are never logged.
|
|
42
|
+
function describeFailure(msg, err) {
|
|
43
|
+
var type = msg && typeof msg.type === 'string' && /^[A-Za-z0-9_]{1,64}$/.test(msg.type) ? msg.type : 'unknown';
|
|
44
|
+
var name = err && typeof err.name === 'string' && /^[A-Za-z]{1,64}$/.test(err.name) ? err.name : 'Error';
|
|
45
|
+
return type + ' entry failed with ' + name;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function deliver(batch, done, dispatch) {
|
|
49
|
+
cancelTimer(batch);
|
|
50
|
+
finishText(batch);
|
|
51
|
+
if (store.get('historyReplayBatch') === batch) store.set({ historyReplayBatch: null });
|
|
52
|
+
var entries = batch.items.concat([done]);
|
|
53
|
+
var failures = 0;
|
|
54
|
+
try {
|
|
55
|
+
// Each entry is isolated like a separate socket message: one bad record
|
|
56
|
+
// must not discard the rest of the transcript or history_done cleanup.
|
|
57
|
+
for (var i = 0; i < entries.length; i++) {
|
|
58
|
+
try {
|
|
59
|
+
dispatch(entries[i]);
|
|
60
|
+
} catch (err) {
|
|
61
|
+
failures++;
|
|
62
|
+
if (failures <= MAX_LOGGED_FAILURES) {
|
|
63
|
+
console.error('[history-replay] Restored history ' + (i + 1) + '/' + entries.length + ': ' + describeFailure(entries[i], err));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (failures > MAX_LOGGED_FAILURES) {
|
|
68
|
+
console.error('[history-replay] ' + (failures - MAX_LOGGED_FAILURES) + ' more restored history failures were not logged.');
|
|
69
|
+
}
|
|
70
|
+
} finally {
|
|
71
|
+
store.set({ replayingHistory: false });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function recover(batch, reason, dispatch) {
|
|
76
|
+
if (store.get('historyReplayBatch') !== batch) return;
|
|
77
|
+
console.warn('[history-replay] history_done missing after ' + batch.received + ' events (' + reason + '); restoring received history.');
|
|
78
|
+
deliver(batch, { type: 'history_done', replayRecovered: true }, dispatch);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// WebSocket history arrives across many browser tasks. Collect it before
|
|
82
|
+
// calling the normal handlers so the browser cannot paint intermediate tools,
|
|
83
|
+
// Thinking segments or old answers between those tasks.
|
|
84
|
+
export function receiveHistoryReplay(msg, dispatch) {
|
|
85
|
+
if (msg.type === 'session_switched' || msg.type === 'project_info') resetHistoryReplayBatch();
|
|
86
|
+
if (msg.type === 'history_meta') {
|
|
87
|
+
cancelTimer(store.get('historyReplayBatch'));
|
|
88
|
+
var started = { items: [msg], textParts: null, received: 1, text: 0, timer: null };
|
|
89
|
+
started.timer = setTimeout(function () { recover(started, 'timeout', dispatch); }, HISTORY_REPLAY_TIMEOUT_MS);
|
|
90
|
+
store.set({ historyReplayBatch: started });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
var batch = store.get('historyReplayBatch');
|
|
94
|
+
if (!batch) {
|
|
95
|
+
dispatch(msg);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (msg.type === 'history_done') {
|
|
99
|
+
deliver(batch, msg, dispatch);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
batch.received++;
|
|
103
|
+
if (typeof msg.text === 'string') batch.text += msg.text.length;
|
|
104
|
+
var previous = batch.items[batch.items.length - 1];
|
|
105
|
+
if (canCombine(previous, msg)) {
|
|
106
|
+
if (!batch.textParts) batch.textParts = [previous.text];
|
|
107
|
+
batch.textParts.push(msg.text);
|
|
108
|
+
} else {
|
|
109
|
+
finishText(batch);
|
|
110
|
+
batch.items.push(Object.assign({}, msg));
|
|
111
|
+
}
|
|
112
|
+
if (batch.received >= HISTORY_REPLAY_MAX_EVENTS || batch.text >= HISTORY_REPLAY_MAX_TEXT) recover(batch, 'size', dispatch);
|
|
113
|
+
}
|
|
@@ -9,6 +9,7 @@ import { store } from './store.js';
|
|
|
9
9
|
import { applyPermissionHints } from './permission-hints.js';
|
|
10
10
|
import { VENDOR_NAMES } from './app-rendering.js';
|
|
11
11
|
import { startThinkingSegment, appendThinkingText, stopThinkingSegment, resetThinkingTurn, clearThinkingState, saveThinkingState, restoreThinkingState, updateThinkingTokenEstimate } from './thinking-lifecycle.js';
|
|
12
|
+
import { buildElicitationCard, markElicitationCard, showElicitationError } from './elicitation-form.js';
|
|
12
13
|
import { findAskUserCard, getAskUserRequestKey, hasAskUserRequest, rememberAskUserRequest, markAskUserRequestAnswered, saveAskUserState, restoreAskUserState, resetAskUserState } from './ask-user-state.js';
|
|
13
14
|
|
|
14
15
|
var ctx;
|
|
@@ -1026,204 +1027,38 @@ export function markPermissionCancelled(requestId) {
|
|
|
1026
1027
|
}
|
|
1027
1028
|
|
|
1028
1029
|
// --- MCP elicitation rendering ---
|
|
1030
|
+
// Form building, validation and resolved states live in elicitation-form.js.
|
|
1029
1031
|
|
|
1030
1032
|
var pendingElicitations = {};
|
|
1031
1033
|
|
|
1034
|
+
function findElicitationCard(requestId) {
|
|
1035
|
+
return pendingElicitations[requestId]
|
|
1036
|
+
|| ctx.messagesEl.querySelector('.elicitation-container[data-request-id="' + requestId + '"]');
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1032
1039
|
export function renderElicitationRequest(msg) {
|
|
1033
1040
|
if (pendingElicitations[msg.requestId]) return;
|
|
1034
1041
|
ctx.finalizeAssistantBlock();
|
|
1035
1042
|
stopThinking();
|
|
1036
1043
|
closeToolGroup();
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
container.className = "permission-container elicitation-container";
|
|
1040
|
-
container.dataset.requestId = msg.requestId;
|
|
1041
|
-
|
|
1042
|
-
// Header
|
|
1043
|
-
var header = document.createElement("div");
|
|
1044
|
-
header.className = "permission-header";
|
|
1045
|
-
header.innerHTML =
|
|
1046
|
-
'<span class="permission-icon">' + iconHtml("key") + '</span>' +
|
|
1047
|
-
'<span class="permission-title">' + escapeHtml(msg.serverName || "MCP Server") + ' requests input</span>';
|
|
1048
|
-
|
|
1049
|
-
// Body
|
|
1050
|
-
var body = document.createElement("div");
|
|
1051
|
-
body.className = "permission-body";
|
|
1052
|
-
|
|
1053
|
-
if (msg.message) {
|
|
1054
|
-
var messageEl = document.createElement("div");
|
|
1055
|
-
messageEl.className = "permission-reason";
|
|
1056
|
-
messageEl.textContent = msg.message;
|
|
1057
|
-
body.appendChild(messageEl);
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
// Form fields (form mode) or URL button (url mode)
|
|
1061
|
-
var formData = {};
|
|
1062
|
-
|
|
1063
|
-
if (msg.mode === "url" && msg.url) {
|
|
1064
|
-
var urlInfo = document.createElement("div");
|
|
1065
|
-
urlInfo.className = "elicitation-url-info";
|
|
1066
|
-
urlInfo.style.cssText = "margin-top: 8px; font-size: 12px; color: var(--text-muted);";
|
|
1067
|
-
urlInfo.textContent = "Opens: " + msg.url;
|
|
1068
|
-
body.appendChild(urlInfo);
|
|
1069
|
-
} else if (msg.requestedSchema && msg.requestedSchema.properties) {
|
|
1070
|
-
var formEl = document.createElement("div");
|
|
1071
|
-
formEl.className = "elicitation-form";
|
|
1072
|
-
formEl.style.cssText = "margin-top: 8px; display: flex; flex-direction: column; gap: 8px;";
|
|
1073
|
-
|
|
1074
|
-
var props = msg.requestedSchema.properties;
|
|
1075
|
-
var required = msg.requestedSchema.required || [];
|
|
1076
|
-
var propNames = Object.keys(props);
|
|
1077
|
-
for (var i = 0; i < propNames.length; i++) {
|
|
1078
|
-
var propName = propNames[i];
|
|
1079
|
-
var prop = props[propName];
|
|
1080
|
-
var isRequired = required.indexOf(propName) !== -1;
|
|
1081
|
-
|
|
1082
|
-
var fieldWrapper = document.createElement("div");
|
|
1083
|
-
fieldWrapper.style.cssText = "display: flex; flex-direction: column; gap: 2px;";
|
|
1084
|
-
|
|
1085
|
-
var label = document.createElement("label");
|
|
1086
|
-
label.style.cssText = "font-size: 12px; font-weight: 500; color: var(--text-secondary);";
|
|
1087
|
-
label.textContent = propName + (isRequired ? " *" : "");
|
|
1088
|
-
if (prop.description) {
|
|
1089
|
-
label.title = prop.description;
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
var input;
|
|
1093
|
-
if (prop.type === "boolean") {
|
|
1094
|
-
input = document.createElement("input");
|
|
1095
|
-
input.type = "checkbox";
|
|
1096
|
-
input.dataset.propName = propName;
|
|
1097
|
-
input.dataset.propType = "boolean";
|
|
1098
|
-
} else if (prop.enum) {
|
|
1099
|
-
input = document.createElement("select");
|
|
1100
|
-
input.dataset.propName = propName;
|
|
1101
|
-
input.dataset.propType = "enum";
|
|
1102
|
-
input.style.cssText = "padding: 4px 8px; border-radius: 4px; border: 1px solid var(--border); background: var(--input-bg); color: var(--text-primary); font-size: 13px;";
|
|
1103
|
-
for (var ei = 0; ei < prop.enum.length; ei++) {
|
|
1104
|
-
var opt = document.createElement("option");
|
|
1105
|
-
opt.value = prop.enum[ei];
|
|
1106
|
-
opt.textContent = prop.enum[ei];
|
|
1107
|
-
input.appendChild(opt);
|
|
1108
|
-
}
|
|
1109
|
-
} else {
|
|
1110
|
-
input = document.createElement("input");
|
|
1111
|
-
input.type = prop.type === "number" || prop.type === "integer" ? "number" : "text";
|
|
1112
|
-
input.dataset.propName = propName;
|
|
1113
|
-
input.dataset.propType = prop.type || "string";
|
|
1114
|
-
input.placeholder = prop.description || propName;
|
|
1115
|
-
input.style.cssText = "padding: 4px 8px; border-radius: 4px; border: 1px solid var(--border); background: var(--input-bg); color: var(--text-primary); font-size: 13px;";
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
fieldWrapper.appendChild(label);
|
|
1119
|
-
fieldWrapper.appendChild(input);
|
|
1120
|
-
formEl.appendChild(fieldWrapper);
|
|
1121
|
-
}
|
|
1122
|
-
body.appendChild(formEl);
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
// Actions
|
|
1126
|
-
var actions = document.createElement("div");
|
|
1127
|
-
actions.className = "permission-actions";
|
|
1128
|
-
|
|
1129
|
-
var acceptBtn = document.createElement("button");
|
|
1130
|
-
acceptBtn.className = "permission-btn permission-allow";
|
|
1131
|
-
|
|
1132
|
-
if (msg.mode === "url" && msg.url) {
|
|
1133
|
-
acceptBtn.textContent = "Open & Approve";
|
|
1134
|
-
acceptBtn.addEventListener("click", function () {
|
|
1135
|
-
window.open(msg.url, "_blank");
|
|
1136
|
-
sendElicitationResponse(container, msg.requestId, "accept", {});
|
|
1137
|
-
});
|
|
1138
|
-
} else {
|
|
1139
|
-
acceptBtn.textContent = "Submit";
|
|
1140
|
-
acceptBtn.addEventListener("click", function () {
|
|
1141
|
-
// Collect form values
|
|
1142
|
-
var content = {};
|
|
1143
|
-
var inputs = container.querySelectorAll("[data-prop-name]");
|
|
1144
|
-
for (var j = 0; j < inputs.length; j++) {
|
|
1145
|
-
var inp = inputs[j];
|
|
1146
|
-
var name = inp.dataset.propName;
|
|
1147
|
-
var pType = inp.dataset.propType;
|
|
1148
|
-
if (pType === "boolean") {
|
|
1149
|
-
content[name] = inp.checked;
|
|
1150
|
-
} else if (pType === "number" || pType === "integer") {
|
|
1151
|
-
content[name] = Number(inp.value);
|
|
1152
|
-
} else {
|
|
1153
|
-
content[name] = inp.value;
|
|
1154
|
-
}
|
|
1155
|
-
}
|
|
1156
|
-
sendElicitationResponse(container, msg.requestId, "accept", content);
|
|
1157
|
-
});
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
var denyBtn = document.createElement("button");
|
|
1161
|
-
denyBtn.className = "permission-btn permission-deny";
|
|
1162
|
-
denyBtn.textContent = "Deny";
|
|
1163
|
-
denyBtn.addEventListener("click", function () {
|
|
1164
|
-
sendElicitationResponse(container, msg.requestId, "reject", null);
|
|
1044
|
+
var container = buildElicitationCard(msg, function () {
|
|
1045
|
+
if (ctx.stopUrgentBlink) ctx.stopUrgentBlink();
|
|
1165
1046
|
});
|
|
1166
|
-
|
|
1167
|
-
actions.appendChild(acceptBtn);
|
|
1168
|
-
actions.appendChild(denyBtn);
|
|
1169
|
-
|
|
1170
|
-
container.appendChild(header);
|
|
1171
|
-
container.appendChild(body);
|
|
1172
|
-
container.appendChild(actions);
|
|
1173
1047
|
ctx.addToMessages(container);
|
|
1174
|
-
|
|
1175
1048
|
pendingElicitations[msg.requestId] = container;
|
|
1176
1049
|
refreshIcons();
|
|
1177
1050
|
ctx.setActivity(null);
|
|
1178
1051
|
maybeScrollToBottom();
|
|
1179
1052
|
}
|
|
1180
1053
|
|
|
1181
|
-
function
|
|
1182
|
-
|
|
1183
|
-
container.classList.add("resolved");
|
|
1184
|
-
if (ctx.stopUrgentBlink) ctx.stopUrgentBlink();
|
|
1185
|
-
|
|
1186
|
-
var label = action === "reject" ? "Denied" : "Submitted";
|
|
1187
|
-
var resolvedClass = action === "reject" ? "resolved-denied" : "resolved-allowed";
|
|
1188
|
-
container.classList.add(resolvedClass);
|
|
1189
|
-
|
|
1190
|
-
var actions = container.querySelector(".permission-actions");
|
|
1191
|
-
if (actions) {
|
|
1192
|
-
actions.innerHTML = '<span class="permission-decision-label">' + label + '</span>';
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
if (ctx.ws && ctx.connected) {
|
|
1196
|
-
var msg = {
|
|
1197
|
-
type: "elicitation_response",
|
|
1198
|
-
requestId: requestId,
|
|
1199
|
-
action: action,
|
|
1200
|
-
};
|
|
1201
|
-
if (action === "accept" && content) {
|
|
1202
|
-
msg.content = content;
|
|
1203
|
-
}
|
|
1204
|
-
ctx.ws.send(JSON.stringify(msg));
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1054
|
+
export function markElicitationResolved(requestId, action) {
|
|
1055
|
+
markElicitationCard(findElicitationCard(requestId), action);
|
|
1207
1056
|
delete pendingElicitations[requestId];
|
|
1208
1057
|
}
|
|
1209
1058
|
|
|
1210
|
-
export function
|
|
1211
|
-
|
|
1212
|
-
if (
|
|
1213
|
-
container = ctx.messagesEl.querySelector('.elicitation-container[data-request-id="' + requestId + '"]');
|
|
1214
|
-
}
|
|
1215
|
-
if (!container || container.classList.contains("resolved")) return;
|
|
1216
|
-
|
|
1217
|
-
container.classList.add("resolved");
|
|
1218
|
-
var isDeny = action === "reject";
|
|
1219
|
-
container.classList.add(isDeny ? "resolved-denied" : "resolved-allowed");
|
|
1220
|
-
|
|
1221
|
-
var label = isDeny ? "Denied" : "Submitted";
|
|
1222
|
-
var actionsEl = container.querySelector(".permission-actions");
|
|
1223
|
-
if (actionsEl) {
|
|
1224
|
-
actionsEl.innerHTML = '<span class="permission-decision-label">' + label + '</span>';
|
|
1225
|
-
}
|
|
1226
|
-
delete pendingElicitations[requestId];
|
|
1059
|
+
export function markElicitationError(requestId, error, expired) {
|
|
1060
|
+
showElicitationError(findElicitationCard(requestId), error, expired);
|
|
1061
|
+
if (expired) delete pendingElicitations[requestId];
|
|
1227
1062
|
}
|
|
1228
1063
|
|
|
1229
1064
|
// --- Host user dialog (SDK request_user_dialog) ---
|
package/lib/public/style.css
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
@import url("css/messages.css?v=20260828-delegated-muted1");
|
|
14
14
|
@import url("css/generated-images.css?v=20260829-save-details1");
|
|
15
15
|
@import url("css/rewind.css");
|
|
16
|
+
@import url("css/elicitation-form.css");
|
|
16
17
|
@import url("css/input.css?v=20261002-stop-inline1");
|
|
17
18
|
@import url("css/pending-message-queue.css?v=20260914-queue1");
|
|
18
19
|
@import url("css/filebrowser.css?v=20260829-terminal-canvas1");
|
package/lib/sdk-bridge.js
CHANGED
|
@@ -392,9 +392,9 @@ function createSDKBridge(opts) {
|
|
|
392
392
|
// --- MCP elicitation ---
|
|
393
393
|
|
|
394
394
|
function handleElicitation(session, request, opts) {
|
|
395
|
-
// Ralph Loop:
|
|
395
|
+
// Ralph Loop: autonomous mode explicitly declines elicitation.
|
|
396
396
|
if (session.loop && session.loop.active && session.loop.role !== "crafting") {
|
|
397
|
-
return Promise.resolve({ action: "
|
|
397
|
+
return Promise.resolve({ action: "decline" });
|
|
398
398
|
}
|
|
399
399
|
|
|
400
400
|
return new Promise(function(resolve) {
|
|
@@ -427,8 +427,10 @@ function createSDKBridge(opts) {
|
|
|
427
427
|
|
|
428
428
|
if (opts.signal) {
|
|
429
429
|
opts.signal.addEventListener("abort", function() {
|
|
430
|
+
if (!session.pendingElicitations || !session.pendingElicitations[requestId]) return;
|
|
430
431
|
delete session.pendingElicitations[requestId];
|
|
431
|
-
|
|
432
|
+
sendAndRecord(session, { type: "elicitation_resolved", requestId: requestId, action: "cancel" });
|
|
433
|
+
resolve({ action: "cancel" });
|
|
432
434
|
});
|
|
433
435
|
}
|
|
434
436
|
});
|
|
@@ -1849,7 +1851,8 @@ function createSDKBridge(opts) {
|
|
|
1849
1851
|
}
|
|
1850
1852
|
return handleElicitation(session, request.diagnostics.elicitation, { signal: elicitationController.signal }).then(function(result) {
|
|
1851
1853
|
if (result && result.action === "accept") return respond.submitContent(result.content || {});
|
|
1852
|
-
return respond.
|
|
1854
|
+
if (result && (result.action === "decline" || result.action === "reject")) return respond.decline("Elicitation declined");
|
|
1855
|
+
return respond.cancel("Elicitation cancelled");
|
|
1853
1856
|
});
|
|
1854
1857
|
}
|
|
1855
1858
|
if (typeof opts.onUserInputRequest !== "function") return respond.cancel("Structured user input is unavailable.");
|
package/lib/sdk-worker.js
CHANGED
|
@@ -186,7 +186,7 @@ function onElicitation(request, opts) {
|
|
|
186
186
|
if (opts.signal) {
|
|
187
187
|
opts.signal.addEventListener("abort", function() {
|
|
188
188
|
delete pendingElicitations[requestId];
|
|
189
|
-
resolve({ action: "
|
|
189
|
+
resolve({ action: "cancel" });
|
|
190
190
|
});
|
|
191
191
|
}
|
|
192
192
|
});
|
package/lib/ws-schema.js
CHANGED
|
@@ -194,6 +194,7 @@ var schema = {
|
|
|
194
194
|
"elicitation_request": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Structured form elicitation request from assistant" },
|
|
195
195
|
"elicitation_response": { direction: "c2s", handler: "lib/project-sessions.js", description: "User response to an elicitation form" },
|
|
196
196
|
"elicitation_resolved": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Elicitation request resolved" },
|
|
197
|
+
"elicitation_error": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Submitted elicitation form failed server validation; the request stays pending" },
|
|
197
198
|
"user_dialog_request": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Host dialog request from assistant (SDK request_user_dialog)" },
|
|
198
199
|
"user_dialog_response": { direction: "c2s", handler: "lib/project-sessions.js", description: "User response to a host dialog request" },
|
|
199
200
|
"user_dialog_resolved": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Host dialog request resolved" },
|
|
@@ -185,7 +185,7 @@ var ACP_AGENT_PROFILES = {
|
|
|
185
185
|
displayName: "Grok Build",
|
|
186
186
|
binaryName: "grok",
|
|
187
187
|
overrideName: "GROK_CLI_PATH",
|
|
188
|
-
args: ["--no-auto-update", "--permission-mode", "
|
|
188
|
+
args: ["--no-auto-update", "--permission-mode", "default", "agent", "stdio"],
|
|
189
189
|
defaultModels: ["auto"],
|
|
190
190
|
defaultModel: "auto",
|
|
191
191
|
sessionResume: null,
|
|
@@ -95,9 +95,14 @@ function normalizeAcpUpdate(update, state) {
|
|
|
95
95
|
|
|
96
96
|
if (type === "tool_call" || type === "tool_call_update") {
|
|
97
97
|
var callId = update.toolCallId;
|
|
98
|
-
var name = toolNameForKind(update.kind, update.title);
|
|
99
|
-
if (callId
|
|
100
|
-
state.toolMeta[callId] = {
|
|
98
|
+
var name = update.name || toolNameForKind(update.kind, update.title);
|
|
99
|
+
if (callId) {
|
|
100
|
+
state.toolMeta[callId] = Object.assign({}, state.toolMeta[callId] || {}, {
|
|
101
|
+
kind: update.kind || (state.toolMeta[callId] && state.toolMeta[callId].kind),
|
|
102
|
+
name: update.name || (state.toolMeta[callId] && state.toolMeta[callId].name),
|
|
103
|
+
title: update.title || (state.toolMeta[callId] && state.toolMeta[callId].title),
|
|
104
|
+
rawInput: update.rawInput || (state.toolMeta[callId] && state.toolMeta[callId].rawInput) || {},
|
|
105
|
+
});
|
|
101
106
|
}
|
|
102
107
|
if (callId && !state.toolBlocks[callId]) {
|
|
103
108
|
state.toolBlocks[callId] = "blk_" + (++state.blockCounter);
|
|
@@ -142,6 +147,15 @@ function normalizeAcpUpdate(update, state) {
|
|
|
142
147
|
return events;
|
|
143
148
|
}
|
|
144
149
|
|
|
150
|
+
if (type === "available_commands_update") {
|
|
151
|
+
var commands = Array.isArray(update.availableCommands) ? update.availableCommands : [];
|
|
152
|
+
events.push({
|
|
153
|
+
yokeType: "commands_changed",
|
|
154
|
+
commandNames: commands.map(function(command) { return command && command.name; }).filter(Boolean),
|
|
155
|
+
});
|
|
156
|
+
return events;
|
|
157
|
+
}
|
|
158
|
+
|
|
145
159
|
events.push({
|
|
146
160
|
yokeType: "runtime_specific",
|
|
147
161
|
vendor: state.vendor,
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
// Shared ACP Query Handle
|
|
2
|
-
// -----------------------
|
|
3
|
-
// Implements the YOKE QueryHandle contract with standard ACP session methods.
|
|
4
1
|
var normalizer = require("./acp-event-normalizer");
|
|
5
2
|
var driverRuntime = require("./acp-driver-runtime");
|
|
6
3
|
var queryAbort = require("./query-abort");
|
|
4
|
+
var acpUserInput = require("./acp-user-input");
|
|
7
5
|
var uuidCounter = 0;
|
|
8
6
|
function generateUuid(vendor) {
|
|
9
7
|
uuidCounter++;
|
|
@@ -31,12 +29,11 @@ function createAcpQueryHandle(acp, queryOpts) {
|
|
|
31
29
|
var canUseTool = queryOpts.canUseTool || null;
|
|
32
30
|
var state = normalizer.createEventState({ vendor: vendor, contextWindow: queryOpts.contextWindow });
|
|
33
31
|
state.sessionId = queryOpts.resumeSessionId || null;
|
|
34
|
-
state.model = queryOpts.model || "auto";
|
|
32
|
+
state.model = queryOpts.model || "auto"; state.effort = queryOpts.effort || null;
|
|
35
33
|
state.done = false;
|
|
36
34
|
state.aborted = false;
|
|
37
35
|
state.loadingSession = false;
|
|
38
36
|
state.toolPolicy = "ask";
|
|
39
|
-
|
|
40
37
|
var eventBuffer = [];
|
|
41
38
|
var eventWaiting = null;
|
|
42
39
|
var iteratorDone = false;
|
|
@@ -45,7 +42,6 @@ function createAcpQueryHandle(acp, queryOpts) {
|
|
|
45
42
|
var messageQueueEnded = false;
|
|
46
43
|
var loopStarted = false;
|
|
47
44
|
var finishedNotified = false;
|
|
48
|
-
|
|
49
45
|
function driverContext(extra) {
|
|
50
46
|
return Object.assign({
|
|
51
47
|
vendor: vendor,
|
|
@@ -55,7 +51,6 @@ function createAcpQueryHandle(acp, queryOpts) {
|
|
|
55
51
|
queryOpts: queryOpts,
|
|
56
52
|
}, extra || {});
|
|
57
53
|
}
|
|
58
|
-
|
|
59
54
|
function notifyFinished() {
|
|
60
55
|
if (finishedNotified) return;
|
|
61
56
|
finishedNotified = true;
|
|
@@ -169,7 +164,7 @@ function createAcpQueryHandle(acp, queryOpts) {
|
|
|
169
164
|
return;
|
|
170
165
|
}
|
|
171
166
|
|
|
172
|
-
var toolName = normalizer.toolNameForKind(toolCall.kind || meta.kind, toolCall.title || meta.title);
|
|
167
|
+
var toolName = toolCall.name || meta.name || normalizer.toolNameForKind(toolCall.kind || meta.kind, toolCall.title || meta.title);
|
|
173
168
|
var input = toolCall.rawInput || meta.rawInput || { title: toolCall.title || meta.title };
|
|
174
169
|
var permission = driverRuntime.call(driver, "mapPermissionRequest", driverContext({
|
|
175
170
|
message: msg,
|
|
@@ -186,6 +181,10 @@ function createAcpQueryHandle(acp, queryOpts) {
|
|
|
186
181
|
}
|
|
187
182
|
|
|
188
183
|
function handleServerEvent(msg) {
|
|
184
|
+
if (msg.method === "elicitation/create") {
|
|
185
|
+
acpUserInput.handleElicitation(acp, msg, queryOpts.onUserInputRequest, queryOpts.onElicitation, cancellation.signal, vendor, driver && driver.displayName);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
189
188
|
if (msg.method === "session/request_permission") {
|
|
190
189
|
handlePermission(msg);
|
|
191
190
|
return;
|
|
@@ -386,6 +385,7 @@ function createAcpQueryHandle(acp, queryOpts) {
|
|
|
386
385
|
await cancellation.wait(driverRuntime.callAsync(driver, "afterSessionOpen", driverContext({ sessionResult: sessionResult }), function() {}));
|
|
387
386
|
await cancellation.wait(ensureSafePermissionMode());
|
|
388
387
|
if (state.model && state.model !== "auto") await setModel(state.model);
|
|
388
|
+
if (state.effort) await setEffort(state.effort);
|
|
389
389
|
if (queryOpts.mode) await setConfig("mode", "mode", queryOpts.mode);
|
|
390
390
|
|
|
391
391
|
while (!isCancelled()) {
|
|
@@ -451,7 +451,7 @@ function createAcpQueryHandle(acp, queryOpts) {
|
|
|
451
451
|
return setModel(model).then(function() { state.model = model; });
|
|
452
452
|
},
|
|
453
453
|
setEffort: function(effort) {
|
|
454
|
-
return setEffort(effort);
|
|
454
|
+
return setEffort(effort).then(function() { state.effort = effort; });
|
|
455
455
|
},
|
|
456
456
|
setToolPolicy: function(policy) {
|
|
457
457
|
state.toolPolicy = policy === "allow-all" ? "allow-all" : "ask";
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// ACP elicitation/create mapping (form mode only; Clay advertises
|
|
2
|
+
// clientCapabilities.elicitation.form and never url).
|
|
3
|
+
var userInput = require("./user-input");
|
|
4
|
+
var elicitationSchema = require("./elicitation-schema");
|
|
5
|
+
|
|
6
|
+
var INVALID_PARAMS = -32602;
|
|
7
|
+
|
|
8
|
+
// Maps a host elicitation result to an ACP response. Legacy "reject" is an
|
|
9
|
+
// explicit refusal; anything that is not accept or decline is a dismissal.
|
|
10
|
+
function protocolResponse(response, schema) {
|
|
11
|
+
var action = response && response.action;
|
|
12
|
+
if (action === "accept") return { action: "accept", content: elicitationSchema.validateContent(response.content || {}, schema) };
|
|
13
|
+
if (action === "decline" || action === "reject") return { action: "decline" };
|
|
14
|
+
return { action: "cancel" };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function handleElicitation(acp, msg, handler, elicitationHandler, signal, provider, displayName) {
|
|
18
|
+
var params = msg.params || {};
|
|
19
|
+
var mode = params.mode || "form";
|
|
20
|
+
if (mode !== "form") {
|
|
21
|
+
acp.respondError(msg.id, INVALID_PARAMS, "Unsupported elicitation mode: " + mode + ". Clay advertises form mode only.");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (typeof handler !== "function" && typeof elicitationHandler !== "function") {
|
|
25
|
+
acp.respond(msg.id, { action: "cancel" });
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
var request = {
|
|
29
|
+
serverName: params.serverName || displayName || "Agent",
|
|
30
|
+
message: params.message || "",
|
|
31
|
+
mode: "form",
|
|
32
|
+
requestedSchema: params.requestedSchema || null,
|
|
33
|
+
};
|
|
34
|
+
var settled = false;
|
|
35
|
+
var abortHandler = null;
|
|
36
|
+
function finish(response, error) {
|
|
37
|
+
if (settled) return;
|
|
38
|
+
settled = true;
|
|
39
|
+
if (signal && abortHandler && typeof signal.removeEventListener === "function") signal.removeEventListener("abort", abortHandler);
|
|
40
|
+
if (error) acp.respondError(msg.id, INVALID_PARAMS, error.message);
|
|
41
|
+
else acp.respond(msg.id, response);
|
|
42
|
+
}
|
|
43
|
+
if (typeof handler !== "function") {
|
|
44
|
+
abortHandler = function () { finish({ action: "cancel" }); };
|
|
45
|
+
if (signal && signal.aborted) return abortHandler();
|
|
46
|
+
if (signal && typeof signal.addEventListener === "function") signal.addEventListener("abort", abortHandler, { once: true });
|
|
47
|
+
Promise.resolve().then(function () {
|
|
48
|
+
return elicitationHandler(request, { signal: signal });
|
|
49
|
+
}).then(function (response) {
|
|
50
|
+
finish(protocolResponse(response, request.requestedSchema));
|
|
51
|
+
}).catch(function (error) {
|
|
52
|
+
finish(null, error);
|
|
53
|
+
});
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
userInput.dispatchElicitation(handler, request, {
|
|
57
|
+
requestId: String(msg.id),
|
|
58
|
+
signal: signal,
|
|
59
|
+
source: "acp_elicitation",
|
|
60
|
+
native: true,
|
|
61
|
+
provider: provider,
|
|
62
|
+
diagnostics: { elicitation: Object.assign({}, params, { serverName: request.serverName, mode: "form" }) },
|
|
63
|
+
}).then(function (result) {
|
|
64
|
+
finish(userInput.elicitationResponse(result, request.requestedSchema));
|
|
65
|
+
}).catch(function (error) {
|
|
66
|
+
finish(null, error);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = { handleElicitation: handleElicitation, protocolResponse: protocolResponse };
|
package/lib/yoke/adapters/acp.js
CHANGED
|
@@ -16,15 +16,24 @@ function modelValues(configOptions) {
|
|
|
16
16
|
var option = options[i];
|
|
17
17
|
if (option.category !== "model" && option.id !== "model") continue;
|
|
18
18
|
var values = Array.isArray(option.options) ? option.options : [];
|
|
19
|
-
|
|
20
|
-
var value = values[j] && values[j].value;
|
|
21
|
-
if (value && models.indexOf(value) === -1) models.push(value);
|
|
22
|
-
}
|
|
19
|
+
collectModelValues(values, models);
|
|
23
20
|
break;
|
|
24
21
|
}
|
|
25
22
|
return models;
|
|
26
23
|
}
|
|
27
24
|
|
|
25
|
+
function collectModelValues(options, models) {
|
|
26
|
+
for (var i = 0; i < options.length; i++) {
|
|
27
|
+
var option = options[i] || {};
|
|
28
|
+
if (Array.isArray(option.options)) {
|
|
29
|
+
collectModelValues(option.options, models);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
var value = option.value || option.id;
|
|
33
|
+
if (value && models.indexOf(value) === -1) models.push(value);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
28
37
|
function createAcpAdapter(vendor, opts) {
|
|
29
38
|
opts = opts || {};
|
|
30
39
|
var driver = opts._profile || opts._driver || profiles.getAcpAgentDriver(vendor);
|
|
@@ -87,7 +96,7 @@ function createAcpAdapter(vendor, opts) {
|
|
|
87
96
|
rewind: false,
|
|
88
97
|
sessionResume: canResumeSession() || canLoadSession(),
|
|
89
98
|
promptSuggestions: false,
|
|
90
|
-
elicitation:
|
|
99
|
+
elicitation: true,
|
|
91
100
|
fileCheckpointing: false,
|
|
92
101
|
contextCompacting: false,
|
|
93
102
|
skillSharing: true,
|
|
@@ -130,6 +139,10 @@ function createAcpAdapter(vendor, opts) {
|
|
|
130
139
|
|
|
131
140
|
var adapter = {
|
|
132
141
|
vendor: vendor,
|
|
142
|
+
// Structured questions use the ask_user_questions fallback tool: shared ACP
|
|
143
|
+
// agents have no standard native question method. Agent-initiated forms
|
|
144
|
+
// still arrive natively through elicitation/create.
|
|
145
|
+
userInputCapability: { mode: "fallback", native: false, nativeElicitation: true, source: "acp_elicitation" },
|
|
133
146
|
|
|
134
147
|
init: function(initOpts) {
|
|
135
148
|
if (shuttingDown) return Promise.reject(new Error(driver.displayName + " adapter is shutting down"));
|
|
@@ -177,6 +190,7 @@ function createAcpAdapter(vendor, opts) {
|
|
|
177
190
|
clientInfo: { name: "clay", version: "1.0.0" },
|
|
178
191
|
clientCapabilities: {
|
|
179
192
|
fs: { readTextFile: false, writeTextFile: false },
|
|
193
|
+
elicitation: { form: {} },
|
|
180
194
|
session: { configOptions: { boolean: {} } },
|
|
181
195
|
},
|
|
182
196
|
});
|
|
@@ -230,11 +244,14 @@ function createAcpAdapter(vendor, opts) {
|
|
|
230
244
|
driver: driver,
|
|
231
245
|
cwd: queryOpts.cwd || cwd,
|
|
232
246
|
model: queryOpts.model || defaultModel,
|
|
247
|
+
effort: queryOpts.effort || null,
|
|
233
248
|
mode: queryOpts.mode || null,
|
|
234
249
|
systemPrompt: queryOpts.systemPrompt || "",
|
|
235
250
|
appendSystemPrompt: [queryOpts.appendSystemPrompt, skillIndex].filter(function(part) { return !!part; }).join("\n\n"),
|
|
236
251
|
abortController: controller,
|
|
237
252
|
canUseTool: queryOpts.canUseTool || null,
|
|
253
|
+
onUserInputRequest: queryOpts.onUserInputRequest || null,
|
|
254
|
+
onElicitation: queryOpts.onElicitation || null,
|
|
238
255
|
resumeSessionId: queryOpts.resumeSessionId || null,
|
|
239
256
|
canLoadSession: canLoadSession(),
|
|
240
257
|
canResumeSession: canResumeSession(),
|