@ai-sdk/harness-opencode 1.0.102 → 1.0.103
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/CHANGELOG.md +8 -0
- package/dist/bridge/index.mjs +154 -13
- package/dist/bridge/index.mjs.map +1 -1
- package/dist/index.d.ts +683 -1
- package/dist/index.js +9 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/bridge/create-emit-stream-event.ts +7 -3
- package/src/bridge/index.ts +92 -3
- package/src/bridge/question-tool.ts +90 -0
- package/src/opencode-harness.ts +9 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @ai-sdk/harness-opencode
|
|
2
2
|
|
|
3
|
+
## 1.0.103
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 951c54d: feat(harness): support `askUserQuestions` tool including support for normalization across harness adapters
|
|
8
|
+
- Updated dependencies [951c54d]
|
|
9
|
+
- @ai-sdk/harness@1.0.101
|
|
10
|
+
|
|
3
11
|
## 1.0.102
|
|
4
12
|
|
|
5
13
|
### Patch Changes
|
package/dist/bridge/index.mjs
CHANGED
|
@@ -214,6 +214,7 @@ async function runBridge(options) {
|
|
|
214
214
|
}
|
|
215
215
|
}
|
|
216
216
|
const pendingToolResults = /* @__PURE__ */ new Map();
|
|
217
|
+
const bufferedToolResults = [];
|
|
217
218
|
const pendingToolApprovals = /* @__PURE__ */ new Map();
|
|
218
219
|
const writeBridgeMeta = async (state) => {
|
|
219
220
|
try {
|
|
@@ -393,9 +394,23 @@ async function runBridge(options) {
|
|
|
393
394
|
const userMessages = createBridgeUserMessageQueue({ respond: emit });
|
|
394
395
|
const turn = {
|
|
395
396
|
emit,
|
|
396
|
-
requestToolResult: (
|
|
397
|
-
|
|
398
|
-
|
|
397
|
+
requestToolResult: (requestInput) => {
|
|
398
|
+
const request = typeof requestInput === "string" ? { toolCallId: requestInput } : requestInput;
|
|
399
|
+
const bufferedIndex = bufferedToolResults.findIndex(
|
|
400
|
+
(buffered) => buffered.toolCallId === request.toolCallId || request.matches?.(buffered.result) === true
|
|
401
|
+
);
|
|
402
|
+
if (bufferedIndex >= 0) {
|
|
403
|
+
return Promise.resolve(
|
|
404
|
+
bufferedToolResults.splice(bufferedIndex, 1)[0].result
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
return new Promise((resolve) => {
|
|
408
|
+
pendingToolResults.set(request.toolCallId, {
|
|
409
|
+
resolve,
|
|
410
|
+
matches: request.matches
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
},
|
|
399
414
|
requestToolApproval: (approvalId) => new Promise((resolve) => {
|
|
400
415
|
pendingToolApprovals.set(approvalId, resolve);
|
|
401
416
|
}),
|
|
@@ -437,10 +452,25 @@ async function runBridge(options) {
|
|
|
437
452
|
return;
|
|
438
453
|
}
|
|
439
454
|
case "tool-result": {
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
455
|
+
const result = {
|
|
456
|
+
output: msg.output,
|
|
457
|
+
isError: msg.isError,
|
|
458
|
+
toolResult: msg.toolResult
|
|
459
|
+
};
|
|
460
|
+
const exactPending = pendingToolResults.get(msg.toolCallId);
|
|
461
|
+
const matchingPending = exactPending == null ? Array.from(pendingToolResults.entries()).find(
|
|
462
|
+
([, pending2]) => pending2.matches?.(result) === true
|
|
463
|
+
) : void 0;
|
|
464
|
+
const pending = exactPending ?? matchingPending?.[1];
|
|
465
|
+
const pendingId = exactPending != null ? msg.toolCallId : matchingPending?.[0];
|
|
466
|
+
if (pending != null && pendingId != null) {
|
|
467
|
+
pendingToolResults.delete(pendingId);
|
|
468
|
+
pending.resolve(result);
|
|
469
|
+
} else {
|
|
470
|
+
bufferedToolResults.push({
|
|
471
|
+
toolCallId: msg.toolCallId,
|
|
472
|
+
result
|
|
473
|
+
});
|
|
444
474
|
}
|
|
445
475
|
return;
|
|
446
476
|
}
|
|
@@ -1144,7 +1174,9 @@ function createEmitStreamEvent({
|
|
|
1144
1174
|
if (type === "session.next.tool.called") {
|
|
1145
1175
|
const callID = String(props.callID ?? event.id);
|
|
1146
1176
|
const rawToolName = String(props.tool ?? "unknown");
|
|
1147
|
-
if (rawToolName === "StructuredOutput")
|
|
1177
|
+
if (rawToolName === "StructuredOutput" || rawToolName === "question") {
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1148
1180
|
const toolName = toWireToolName2(rawToolName);
|
|
1149
1181
|
state.toolNames.set(callID, { rawToolName, toolName });
|
|
1150
1182
|
const hostToolName = getHostToolName2(toolName, props.tool);
|
|
@@ -1173,7 +1205,9 @@ function createEmitStreamEvent({
|
|
|
1173
1205
|
const callID = String(props.callID ?? event.id);
|
|
1174
1206
|
const cachedTool = state.toolNames.get(callID);
|
|
1175
1207
|
const rawToolName = cachedTool?.rawToolName ?? String(props.tool ?? "");
|
|
1176
|
-
if (rawToolName === "StructuredOutput")
|
|
1208
|
+
if (rawToolName === "StructuredOutput" || rawToolName === "question") {
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1177
1211
|
const toolName = cachedTool?.toolName ?? toWireToolName2(rawToolName || "unknown");
|
|
1178
1212
|
if (getHostToolName2(toolName, rawToolName)) return;
|
|
1179
1213
|
emit({
|
|
@@ -1293,7 +1327,7 @@ function emitLegacyToolPart({
|
|
|
1293
1327
|
}
|
|
1294
1328
|
const callID = toolPart.callID;
|
|
1295
1329
|
const rawToolName = toolPart.tool;
|
|
1296
|
-
if (rawToolName === "StructuredOutput") return;
|
|
1330
|
+
if (rawToolName === "StructuredOutput" || rawToolName === "question") return;
|
|
1297
1331
|
const toolName = toWireToolName2(rawToolName);
|
|
1298
1332
|
if (toolName === "agent") {
|
|
1299
1333
|
const metadata = {
|
|
@@ -1642,6 +1676,54 @@ function closeServer(server) {
|
|
|
1642
1676
|
}
|
|
1643
1677
|
}
|
|
1644
1678
|
|
|
1679
|
+
// src/bridge/question-tool.ts
|
|
1680
|
+
function openCodeQuestionKey(nativeRequest) {
|
|
1681
|
+
return JSON.stringify(nativeRequest.questions);
|
|
1682
|
+
}
|
|
1683
|
+
function toHarnessQuestionsInput(nativeRequest) {
|
|
1684
|
+
return {
|
|
1685
|
+
allowPartialAnswers: true,
|
|
1686
|
+
questions: nativeRequest.questions.map((question, questionIndex) => ({
|
|
1687
|
+
id: `question-${questionIndex + 1}`,
|
|
1688
|
+
question: question.question,
|
|
1689
|
+
header: question.header,
|
|
1690
|
+
options: question.options.map((option, optionIndex) => ({
|
|
1691
|
+
id: `option-${optionIndex + 1}`,
|
|
1692
|
+
label: option.label,
|
|
1693
|
+
description: option.description
|
|
1694
|
+
})),
|
|
1695
|
+
...question.multiple !== void 0 ? { allowMultiple: question.multiple } : {},
|
|
1696
|
+
...question.custom !== void 0 ? { allowFreeForm: question.custom } : {}
|
|
1697
|
+
}))
|
|
1698
|
+
};
|
|
1699
|
+
}
|
|
1700
|
+
function toOpenCodeQuestionResponse(input) {
|
|
1701
|
+
if (input.output.action === "declined" || input.output.action === "cancelled") {
|
|
1702
|
+
return { action: "reject" };
|
|
1703
|
+
}
|
|
1704
|
+
const answers = input.nativeRequest.questions.map(
|
|
1705
|
+
(nativeQuestion, questionIndex) => {
|
|
1706
|
+
const answer = input.output.action === "answered" || input.output.action === "partially-answered" ? input.output.answers[`question-${questionIndex + 1}`] : void 0;
|
|
1707
|
+
if (answer == null) return [];
|
|
1708
|
+
const selectedLabels = answer.optionIds.flatMap((optionId) => {
|
|
1709
|
+
const index = positionalIdIndex({
|
|
1710
|
+
id: optionId,
|
|
1711
|
+
prefix: "option-"
|
|
1712
|
+
});
|
|
1713
|
+
const option = index == null ? void 0 : nativeQuestion.options[index];
|
|
1714
|
+
return option == null ? [] : [option.label];
|
|
1715
|
+
});
|
|
1716
|
+
return answer.freeform === void 0 ? selectedLabels : [...selectedLabels, answer.freeform];
|
|
1717
|
+
}
|
|
1718
|
+
);
|
|
1719
|
+
return { action: "reply", answers };
|
|
1720
|
+
}
|
|
1721
|
+
function positionalIdIndex(input) {
|
|
1722
|
+
if (!input.id.startsWith(input.prefix)) return void 0;
|
|
1723
|
+
const oneBasedIndex = Number(input.id.slice(input.prefix.length));
|
|
1724
|
+
return Number.isInteger(oneBasedIndex) && oneBasedIndex > 0 ? oneBasedIndex - 1 : void 0;
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1645
1727
|
// src/bridge/index.ts
|
|
1646
1728
|
var NATIVE_TO_COMMON = {
|
|
1647
1729
|
view: "read",
|
|
@@ -1650,7 +1732,8 @@ var NATIVE_TO_COMMON = {
|
|
|
1650
1732
|
edit: "edit",
|
|
1651
1733
|
bash: "bash",
|
|
1652
1734
|
glob: "glob",
|
|
1653
|
-
grep: "grep"
|
|
1735
|
+
grep: "grep",
|
|
1736
|
+
question: "askUserQuestions"
|
|
1654
1737
|
};
|
|
1655
1738
|
var OPENCODE_TO_WIRE = {
|
|
1656
1739
|
list: "ls",
|
|
@@ -1658,6 +1741,7 @@ var OPENCODE_TO_WIRE = {
|
|
|
1658
1741
|
webfetch: "webfetch",
|
|
1659
1742
|
task: "agent",
|
|
1660
1743
|
agent: "agent",
|
|
1744
|
+
askUserQuestions: "question",
|
|
1661
1745
|
subtask: "agent"
|
|
1662
1746
|
};
|
|
1663
1747
|
var PUBLIC_TO_NATIVE = {
|
|
@@ -1803,7 +1887,7 @@ function buildOpenCodeConfig({
|
|
|
1803
1887
|
webfetch: "ask",
|
|
1804
1888
|
doom_loop: "ask",
|
|
1805
1889
|
task: "ask",
|
|
1806
|
-
question: "
|
|
1890
|
+
question: "allow"
|
|
1807
1891
|
}
|
|
1808
1892
|
};
|
|
1809
1893
|
if (start.model) config.model = start.model;
|
|
@@ -2434,7 +2518,14 @@ async function consumeEvents({
|
|
|
2434
2518
|
const scopedSessionId = !eventSessionId || eventSessionId === sessionId ? sessionId : taskSessionIds.has(eventSessionId) ? eventSessionId : void 0;
|
|
2435
2519
|
if (!scopedSessionId) continue;
|
|
2436
2520
|
const isDescendant = scopedSessionId !== sessionId;
|
|
2437
|
-
if (event.type === "
|
|
2521
|
+
if (event.type === "question.asked") {
|
|
2522
|
+
await handleQuestion({
|
|
2523
|
+
client,
|
|
2524
|
+
turn,
|
|
2525
|
+
emit,
|
|
2526
|
+
event
|
|
2527
|
+
});
|
|
2528
|
+
} else if (event.type === "permission.v2.asked") {
|
|
2438
2529
|
await handlePermissionV2({
|
|
2439
2530
|
client,
|
|
2440
2531
|
sessionId: scopedSessionId,
|
|
@@ -2472,6 +2563,56 @@ function getSubagentStepId(event) {
|
|
|
2472
2563
|
if (event?.type !== "session.next.step.ended") return void 0;
|
|
2473
2564
|
return stringValue2(event.properties?.stepID) ?? event.id;
|
|
2474
2565
|
}
|
|
2566
|
+
async function handleQuestion({
|
|
2567
|
+
client,
|
|
2568
|
+
turn,
|
|
2569
|
+
emit,
|
|
2570
|
+
event
|
|
2571
|
+
}) {
|
|
2572
|
+
const nativeRequest = event.properties;
|
|
2573
|
+
if (nativeRequest == null || typeof nativeRequest.id !== "string" || typeof nativeRequest.sessionID !== "string" || !Array.isArray(nativeRequest.questions)) {
|
|
2574
|
+
return;
|
|
2575
|
+
}
|
|
2576
|
+
const toolCallId = nativeRequest.tool?.callID ?? nativeRequest.id;
|
|
2577
|
+
emit({
|
|
2578
|
+
type: "tool-call",
|
|
2579
|
+
toolCallId,
|
|
2580
|
+
toolName: "askUserQuestions",
|
|
2581
|
+
nativeName: "question",
|
|
2582
|
+
input: JSON.stringify(toHarnessQuestionsInput(nativeRequest)),
|
|
2583
|
+
providerExecuted: false,
|
|
2584
|
+
providerMetadata: {
|
|
2585
|
+
opencode: {
|
|
2586
|
+
nativeRequest
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
});
|
|
2590
|
+
const questionKey = openCodeQuestionKey(nativeRequest);
|
|
2591
|
+
const result = await turn.requestToolResult({
|
|
2592
|
+
toolCallId,
|
|
2593
|
+
matches: (candidate) => {
|
|
2594
|
+
const continuedRequest = candidate.toolResult?.providerOptions?.opencode?.nativeRequest;
|
|
2595
|
+
return continuedRequest != null && openCodeQuestionKey(continuedRequest) === questionKey;
|
|
2596
|
+
}
|
|
2597
|
+
});
|
|
2598
|
+
const nativeResponse = toOpenCodeQuestionResponse({
|
|
2599
|
+
nativeRequest,
|
|
2600
|
+
output: result.output
|
|
2601
|
+
});
|
|
2602
|
+
const response = nativeResponse.action === "reject" ? await client.question.reject({
|
|
2603
|
+
requestID: nativeRequest.id,
|
|
2604
|
+
directory: workdir
|
|
2605
|
+
}) : await client.question.reply({
|
|
2606
|
+
requestID: nativeRequest.id,
|
|
2607
|
+
directory: workdir,
|
|
2608
|
+
answers: nativeResponse.answers
|
|
2609
|
+
});
|
|
2610
|
+
if (response.error != null) {
|
|
2611
|
+
throw new Error(
|
|
2612
|
+
`OpenCode question response failed: ${formatError(response.error)}`
|
|
2613
|
+
);
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2475
2616
|
function sanitizeMcpToolName(value) {
|
|
2476
2617
|
return value.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
2477
2618
|
}
|