@ai-sdk/harness-acp 1.0.2 → 1.0.3

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.2",
3
+ "version": "1.0.3",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "ws": "^8.21.0",
30
- "@ai-sdk/harness": "1.0.65",
30
+ "@ai-sdk/harness": "1.0.66",
31
31
  "@ai-sdk/provider-utils": "5.0.26"
32
32
  },
33
33
  "peerDependencies": {
@@ -37,6 +37,7 @@ export type ACPHarnessSettings<TBuiltinTools extends ToolSet = {}> = {
37
37
  readonly authentication?: ACPV1Settings['authentication'];
38
38
  readonly providerAuthentication?: ACPV1Settings['providerAuthentication'];
39
39
  readonly modelId?: ACPV1Settings['modelId'];
40
+ readonly instructionMapping?: ACPV1Settings['instructionMapping'];
40
41
  readonly permissionModeMapping?: ACPV1Settings['permissionModeMapping'];
41
42
  readonly session?: ACPV1Settings['session'];
42
43
  readonly mintBridgeToken?: ACPV1Settings['mintBridgeToken'];
package/src/index.ts CHANGED
@@ -21,6 +21,7 @@ export type {
21
21
  } from './acp-tool-call';
22
22
  export type {
23
23
  ACPAuthentication,
24
+ ACPInstructionMapping,
24
25
  ACPNpmLockedSource,
25
26
  ACPNpmSimpleSource,
26
27
  ACPPermissionModeMapping,
@@ -12,7 +12,10 @@ import {
12
12
  } from '@ai-sdk/harness';
13
13
  import { z } from 'zod/v4';
14
14
  import type { ACPToolCall } from '../acp-tool-call';
15
- import type { ACPPermissionModeMapping } from './acp-v1-settings';
15
+ import type {
16
+ ACPInstructionMapping,
17
+ ACPPermissionModeMapping,
18
+ } from './acp-v1-settings';
16
19
  import { acpTextContentBlockSchema } from './acp-v1-prompt';
17
20
 
18
21
  export type ACPBuiltinToolMapping = {
@@ -82,6 +85,21 @@ const permissionModeMappingSchema: z.ZodType<ACPPermissionModeMapping> =
82
85
  'allow-all': permissionModeTargetSchema.nullable(),
83
86
  });
84
87
 
88
+ const instructionMappingPathSchema = z.array(z.string().min(1)).min(1);
89
+
90
+ const instructionMappingSchema: z.ZodType<ACPInstructionMapping> =
91
+ z.discriminatedUnion('type', [
92
+ z.object({
93
+ type: z.literal('session-meta'),
94
+ path: instructionMappingPathSchema,
95
+ }),
96
+ z.object({
97
+ type: z.literal('launch-env-json'),
98
+ variable: z.string().min(1),
99
+ path: instructionMappingPathSchema,
100
+ }),
101
+ ]);
102
+
85
103
  export type ACPTurnStartConfig = {
86
104
  readonly version: 1;
87
105
  readonly configurationFingerprint: string;
@@ -146,6 +164,8 @@ export type OutboundMessage = z.infer<typeof outboundMessageSchema>;
146
164
 
147
165
  export const startMessageSchema = harnessV1BridgeStartBaseSchema.extend({
148
166
  prompt: z.array(acpTextContentBlockSchema),
167
+ instructions: z.string().optional(),
168
+ instructionMapping: instructionMappingSchema.optional(),
149
169
  mcpServers: z.record(z.string(), z.unknown()).optional(),
150
170
  tools: z.array(acpSerializableToolSpecSchema).optional(),
151
171
  builtinTools: z.array(builtinToolSchema).readonly().default([]),
@@ -77,6 +77,7 @@ import {
77
77
  type ACPSkillCatalogEntry,
78
78
  } from './acp-v1-prompt';
79
79
  import type {
80
+ ACPInstructionMapping,
80
81
  ACPPermissionModeMapping,
81
82
  ACPPermissionModeTarget,
82
83
  ACPSerializableValue,
@@ -376,6 +377,7 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
376
377
  proc: undefined,
377
378
  modelId: settings.modelId,
378
379
  sessionMeta: settings.session?.meta,
380
+ instructionMapping: settings.instructionMapping,
379
381
  debug: startOptions.observability?.debug,
380
382
  implementationIdentity,
381
383
  authenticationProfile,
@@ -433,6 +435,7 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
433
435
  turnStartConfig,
434
436
  authenticationProfile,
435
437
  sessionMeta: settings.session?.meta,
438
+ instructionMapping: settings.instructionMapping,
436
439
  builtinTools: builtinToolCatalog,
437
440
  permissionModeMapping,
438
441
  mcpServers: settings.mcpServers,
@@ -463,6 +466,7 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
463
466
  permissionMode,
464
467
  authenticationProfile,
465
468
  sessionMeta: settings.session?.meta,
469
+ instructionMapping: settings.instructionMapping,
466
470
  builtinTools: builtinToolCatalog,
467
471
  permissionModeMapping,
468
472
  mcpServers: settings.mcpServers,
@@ -589,6 +593,9 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
589
593
  builtinTools: builtinToolCatalog,
590
594
  permissionMode,
591
595
  permissionModeMapping,
596
+ ...(settings.instructionMapping == null
597
+ ? {}
598
+ : { instructionMapping: settings.instructionMapping }),
592
599
  ...(settings.mcpServers == null
593
600
  ? {}
594
601
  : { mcpServers: settings.mcpServers }),
@@ -622,6 +629,7 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
622
629
  proc,
623
630
  modelId: settings.modelId,
624
631
  sessionMeta: settings.session?.meta,
632
+ instructionMapping: settings.instructionMapping,
625
633
  debug: startOptions.observability?.debug,
626
634
  implementationIdentity,
627
635
  authenticationProfile,
@@ -825,6 +833,7 @@ function createSession({
825
833
  proc,
826
834
  modelId,
827
835
  sessionMeta,
836
+ instructionMapping,
828
837
  debug,
829
838
  implementationIdentity,
830
839
  authenticationProfile,
@@ -855,6 +864,7 @@ function createSession({
855
864
  proc: Experimental_SandboxProcess | undefined;
856
865
  modelId: string | undefined;
857
866
  sessionMeta: Readonly<Record<string, ACPSerializableValue>> | undefined;
867
+ instructionMapping: ACPInstructionMapping | undefined;
858
868
  debug: HarnessV1DebugConfig | undefined;
859
869
  implementationIdentity: string;
860
870
  authenticationProfile: ACPAuthenticationProfileIdentity;
@@ -1181,6 +1191,7 @@ function createSession({
1181
1191
  debug,
1182
1192
  authenticationProfile,
1183
1193
  sessionMeta,
1194
+ instructionMapping,
1184
1195
  });
1185
1196
  const control = wireTurn({
1186
1197
  emit: options.emit,
@@ -1198,9 +1209,20 @@ function createSession({
1198
1209
  ? prompt
1199
1210
  : prependACPInitialGuidance({
1200
1211
  prompt,
1201
- instructions: options.instructions,
1212
+ instructions:
1213
+ instructionMapping == null
1214
+ ? options.instructions
1215
+ : undefined,
1202
1216
  skills: skillCatalog,
1203
1217
  }),
1218
+ ...(instructionMapping == null
1219
+ ? {}
1220
+ : {
1221
+ instructionMapping,
1222
+ ...(options.instructions == null
1223
+ ? {}
1224
+ : { instructions: options.instructions }),
1225
+ }),
1204
1226
  debug,
1205
1227
  builtinTools,
1206
1228
  permissionMode,
@@ -1243,6 +1265,14 @@ function createSession({
1243
1265
  builtinTools: turnStartConfig.builtinTools,
1244
1266
  permissionMode: turnStartConfig.permissionMode,
1245
1267
  permissionModeMapping: turnStartConfig.permissionModeMapping,
1268
+ ...(instructionMapping == null
1269
+ ? {}
1270
+ : {
1271
+ instructionMapping,
1272
+ ...(options.instructions == null
1273
+ ? {}
1274
+ : { instructions: options.instructions }),
1275
+ }),
1246
1276
  ...(mcpServers == null ? {} : { mcpServers }),
1247
1277
  tools: turnStartConfig.tools,
1248
1278
  turnStartConfig,
@@ -1406,6 +1436,7 @@ function validateACPTurnStartConfig({
1406
1436
  turnStartConfig,
1407
1437
  authenticationProfile,
1408
1438
  sessionMeta,
1439
+ instructionMapping,
1409
1440
  builtinTools,
1410
1441
  permissionModeMapping,
1411
1442
  mcpServers,
@@ -1413,6 +1444,7 @@ function validateACPTurnStartConfig({
1413
1444
  turnStartConfig: ACPTurnStartConfig;
1414
1445
  authenticationProfile: ACPAuthenticationProfileIdentity;
1415
1446
  sessionMeta: Readonly<Record<string, ACPSerializableValue>> | undefined;
1447
+ instructionMapping: ACPInstructionMapping | undefined;
1416
1448
  builtinTools: ReadonlyArray<ACPBuiltinToolMapping>;
1417
1449
  permissionModeMapping: ACPPermissionModeMapping | undefined;
1418
1450
  mcpServers: Record<string, unknown> | undefined;
@@ -1427,6 +1459,7 @@ function validateACPTurnStartConfig({
1427
1459
  debug: turnStartConfig.debug,
1428
1460
  authenticationProfile,
1429
1461
  sessionMeta,
1462
+ instructionMapping,
1430
1463
  });
1431
1464
  if (
1432
1465
  current.configurationFingerprint !==
@@ -1444,6 +1477,7 @@ function validateACPColdSessionConfiguration({
1444
1477
  permissionMode,
1445
1478
  authenticationProfile,
1446
1479
  sessionMeta,
1480
+ instructionMapping,
1447
1481
  builtinTools,
1448
1482
  permissionModeMapping,
1449
1483
  mcpServers,
@@ -1454,6 +1488,7 @@ function validateACPColdSessionConfiguration({
1454
1488
  permissionMode: NonNullable<StartMessage['permissionMode']>;
1455
1489
  authenticationProfile: ACPAuthenticationProfileIdentity;
1456
1490
  sessionMeta: Readonly<Record<string, ACPSerializableValue>> | undefined;
1491
+ instructionMapping: ACPInstructionMapping | undefined;
1457
1492
  builtinTools: ReadonlyArray<ACPBuiltinToolMapping>;
1458
1493
  permissionModeMapping: ACPPermissionModeMapping | undefined;
1459
1494
  mcpServers: Record<string, unknown> | undefined;
@@ -1469,6 +1504,7 @@ function validateACPColdSessionConfiguration({
1469
1504
  debug,
1470
1505
  authenticationProfile,
1471
1506
  sessionMeta,
1507
+ instructionMapping,
1472
1508
  });
1473
1509
  if (
1474
1510
  current.configurationFingerprint !== coldSession.configurationFingerprint ||
@@ -75,6 +75,22 @@ export type ACPPermissionModeMapping = Readonly<
75
75
  Record<HarnessV1PermissionMode, ACPPermissionModeTarget | null>
76
76
  >;
77
77
 
78
+ /**
79
+ * Describes where an ACP implementation accepts native session instructions.
80
+ * Session metadata paths are relative to `_meta`; launch environment paths are
81
+ * relative to the parsed JSON object stored in `variable`.
82
+ */
83
+ export type ACPInstructionMapping =
84
+ | {
85
+ readonly type: 'session-meta';
86
+ readonly path: ReadonlyArray<string>;
87
+ }
88
+ | {
89
+ readonly type: 'launch-env-json';
90
+ readonly variable: string;
91
+ readonly path: ReadonlyArray<string>;
92
+ };
93
+
78
94
  export type ACPV1Settings = {
79
95
  readonly version?: 'v1';
80
96
  readonly harnessId: string;
@@ -93,6 +109,11 @@ export type ACPV1Settings = {
93
109
  readonly authentication?: ACPAuthentication;
94
110
  readonly providerAuthentication?: ACPProviderAuthentication;
95
111
  readonly modelId?: string;
112
+ /**
113
+ * Routes HarnessAgent instructions to a runtime-native system or developer
114
+ * prompt. When omitted, instructions are prepended to the first user prompt.
115
+ */
116
+ readonly instructionMapping?: ACPInstructionMapping;
96
117
  readonly permissionModeMapping?: ACPPermissionModeMapping;
97
118
  readonly session?: {
98
119
  readonly meta?: Readonly<Record<string, ACPSerializableValue>>;
@@ -10,6 +10,7 @@ import {
10
10
  } from './acp-v1-bridge-protocol';
11
11
  import type { ACPTextContentBlock } from './acp-v1-prompt';
12
12
  import type {
13
+ ACPInstructionMapping,
13
14
  ACPPermissionModeMapping,
14
15
  ACPSerializableValue,
15
16
  } from './acp-v1-settings';
@@ -24,6 +25,7 @@ export function createACPTurnStartConfig({
24
25
  debug,
25
26
  authenticationProfile,
26
27
  sessionMeta,
28
+ instructionMapping,
27
29
  }: {
28
30
  prompt: ReadonlyArray<ACPTextContentBlock>;
29
31
  tools: ReadonlyArray<HarnessV1ToolSpec>;
@@ -34,6 +36,7 @@ export function createACPTurnStartConfig({
34
36
  debug: HarnessV1DebugConfig | undefined;
35
37
  authenticationProfile: ACPAuthenticationProfileIdentity;
36
38
  sessionMeta: Readonly<Record<string, ACPSerializableValue>> | undefined;
39
+ instructionMapping: ACPInstructionMapping | undefined;
37
40
  }): ACPTurnStartConfig {
38
41
  return {
39
42
  version: 1,
@@ -42,6 +45,7 @@ export function createACPTurnStartConfig({
42
45
  stableStringify({
43
46
  authenticationProfile,
44
47
  sessionMeta: sessionMeta ?? null,
48
+ ...(instructionMapping == null ? {} : { instructionMapping }),
45
49
  builtinTools,
46
50
  permissionModeMapping: permissionModeMapping ?? null,
47
51
  mcpServers: mcpServers ?? null,
@@ -32,6 +32,7 @@ import {
32
32
  } from './acp-diagnostics';
33
33
  import { monitorACPAgentStderr } from './agent-stderr-monitor';
34
34
  import { createEmitStreamEvent } from './create-emit-stream-event';
35
+ import { resolveACPInstructionConfiguration } from './instruction-mapping';
35
36
  import {
36
37
  startHostToolRelay,
37
38
  type HostToolRelay,
@@ -326,6 +327,7 @@ async function ensureSession({
326
327
  authentication: bridgeConfiguration.authentication,
327
328
  providerAuthentication: bridgeConfiguration.providerAuthentication,
328
329
  sessionMeta: bridgeConfiguration.sessionMeta,
330
+ instructionMapping: start.instructionMapping,
329
331
  permissionMode: start.permissionMode,
330
332
  permissionModeMapping: start.permissionModeMapping,
331
333
  mcpServers: start.mcpServers,
@@ -367,12 +369,18 @@ async function ensureSession({
367
369
  providerAuthentication: bridgeConfiguration.providerAuthentication,
368
370
  gateway,
369
371
  });
372
+ const instructionConfiguration = await resolveACPInstructionConfiguration({
373
+ instructions: start.instructions,
374
+ instructionMapping: start.instructionMapping,
375
+ sessionMeta: bridgeConfiguration.sessionMeta,
376
+ environment: createChildEnvironment({ launchEnv }),
377
+ });
370
378
  child = spawn(
371
379
  `${implementationDir}/node_modules/.bin/${implementation.executable}`,
372
380
  [...implementation.args],
373
381
  {
374
382
  cwd: workDir,
375
- env: createChildEnvironment({ launchEnv }),
383
+ env: instructionConfiguration.environment,
376
384
  shell: false,
377
385
  stdio: ['pipe', 'pipe', 'pipe'],
378
386
  },
@@ -490,9 +498,9 @@ async function ensureSession({
490
498
  sessionId: recoveredSessionId,
491
499
  cwd: workDir,
492
500
  mcpServers,
493
- ...(bridgeConfiguration.sessionMeta == null
501
+ ...(instructionConfiguration.sessionMeta == null
494
502
  ? {}
495
- : { _meta: bridgeConfiguration.sessionMeta }),
503
+ : { _meta: instructionConfiguration.sessionMeta }),
496
504
  });
497
505
  createdSession = createACPRecoveredSession({
498
506
  agent: connection.agent,
@@ -509,7 +517,7 @@ async function ensureSession({
509
517
  sessionId: recoveredSessionId,
510
518
  cwd: workDir,
511
519
  mcpServers,
512
- meta: bridgeConfiguration.sessionMeta,
520
+ meta: instructionConfiguration.sessionMeta,
513
521
  harnessId: bridgeType,
514
522
  setHistoricalUpdatesSuppressed: ({ suppressed }) => {
515
523
  historicalUpdatesSuppressed = suppressed;
@@ -530,9 +538,9 @@ async function ensureSession({
530
538
  .buildSession({
531
539
  cwd: workDir,
532
540
  mcpServers,
533
- ...(bridgeConfiguration.sessionMeta == null
541
+ ...(instructionConfiguration.sessionMeta == null
534
542
  ? {}
535
- : { _meta: bridgeConfiguration.sessionMeta }),
543
+ : { _meta: instructionConfiguration.sessionMeta }),
536
544
  })
537
545
  .start();
538
546
  }
@@ -0,0 +1,132 @@
1
+ import { z } from 'zod/v4';
2
+ import type {
3
+ ACPInstructionMapping,
4
+ ACPSerializableValue,
5
+ } from '../acp-v1-settings';
6
+
7
+ const UNSAFE_PATH_SEGMENTS = new Set(['__proto__', 'constructor', 'prototype']);
8
+
9
+ const serializableRecordSchema: z.ZodType<
10
+ Readonly<Record<string, ACPSerializableValue>>
11
+ > = z.record(z.string(), z.json());
12
+
13
+ export async function resolveACPInstructionConfiguration({
14
+ instructions,
15
+ instructionMapping,
16
+ sessionMeta,
17
+ environment,
18
+ }: {
19
+ instructions: string | undefined;
20
+ instructionMapping: ACPInstructionMapping | undefined;
21
+ sessionMeta: Readonly<Record<string, ACPSerializableValue>> | undefined;
22
+ environment: Readonly<Record<string, string | undefined>>;
23
+ }): Promise<{
24
+ sessionMeta: Readonly<Record<string, ACPSerializableValue>> | undefined;
25
+ environment: Record<string, string | undefined>;
26
+ }> {
27
+ const resolvedEnvironment = { ...environment };
28
+ if (
29
+ instructionMapping == null ||
30
+ instructions == null ||
31
+ instructions.length === 0
32
+ ) {
33
+ return { sessionMeta, environment: resolvedEnvironment };
34
+ }
35
+
36
+ assertSafePath({ path: instructionMapping.path });
37
+
38
+ if (instructionMapping.type === 'session-meta') {
39
+ return {
40
+ sessionMeta: setStringAtPath({
41
+ record: sessionMeta ?? {},
42
+ path: instructionMapping.path,
43
+ value: instructions,
44
+ }),
45
+ environment: resolvedEnvironment,
46
+ };
47
+ }
48
+
49
+ const serialized = resolvedEnvironment[instructionMapping.variable];
50
+ let configuration: Readonly<Record<string, ACPSerializableValue>> = {};
51
+ if (serialized != null && serialized.length > 0) {
52
+ configuration = parseSerializableRecord({
53
+ serialized,
54
+ variable: instructionMapping.variable,
55
+ });
56
+ }
57
+
58
+ resolvedEnvironment[instructionMapping.variable] = JSON.stringify(
59
+ setStringAtPath({
60
+ record: configuration,
61
+ path: instructionMapping.path,
62
+ value: instructions,
63
+ }),
64
+ );
65
+
66
+ return { sessionMeta, environment: resolvedEnvironment };
67
+ }
68
+
69
+ /*
70
+ * The bridge runs in an isolated runtime whose dependencies are limited to the
71
+ * ones declared in this directory's package.json, so AI SDK JSON helpers are
72
+ * unavailable here. Schema validation plus a guarded `JSON.parse` provides the
73
+ * same guarantee: nothing but a validated serializable record escapes.
74
+ */
75
+ function parseSerializableRecord({
76
+ serialized,
77
+ variable,
78
+ }: {
79
+ serialized: string;
80
+ variable: string;
81
+ }): Readonly<Record<string, ACPSerializableValue>> {
82
+ try {
83
+ const result = serializableRecordSchema.safeParse(JSON.parse(serialized));
84
+ if (result.success) return result.data;
85
+ } catch {}
86
+ throw new Error(
87
+ `ACP instruction mapping environment variable ${JSON.stringify(variable)} must contain a JSON object.`,
88
+ );
89
+ }
90
+
91
+ function setStringAtPath({
92
+ record,
93
+ path,
94
+ value,
95
+ }: {
96
+ record: Readonly<Record<string, ACPSerializableValue>>;
97
+ path: ReadonlyArray<string>;
98
+ value: string;
99
+ }): Record<string, ACPSerializableValue> {
100
+ const [key, ...remainingPath] = path;
101
+ if (key == null) return { ...record };
102
+ return {
103
+ ...record,
104
+ [key]:
105
+ remainingPath.length === 0
106
+ ? value
107
+ : setStringAtPath({
108
+ record: isRecord(record[key]) ? record[key] : {},
109
+ path: remainingPath,
110
+ value,
111
+ }),
112
+ };
113
+ }
114
+
115
+ function assertSafePath({ path }: { path: ReadonlyArray<string> }): void {
116
+ if (
117
+ path.length === 0 ||
118
+ path.some(
119
+ segment => segment.length === 0 || UNSAFE_PATH_SEGMENTS.has(segment),
120
+ )
121
+ ) {
122
+ throw new Error(
123
+ 'ACP instruction mapping path must contain only safe, non-empty property names.',
124
+ );
125
+ }
126
+ }
127
+
128
+ function isRecord(
129
+ value: ACPSerializableValue | undefined,
130
+ ): value is Readonly<Record<string, ACPSerializableValue>> {
131
+ return value != null && typeof value === 'object' && !Array.isArray(value);
132
+ }
package/src/v1/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { createACPV1 } from './acp-v1-harness';
2
2
  export type {
3
3
  ACPAuthentication,
4
+ ACPInstructionMapping,
4
5
  ACPNpmLockedSource,
5
6
  ACPNpmSimpleSource,
6
7
  ACPPermissionModeMapping,