clay-server 4.7.1-beta.1 → 4.7.1-beta.3

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.
@@ -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
  });
@@ -539,9 +541,10 @@ function createSDKBridge(opts) {
539
541
  return { behavior: "allow", updatedInput: input };
540
542
  }
541
543
 
542
- // Session search is read-only; its handler enforces project and session access.
543
- if (toolName === "search_other_driver_sessions"
544
- || toolName === "mcp__clay-handoff__search_other_driver_sessions") {
544
+ // Session discovery and reads enforce project and session access in their handlers.
545
+ if (["list_other_driver_sessions", "search_other_driver_sessions", "read_other_driver_session"].some(function (name) {
546
+ return toolName === name || toolName === "mcp__clay-handoff__" + name;
547
+ })) {
545
548
  return { behavior: "allow", updatedInput: input };
546
549
  }
547
550
 
@@ -1848,7 +1851,8 @@ function createSDKBridge(opts) {
1848
1851
  }
1849
1852
  return handleElicitation(session, request.diagnostics.elicitation, { signal: elicitationController.signal }).then(function(result) {
1850
1853
  if (result && result.action === "accept") return respond.submitContent(result.content || {});
1851
- 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");
1852
1856
  });
1853
1857
  }
1854
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(),
@@ -74,6 +74,23 @@ function toolName(name) {
74
74
  return name || "Tool";
75
75
  }
76
76
 
77
+ var USAGE_FIELDS = ["input_tokens", "output_tokens", "cache_read_tokens"];
78
+ var ZERO_USAGE = { input_tokens: 0, output_tokens: 0, cache_read_tokens: 0 };
79
+
80
+ // Result counters are cumulative for the conversation. A per-turn delta is
81
+ // trustworthy only against a known baseline that has not gone backwards.
82
+ function usageDelta(current, baseline) {
83
+ if (!current || !baseline) return null;
84
+ var result = {};
85
+ for (var i = 0; i < USAGE_FIELDS.length; i++) {
86
+ var field = USAGE_FIELDS[i];
87
+ var delta = (current[field] || 0) - (baseline[field] || 0);
88
+ if (delta < 0) return null;
89
+ result[field] = delta;
90
+ }
91
+ return result;
92
+ }
93
+
77
94
  function createAntigravityQueryHandle(binaryPath, queryOpts, onFinished) {
78
95
  var processHandle = null;
79
96
  var removeAbort = function () {};
@@ -97,6 +114,14 @@ function createAntigravityQueryHandle(binaryPath, queryOpts, onFinished) {
97
114
  var textBlockId = null;
98
115
  var toolBlocks = {};
99
116
  var latestUsage = null;
117
+ // Trusted cumulative counters from the previous result. A fresh conversation
118
+ // starts at zero; a resumed one has no trustworthy baseline until its first
119
+ // result, and a result without counters invalidates the baseline again.
120
+ var usageBaseline = queryOpts.resumeSessionId ? null : ZERO_USAGE;
121
+ var durationBaseline = queryOpts.resumeSessionId ? null : 0;
122
+ var turnUsage = null;
123
+ var turnDurationSeconds = null;
124
+ var countedStepIndexes = {};
100
125
  var firstMessage = true;
101
126
 
102
127
  function notifyFinished() {
@@ -146,6 +171,9 @@ function createAntigravityQueryHandle(binaryPath, queryOpts, onFinished) {
146
171
  activeTurn = true;
147
172
  textBlockId = null;
148
173
  toolBlocks = {};
174
+ turnUsage = null;
175
+ turnDurationSeconds = null;
176
+ countedStepIndexes = {};
149
177
  pushEvent({ yokeType: "turn_start", messageType: "user" });
150
178
  processHandle.stdin.write(JSON.stringify({
151
179
  event: "user",
@@ -156,7 +184,17 @@ function createAntigravityQueryHandle(binaryPath, queryOpts, onFinished) {
156
184
  function finishTurn(result) {
157
185
  result = result || {};
158
186
  if (result.conversation_id) sessionId = result.conversation_id;
159
- if (result.usage) latestUsage = result.usage;
187
+ // Prefer the cumulative counter delta; otherwise fall back to this turn's
188
+ // completed-step totals, or leave the turn unknown.
189
+ var resultUsage = result.usage || null;
190
+ var emittedUsage = usageDelta(resultUsage, usageBaseline) || turnUsage;
191
+ usageBaseline = resultUsage;
192
+ if (resultUsage) latestUsage = resultUsage;
193
+ var totalSeconds = typeof result.duration_seconds === "number" ? result.duration_seconds : null;
194
+ var durationSeconds = totalSeconds != null && durationBaseline != null && totalSeconds >= durationBaseline
195
+ ? totalSeconds - durationBaseline
196
+ : turnDurationSeconds;
197
+ durationBaseline = totalSeconds;
160
198
  var status = result.status || "SUCCESS";
161
199
  if (status !== "SUCCESS") {
162
200
  if (status === "CANCELED" || status === "INTERRUPTED") {
@@ -172,11 +210,11 @@ function createAntigravityQueryHandle(binaryPath, queryOpts, onFinished) {
172
210
  yokeType: "result",
173
211
  messageType: "assistant",
174
212
  cost: null,
175
- duration: typeof result.duration_seconds === "number" ? result.duration_seconds * 1000 : null,
176
- usage: latestUsage ? {
177
- input_tokens: latestUsage.input_tokens || 0,
178
- output_tokens: latestUsage.output_tokens || 0,
179
- cache_read_input_tokens: latestUsage.cache_read_tokens || 0,
213
+ duration: typeof durationSeconds === "number" ? durationSeconds * 1000 : null,
214
+ usage: emittedUsage ? {
215
+ input_tokens: emittedUsage.input_tokens || 0,
216
+ output_tokens: emittedUsage.output_tokens || 0,
217
+ cache_read_input_tokens: emittedUsage.cache_read_tokens || 0,
180
218
  cache_creation_input_tokens: 0,
181
219
  } : null,
182
220
  sessionId: sessionId,
@@ -189,6 +227,17 @@ function createAntigravityQueryHandle(binaryPath, queryOpts, onFinished) {
189
227
 
190
228
  function handleStep(step) {
191
229
  if (!step) return;
230
+ var stepKey = step.step_index == null ? null : String(step.step_index);
231
+ if (step.state === "DONE" && (stepKey == null || !countedStepIndexes[stepKey])) {
232
+ if (stepKey != null) countedStepIndexes[stepKey] = true;
233
+ if (step.usage) {
234
+ if (!turnUsage) turnUsage = {};
235
+ USAGE_FIELDS.forEach(function(field) {
236
+ turnUsage[field] = (turnUsage[field] || 0) + (step.usage[field] || 0);
237
+ });
238
+ }
239
+ if (typeof step.duration_seconds === "number") turnDurationSeconds = (turnDurationSeconds || 0) + step.duration_seconds;
240
+ }
192
241
  if (step.step_type === "agent_response") {
193
242
  if (!textBlockId) {
194
243
  textBlockId = "agy_blk_" + (++blockCounter);