@ai-sdk/harness-acp 1.0.1 → 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/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # @ai-sdk/harness-acp
2
2
 
3
+ ## 1.0.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 9ee30bf: fix(harness): pass instructions appended to system / developer prompt instead of using the workaround of first user prompt
8
+ - Updated dependencies [9ee30bf]
9
+ - @ai-sdk/harness@1.0.66
10
+
11
+ ## 1.0.2
12
+
13
+ ### Patch Changes
14
+
15
+ - a03ff6c: feat(harness): add support for per-harness MCP servers
16
+ - fc3baaf: feat(harness): add optional `mintBridgeToken(sandboxId)` to harness settings to control the bridge token value
17
+ - Updated dependencies [401a4ba]
18
+ - Updated dependencies [a03ff6c]
19
+ - @ai-sdk/provider-utils@5.0.26
20
+ - @ai-sdk/harness@1.0.65
21
+
3
22
  ## 1.0.1
4
23
 
5
24
  ### Patch Changes
package/README.md CHANGED
@@ -34,6 +34,11 @@ const codexACP = createACP({
34
34
  },
35
35
  executable: 'codex-acp',
36
36
  forwardEnv: ['CODEX_API_KEY', 'OPENAI_API_KEY'],
37
+ instructionMapping: {
38
+ type: 'launch-env-json',
39
+ variable: 'CODEX_CONFIG',
40
+ path: ['developer_instructions'],
41
+ },
37
42
  permissionModeMapping: {
38
43
  'allow-reads': null,
39
44
  'allow-edits': null,
@@ -69,3 +74,11 @@ resolves the value when the ACP process starts and does not store it in the
69
74
  profile. Codex ACP supports only `permissionMode: 'allow-all'` because its
70
75
  restrictive modes enable Codex's internal sandbox. A bridge-backed ACP harness
71
76
  requires a sandbox with at least one exposed port.
77
+
78
+ Use `instructionMapping` when the ACP implementation exposes a native system
79
+ or developer prompt. A `session-meta` mapping writes `HarnessAgent`
80
+ instructions below the ACP session request's `_meta` field. A
81
+ `launch-env-json` mapping merges them into a JSON environment variable before
82
+ the implementation starts. Without a mapping, the adapter preserves its
83
+ backward-compatible behavior and prepends instructions to the first user
84
+ prompt.
@@ -12,7 +12,7 @@ import {
12
12
  ListToolsRequestSchema,
13
13
  McpError
14
14
  } from "@modelcontextprotocol/sdk/types.js";
15
- var VERSION = true ? "1.0.1" : "0.0.0-test";
15
+ var VERSION = true ? "1.0.3" : "0.0.0-test";
16
16
  function createHostToolMCPServer({
17
17
  tools: tools2,
18
18
  revision = 1,
@@ -890,6 +890,7 @@ function monitorACPAgentStderr({
890
890
  // src/v1/bridge/stream-translator.ts
891
891
  function createACPStreamTranslator({
892
892
  emit,
893
+ emitToolCallCandidate,
893
894
  builtinTools = []
894
895
  }) {
895
896
  let openBlock;
@@ -1000,13 +1001,12 @@ function createACPStreamTranslator({
1000
1001
  if (builtin != null) {
1001
1002
  state.toolName = builtin.toolName;
1002
1003
  state.nativeName = builtin.nativeName != null && builtin.toolName !== builtin.nativeName ? builtin.nativeName : void 0;
1003
- state.dynamic = false;
1004
1004
  } else {
1005
1005
  state.toolName = createDynamicToolName({
1006
1006
  programmaticName,
1007
1007
  toolCallId: state.toolCallId
1008
1008
  });
1009
- state.dynamic = true;
1009
+ emitToolCallCandidate?.({ toolCall: createACPToolCall({ state }) });
1010
1010
  }
1011
1011
  emit({
1012
1012
  type: "tool-call",
@@ -1014,7 +1014,6 @@ function createACPStreamTranslator({
1014
1014
  toolName: state.toolName,
1015
1015
  input: stringifyToolInput({ input: state.values.rawInput }),
1016
1016
  providerExecuted: true,
1017
- ...state.dynamic ? { dynamic: true } : {},
1018
1017
  ...state.nativeName == null ? {} : { nativeName: state.nativeName }
1019
1018
  });
1020
1019
  state.emittedCall = true;
@@ -1029,8 +1028,7 @@ function createACPStreamTranslator({
1029
1028
  toolCallId: state.toolCallId,
1030
1029
  toolName: state.toolName,
1031
1030
  result: createToolResult({ state }),
1032
- ...state.values.status === "failed" ? { isError: true } : {},
1033
- ...state.dynamic ? { dynamic: true } : {}
1031
+ ...state.values.status === "failed" ? { isError: true } : {}
1034
1032
  });
1035
1033
  state.emittedResult = true;
1036
1034
  pendingToolCallIds.delete(state.toolCallId);
@@ -1345,6 +1343,31 @@ function mergeToolUpdate({
1345
1343
  });
1346
1344
  if (programmaticName != null) state.values.name = programmaticName;
1347
1345
  }
1346
+ function createACPToolCall({ state }) {
1347
+ const title = getStringProperty2({
1348
+ value: state.values,
1349
+ property: "title"
1350
+ });
1351
+ const kind = state.values.kind;
1352
+ const status = state.values.status;
1353
+ return {
1354
+ toolCallId: state.toolCallId,
1355
+ title: title ?? state.toolName ?? `Tool ${state.toolCallId}`,
1356
+ ...isACPToolKind(kind) ? { kind } : {},
1357
+ ...isACPToolCallStatus(status) ? { status } : {},
1358
+ ...Array.isArray(state.values.content) ? { content: state.values.content } : {},
1359
+ ...Array.isArray(state.values.locations) ? { locations: state.values.locations } : {},
1360
+ ...Object.prototype.hasOwnProperty.call(state.values, "rawInput") ? { rawInput: state.values.rawInput } : {},
1361
+ ...Object.prototype.hasOwnProperty.call(state.values, "rawOutput") ? { rawOutput: state.values.rawOutput } : {},
1362
+ ...state.values._meta === null || isRecord3(state.values._meta) ? { _meta: state.values._meta } : {}
1363
+ };
1364
+ }
1365
+ function isACPToolKind(value) {
1366
+ return value === "read" || value === "edit" || value === "delete" || value === "move" || value === "search" || value === "execute" || value === "think" || value === "fetch" || value === "switch_mode" || value === "other";
1367
+ }
1368
+ function isACPToolCallStatus(value) {
1369
+ return value === "pending" || value === "in_progress" || value === "completed" || value === "failed";
1370
+ }
1348
1371
  function emitFileChanges({
1349
1372
  state,
1350
1373
  emit
@@ -1946,11 +1969,16 @@ function isTerminalToolUpdate(message) {
1946
1969
  // src/v1/bridge/create-emit-stream-event.ts
1947
1970
  function createEmitStreamEvent({
1948
1971
  emit,
1972
+ emitToolCallCandidate,
1949
1973
  builtinTools,
1950
1974
  hostToolServerName,
1951
1975
  hostTools
1952
1976
  }) {
1953
- const translator = createACPStreamTranslator({ emit, builtinTools });
1977
+ const translator = createACPStreamTranslator({
1978
+ emit,
1979
+ emitToolCallCandidate,
1980
+ builtinTools
1981
+ });
1954
1982
  const correlation = createHostToolCorrelation({
1955
1983
  emitSemanticUpdate: ({ message, rawUpdate }) => {
1956
1984
  translator.update({
@@ -1996,6 +2024,90 @@ function createEmitStreamEvent({
1996
2024
  };
1997
2025
  }
1998
2026
 
2027
+ // src/v1/bridge/instruction-mapping.ts
2028
+ import { z as z2 } from "zod/v4";
2029
+ var UNSAFE_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
2030
+ var serializableRecordSchema2 = z2.record(z2.string(), z2.json());
2031
+ async function resolveACPInstructionConfiguration({
2032
+ instructions,
2033
+ instructionMapping,
2034
+ sessionMeta,
2035
+ environment
2036
+ }) {
2037
+ const resolvedEnvironment = { ...environment };
2038
+ if (instructionMapping == null || instructions == null || instructions.length === 0) {
2039
+ return { sessionMeta, environment: resolvedEnvironment };
2040
+ }
2041
+ assertSafePath({ path: instructionMapping.path });
2042
+ if (instructionMapping.type === "session-meta") {
2043
+ return {
2044
+ sessionMeta: setStringAtPath({
2045
+ record: sessionMeta ?? {},
2046
+ path: instructionMapping.path,
2047
+ value: instructions
2048
+ }),
2049
+ environment: resolvedEnvironment
2050
+ };
2051
+ }
2052
+ const serialized = resolvedEnvironment[instructionMapping.variable];
2053
+ let configuration = {};
2054
+ if (serialized != null && serialized.length > 0) {
2055
+ configuration = parseSerializableRecord({
2056
+ serialized,
2057
+ variable: instructionMapping.variable
2058
+ });
2059
+ }
2060
+ resolvedEnvironment[instructionMapping.variable] = JSON.stringify(
2061
+ setStringAtPath({
2062
+ record: configuration,
2063
+ path: instructionMapping.path,
2064
+ value: instructions
2065
+ })
2066
+ );
2067
+ return { sessionMeta, environment: resolvedEnvironment };
2068
+ }
2069
+ function parseSerializableRecord({
2070
+ serialized,
2071
+ variable
2072
+ }) {
2073
+ try {
2074
+ const result = serializableRecordSchema2.safeParse(JSON.parse(serialized));
2075
+ if (result.success) return result.data;
2076
+ } catch {
2077
+ }
2078
+ throw new Error(
2079
+ `ACP instruction mapping environment variable ${JSON.stringify(variable)} must contain a JSON object.`
2080
+ );
2081
+ }
2082
+ function setStringAtPath({
2083
+ record,
2084
+ path,
2085
+ value
2086
+ }) {
2087
+ const [key, ...remainingPath] = path;
2088
+ if (key == null) return { ...record };
2089
+ return {
2090
+ ...record,
2091
+ [key]: remainingPath.length === 0 ? value : setStringAtPath({
2092
+ record: isRecord5(record[key]) ? record[key] : {},
2093
+ path: remainingPath,
2094
+ value
2095
+ })
2096
+ };
2097
+ }
2098
+ function assertSafePath({ path }) {
2099
+ if (path.length === 0 || path.some(
2100
+ (segment) => segment.length === 0 || UNSAFE_PATH_SEGMENTS.has(segment)
2101
+ )) {
2102
+ throw new Error(
2103
+ "ACP instruction mapping path must contain only safe, non-empty property names."
2104
+ );
2105
+ }
2106
+ }
2107
+ function isRecord5(value) {
2108
+ return value != null && typeof value === "object" && !Array.isArray(value);
2109
+ }
2110
+
1999
2111
  // src/v1/bridge/host-tool-relay.ts
2000
2112
  import { randomBytes, timingSafeEqual } from "crypto";
2001
2113
  import { createServer } from "http";
@@ -2122,7 +2234,7 @@ async function handleCatalogNext({
2122
2234
  body,
2123
2235
  state
2124
2236
  }) {
2125
- if (!isRecord5(body) || !Number.isSafeInteger(body.afterRevision) || body.afterRevision < 0) {
2237
+ if (!isRecord6(body) || !Number.isSafeInteger(body.afterRevision) || body.afterRevision < 0) {
2126
2238
  throw new RelayRequestError({
2127
2239
  status: 400,
2128
2240
  message: "Invalid host tool catalog poll request."
@@ -2141,7 +2253,7 @@ function handleCatalogSeen({
2141
2253
  body,
2142
2254
  state
2143
2255
  }) {
2144
- if (!isRecord5(body) || !Number.isSafeInteger(body.revision) || body.revision < 1 || body.revision > state.revision) {
2256
+ if (!isRecord6(body) || !Number.isSafeInteger(body.revision) || body.revision < 1 || body.revision > state.revision) {
2145
2257
  throw new RelayRequestError({
2146
2258
  status: 400,
2147
2259
  message: "Invalid host tool catalog acknowledgment."
@@ -2167,7 +2279,7 @@ async function handleInvocation({
2167
2279
  message: "No ACP prompt turn is active."
2168
2280
  });
2169
2281
  }
2170
- if (!isRecord5(body) || typeof body.requestId !== "string" || typeof body.toolName !== "string" || !isRecord5(body.input) || !Number.isSafeInteger(body.catalogRevision)) {
2282
+ if (!isRecord6(body) || typeof body.requestId !== "string" || typeof body.toolName !== "string" || !isRecord6(body.input) || !Number.isSafeInteger(body.catalogRevision)) {
2171
2283
  throw new RelayRequestError({
2172
2284
  status: 400,
2173
2285
  message: "Invalid host tool relay request."
@@ -2283,7 +2395,7 @@ function canonicalizeJSON2({ value }) {
2283
2395
  if (Array.isArray(value)) {
2284
2396
  return value.map((item) => canonicalizeJSON2({ value: item }));
2285
2397
  }
2286
- if (!isRecord5(value)) return value;
2398
+ if (!isRecord6(value)) return value;
2287
2399
  return Object.fromEntries(
2288
2400
  Object.keys(value).sort().filter((key) => value[key] !== void 0).map((key) => [key, canonicalizeJSON2({ value: value[key] })])
2289
2401
  );
@@ -2341,7 +2453,7 @@ function closeServer({ server }) {
2341
2453
  });
2342
2454
  });
2343
2455
  }
2344
- function isRecord5(value) {
2456
+ function isRecord6(value) {
2345
2457
  return value != null && typeof value === "object" && !Array.isArray(value);
2346
2458
  }
2347
2459
  var RelayRequestError = class extends Error {
@@ -2906,6 +3018,12 @@ async function runTurn(start, turn) {
2906
3018
  turn.emit({ type: "stream-start" });
2907
3019
  const emitStreamEvent = createEmitStreamEvent({
2908
3020
  emit: (event) => turn.emit(event),
3021
+ emitToolCallCandidate: ({ toolCall }) => {
3022
+ turn.emit({
3023
+ type: "acp-tool-call-candidate",
3024
+ toolCall
3025
+ });
3026
+ },
2909
3027
  builtinTools: start.builtinTools,
2910
3028
  hostToolServerName: HOST_TOOL_MCP_SERVER_NAME,
2911
3029
  hostTools: start.tools ?? []
@@ -2990,8 +3108,10 @@ async function ensureSession({
2990
3108
  authentication: bridgeConfiguration.authentication,
2991
3109
  providerAuthentication: bridgeConfiguration.providerAuthentication,
2992
3110
  sessionMeta: bridgeConfiguration.sessionMeta,
3111
+ instructionMapping: start.instructionMapping,
2993
3112
  permissionMode: start.permissionMode,
2994
- permissionModeMapping: start.permissionModeMapping
3113
+ permissionModeMapping: start.permissionModeMapping,
3114
+ mcpServers: start.mcpServers
2995
3115
  });
2996
3116
  if (session != null) {
2997
3117
  if (catalogRefreshError != null) throw catalogRefreshError;
@@ -3026,12 +3146,18 @@ async function ensureSession({
3026
3146
  providerAuthentication: bridgeConfiguration.providerAuthentication,
3027
3147
  gateway
3028
3148
  });
3149
+ const instructionConfiguration = await resolveACPInstructionConfiguration({
3150
+ instructions: start.instructions,
3151
+ instructionMapping: start.instructionMapping,
3152
+ sessionMeta: bridgeConfiguration.sessionMeta,
3153
+ environment: createChildEnvironment({ launchEnv })
3154
+ });
3029
3155
  child = spawn(
3030
3156
  `${implementationDir}/node_modules/.bin/${implementation.executable}`,
3031
3157
  [...implementation.args],
3032
3158
  {
3033
3159
  cwd: workDir,
3034
- env: createChildEnvironment({ launchEnv }),
3160
+ env: instructionConfiguration.environment,
3035
3161
  shell: false,
3036
3162
  stdio: ["pipe", "pipe", "pipe"]
3037
3163
  }
@@ -3088,6 +3214,10 @@ async function ensureSession({
3088
3214
  authentication
3089
3215
  });
3090
3216
  }
3217
+ const externalMcpServers = createExternalMcpServers({
3218
+ mcpServers: start.mcpServers,
3219
+ initialization
3220
+ });
3091
3221
  const tools = start.tools ?? [];
3092
3222
  const catalogPath = `${bridgeStateDir}/host-tools.json`;
3093
3223
  await writeFile2(catalogPath, JSON.stringify(tools), { mode: 384 });
@@ -3096,6 +3226,7 @@ async function ensureSession({
3096
3226
  serverName: HOST_TOOL_MCP_SERVER_NAME
3097
3227
  });
3098
3228
  const mcpServers = [
3229
+ ...externalMcpServers,
3099
3230
  {
3100
3231
  name: HOST_TOOL_MCP_SERVER_NAME,
3101
3232
  command: process.execPath,
@@ -3125,7 +3256,7 @@ async function ensureSession({
3125
3256
  sessionId: recoveredSessionId,
3126
3257
  cwd: workDir,
3127
3258
  mcpServers,
3128
- ...bridgeConfiguration.sessionMeta == null ? {} : { _meta: bridgeConfiguration.sessionMeta }
3259
+ ...instructionConfiguration.sessionMeta == null ? {} : { _meta: instructionConfiguration.sessionMeta }
3129
3260
  });
3130
3261
  createdSession = createACPRecoveredSession({
3131
3262
  agent: connection.agent,
@@ -3142,7 +3273,7 @@ async function ensureSession({
3142
3273
  sessionId: recoveredSessionId,
3143
3274
  cwd: workDir,
3144
3275
  mcpServers,
3145
- meta: bridgeConfiguration.sessionMeta,
3276
+ meta: instructionConfiguration.sessionMeta,
3146
3277
  harnessId: bridgeType,
3147
3278
  setHistoricalUpdatesSuppressed: ({ suppressed }) => {
3148
3279
  historicalUpdatesSuppressed = suppressed;
@@ -3162,7 +3293,7 @@ async function ensureSession({
3162
3293
  createdSession = await connection.agent.buildSession({
3163
3294
  cwd: workDir,
3164
3295
  mcpServers,
3165
- ...bridgeConfiguration.sessionMeta == null ? {} : { _meta: bridgeConfiguration.sessionMeta }
3296
+ ...instructionConfiguration.sessionMeta == null ? {} : { _meta: instructionConfiguration.sessionMeta }
3166
3297
  }).start();
3167
3298
  }
3168
3299
  try {
@@ -3197,6 +3328,33 @@ async function ensureSession({
3197
3328
  });
3198
3329
  sessionConfigurationFingerprint = fingerprint;
3199
3330
  }
3331
+ function createExternalMcpServers({
3332
+ mcpServers,
3333
+ initialization
3334
+ }) {
3335
+ if (mcpServers == null) return [];
3336
+ return Object.entries(mcpServers).map(([name2, value]) => {
3337
+ if (!isRecord7(value)) {
3338
+ throw new Error(
3339
+ `ACP MCP server ${JSON.stringify(name2)} must be configured with an object value.`
3340
+ );
3341
+ }
3342
+ if (value.type === "acp") {
3343
+ throw new HarnessBridgeCapabilityUnsupportedError({
3344
+ harnessId: bridgeType,
3345
+ message: "ACP-transport MCP servers require client-side mcp/connect handling, which this harness does not provide."
3346
+ });
3347
+ }
3348
+ const mcpCapabilities = initialization.agentCapabilities?.mcpCapabilities;
3349
+ if (value.type === "http" && mcpCapabilities?.http !== true || value.type === "sse" && mcpCapabilities?.sse !== true) {
3350
+ throw new HarnessBridgeCapabilityUnsupportedError({
3351
+ harnessId: bridgeType,
3352
+ message: `The ACP agent does not advertise support for ${value.type.toUpperCase()} MCP servers.`
3353
+ });
3354
+ }
3355
+ return { ...value, name: name2 };
3356
+ });
3357
+ }
3200
3358
  function unknownUsage2() {
3201
3359
  return {
3202
3360
  inputTokens: {
@@ -3272,7 +3430,7 @@ function createChildEnvironment({
3272
3430
  ...launchEnv
3273
3431
  };
3274
3432
  }
3275
- function isRecord6(value) {
3433
+ function isRecord7(value) {
3276
3434
  return value != null && typeof value === "object" && !Array.isArray(value);
3277
3435
  }
3278
3436
  async function readImplementationDescriptor({
@@ -3282,7 +3440,7 @@ async function readImplementationDescriptor({
3282
3440
  const value = await new Response(text, {
3283
3441
  headers: { "content-type": "application/json" }
3284
3442
  }).json();
3285
- if (!isRecord6(value) || typeof value.executable !== "string" || !Array.isArray(value.args) || !value.args.every((item) => typeof item === "string") || !Array.isArray(value.envKeys) || !value.envKeys.every((item) => typeof item === "string")) {
3443
+ if (!isRecord7(value) || typeof value.executable !== "string" || !Array.isArray(value.args) || !value.args.every((item) => typeof item === "string") || !Array.isArray(value.envKeys) || !value.envKeys.every((item) => typeof item === "string")) {
3286
3444
  throw new Error("Invalid ACP implementation descriptor.");
3287
3445
  }
3288
3446
  return {