@ai-sdk/harness-opencode 1.0.102 → 1.0.104

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,25 @@
1
1
  # @ai-sdk/harness-opencode
2
2
 
3
+ ## 1.0.104
4
+
5
+ ### Patch Changes
6
+
7
+ - eeed977: feat(harness): allow passing arbitrary headers with inference requests via `headers` property in `HarnessAgentSettings`
8
+ - a39c8bf: chore(harness): clarify `writeSkills` helper intent to require materializing skills in HOME directory
9
+ - Updated dependencies [fe86f8f]
10
+ - Updated dependencies [255cccf]
11
+ - Updated dependencies [eeed977]
12
+ - Updated dependencies [a39c8bf]
13
+ - @ai-sdk/harness@1.0.102
14
+
15
+ ## 1.0.103
16
+
17
+ ### Patch Changes
18
+
19
+ - 951c54d: feat(harness): support `askUserQuestions` tool including support for normalization across harness adapters
20
+ - Updated dependencies [951c54d]
21
+ - @ai-sdk/harness@1.0.101
22
+
3
23
  ## 1.0.102
4
24
 
5
25
  ### 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;
@@ -1871,7 +1955,12 @@ function buildProviderConfig(start) {
1871
1955
  options: {
1872
1956
  apiKey: procEnv2.AI_GATEWAY_API_KEY,
1873
1957
  baseURL: toOpenCodeGatewayBaseUrl(procEnv2.AI_GATEWAY_BASE_URL),
1874
- ...HARNESS_CLIENT_APP ? { headers: { "x-client-app": HARNESS_CLIENT_APP } } : {}
1958
+ ...HARNESS_CLIENT_APP ? {
1959
+ headers: {
1960
+ ...start.headers,
1961
+ "x-client-app": HARNESS_CLIENT_APP
1962
+ }
1963
+ } : start.headers ? { headers: start.headers } : {}
1875
1964
  },
1876
1965
  ...modelID ? {
1877
1966
  models: {
@@ -1888,6 +1977,7 @@ function buildProviderConfig(start) {
1888
1977
  options: {
1889
1978
  ...procEnv2.OPENAI_API_KEY ? { apiKey: procEnv2.OPENAI_API_KEY } : {},
1890
1979
  ...procEnv2.OPENAI_BASE_URL ? { baseURL: procEnv2.OPENAI_BASE_URL } : {},
1980
+ ...start.headers ? { headers: start.headers } : {},
1891
1981
  ...parseOpenAIQueryParams()
1892
1982
  },
1893
1983
  ...modelID ? {
@@ -1904,7 +1994,8 @@ function buildProviderConfig(start) {
1904
1994
  options: {
1905
1995
  ...procEnv2.ANTHROPIC_API_KEY ? { apiKey: procEnv2.ANTHROPIC_API_KEY } : {},
1906
1996
  ...procEnv2.ANTHROPIC_AUTH_TOKEN ? { authToken: procEnv2.ANTHROPIC_AUTH_TOKEN } : {},
1907
- ...procEnv2.ANTHROPIC_BASE_URL ? { baseURL: procEnv2.ANTHROPIC_BASE_URL } : {}
1997
+ ...procEnv2.ANTHROPIC_BASE_URL ? { baseURL: procEnv2.ANTHROPIC_BASE_URL } : {},
1998
+ ...start.headers ? { headers: start.headers } : {}
1908
1999
  }
1909
2000
  }
1910
2001
  };
@@ -1917,6 +2008,7 @@ function buildProviderConfig(start) {
1917
2008
  ...procEnv2.OPENAI_BASE_URL ? { baseURL: procEnv2.OPENAI_BASE_URL } : {},
1918
2009
  ...procEnv2.OPENAI_ORGANIZATION ? { organization: procEnv2.OPENAI_ORGANIZATION } : {},
1919
2010
  ...procEnv2.OPENAI_PROJECT ? { project: procEnv2.OPENAI_PROJECT } : {},
2011
+ ...start.headers ? { headers: start.headers } : {},
1920
2012
  ...parseOpenAIQueryParams()
1921
2013
  }
1922
2014
  }
@@ -2434,7 +2526,14 @@ async function consumeEvents({
2434
2526
  const scopedSessionId = !eventSessionId || eventSessionId === sessionId ? sessionId : taskSessionIds.has(eventSessionId) ? eventSessionId : void 0;
2435
2527
  if (!scopedSessionId) continue;
2436
2528
  const isDescendant = scopedSessionId !== sessionId;
2437
- if (event.type === "permission.v2.asked") {
2529
+ if (event.type === "question.asked") {
2530
+ await handleQuestion({
2531
+ client,
2532
+ turn,
2533
+ emit,
2534
+ event
2535
+ });
2536
+ } else if (event.type === "permission.v2.asked") {
2438
2537
  await handlePermissionV2({
2439
2538
  client,
2440
2539
  sessionId: scopedSessionId,
@@ -2472,6 +2571,56 @@ function getSubagentStepId(event) {
2472
2571
  if (event?.type !== "session.next.step.ended") return void 0;
2473
2572
  return stringValue2(event.properties?.stepID) ?? event.id;
2474
2573
  }
2574
+ async function handleQuestion({
2575
+ client,
2576
+ turn,
2577
+ emit,
2578
+ event
2579
+ }) {
2580
+ const nativeRequest = event.properties;
2581
+ if (nativeRequest == null || typeof nativeRequest.id !== "string" || typeof nativeRequest.sessionID !== "string" || !Array.isArray(nativeRequest.questions)) {
2582
+ return;
2583
+ }
2584
+ const toolCallId = nativeRequest.tool?.callID ?? nativeRequest.id;
2585
+ emit({
2586
+ type: "tool-call",
2587
+ toolCallId,
2588
+ toolName: "askUserQuestions",
2589
+ nativeName: "question",
2590
+ input: JSON.stringify(toHarnessQuestionsInput(nativeRequest)),
2591
+ providerExecuted: false,
2592
+ providerMetadata: {
2593
+ opencode: {
2594
+ nativeRequest
2595
+ }
2596
+ }
2597
+ });
2598
+ const questionKey = openCodeQuestionKey(nativeRequest);
2599
+ const result = await turn.requestToolResult({
2600
+ toolCallId,
2601
+ matches: (candidate) => {
2602
+ const continuedRequest = candidate.toolResult?.providerOptions?.opencode?.nativeRequest;
2603
+ return continuedRequest != null && openCodeQuestionKey(continuedRequest) === questionKey;
2604
+ }
2605
+ });
2606
+ const nativeResponse = toOpenCodeQuestionResponse({
2607
+ nativeRequest,
2608
+ output: result.output
2609
+ });
2610
+ const response = nativeResponse.action === "reject" ? await client.question.reject({
2611
+ requestID: nativeRequest.id,
2612
+ directory: workdir
2613
+ }) : await client.question.reply({
2614
+ requestID: nativeRequest.id,
2615
+ directory: workdir,
2616
+ answers: nativeResponse.answers
2617
+ });
2618
+ if (response.error != null) {
2619
+ throw new Error(
2620
+ `OpenCode question response failed: ${formatError(response.error)}`
2621
+ );
2622
+ }
2623
+ }
2475
2624
  function sanitizeMcpToolName(value) {
2476
2625
  return value.replace(/[^a-zA-Z0-9_-]/g, "_");
2477
2626
  }