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
|
@@ -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
|
-
|
|
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
|
|
176
|
-
usage:
|
|
177
|
-
input_tokens:
|
|
178
|
-
output_tokens:
|
|
179
|
-
cache_read_input_tokens:
|
|
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);
|
|
@@ -370,7 +370,7 @@ function onElicitation(request, opts) {
|
|
|
370
370
|
if (opts.signal) {
|
|
371
371
|
opts.signal.addEventListener("abort", function() {
|
|
372
372
|
delete pendingElicitations[requestId];
|
|
373
|
-
resolve({ action: "
|
|
373
|
+
resolve({ action: "cancel" });
|
|
374
374
|
});
|
|
375
375
|
}
|
|
376
376
|
});
|
|
@@ -1079,10 +1079,13 @@ function createCodexQueryHandle(appServer, queryOpts) {
|
|
|
1079
1079
|
onElicitation(request, {
|
|
1080
1080
|
signal: { addEventListener: function() {} },
|
|
1081
1081
|
}).then(function(result) {
|
|
1082
|
-
|
|
1082
|
+
var resultAction = result && result.action === "reject" ? "decline" : result && result.action;
|
|
1083
|
+
appServer.respond(msg.id, resultAction === "accept" || resultAction === "decline"
|
|
1084
|
+
? Object.assign({}, result, { action: resultAction })
|
|
1085
|
+
: { action: "cancel" });
|
|
1083
1086
|
}).catch(function(err) {
|
|
1084
1087
|
console.error("[yoke/codex] elicitation_response send failed:", err.message || err);
|
|
1085
|
-
appServer.respond(msg.id, { action: "
|
|
1088
|
+
appServer.respond(msg.id, { action: "cancel" });
|
|
1086
1089
|
});
|
|
1087
1090
|
} else if (canUseTool) {
|
|
1088
1091
|
canUseTool("mcp__" + (mcpParams.serverName || "unknown") + "__" + (mcpMeta.tool || "call"), mcpParams, {}).then(function(decision) {
|
|
@@ -204,12 +204,12 @@ function flattenUpdate(update, state) {
|
|
|
204
204
|
// Tool call announced
|
|
205
205
|
if (type === "tool_call") {
|
|
206
206
|
var callId = update.toolCallId;
|
|
207
|
-
var toolName = toolNameForKind(update.kind, update.title);
|
|
207
|
+
var toolName = update.name || toolNameForKind(update.kind, update.title);
|
|
208
208
|
// Cache kind/rawInput so the permission handler (whose request payload only
|
|
209
209
|
// carries { toolCallId, title }) can pass a canonical tool name + input to
|
|
210
210
|
// canUseTool. Clay's permission whitelist keys on canonical names.
|
|
211
211
|
if (callId) {
|
|
212
|
-
state.toolMeta[callId] = { kind: update.kind, title: update.title, rawInput: update.rawInput || {} };
|
|
212
|
+
state.toolMeta[callId] = { kind: update.kind, name: update.name, title: update.title, rawInput: update.rawInput || {} };
|
|
213
213
|
}
|
|
214
214
|
if (callId && !state.toolBlocks[callId]) {
|
|
215
215
|
state.blockCounter++;
|
|
@@ -501,8 +501,13 @@ function createKiroQueryHandle(acp, queryOpts) {
|
|
|
501
501
|
}
|
|
502
502
|
return null;
|
|
503
503
|
}
|
|
504
|
-
var allowId = pickOption(["allow_once", "allow_always"])
|
|
505
|
-
var rejectId = pickOption(["reject_once", "reject_always"])
|
|
504
|
+
var allowId = pickOption(["allow_once", "allow_always"]);
|
|
505
|
+
var rejectId = pickOption(["reject_once", "reject_always"]);
|
|
506
|
+
function permissionResponse(optionId) {
|
|
507
|
+
return optionId
|
|
508
|
+
? { outcome: { outcome: "selected", optionId: optionId } }
|
|
509
|
+
: { outcome: { outcome: "cancelled" } };
|
|
510
|
+
}
|
|
506
511
|
|
|
507
512
|
if (isCancelled()) {
|
|
508
513
|
acp.respond(msg.id, { outcome: { outcome: "cancelled" } });
|
|
@@ -512,21 +517,21 @@ function createKiroQueryHandle(acp, queryOpts) {
|
|
|
512
517
|
// The permission request payload carries only { toolCallId, title }.
|
|
513
518
|
// Recover kind + rawInput from the tool_call notification we cached.
|
|
514
519
|
var meta = (tc.toolCallId && state.toolMeta[tc.toolCallId]) || {};
|
|
515
|
-
var toolName = toolNameForKind(tc.kind || meta.kind, tc.title || meta.title);
|
|
520
|
+
var toolName = tc.name || meta.name || toolNameForKind(tc.kind || meta.kind, tc.title || meta.title);
|
|
516
521
|
var toolInput = tc.rawInput || meta.rawInput || { title: tc.title || meta.title };
|
|
517
522
|
cancellation.wait(Promise.resolve().then(function () { cancellation.check(); return canUseTool(toolName, toolInput, { signal: cancellation.signal }); })).then(function(decision) {
|
|
518
|
-
acp.respond(msg.id, isCancelled() ? { outcome: { outcome: "cancelled" } } :
|
|
523
|
+
acp.respond(msg.id, isCancelled() ? { outcome: { outcome: "cancelled" } } : permissionResponse(isApproved(decision) ? allowId : rejectId));
|
|
519
524
|
}).catch(function(err) {
|
|
520
525
|
if (isCancelled()) { acp.respond(msg.id, { outcome: { outcome: "cancelled" } }); return; }
|
|
521
526
|
console.error("[yoke/kiro] canUseTool error:", err.message);
|
|
522
|
-
acp.respond(msg.id,
|
|
527
|
+
acp.respond(msg.id, permissionResponse(rejectId));
|
|
523
528
|
});
|
|
524
529
|
} else {
|
|
525
530
|
// No approver wired up. Deny: Clay is the only thing standing between
|
|
526
531
|
// the agent and the user's filesystem, so absence of an approver must
|
|
527
532
|
// never mean approval.
|
|
528
533
|
console.warn("[yoke/kiro] permission request with no canUseTool callback, denying");
|
|
529
|
-
acp.respond(msg.id,
|
|
534
|
+
acp.respond(msg.id, permissionResponse(rejectId));
|
|
530
535
|
}
|
|
531
536
|
return;
|
|
532
537
|
}
|
|
@@ -652,12 +657,6 @@ function createKiroQueryHandle(acp, queryOpts) {
|
|
|
652
657
|
cwd: queryOpts.cwd,
|
|
653
658
|
mcpServers: queryOpts.mcpServers || [],
|
|
654
659
|
}, 60000);
|
|
655
|
-
} catch (e) {
|
|
656
|
-
if (isCancelled()) throw e;
|
|
657
|
-
// If load fails (unknown session), fall back to a fresh session.
|
|
658
|
-
console.warn("[yoke/kiro] session/load failed, starting fresh:", e.message);
|
|
659
|
-
state.sessionId = null;
|
|
660
|
-
handlerEntry.sessionId = null;
|
|
661
660
|
} finally {
|
|
662
661
|
state.loadingSession = false;
|
|
663
662
|
}
|
|
@@ -681,12 +680,12 @@ function createKiroQueryHandle(acp, queryOpts) {
|
|
|
681
680
|
}, 15000);
|
|
682
681
|
}
|
|
683
682
|
|
|
684
|
-
// Select model
|
|
683
|
+
// Select the requested model and mode before the prompt is sent.
|
|
685
684
|
if (queryOpts.model) {
|
|
686
|
-
await setSessionModel(queryOpts.model)
|
|
685
|
+
await setSessionModel(queryOpts.model);
|
|
687
686
|
}
|
|
688
687
|
if (queryOpts.mode) {
|
|
689
|
-
await send("session/set_mode", { sessionId: state.sessionId, modeId: queryOpts.mode }, 15000)
|
|
688
|
+
await send("session/set_mode", { sessionId: state.sessionId, modeId: queryOpts.mode }, 15000);
|
|
690
689
|
}
|
|
691
690
|
|
|
692
691
|
while (!isCancelled()) {
|
|
@@ -14,15 +14,13 @@ function ask(handler, input, opts) {
|
|
|
14
14
|
|
|
15
15
|
function elicitation(handler, request, opts) {
|
|
16
16
|
opts = opts || {};
|
|
17
|
-
return userInput.
|
|
17
|
+
return userInput.dispatchElicitation(handler, request, {
|
|
18
18
|
requestId: opts.requestId || request.elicitationId,
|
|
19
19
|
signal: opts.signal,
|
|
20
20
|
source: "claude_elicitation",
|
|
21
21
|
native: true,
|
|
22
22
|
provider: "claude",
|
|
23
|
-
|
|
24
|
-
diagnostics: { elicitation: request },
|
|
25
|
-
}).then(userInput.elicitationResponse);
|
|
23
|
+
}).then(function (result) { return userInput.elicitationResponse(result, request.requestedSchema); });
|
|
26
24
|
}
|
|
27
25
|
|
|
28
26
|
function canUseTool(queryOpts) {
|
|
@@ -45,8 +45,7 @@ function handleElicitation(appServer, msg, handler, signal) {
|
|
|
45
45
|
elicitationId: params.elicitationId || null,
|
|
46
46
|
requestedSchema: params.requestedSchema || null,
|
|
47
47
|
};
|
|
48
|
-
var
|
|
49
|
-
userInput.dispatchUserInput(handler, input, {
|
|
48
|
+
var dispatchOpts = {
|
|
50
49
|
requestId: request.elicitationId || String(msg.id),
|
|
51
50
|
signal: signal,
|
|
52
51
|
source: "codex_mcp_elicitation",
|
|
@@ -54,8 +53,12 @@ function handleElicitation(appServer, msg, handler, signal) {
|
|
|
54
53
|
provider: "codex",
|
|
55
54
|
presentation: "elicitation",
|
|
56
55
|
diagnostics: { threadId: params.threadId || null, elicitation: request },
|
|
57
|
-
}
|
|
58
|
-
|
|
56
|
+
};
|
|
57
|
+
var pending = Array.isArray(params.questions)
|
|
58
|
+
? userInput.dispatchUserInput(handler, nativeInput(params), dispatchOpts)
|
|
59
|
+
: userInput.dispatchElicitation(handler, request, dispatchOpts);
|
|
60
|
+
pending.then(function (result) {
|
|
61
|
+
appServer.respond(msg.id, userInput.elicitationResponse(result, request.requestedSchema));
|
|
59
62
|
}).catch(function (error) {
|
|
60
63
|
appServer.respondError(msg.id, -32602, error.message);
|
|
61
64
|
});
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// Restricted elicitation schema handling shared by ACP, Claude and Codex.
|
|
2
|
+
// Mirrors lib/public/modules/elicitation-schema.js; keep both in sync
|
|
3
|
+
// (test/elicitation-schema-parity.test.js runs one fixture table on both).
|
|
4
|
+
|
|
5
|
+
var PATTERN_MAX_LENGTH = 200;
|
|
6
|
+
var PATTERN_MAX_VARIABLE_QUANTIFIERS = 2;
|
|
7
|
+
var PATTERN_MAX_INPUT_LENGTH = 256;
|
|
8
|
+
|
|
9
|
+
function hasOwn(object, key) {
|
|
10
|
+
return !!object && Object.prototype.hasOwnProperty.call(object, key);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function constChoices(list) {
|
|
14
|
+
if (!Array.isArray(list)) return null;
|
|
15
|
+
var values = [];
|
|
16
|
+
for (var i = 0; i < list.length; i++) {
|
|
17
|
+
if (!hasOwn(list[i], "const")) return null;
|
|
18
|
+
values.push({ value: list[i].const, label: String(list[i].title || list[i].const), description: list[i].description || "" });
|
|
19
|
+
}
|
|
20
|
+
return values.length ? values : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function enumChoices(list) {
|
|
24
|
+
if (!Array.isArray(list) || !list.length) return null;
|
|
25
|
+
return list.map(function (value) { return { value: value, label: String(value), description: "" }; });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Returns { kind, multi, choices } where kind is one of string, number,
|
|
29
|
+
// integer, boolean, array or unsupported. Unknown types are never rendered
|
|
30
|
+
// or validated as a known control.
|
|
31
|
+
function fieldInfo(property) {
|
|
32
|
+
property = property || {};
|
|
33
|
+
var type = property.type || (Array.isArray(property.enum) || Array.isArray(property.oneOf) ? "string" : null);
|
|
34
|
+
if (type === "array") {
|
|
35
|
+
var items = property.items || {};
|
|
36
|
+
var itemType = items.type || "string";
|
|
37
|
+
var itemChoices = itemType === "string" ? (enumChoices(items.enum) || constChoices(items.anyOf)) : null;
|
|
38
|
+
return itemChoices ? { kind: "array", multi: true, choices: itemChoices } : { kind: "unsupported", multi: false, choices: null };
|
|
39
|
+
}
|
|
40
|
+
if (type === "string") return { kind: "string", multi: false, choices: enumChoices(property.enum) || constChoices(property.oneOf) };
|
|
41
|
+
if (type === "number" || type === "integer" || type === "boolean") return { kind: type, multi: false, choices: null };
|
|
42
|
+
return { kind: "unsupported", multi: false, choices: null };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Agent-supplied patterns are evaluated only inside a subset whose backtracking
|
|
46
|
+
// cost is bounded: no backreferences, lookaround or quantified groups, at most
|
|
47
|
+
// two variable quantifiers, and short inputs. Anything else is an unenforced hint.
|
|
48
|
+
function patternIsBounded(pattern) {
|
|
49
|
+
if (typeof pattern !== "string" || !pattern || pattern.length > PATTERN_MAX_LENGTH) return false;
|
|
50
|
+
var variable = 0;
|
|
51
|
+
var inClass = false;
|
|
52
|
+
for (var i = 0; i < pattern.length; i++) {
|
|
53
|
+
var ch = pattern[i];
|
|
54
|
+
if (ch === "\\") {
|
|
55
|
+
var next = pattern[i + 1];
|
|
56
|
+
if (next === undefined || /[1-9k]/.test(next)) return false;
|
|
57
|
+
i++;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (inClass) { if (ch === "]") inClass = false; continue; }
|
|
61
|
+
if (ch === "[") { inClass = true; continue; }
|
|
62
|
+
if (ch === "(" && pattern[i + 1] === "?") {
|
|
63
|
+
if (pattern[i + 2] !== ":") return false;
|
|
64
|
+
i += 2;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (ch === "*" || ch === "+" || ch === "?" || ch === "{") {
|
|
68
|
+
if (pattern[i - 1] === ")") return false;
|
|
69
|
+
if (ch === "{") {
|
|
70
|
+
var close = pattern.indexOf("}", i);
|
|
71
|
+
if (close === -1) return false;
|
|
72
|
+
var body = pattern.slice(i + 1, close);
|
|
73
|
+
if (!/^\d+(,\d*)?$/.test(body)) return false;
|
|
74
|
+
if (body.indexOf(",") !== -1) variable++;
|
|
75
|
+
i = close;
|
|
76
|
+
} else {
|
|
77
|
+
variable++;
|
|
78
|
+
}
|
|
79
|
+
if (pattern[i + 1] === "?") i++;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (inClass || variable > PATTERN_MAX_VARIABLE_QUANTIFIERS) return false;
|
|
83
|
+
try { new RegExp(pattern, "u"); } catch (e) { return false; }
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// true or false when evaluated; null when the pattern is left as a hint.
|
|
88
|
+
function patternMatches(pattern, value) {
|
|
89
|
+
if (!patternIsBounded(pattern) || value.length > PATTERN_MAX_INPUT_LENGTH) return null;
|
|
90
|
+
return new RegExp(pattern, "u").test(value);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function sameValue(left, right) {
|
|
94
|
+
return left === right;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function inChoices(choices, value) {
|
|
98
|
+
return choices.some(function (choice) { return sameValue(choice.value, value); });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Returns an error message or null. `value` must already be typed.
|
|
102
|
+
function valueError(value, property) {
|
|
103
|
+
property = property || {};
|
|
104
|
+
var info = fieldInfo(property);
|
|
105
|
+
if (info.kind === "unsupported") return null;
|
|
106
|
+
if (info.kind === "boolean") return typeof value === "boolean" ? null : "Choose yes or no.";
|
|
107
|
+
if (info.kind === "number" || info.kind === "integer") {
|
|
108
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return "Enter a number.";
|
|
109
|
+
if (info.kind === "integer" && !Number.isInteger(value)) return "Enter a whole number.";
|
|
110
|
+
if (typeof property.minimum === "number" && value < property.minimum) return "Must be at least " + property.minimum + ".";
|
|
111
|
+
if (typeof property.maximum === "number" && value > property.maximum) return "Must be at most " + property.maximum + ".";
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
if (info.kind === "array") {
|
|
115
|
+
if (!Array.isArray(value)) return "Choose from the listed options.";
|
|
116
|
+
for (var i = 0; i < value.length; i++) {
|
|
117
|
+
if (!inChoices(info.choices, value[i])) return "Choose only listed options.";
|
|
118
|
+
if (value.indexOf(value[i]) !== i) return "Choose each option once.";
|
|
119
|
+
}
|
|
120
|
+
if (typeof property.minItems === "number" && value.length < property.minItems) return "Choose at least " + property.minItems + ".";
|
|
121
|
+
if (typeof property.maxItems === "number" && value.length > property.maxItems) return "Choose at most " + property.maxItems + ".";
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
if (typeof value !== "string") return "Enter text.";
|
|
125
|
+
if (info.choices) return inChoices(info.choices, value) ? null : "Choose one of the listed options.";
|
|
126
|
+
var length = Array.from(value).length;
|
|
127
|
+
if (typeof property.minLength === "number" && length < property.minLength) return "Must be at least " + property.minLength + " characters.";
|
|
128
|
+
if (typeof property.maxLength === "number" && length > property.maxLength) return "Must be at most " + property.maxLength + " characters.";
|
|
129
|
+
if (patternMatches(property.pattern, value) === false) return "Does not match the required format.";
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Accepts string answers from the question-card path and returns a typed value.
|
|
134
|
+
function coerceValue(value, property) {
|
|
135
|
+
var info = fieldInfo(property);
|
|
136
|
+
if (info.kind === "boolean" && (value === "true" || value === "false")) return value === "true";
|
|
137
|
+
if ((info.kind === "number" || info.kind === "integer") && typeof value === "string" && value.trim()) return Number(value);
|
|
138
|
+
if (info.kind === "array" && !Array.isArray(value)) return value == null ? [] : [value];
|
|
139
|
+
return value;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Required means presence only. Throws on the first invalid property.
|
|
143
|
+
function validateContent(content, schema) {
|
|
144
|
+
if (!content || typeof content !== "object" || Array.isArray(content)) throw new Error("Elicitation content must be an object.");
|
|
145
|
+
var properties = schema && schema.properties && typeof schema.properties === "object" ? schema.properties : {};
|
|
146
|
+
var required = schema && Array.isArray(schema.required) ? schema.required : [];
|
|
147
|
+
var validated = Object.assign({}, content);
|
|
148
|
+
for (var i = 0; i < required.length; i++) {
|
|
149
|
+
if (hasOwn(properties, required[i]) && !hasOwn(content, required[i])) throw new Error("An answer is required for " + required[i] + ".");
|
|
150
|
+
}
|
|
151
|
+
Object.keys(properties).forEach(function (id) {
|
|
152
|
+
if (!hasOwn(content, id)) return;
|
|
153
|
+
var value = coerceValue(content[id], properties[id]);
|
|
154
|
+
var error = valueError(value, properties[id]);
|
|
155
|
+
if (error) throw new Error("Invalid elicitation answer for " + id + ": " + error);
|
|
156
|
+
validated[id] = value;
|
|
157
|
+
});
|
|
158
|
+
return validated;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function clip(text, max) {
|
|
162
|
+
text = typeof text === "string" ? text.trim() : "";
|
|
163
|
+
return text.length > max ? text.slice(0, max - 1) + "…" : text;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Provider-neutral question view of a form. Unlike structured question tools,
|
|
167
|
+
// forms have no property-count or text-length limits; text is only clipped.
|
|
168
|
+
function questionsFromSchema(request) {
|
|
169
|
+
var schema = request && request.requestedSchema;
|
|
170
|
+
var properties = schema && schema.properties && typeof schema.properties === "object" ? schema.properties : null;
|
|
171
|
+
var required = schema && Array.isArray(schema.required) ? schema.required : [];
|
|
172
|
+
var header = clip(request && request.serverName, 40) || "Input";
|
|
173
|
+
var questions = [];
|
|
174
|
+
Object.keys(properties || {}).forEach(function (id) {
|
|
175
|
+
var property = properties[id] || {};
|
|
176
|
+
var info = fieldInfo(property);
|
|
177
|
+
var choices = info.choices || (info.kind === "boolean" ? [{ value: true, label: "true", description: "Yes" }, { value: false, label: "false", description: "No" }] : []);
|
|
178
|
+
questions.push({
|
|
179
|
+
id: id,
|
|
180
|
+
header: header,
|
|
181
|
+
question: clip(property.description || property.title || (request && request.message) || id, 2000) || id,
|
|
182
|
+
multiSelect: info.multi,
|
|
183
|
+
allowOther: !info.choices && info.kind !== "boolean",
|
|
184
|
+
required: required.indexOf(id) !== -1,
|
|
185
|
+
secret: false,
|
|
186
|
+
options: choices.map(function (choice) { return { label: clip(String(choice.value), 60) || "(empty)", description: clip(choice.description || (choice.label !== String(choice.value) ? choice.label : ""), 160) }; }),
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
if (!questions.length) {
|
|
190
|
+
questions.push({ id: "response", header: header, question: clip(request && request.message, 2000) || "What should the tool use?", multiSelect: false, allowOther: true, required: true, secret: false, options: [] });
|
|
191
|
+
}
|
|
192
|
+
return questions;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
module.exports = {
|
|
196
|
+
PATTERN_MAX_INPUT_LENGTH: PATTERN_MAX_INPUT_LENGTH,
|
|
197
|
+
fieldInfo: fieldInfo,
|
|
198
|
+
patternIsBounded: patternIsBounded,
|
|
199
|
+
patternMatches: patternMatches,
|
|
200
|
+
valueError: valueError,
|
|
201
|
+
coerceValue: coerceValue,
|
|
202
|
+
validateContent: validateContent,
|
|
203
|
+
questionsFromSchema: questionsFromSchema,
|
|
204
|
+
};
|
package/lib/yoke/interface.js
CHANGED
|
@@ -47,17 +47,22 @@ var INITIALIZE_TIMEOUT_MS = 90000;
|
|
|
47
47
|
* this method may only use completeOnce's explicit unknown-persistence path.
|
|
48
48
|
*
|
|
49
49
|
* Optional structured user-input capability:
|
|
50
|
-
* .userInputCapability: { mode: "native"|"fallback", native: boolean,
|
|
50
|
+
* .userInputCapability: { mode: "native"|"fallback", native: boolean,
|
|
51
|
+
* nativeElicitation?: boolean, transport: string }
|
|
51
52
|
* createQuery({ userInputMode: "auto"|"native"|"fallback",
|
|
52
53
|
* onUserInputRequest(request, respond) })
|
|
53
54
|
* request.presentation is "questions" or "elicitation";
|
|
54
|
-
*
|
|
55
|
+
* question requests contain 1-3 { id, header, question, multiSelect,
|
|
55
56
|
* allowOther, secret, options }; options contain zero or 2-6 safe
|
|
56
|
-
* { label, description } entries.
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
57
|
+
* { label, description } entries. Elicitation requests carry the raw
|
|
58
|
+
* form in request.diagnostics.elicitation and have no question limit.
|
|
59
|
+
* respond(answerMap), respond.submitContent(typedMap),
|
|
60
|
+
* respond.decline(reason) and respond.cancel(reason) settle exactly once.
|
|
61
|
+
* submitContent preserves typed MCP elicitation forms.
|
|
62
|
+
* mode governs structured questions only: "native" adapters translate
|
|
63
|
+
* their provider question protocol, others use createFallbackServer() /
|
|
64
|
+
* fallbackToolDefs() from yoke/user-input. nativeElicitation marks
|
|
65
|
+
* adapters that also forward agent-initiated forms on fallback mode.
|
|
61
66
|
*
|
|
62
67
|
* Lightweight utilities:
|
|
63
68
|
* .generateTitle(messages, opts) : Promise<string> - generate a short session title
|
package/lib/yoke/user-input.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Provider-neutral structured user-input lifecycle for YOKE queries.
|
|
2
2
|
|
|
3
3
|
var crypto = require("crypto");
|
|
4
|
+
var elicitationSchema = require("./elicitation-schema");
|
|
4
5
|
var z;
|
|
5
6
|
try { z = require("zod"); } catch (e) { z = null; }
|
|
6
7
|
|
|
@@ -46,6 +47,7 @@ function normalizeQuestions(input) {
|
|
|
46
47
|
question: cleanText(item.question || item.prompt, "Question", 2000),
|
|
47
48
|
multiSelect: item.multiSelect === true,
|
|
48
49
|
allowOther: item.allowOther !== false && item.isOther !== false,
|
|
50
|
+
required: item.required !== false,
|
|
49
51
|
secret: item.secret === true || item.isSecret === true,
|
|
50
52
|
options: normalizeOptions(item.options, i),
|
|
51
53
|
});
|
|
@@ -54,23 +56,7 @@ function normalizeQuestions(input) {
|
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
function questionsFromElicitation(request) {
|
|
57
|
-
|
|
58
|
-
var properties = schema && schema.properties && typeof schema.properties === "object" ? schema.properties : null;
|
|
59
|
-
var questions = [];
|
|
60
|
-
if (properties) {
|
|
61
|
-
Object.keys(properties).forEach(function (id) {
|
|
62
|
-
var property = properties[id] || {};
|
|
63
|
-
var values = Array.isArray(property.enum) ? property.enum : [];
|
|
64
|
-
questions.push({
|
|
65
|
-
id: id,
|
|
66
|
-
header: request.serverName || "Input",
|
|
67
|
-
question: property.description || request.message || id,
|
|
68
|
-
options: values.map(function (value) { return { label: String(value), description: "" }; }),
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
if (!questions.length) questions.push({ id: "response", header: request.serverName || "Input", question: request.message || "What should the tool use?", options: [] });
|
|
73
|
-
return { questions: questions };
|
|
59
|
+
return { questions: elicitationSchema.questionsFromSchema(request) };
|
|
74
60
|
}
|
|
75
61
|
|
|
76
62
|
function answerValues(value) {
|
|
@@ -86,6 +72,7 @@ function normalizeAnswers(questions, raw) {
|
|
|
86
72
|
var question = questions[i];
|
|
87
73
|
var value = Object.prototype.hasOwnProperty.call(raw, question.id) ? raw[question.id] : raw[i];
|
|
88
74
|
var values = answerValues(value);
|
|
75
|
+
if (!values.length && question.required === false) continue;
|
|
89
76
|
if (!values.length) throw new Error("An answer is required for " + question.id + ".");
|
|
90
77
|
if (!question.multiSelect && values.length > 1) throw new Error("Only one answer is allowed for " + question.id + ".");
|
|
91
78
|
answers[question.id] = values;
|
|
@@ -98,18 +85,33 @@ function normalizeAnswers(questions, raw) {
|
|
|
98
85
|
*
|
|
99
86
|
* handler receives:
|
|
100
87
|
* request: { id, questions, source, native, provider, diagnostics }
|
|
101
|
-
* respond(answers): submits once; respond.cancel(reason) cancels once
|
|
88
|
+
* respond(answers): submits once; respond.cancel(reason) cancels once;
|
|
89
|
+
* respond.decline(reason) records an explicit refusal once.
|
|
102
90
|
*
|
|
103
91
|
* The returned promise resolves to { status, answers|reason }. Provider
|
|
104
92
|
* adapters translate that result back to their own wire protocol. Nothing in
|
|
105
93
|
* this lifecycle is persisted; Clay callers may persist sanitized events.
|
|
106
94
|
*/
|
|
107
95
|
function dispatchUserInput(handler, input, opts) {
|
|
108
|
-
opts = opts || {};
|
|
109
96
|
var questions;
|
|
110
97
|
try { questions = normalizeQuestions(input); } catch (e) { return Promise.reject(e); }
|
|
98
|
+
return startRequest(handler, questions, opts || {}, input && input.id);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Start one form elicitation. Forms keep their raw schema in
|
|
103
|
+
* diagnostics.elicitation and are not subject to the 1-3 question limit of
|
|
104
|
+
* structured question tools.
|
|
105
|
+
*/
|
|
106
|
+
function dispatchElicitation(handler, request, opts) {
|
|
107
|
+
opts = Object.assign({}, opts || {}, { presentation: "elicitation" });
|
|
108
|
+
opts.diagnostics = Object.assign({}, opts.diagnostics || {}, { elicitation: (opts.diagnostics && opts.diagnostics.elicitation) || request });
|
|
109
|
+
return startRequest(handler, elicitationSchema.questionsFromSchema(request), opts, null);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function startRequest(handler, questions, opts, inputId) {
|
|
111
113
|
if (typeof handler !== "function") return Promise.reject(new Error("This query cannot request structured user input."));
|
|
112
|
-
var id = opts.requestId ||
|
|
114
|
+
var id = opts.requestId || inputId || "yoke_input_" + crypto.randomUUID();
|
|
113
115
|
var settled = false;
|
|
114
116
|
var abortHandler = null;
|
|
115
117
|
var settleListeners = [];
|
|
@@ -143,6 +145,7 @@ function dispatchUserInput(handler, input, opts) {
|
|
|
143
145
|
return finish({ status: "submitted", answers: {}, content: content });
|
|
144
146
|
};
|
|
145
147
|
respond.cancel = function (reason) { return finish({ status: "cancelled", reason: reason || "Cancelled" }); };
|
|
148
|
+
respond.decline = function (reason) { return finish({ status: "declined", reason: reason || "Declined" }); };
|
|
146
149
|
respond.isPending = function () { return !settled; };
|
|
147
150
|
respond.onSettle = function (listener) {
|
|
148
151
|
if (typeof listener === "function" && !settled) settleListeners.push(listener);
|
|
@@ -187,12 +190,17 @@ function codexResponse(result) {
|
|
|
187
190
|
return { answers: answers };
|
|
188
191
|
}
|
|
189
192
|
|
|
190
|
-
function elicitationResponse(result) {
|
|
191
|
-
if (!result || result.status !== "submitted") return { action: "
|
|
192
|
-
if (result.content) return { action: "accept", content: result.content };
|
|
193
|
+
function elicitationResponse(result, schema) {
|
|
194
|
+
if (!result || result.status !== "submitted") return { action: result && result.status === "declined" ? "decline" : "cancel" };
|
|
195
|
+
if (result.content) return { action: "accept", content: elicitationSchema.validateContent(result.content, schema) };
|
|
193
196
|
var content = {};
|
|
194
|
-
|
|
195
|
-
|
|
197
|
+
var properties = schema && schema.properties ? schema.properties : {};
|
|
198
|
+
Object.keys(result.answers).forEach(function (id) {
|
|
199
|
+
var values = result.answers[id];
|
|
200
|
+
var multi = elicitationSchema.fieldInfo(properties[id]).multi;
|
|
201
|
+
content[id] = multi ? values : (values.length === 1 ? values[0] : values);
|
|
202
|
+
});
|
|
203
|
+
return { action: "accept", content: elicitationSchema.validateContent(content, schema) };
|
|
196
204
|
}
|
|
197
205
|
|
|
198
206
|
function buildQuestionShape(maxQuestions) {
|
|
@@ -258,6 +266,7 @@ module.exports = {
|
|
|
258
266
|
questionsFromElicitation: questionsFromElicitation,
|
|
259
267
|
normalizeAnswers: normalizeAnswers,
|
|
260
268
|
dispatchUserInput: dispatchUserInput,
|
|
269
|
+
dispatchElicitation: dispatchElicitation,
|
|
261
270
|
claudePermissionResult: claudePermissionResult,
|
|
262
271
|
codexResponse: codexResponse,
|
|
263
272
|
elicitationResponse: elicitationResponse,
|
package/package.json
CHANGED