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.
@@ -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
- var container = document.createElement("div");
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 sendElicitationResponse(container, requestId, action, content) {
1182
- if (container.classList.contains("resolved")) return;
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 markElicitationResolved(requestId, action) {
1211
- var container = pendingElicitations[requestId];
1212
- if (!container) {
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) ---
@@ -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: auto-reject elicitation in autonomous mode
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: "reject" });
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
- resolve({ action: "reject" });
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.cancel("Elicitation rejected");
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: "reject" });
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", "ask", "agent", "stdio"],
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 && update.kind) {
100
- state.toolMeta[callId] = { kind: update.kind, title: update.title, rawInput: update.rawInput || {} };
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 };
@@ -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
- for (var j = 0; j < values.length; j++) {
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: false,
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(),