@ai-sdk/harness-acp 1.0.38 → 1.0.40

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/harness-acp",
3
- "version": "1.0.38",
3
+ "version": "1.0.40",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -26,7 +26,7 @@
26
26
  }
27
27
  },
28
28
  "dependencies": {
29
- "@ai-sdk/harness": "1.0.100",
29
+ "@ai-sdk/harness": "1.0.102",
30
30
  "@ai-sdk/provider-utils": "5.0.36",
31
31
  "ws": "^8.21.0"
32
32
  },
@@ -1,9 +1,17 @@
1
- import type { HarnessV1, HarnessV1PortEndpoint } from '@ai-sdk/harness';
1
+ import {
2
+ HARNESS_V1_BUILTIN_TOOLS,
3
+ type HarnessV1,
4
+ type HarnessV1PortEndpoint,
5
+ } from '@ai-sdk/harness';
2
6
  import type { ToolSet } from '@ai-sdk/provider-utils';
3
7
  import { z } from 'zod/v4';
4
8
  import type { ACPClientApp } from './acp-auth';
5
9
  import type { ACPToolCall } from './acp-tool-call';
6
- import { createACPV1, type ACPV1Settings } from './v1';
10
+ import {
11
+ createACPV1,
12
+ type ACPAskUserQuestionsSettings,
13
+ type ACPV1Settings,
14
+ } from './v1';
7
15
  import {
8
16
  acpColdSessionStateSchema,
9
17
  acpTurnStartConfigSchema,
@@ -15,7 +23,12 @@ const ACP_CLIENT_APP = {
15
23
  version: VERSION,
16
24
  } as const satisfies ACPClientApp;
17
25
 
18
- export type ACPHarnessSettings<TBuiltinTools extends ToolSet = {}> = {
26
+ export type ACPHarnessSettings<
27
+ TBuiltinTools extends ToolSet = {},
28
+ TAskUserQuestions extends ACPAskUserQuestionsSettings | undefined =
29
+ | ACPAskUserQuestionsSettings
30
+ | undefined,
31
+ > = {
19
32
  readonly builtinTools?: TBuiltinTools;
20
33
  /**
21
34
  * MCP server definitions keyed by server name. Each definition uses the
@@ -61,6 +74,15 @@ export type ACPHarnessSettings<TBuiltinTools extends ToolSet = {}> = {
61
74
  readonly skillsDirectory?: ACPV1Settings['skillsDirectory'];
62
75
  readonly instructionMapping?: ACPV1Settings['instructionMapping'];
63
76
  readonly outputSchemaMapping?: ACPV1Settings['outputSchemaMapping'];
77
+ /**
78
+ * Transport used for the harness-owned MCP server that exposes host tools to
79
+ * the ACP implementation. Defaults to `stdio`. Set this to `http` for
80
+ * implementations that only accept HTTP or SSE MCP servers from the client,
81
+ * which requires the implementation to advertise
82
+ * `agentCapabilities.mcpCapabilities.http`.
83
+ */
84
+ readonly hostToolMcpTransport?: ACPV1Settings['hostToolMcpTransport'];
85
+ readonly askUserQuestions?: TAskUserQuestions;
64
86
  readonly permissionModeMapping?: ACPV1Settings['permissionModeMapping'];
65
87
  readonly session?: ACPV1Settings['session'];
66
88
  readonly mintBridgeToken?: ACPV1Settings['mintBridgeToken'];
@@ -117,9 +139,22 @@ const acpResumeStateSchema = z.object({
117
139
 
118
140
  type ACPBridgeCoords = z.infer<typeof acpBridgeCoordsSchema>;
119
141
 
120
- export function createACP<TBuiltinTools extends ToolSet = {}>(
121
- settings: ACPHarnessSettings<TBuiltinTools>,
122
- ): HarnessV1<TBuiltinTools> {
142
+ type ACPHarnessTools<
143
+ TBuiltinTools extends ToolSet,
144
+ TAskUserQuestions,
145
+ > = TBuiltinTools &
146
+ ([TAskUserQuestions] extends [undefined]
147
+ ? {}
148
+ : {
149
+ askUserQuestions: typeof HARNESS_V1_BUILTIN_TOOLS.askUserQuestions;
150
+ });
151
+
152
+ export function createACP<
153
+ TBuiltinTools extends ToolSet = {},
154
+ TAskUserQuestions extends ACPAskUserQuestionsSettings | undefined = undefined,
155
+ >(
156
+ settings: ACPHarnessSettings<TBuiltinTools, TAskUserQuestions>,
157
+ ): HarnessV1<ACPHarnessTools<TBuiltinTools, TAskUserQuestions>> {
123
158
  if (
124
159
  (settings.credentialEnv == null) !==
125
160
  (settings.credentialBrokering == null)
@@ -143,10 +178,17 @@ export function createACP<TBuiltinTools extends ToolSet = {}>(
143
178
  switch (version) {
144
179
  case 'v1': {
145
180
  const clientApp = settings.clientApp ?? ACP_CLIENT_APP;
181
+ const builtinTools =
182
+ settings.askUserQuestions == null
183
+ ? (settings.builtinTools ?? (ACP_BUILTIN_TOOLS as TBuiltinTools))
184
+ : {
185
+ ...(settings.builtinTools ??
186
+ (ACP_BUILTIN_TOOLS as TBuiltinTools)),
187
+ askUserQuestions: HARNESS_V1_BUILTIN_TOOLS.askUserQuestions,
188
+ };
146
189
  return createACPV1({
147
190
  settings,
148
- builtinTools:
149
- settings.builtinTools ?? (ACP_BUILTIN_TOOLS as TBuiltinTools),
191
+ builtinTools,
150
192
  port: settings.port,
151
193
  portEndpoint: settings.portEndpoint,
152
194
  startupTimeoutMs: settings.startupTimeoutMs,
@@ -154,7 +196,7 @@ export function createACP<TBuiltinTools extends ToolSet = {}>(
154
196
  lifecycleStateSchema: acpResumeStateSchema satisfies z.ZodType<{
155
197
  bridge?: ACPBridgeCoords;
156
198
  }>,
157
- });
199
+ }) as HarnessV1<ACPHarnessTools<TBuiltinTools, TAskUserQuestions>>;
158
200
  }
159
201
  default:
160
202
  throw new Error(
package/src/index.ts CHANGED
@@ -20,8 +20,10 @@ export type {
20
20
  ACPToolKind,
21
21
  } from './acp-tool-call';
22
22
  export type {
23
+ ACPAskUserQuestionsSettings,
23
24
  ACPCredentialBrokering,
24
25
  ACPAuthentication,
26
+ ACPHostToolMCPTransport,
25
27
  ACPInstallCommandSource,
26
28
  ACPInstructionMapping,
27
29
  ACPModelMapping,
@@ -113,6 +113,10 @@ const instructionMappingSchema: z.ZodType<ACPInstructionMapping> =
113
113
  variable: z.string().min(1),
114
114
  path: instructionMappingPathSchema,
115
115
  }),
116
+ z.object({
117
+ type: z.literal('filesystem'),
118
+ path: z.string().min(1),
119
+ }),
116
120
  ]);
117
121
 
118
122
  type ACPSerializableOutputSchemaMapping = {
@@ -199,6 +203,7 @@ const coldRestoreSchema = z.object({
199
203
 
200
204
  const acpToolCallCandidateSchema = z.object({
201
205
  type: z.literal('acp-tool-call-candidate'),
206
+ requestId: z.string(),
202
207
  toolCall: z.custom<ACPToolCall>(
203
208
  value =>
204
209
  value != null &&
@@ -208,9 +213,23 @@ const acpToolCallCandidateSchema = z.object({
208
213
  ),
209
214
  });
210
215
 
216
+ const acpQuestionRequestSchema = z.object({
217
+ type: z.literal('acp-question-request'),
218
+ requestId: z.string(),
219
+ nativeRequest: z.unknown(),
220
+ nativeToolCall: acpToolCallCandidateSchema.shape.toolCall.optional(),
221
+ });
222
+
223
+ const acpQuestionResolvedSchema = z.object({
224
+ type: z.literal('acp-question-resolved'),
225
+ requestId: z.string(),
226
+ });
227
+
211
228
  export const outboundMessageSchema = z.union([
212
229
  harnessV1BridgeOutboundMessageSchema,
213
230
  acpToolCallCandidateSchema,
231
+ acpQuestionRequestSchema,
232
+ acpQuestionResolvedSchema,
214
233
  ]);
215
234
  export type OutboundMessage = z.infer<typeof outboundMessageSchema>;
216
235
 
@@ -32,11 +32,14 @@ import {
32
32
  warnCredentialBrokeringUnavailable,
33
33
  waitForBridgeReady,
34
34
  withBridgeToken,
35
+ writeInstructions,
36
+ writeSkills,
35
37
  } from '@ai-sdk/harness/utils';
36
38
  import {
37
39
  asSchema,
38
40
  type Experimental_SandboxProcess,
39
41
  type Experimental_SandboxSession as SandboxSession,
42
+ type ToolResultPart,
40
43
  type ToolSet,
41
44
  } from '@ai-sdk/provider-utils';
42
45
  import { WebSocket } from 'ws';
@@ -81,6 +84,7 @@ import {
81
84
  prependACPInstructionGuidance,
82
85
  } from './acp-v1-prompt';
83
86
  import type {
87
+ ACPAskUserQuestionsSettings,
84
88
  ACPInstructionMapping,
85
89
  ACPModelMapping,
86
90
  ACPOutputSchemaMapping,
@@ -91,9 +95,11 @@ import type {
91
95
  ACPV1Settings,
92
96
  } from './acp-v1-settings';
93
97
  import {
94
- materializeACPSkills,
98
+ ACP_SKILL_NAME_PATTERN,
99
+ DEFAULT_ACP_SKILLS_DIRECTORY,
95
100
  resolveACPPrivateSessionDirectory,
96
101
  resolveACPSkillsDirectory,
102
+ validateACPSkills,
97
103
  } from './acp-v1-skills';
98
104
 
99
105
  const HARNESS_ID_REGEXP = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
@@ -322,6 +328,9 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
322
328
  const requestTransformations = settings.credentialBrokering({
323
329
  env: brokeringEnvironment,
324
330
  sandboxEnv: sandboxImplementationEnvironment,
331
+ ...(startOptions.headers == null
332
+ ? {}
333
+ : { headers: startOptions.headers }),
325
334
  });
326
335
  if (requestTransformations.length > 0) {
327
336
  await sandboxSession.addRequestTransformations(
@@ -370,17 +379,18 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
370
379
  });
371
380
  const privateSessionDir = resolveACPPrivateSessionDirectory({
372
381
  sandboxHomeDir,
373
- sessionWorkDir: workDir,
374
382
  harnessId: settings.harnessId,
375
383
  sessionId: startOptions.sessionId,
376
384
  });
385
+ const implementationHomeDir =
386
+ implementation.source.type === 'install-command'
387
+ ? `${resolvedImplementationDir}/home`
388
+ : sandboxHomeDir;
389
+ const skillsDir =
390
+ settings.skillsDirectory ?? DEFAULT_ACP_SKILLS_DIRECTORY;
377
391
  const skillsDirectory = resolveACPSkillsDirectory({
378
- implementationHomeDir:
379
- implementation.source.type === 'install-command'
380
- ? `${resolvedImplementationDir}/home`
381
- : sandboxHomeDir,
392
+ implementationHomeDir,
382
393
  skillsDirectory: settings.skillsDirectory,
383
- sessionWorkDir: workDir,
384
394
  });
385
395
  const bridgeStateDir = `${privateSessionDir}/bridge`;
386
396
  const report = startOptions.observability?.report;
@@ -442,6 +452,7 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
442
452
  sessionMeta: settings.session?.meta,
443
453
  instructionMapping: settings.instructionMapping,
444
454
  outputSchemaMapping: settings.outputSchemaMapping,
455
+ askUserQuestions: settings.askUserQuestions,
445
456
  debug: startOptions.observability?.debug,
446
457
  implementationIdentity,
447
458
  authenticationProfile,
@@ -456,7 +467,8 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
456
467
  }),
457
468
  instructionsFingerprint: lifecycleData.instructionsFingerprint,
458
469
  sandbox: toolSafeSandboxSession,
459
- sessionWorkDir: workDir,
470
+ homePath: implementationHomeDir,
471
+ skillsDir,
460
472
  skillsDirectory,
461
473
  acpSessionId: lifecycleData.acpSessionId,
462
474
  bridgePort: coords.port,
@@ -615,6 +627,9 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
615
627
  sandboxProviderEnvironment == null ? undefined : {},
616
628
  sessionMeta: settings.session?.meta,
617
629
  clientCapabilities: settings.clientCapabilities,
630
+ askUserQuestionsRequestMethod:
631
+ settings.askUserQuestions?.requestMethod,
632
+ hostToolMcpTransport: settings.hostToolMcpTransport,
618
633
  }),
619
634
  ...sandboxProviderAuthenticationEnvironment,
620
635
  BRIDGE_CHANNEL_TOKEN: token,
@@ -732,6 +747,7 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
732
747
  sessionMeta: settings.session?.meta,
733
748
  instructionMapping: settings.instructionMapping,
734
749
  outputSchemaMapping: settings.outputSchemaMapping,
750
+ askUserQuestions: settings.askUserQuestions,
735
751
  debug: startOptions.observability?.debug,
736
752
  implementationIdentity,
737
753
  authenticationProfile,
@@ -746,7 +762,8 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
746
762
  }),
747
763
  instructionsFingerprint: lifecycleData?.instructionsFingerprint,
748
764
  sandbox: toolSafeSandboxSession,
749
- sessionWorkDir: workDir,
765
+ homePath: implementationHomeDir,
766
+ skillsDir,
750
767
  skillsDirectory,
751
768
  acpSessionId: lifecycleData?.acpSessionId,
752
769
  bridgePort: boundPort,
@@ -1045,6 +1062,7 @@ function createSession({
1045
1062
  sessionMeta,
1046
1063
  instructionMapping,
1047
1064
  outputSchemaMapping,
1065
+ askUserQuestions,
1048
1066
  debug,
1049
1067
  implementationIdentity,
1050
1068
  authenticationProfile,
@@ -1056,7 +1074,8 @@ function createSession({
1056
1074
  initialGuidanceApplied: initialGuidanceAppliedAtStart,
1057
1075
  instructionsFingerprint: instructionsFingerprintAtStart,
1058
1076
  sandbox,
1059
- sessionWorkDir,
1077
+ homePath,
1078
+ skillsDir,
1060
1079
  skillsDirectory,
1061
1080
  acpSessionId: acpSessionIdAtStart,
1062
1081
  bridgePort,
@@ -1081,6 +1100,7 @@ function createSession({
1081
1100
  sessionMeta: Readonly<Record<string, ACPSerializableValue>> | undefined;
1082
1101
  instructionMapping: ACPInstructionMapping | undefined;
1083
1102
  outputSchemaMapping: ACPOutputSchemaMapping | undefined;
1103
+ askUserQuestions: ACPAskUserQuestionsSettings | undefined;
1084
1104
  debug: HarnessV1DebugConfig | undefined;
1085
1105
  implementationIdentity: string;
1086
1106
  authenticationProfile: ACPAuthenticationProfileIdentity;
@@ -1092,7 +1112,8 @@ function createSession({
1092
1112
  initialGuidanceApplied: boolean;
1093
1113
  instructionsFingerprint: string | undefined;
1094
1114
  sandbox: SandboxSession;
1095
- sessionWorkDir: string;
1115
+ homePath: string;
1116
+ skillsDir: string;
1096
1117
  skillsDirectory: string;
1097
1118
  acpSessionId: string | undefined;
1098
1119
  bridgePort: number;
@@ -1114,6 +1135,14 @@ function createSession({
1114
1135
  let instructionsFingerprint = instructionsFingerprintAtStart;
1115
1136
  let latestACPSessionId = acpSessionIdAtStart;
1116
1137
  let latestTurnStartConfig = turnStartConfigAtStart;
1138
+ const bufferedQuestionResults = new Map<
1139
+ string,
1140
+ {
1141
+ readonly output: unknown;
1142
+ readonly isError?: boolean;
1143
+ readonly toolResult: ToolResultPart;
1144
+ }
1145
+ >();
1117
1146
 
1118
1147
  const markTurnFinished = () => {
1119
1148
  turnInFlight = false;
@@ -1154,6 +1183,14 @@ function createSession({
1154
1183
  | undefined;
1155
1184
  const dynamicToolCalls = new Map<string, boolean>();
1156
1185
  const toolCallClassificationErrors = new Map<string, unknown>();
1186
+ const activeQuestionRequests = new Map<
1187
+ string,
1188
+ {
1189
+ readonly requestId: string;
1190
+ readonly nativeRequest: unknown;
1191
+ }
1192
+ >();
1193
+ const questionToolCallIdsByRequestId = new Map<string, string>();
1157
1194
  const subscriptions: Array<() => void> = [];
1158
1195
  const forward = (event: HarnessV1StreamPart) => {
1159
1196
  if (event.type === 'text-start' || event.type === 'reasoning-start') {
@@ -1220,12 +1257,120 @@ function createSession({
1220
1257
  subscriptions.push(
1221
1258
  channel.on('acp-tool-call-candidate', event => {
1222
1259
  try {
1260
+ const suppress =
1261
+ askUserQuestions?.isNativeToolCall?.({
1262
+ nativeToolCall: event.toolCall,
1263
+ }) === true;
1223
1264
  dynamicToolCalls.set(
1224
1265
  event.toolCall.toolCallId,
1225
1266
  isMcpToolCall?.(event.toolCall) === true,
1226
1267
  );
1268
+ channel.send({
1269
+ type: 'tool-result',
1270
+ toolCallId: event.requestId,
1271
+ output: { suppress },
1272
+ });
1227
1273
  } catch (error) {
1228
1274
  toolCallClassificationErrors.set(event.toolCall.toolCallId, error);
1275
+ channel.send({
1276
+ type: 'tool-result',
1277
+ toolCallId: event.requestId,
1278
+ output: { suppress: false },
1279
+ });
1280
+ }
1281
+ }),
1282
+ );
1283
+ subscriptions.push(
1284
+ channel.on('acp-question-request', event => {
1285
+ if (askUserQuestions == null) {
1286
+ channel.send({
1287
+ type: 'tool-result',
1288
+ toolCallId: event.requestId,
1289
+ output: { type: 'unhandled' },
1290
+ });
1291
+ return;
1292
+ }
1293
+ try {
1294
+ const nativeToolCall = askUserQuestions.fromNativeRequest({
1295
+ nativeRequest: event.nativeRequest,
1296
+ nativeToolCall: event.nativeToolCall,
1297
+ });
1298
+ if (nativeToolCall == null) {
1299
+ channel.send({
1300
+ type: 'tool-result',
1301
+ toolCallId: event.requestId,
1302
+ output: { type: 'unhandled' },
1303
+ });
1304
+ return;
1305
+ }
1306
+ if (
1307
+ nativeToolCall.toolName !== 'askUserQuestions' ||
1308
+ nativeToolCall.providerExecuted !== false
1309
+ ) {
1310
+ throw new Error(
1311
+ `${harnessId} ACP askUserQuestions.fromNativeRequest must return a client-executed askUserQuestions tool call.`,
1312
+ );
1313
+ }
1314
+
1315
+ const toolCall = withNativeQuestionRequest({
1316
+ harnessId,
1317
+ nativeRequest: event.nativeRequest,
1318
+ toolCall: nativeToolCall,
1319
+ });
1320
+ const bufferedResult = takeBufferedQuestionResult({
1321
+ bufferedQuestionResults,
1322
+ toolCallId: toolCall.toolCallId,
1323
+ nativeRequest: event.nativeRequest,
1324
+ matchesNativeRequest: askUserQuestions.matchesNativeRequest,
1325
+ harnessId,
1326
+ });
1327
+ activeQuestionRequests.set(toolCall.toolCallId, {
1328
+ requestId: event.requestId,
1329
+ nativeRequest: event.nativeRequest,
1330
+ });
1331
+ questionToolCallIdsByRequestId.set(
1332
+ event.requestId,
1333
+ toolCall.toolCallId,
1334
+ );
1335
+ channel.send({
1336
+ type: 'tool-result',
1337
+ toolCallId: event.requestId,
1338
+ output: {
1339
+ type: 'handled',
1340
+ toolCallId: toolCall.toolCallId,
1341
+ },
1342
+ });
1343
+
1344
+ if (bufferedResult == null) {
1345
+ forward(toolCall);
1346
+ return;
1347
+ }
1348
+ channel.send({
1349
+ type: 'tool-result',
1350
+ toolCallId: toolCall.toolCallId,
1351
+ output: askUserQuestions.toNativeResponse({
1352
+ nativeRequest: event.nativeRequest,
1353
+ toolResult: bufferedResult.toolResult,
1354
+ }),
1355
+ isError: bufferedResult.isError,
1356
+ toolResult: bufferedResult.toolResult,
1357
+ });
1358
+ } catch (error) {
1359
+ closeForwardedBlock();
1360
+ forward({ type: 'error', error });
1361
+ try {
1362
+ channel.send({ type: 'abort' });
1363
+ } catch {}
1364
+ settle({ error });
1365
+ }
1366
+ }),
1367
+ );
1368
+ subscriptions.push(
1369
+ channel.on('acp-question-resolved', event => {
1370
+ const toolCallId = questionToolCallIdsByRequestId.get(event.requestId);
1371
+ if (toolCallId != null) {
1372
+ activeQuestionRequests.delete(toolCallId);
1373
+ questionToolCallIdsByRequestId.delete(event.requestId);
1229
1374
  }
1230
1375
  }),
1231
1376
  );
@@ -1315,11 +1460,52 @@ function createSession({
1315
1460
  });
1316
1461
  },
1317
1462
  submitToolResult: async input => {
1463
+ if (
1464
+ askUserQuestions != null &&
1465
+ input.toolResult?.toolName === 'askUserQuestions'
1466
+ ) {
1467
+ const activeRequest = activeQuestionRequests.get(input.toolCallId);
1468
+ if (activeRequest == null) {
1469
+ const previousNativeRequest =
1470
+ input.toolResult.providerOptions?.[harnessId]?.nativeRequest;
1471
+ if (!lossyRerun && previousNativeRequest !== undefined) {
1472
+ channel.send({
1473
+ type: 'tool-result',
1474
+ toolCallId: input.toolCallId,
1475
+ output: askUserQuestions.toNativeResponse({
1476
+ nativeRequest: previousNativeRequest,
1477
+ toolResult: input.toolResult,
1478
+ }),
1479
+ isError: input.isError,
1480
+ toolResult: input.toolResult,
1481
+ });
1482
+ return;
1483
+ }
1484
+ bufferedQuestionResults.set(input.toolCallId, {
1485
+ output: input.output,
1486
+ ...(input.isError == null ? {} : { isError: input.isError }),
1487
+ toolResult: input.toolResult,
1488
+ });
1489
+ return;
1490
+ }
1491
+ channel.send({
1492
+ type: 'tool-result',
1493
+ toolCallId: input.toolCallId,
1494
+ output: askUserQuestions.toNativeResponse({
1495
+ nativeRequest: activeRequest.nativeRequest,
1496
+ toolResult: input.toolResult,
1497
+ }),
1498
+ isError: input.isError,
1499
+ toolResult: input.toolResult,
1500
+ });
1501
+ return;
1502
+ }
1318
1503
  channel.send({
1319
1504
  type: 'tool-result',
1320
1505
  toolCallId: input.toolCallId,
1321
1506
  output: input.output,
1322
1507
  isError: input.isError,
1508
+ toolResult: input.toolResult,
1323
1509
  });
1324
1510
  },
1325
1511
  done,
@@ -1398,15 +1584,41 @@ function createSession({
1398
1584
  skills: ReadonlyArray<HarnessV1Skill>;
1399
1585
  abortSignal?: AbortSignal;
1400
1586
  }): Promise<void> => {
1401
- await materializeACPSkills({
1587
+ validateACPSkills({ skills });
1588
+ await writeSkills({
1402
1589
  sandbox,
1403
- rootDir: skillsDirectory,
1404
- sessionWorkDir,
1590
+ homePath,
1591
+ skillsDir,
1405
1592
  skills,
1406
1593
  abortSignal,
1594
+ skillNamePattern: ACP_SKILL_NAME_PATTERN,
1595
+ invalidSkillNameMessage: ({ name }) =>
1596
+ `Invalid ACP skill name ${JSON.stringify(name)}: expected a kebab-case slug.`,
1597
+ invalidSkillFilePathMessage: ({ skillName, filePath }) =>
1598
+ `Invalid ACP skill file path ${JSON.stringify(filePath)} for skill ${JSON.stringify(
1599
+ skillName,
1600
+ )}: expected a relative POSIX path without traversal.`,
1407
1601
  });
1408
1602
  };
1409
1603
 
1604
+ const synchronizeInstructions = async ({
1605
+ instructions,
1606
+ abortSignal,
1607
+ }: {
1608
+ instructions: string | undefined;
1609
+ abortSignal?: AbortSignal;
1610
+ }): Promise<void> => {
1611
+ if (instructionMapping?.type === 'filesystem') {
1612
+ await writeInstructions({
1613
+ sandbox,
1614
+ homePath,
1615
+ instructionsFile: instructionMapping.path,
1616
+ instructions,
1617
+ abortSignal,
1618
+ });
1619
+ }
1620
+ };
1621
+
1410
1622
  return {
1411
1623
  sessionId,
1412
1624
  isResume,
@@ -1415,6 +1627,10 @@ function createSession({
1415
1627
  skills: options.skills,
1416
1628
  abortSignal: options.abortSignal,
1417
1629
  });
1630
+ await synchronizeInstructions({
1631
+ instructions: options.instructions,
1632
+ abortSignal: options.abortSignal,
1633
+ });
1418
1634
  if (options.responseFormat?.type === 'json') {
1419
1635
  if (options.responseFormat.schema == null) {
1420
1636
  throw unsupported({
@@ -1477,6 +1693,7 @@ function createSession({
1477
1693
  channel.send({
1478
1694
  type: 'start',
1479
1695
  prompt:
1696
+ instructionMapping?.type !== 'filesystem' &&
1480
1697
  instructionsFingerprint !== nextInstructionsFingerprint &&
1481
1698
  (instructionMapping == null || initialGuidanceApplied)
1482
1699
  ? prependACPInstructionGuidance({
@@ -1519,6 +1736,10 @@ function createSession({
1519
1736
  skills: options.skills,
1520
1737
  abortSignal: options.abortSignal,
1521
1738
  });
1739
+ await synchronizeInstructions({
1740
+ instructions: options.instructions,
1741
+ abortSignal: options.abortSignal,
1742
+ });
1522
1743
  if (options.responseFormat?.type === 'json') {
1523
1744
  if (options.responseFormat.schema == null) {
1524
1745
  throw unsupported({
@@ -1604,11 +1825,6 @@ function createSession({
1604
1825
  `${harnessId} ACP session ${sessionId} is stopped; cannot suspend.`,
1605
1826
  );
1606
1827
  }
1607
- if (!turnInFlight) {
1608
- throw new Error(
1609
- `${harnessId} ACP session ${sessionId} has no in-flight turn to suspend.`,
1610
- );
1611
- }
1612
1828
  stopped = true;
1613
1829
  const lastSeenEventId = await channel.suspend();
1614
1830
  return {
@@ -1682,6 +1898,78 @@ function isCompletePermissionModeMapping({
1682
1898
  );
1683
1899
  }
1684
1900
 
1901
+ function withNativeQuestionRequest({
1902
+ harnessId,
1903
+ nativeRequest,
1904
+ toolCall,
1905
+ }: {
1906
+ harnessId: string;
1907
+ nativeRequest: unknown;
1908
+ toolCall: Extract<HarnessV1StreamPart, { type: 'tool-call' }>;
1909
+ }): Extract<HarnessV1StreamPart, { type: 'tool-call' }> {
1910
+ const harnessMetadata = toolCall.providerMetadata?.[harnessId];
1911
+ return {
1912
+ ...toolCall,
1913
+ providerMetadata: {
1914
+ ...toolCall.providerMetadata,
1915
+ [harnessId]: {
1916
+ ...(harnessMetadata ?? {}),
1917
+ nativeRequest,
1918
+ } as NonNullable<
1919
+ Extract<HarnessV1StreamPart, { type: 'tool-call' }>['providerMetadata']
1920
+ >[string],
1921
+ },
1922
+ };
1923
+ }
1924
+
1925
+ function takeBufferedQuestionResult({
1926
+ bufferedQuestionResults,
1927
+ toolCallId,
1928
+ nativeRequest,
1929
+ matchesNativeRequest,
1930
+ harnessId,
1931
+ }: {
1932
+ bufferedQuestionResults: Map<
1933
+ string,
1934
+ {
1935
+ readonly output: unknown;
1936
+ readonly isError?: boolean;
1937
+ readonly toolResult: ToolResultPart;
1938
+ }
1939
+ >;
1940
+ toolCallId: string;
1941
+ nativeRequest: unknown;
1942
+ matchesNativeRequest:
1943
+ | ACPAskUserQuestionsSettings['matchesNativeRequest']
1944
+ | undefined;
1945
+ harnessId: string;
1946
+ }):
1947
+ | {
1948
+ readonly output: unknown;
1949
+ readonly isError?: boolean;
1950
+ readonly toolResult: ToolResultPart;
1951
+ }
1952
+ | undefined {
1953
+ const exact = bufferedQuestionResults.get(toolCallId);
1954
+ if (exact != null) {
1955
+ bufferedQuestionResults.delete(toolCallId);
1956
+ return exact;
1957
+ }
1958
+ if (matchesNativeRequest == null) return undefined;
1959
+ for (const [bufferedToolCallId, buffered] of bufferedQuestionResults) {
1960
+ const previousNativeRequest =
1961
+ buffered.toolResult.providerOptions?.[harnessId]?.nativeRequest;
1962
+ if (
1963
+ previousNativeRequest !== undefined &&
1964
+ matchesNativeRequest({ previousNativeRequest, nativeRequest })
1965
+ ) {
1966
+ bufferedQuestionResults.delete(bufferedToolCallId);
1967
+ return buffered;
1968
+ }
1969
+ }
1970
+ return undefined;
1971
+ }
1972
+
1685
1973
  function isPermissionModeMappingValue({
1686
1974
  value,
1687
1975
  }: {