@ai-sdk/harness-opencode 1.0.101 → 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 CHANGED
@@ -1,5 +1,21 @@
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
+
11
+ ## 1.0.102
12
+
13
+ ### Patch Changes
14
+
15
+ - 0c37bf0: fix(harness): avoid warning about lack of credential brokering support when `credentialForwarding` callback is used to replace all credentials with ephemeral fake secrets
16
+ - Updated dependencies [0c37bf0]
17
+ - @ai-sdk/harness@1.0.100
18
+
3
19
  ## 1.0.101
4
20
 
5
21
  ### Patch Changes
@@ -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: (toolCallId) => new Promise((resolve) => {
397
- pendingToolResults.set(toolCallId, resolve);
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 resolver = pendingToolResults.get(msg.toolCallId);
441
- if (resolver) {
442
- pendingToolResults.delete(msg.toolCallId);
443
- resolver({ output: msg.output, isError: msg.isError });
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") return;
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") return;
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: "deny"
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 === "permission.v2.asked") {
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
  }