@ai-sdk/harness-acp 1.0.0 → 1.0.1

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.0",
3
+ "version": "1.0.1",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -27,8 +27,8 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "ws": "^8.21.0",
30
- "@ai-sdk/harness": "1.0.63",
31
- "@ai-sdk/provider-utils": "5.0.24"
30
+ "@ai-sdk/harness": "1.0.64",
31
+ "@ai-sdk/provider-utils": "5.0.25"
32
32
  },
33
33
  "peerDependencies": {
34
34
  "zod": "^3.25.76 || ^4.1.8"
@@ -1,19 +1,11 @@
1
1
  import type { HarnessV1 } from '@ai-sdk/harness';
2
2
  import type { ToolSet } from '@ai-sdk/provider-utils';
3
3
  import { z } from 'zod/v4';
4
- import {
5
- createACPAuthenticationProfileIdentity,
6
- resolveACPProviderAuthenticationCompatibility,
7
- type ACPAuthenticationProfileIdentity,
8
- type ACPClientApp,
9
- } from './acp-auth';
4
+ import type { ACPClientApp } from './acp-auth';
10
5
  import { createACPV1, type ACPV1Settings } from './v1';
11
- import { createImplementationIdentity } from './v1/implementation';
12
6
  import {
13
7
  acpColdSessionStateSchema,
14
8
  acpTurnStartConfigSchema,
15
- type ACPColdSessionState,
16
- type ACPTurnStartConfig,
17
9
  } from './v1/acp-v1-bridge-protocol';
18
10
  import { VERSION } from './version';
19
11
 
@@ -30,7 +22,11 @@ export type ACPHarnessSettings<TBuiltinTools extends ToolSet = {}> = {
30
22
  readonly version?: ACPV1Settings['version'];
31
23
  readonly harnessId: ACPV1Settings['harnessId'];
32
24
  readonly auth?: ACPV1Settings['auth'];
33
- readonly implementation: ACPV1Settings['implementation'];
25
+ readonly source: ACPV1Settings['source'];
26
+ readonly executable: ACPV1Settings['executable'];
27
+ readonly args?: ACPV1Settings['args'];
28
+ readonly forwardEnv?: ACPV1Settings['forwardEnv'];
29
+ readonly env?: ACPV1Settings['env'];
34
30
  readonly authentication?: ACPV1Settings['authentication'];
35
31
  readonly providerAuthentication?: ACPV1Settings['providerAuthentication'];
36
32
  readonly modelId?: ACPV1Settings['modelId'];
@@ -86,8 +82,6 @@ const acpResumeStateSchema = z.object({
86
82
  skillsFingerprint: z.string().optional(),
87
83
  });
88
84
 
89
- type ACPBridgeCoords = z.infer<typeof acpBridgeCoordsSchema>;
90
-
91
85
  export function createACP<TBuiltinTools extends ToolSet = {}>(
92
86
  settings: ACPHarnessSettings<TBuiltinTools>,
93
87
  ): HarnessV1<TBuiltinTools> {
@@ -95,50 +89,6 @@ export function createACP<TBuiltinTools extends ToolSet = {}>(
95
89
  switch (version) {
96
90
  case 'v1': {
97
91
  const clientApp = settings.clientApp ?? ACP_CLIENT_APP;
98
- const providerAuthenticationCompatibility =
99
- resolveACPProviderAuthenticationCompatibility({
100
- auth: settings.auth,
101
- providerAuthentication: settings.providerAuthentication,
102
- env: process.env,
103
- });
104
- const implementationIdentity = createImplementationIdentity({
105
- harnessId: settings.harnessId,
106
- acpVersion: version,
107
- implementation: settings.implementation,
108
- clientApp,
109
- providerAuthentication: providerAuthenticationCompatibility,
110
- permissionModeMapping: settings.permissionModeMapping,
111
- });
112
- const authenticationProfile = createACPAuthenticationProfileIdentity({
113
- authentication: settings.authentication,
114
- providerAuthenticationCompatibility,
115
- });
116
- const lifecycleStateSchema: z.ZodType<{
117
- readonly implementationIdentity: string;
118
- readonly authenticationProfile?: ACPAuthenticationProfileIdentity;
119
- readonly acpSessionId?: string;
120
- readonly bridge?: ACPBridgeCoords;
121
- readonly coldSession?: ACPColdSessionState;
122
- readonly turnStartConfig?: ACPTurnStartConfig;
123
- readonly recovery?: {
124
- readonly mode: 'disk-replay' | 'lossy-rerun';
125
- readonly reason: string;
126
- };
127
- readonly restoration?: {
128
- readonly method: 'resume' | 'load';
129
- };
130
- readonly initialGuidanceApplied?: boolean;
131
- readonly skillsMaterialized?: boolean;
132
- readonly skillsFingerprint?: string;
133
- }> = acpResumeStateSchema.extend({
134
- implementationIdentity: z.literal(implementationIdentity),
135
- authenticationProfile: acpResumeStateSchema.shape.authenticationProfile
136
- .unwrap()
137
- .extend({
138
- digest: z.literal(authenticationProfile.digest),
139
- })
140
- .optional(),
141
- });
142
92
  return createACPV1({
143
93
  settings,
144
94
  builtinTools:
@@ -146,10 +96,7 @@ export function createACP<TBuiltinTools extends ToolSet = {}>(
146
96
  port: settings.port,
147
97
  startupTimeoutMs: settings.startupTimeoutMs,
148
98
  clientApp,
149
- implementationIdentity,
150
- authenticationProfile,
151
- providerAuthenticationCompatibility,
152
- lifecycleStateSchema,
99
+ lifecycleStateSchema: acpResumeStateSchema,
153
100
  });
154
101
  }
155
102
  default:
package/src/index.ts CHANGED
@@ -3,8 +3,8 @@ export type { ACPAuthOptions, ACPClientApp } from './acp-auth';
3
3
  export type { ACPHarnessSettings } from './acp-harness';
4
4
  export type {
5
5
  ACPAuthentication,
6
- ACPLockedNpmImplementation,
7
- ACPNpmImplementation,
6
+ ACPNpmLockedSource,
7
+ ACPNpmSimpleSource,
8
8
  ACPPermissionModeMapping,
9
9
  ACPPermissionModeTarget,
10
10
  ACPProfileValue,
@@ -12,7 +12,7 @@ export type {
12
12
  ACPProviderAuthenticationMode,
13
13
  ACPSerializablePrimitive,
14
14
  ACPSerializableValue,
15
- ACPSimpleNpmImplementation,
15
+ ACPSource,
16
16
  ACPV1Settings,
17
17
  ACPValueSource,
18
18
  } from './v1';
@@ -34,13 +34,16 @@ import {
34
34
  } from '@ai-sdk/provider-utils';
35
35
  import { WebSocket } from 'ws';
36
36
  import {
37
+ createACPAuthenticationProfileIdentity,
37
38
  resolveACPProviderAuthentication,
39
+ resolveACPProviderAuthenticationCompatibility,
38
40
  type ACPAuthenticationProfileIdentity,
39
41
  type ACPClientApp,
40
- type ACPProviderAuthenticationCompatibility,
41
42
  } from '../acp-auth';
42
43
  import {
44
+ createACPV1Implementation,
43
45
  createImplementationDescriptor,
46
+ createImplementationIdentity,
44
47
  createImplementationInstallCommand,
45
48
  createImplementationManifest,
46
49
  getImplementationLockfile,
@@ -112,9 +115,6 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
112
115
  port: portOverride,
113
116
  startupTimeoutMs,
114
117
  clientApp,
115
- implementationIdentity,
116
- authenticationProfile,
117
- providerAuthenticationCompatibility,
118
118
  lifecycleStateSchema,
119
119
  }: {
120
120
  settings: ACPV1Settings;
@@ -122,11 +122,6 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
122
122
  port?: number;
123
123
  startupTimeoutMs?: number;
124
124
  clientApp: ACPClientApp;
125
- implementationIdentity: string;
126
- authenticationProfile: ACPAuthenticationProfileIdentity;
127
- providerAuthenticationCompatibility:
128
- | ACPProviderAuthenticationCompatibility
129
- | undefined;
130
125
  lifecycleStateSchema: NonNullable<
131
126
  HarnessV1<TBuiltinTools>['lifecycleStateSchema']
132
127
  >;
@@ -136,7 +131,8 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
136
131
  `ACP harnessId must be a stable kebab-case identifier; received ${JSON.stringify(settings.harnessId)}.`,
137
132
  );
138
133
  }
139
- validateACPV1Implementation(settings.implementation);
134
+ const implementation = createACPV1Implementation({ settings });
135
+ validateACPV1Implementation(implementation);
140
136
 
141
137
  const BOOTSTRAP_DIR = `.harness-bootstrap/${settings.harnessId}`;
142
138
 
@@ -164,7 +160,7 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
164
160
  readBridgeAsset({ name: 'host-tool-mcp.mjs' }),
165
161
  ]);
166
162
  const implementationLock = getImplementationLockfile({
167
- implementation: settings.implementation,
163
+ implementation,
168
164
  });
169
165
  cachedBootstrap = {
170
166
  harnessId: settings.harnessId,
@@ -186,14 +182,13 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
186
182
  {
187
183
  path: `${BOOTSTRAP_DIR}/implementation/package.json`,
188
184
  content: createImplementationManifest({
189
- implementation: settings.implementation,
185
+ implementation,
190
186
  }),
191
187
  },
192
188
  {
193
189
  path: `${BOOTSTRAP_DIR}/implementation/implementation.json`,
194
190
  content: createImplementationDescriptor({
195
- implementation: settings.implementation,
196
- implementationIdentity,
191
+ implementation,
197
192
  }),
198
193
  },
199
194
  ...(implementationLock == null
@@ -213,7 +208,7 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
213
208
  command: createImplementationInstallCommand({
214
209
  implementationDir: 'implementation',
215
210
  storeDir: '../.pnpm-store',
216
- implementation: settings.implementation,
211
+ implementation,
217
212
  }),
218
213
  },
219
214
  ],
@@ -229,6 +224,25 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
229
224
  });
230
225
  }
231
226
  const permissionMode = startOptions.permissionMode ?? 'allow-all';
227
+ const env = { ...process.env };
228
+ const providerAuthenticationCompatibility =
229
+ resolveACPProviderAuthenticationCompatibility({
230
+ auth: settings.auth,
231
+ providerAuthentication: settings.providerAuthentication,
232
+ env,
233
+ });
234
+ const implementationIdentity = createImplementationIdentity({
235
+ harnessId: settings.harnessId,
236
+ acpVersion: 'v1',
237
+ implementation,
238
+ clientApp,
239
+ providerAuthentication: providerAuthenticationCompatibility,
240
+ permissionModeMapping: settings.permissionModeMapping,
241
+ });
242
+ const authenticationProfile = createACPAuthenticationProfileIdentity({
243
+ authentication: settings.authentication,
244
+ providerAuthenticationCompatibility,
245
+ });
232
246
 
233
247
  const resolvedProviderAuthentication = resolveACPProviderAuthentication({
234
248
  auth: {
@@ -236,7 +250,7 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
236
250
  providerAuthentication: settings.providerAuthentication,
237
251
  clientApp,
238
252
  },
239
- env: process.env,
253
+ env,
240
254
  compatibility: providerAuthenticationCompatibility,
241
255
  });
242
256
  const sandboxSession = startOptions.sandboxSession;
@@ -472,8 +486,8 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
472
486
  ` --bridge-type ${shellQuote(settings.harnessId)}`,
473
487
  env: {
474
488
  ...resolveImplementationEnvironment({
475
- implementation: settings.implementation,
476
- env: process.env,
489
+ implementation,
490
+ env,
477
491
  }),
478
492
  ...createACPBridgeEnvironment({
479
493
  authentication: settings.authentication,
@@ -26,33 +26,24 @@ export type ACPProfileValue =
26
26
  | ReadonlyArray<ACPProfileValue>
27
27
  | { readonly [key: string]: ACPProfileValue };
28
28
 
29
- type ACPNpmLaunchSettings = {
30
- readonly type: 'npm';
31
- readonly executable: string;
32
- readonly args?: ReadonlyArray<string>;
33
- readonly forwardEnv?: ReadonlyArray<string>;
29
+ export type ACPNpmSimpleSource = {
30
+ readonly type: 'npm-simple';
31
+ readonly packageName: string;
34
32
  /**
35
- * Runtime environment values that are safe to persist in bootstrap and
36
- * lifecycle compatibility identity.
33
+ * Exact version to install. When omitted, the package's `latest` dist-tag is
34
+ * installed and no version takes part in the implementation identity, so a
35
+ * new upstream release does not invalidate existing lifecycle state.
37
36
  */
38
- readonly env?: Readonly<Record<string, string>>;
39
- };
40
-
41
- export type ACPSimpleNpmImplementation = ACPNpmLaunchSettings & {
42
- readonly mode: 'simple';
43
- readonly packageName: string;
44
- readonly version: string;
37
+ readonly packageVersion?: string;
45
38
  };
46
39
 
47
- export type ACPLockedNpmImplementation = ACPNpmLaunchSettings & {
48
- readonly mode: 'locked';
40
+ export type ACPNpmLockedSource = {
41
+ readonly type: 'npm-locked';
49
42
  readonly packageJson: string;
50
43
  readonly pnpmLockYaml: string;
51
44
  };
52
45
 
53
- export type ACPNpmImplementation =
54
- | ACPSimpleNpmImplementation
55
- | ACPLockedNpmImplementation;
46
+ export type ACPSource = ACPNpmSimpleSource | ACPNpmLockedSource;
56
47
 
57
48
  export type ACPAuthentication = {
58
49
  readonly methodId: string;
@@ -87,7 +78,15 @@ export type ACPV1Settings = {
87
78
  readonly version?: 'v1';
88
79
  readonly harnessId: string;
89
80
  readonly auth?: ACPProviderAuthenticationMode;
90
- readonly implementation: ACPNpmImplementation;
81
+ readonly source: ACPSource;
82
+ readonly executable: string;
83
+ readonly args?: ReadonlyArray<string>;
84
+ readonly forwardEnv?: ReadonlyArray<string>;
85
+ /**
86
+ * Runtime environment values that are safe to persist in bootstrap and
87
+ * lifecycle compatibility identity.
88
+ */
89
+ readonly env?: Readonly<Record<string, string>>;
91
90
  readonly authentication?: ACPAuthentication;
92
91
  readonly providerAuthentication?: ACPProviderAuthentication;
93
92
  readonly modelId?: string;
@@ -39,11 +39,37 @@ export function createACPBridgeError({
39
39
  | 'prompt update stream';
40
40
  cause: unknown;
41
41
  }): Error {
42
- const error = new Error(`ACP ${stage} failed.`);
42
+ const causeMessage = getErrorMessage({ error: cause });
43
+ const error = new Error(
44
+ causeMessage == null
45
+ ? `ACP ${stage} failed.`
46
+ : `ACP ${stage} failed: ${causeMessage}`,
47
+ );
43
48
  (error as Error & { cause?: unknown }).cause = cause;
44
49
  return error;
45
50
  }
46
51
 
52
+ function getErrorMessage({ error }: { error: unknown }): string | undefined {
53
+ if (error instanceof Error) {
54
+ return error.message;
55
+ }
56
+
57
+ if (
58
+ error != null &&
59
+ typeof error === 'object' &&
60
+ 'message' in error &&
61
+ typeof error.message === 'string'
62
+ ) {
63
+ return error.message;
64
+ }
65
+
66
+ if (typeof error === 'string') {
67
+ return error;
68
+ }
69
+
70
+ return undefined;
71
+ }
72
+
47
73
  function stripMetadata({ value }: { value: unknown }): unknown {
48
74
  if (Array.isArray(value)) {
49
75
  return value.map(item => stripMetadata({ value: item }));
@@ -0,0 +1,52 @@
1
+ import type { Readable } from 'node:stream';
2
+ import { stripVTControlCharacters } from 'node:util';
3
+
4
+ export function monitorACPAgentStderr({
5
+ stderr,
6
+ onStderrLine,
7
+ }: {
8
+ stderr: Readable;
9
+ onStderrLine: (line: string) => void;
10
+ }): Promise<never> {
11
+ let rejectFailure!: (error: unknown) => void;
12
+ const failure = new Promise<never>((_, reject) => {
13
+ rejectFailure = reject;
14
+ });
15
+ void failure.catch(() => {});
16
+
17
+ void (async () => {
18
+ stderr.setEncoding('utf8');
19
+ let pending = '';
20
+ for await (const chunk of stderr) {
21
+ pending += chunk;
22
+ const lines = pending.split('\n');
23
+ pending = lines.pop() ?? '';
24
+ for (const line of lines) handleStderrLine({ line });
25
+ }
26
+ if (pending.length > 0) handleStderrLine({ line: pending });
27
+ })().catch(error => rejectFailure(error));
28
+
29
+ return failure;
30
+
31
+ function handleStderrLine({ line }: { line: string }): void {
32
+ if (line.length === 0) return;
33
+ onStderrLine(line);
34
+ const normalizedLine = stripVTControlCharacters(line);
35
+
36
+ /*
37
+ * An ACP agent can log a response-stream decoding failure without rejecting
38
+ * its pending JSON-RPC prompt. Once the provider stream cannot be decoded,
39
+ * no valid prompt completion can reach the ACP client, so the bridge must
40
+ * fail the turn instead of waiting indefinitely.
41
+ */
42
+ if (
43
+ normalizedLine
44
+ .toLowerCase()
45
+ .includes('failed to deserialize responsestreamevent from stream')
46
+ ) {
47
+ rejectFailure(
48
+ new Error('ACP agent failed to deserialize a streamed response.'),
49
+ );
50
+ }
51
+ }
52
+ }
@@ -30,6 +30,7 @@ import {
30
30
  createACPBridgeError,
31
31
  createACPInitializationDiagnostic,
32
32
  } from './acp-diagnostics';
33
+ import { monitorACPAgentStderr } from './agent-stderr-monitor';
33
34
  import { createEmitStreamEvent } from './create-emit-stream-event';
34
35
  import {
35
36
  startHostToolRelay,
@@ -79,6 +80,7 @@ const implementation = await readImplementationDescriptor({
79
80
  const bridgeConfiguration = await readACPBridgeEnvironment({ env: processEnv });
80
81
 
81
82
  let child: ChildProcessWithoutNullStreams | undefined;
83
+ let agentResponseStreamFailure: Promise<never> | undefined;
82
84
  let connection: acp.ClientConnection | undefined;
83
85
  let session: ACPActiveSession | undefined;
84
86
  let recoveredSessionUpdates:
@@ -139,6 +141,12 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
139
141
  if (activeSession == null) {
140
142
  throw new Error('ACP session initialization did not produce a session.');
141
143
  }
144
+ const activeAgentResponseStreamFailure = agentResponseStreamFailure;
145
+ if (activeAgentResponseStreamFailure == null) {
146
+ throw new Error(
147
+ 'ACP session initialization did not start stderr monitoring.',
148
+ );
149
+ }
142
150
  if (start.recoveryMode?.type === 'lossy-rerun') {
143
151
  const marker = {
144
152
  type: 'acp-recovery',
@@ -260,6 +268,7 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
260
268
  message = await Promise.race([
261
269
  activeSession.nextUpdate(),
262
270
  cancellationFailure,
271
+ activeAgentResponseStreamFailure,
263
272
  ]);
264
273
  } catch (error) {
265
274
  for (const rawValue of streamCapture?.drainRawValues() ?? []) {
@@ -361,7 +370,16 @@ async function ensureSession({
361
370
  stdio: ['pipe', 'pipe', 'pipe'],
362
371
  },
363
372
  );
364
- forwardAgentStderr({ child, turn });
373
+ agentResponseStreamFailure = monitorACPAgentStderr({
374
+ stderr: child.stderr,
375
+ onStderrLine: line => {
376
+ turn.bridgeLog({
377
+ level: 'warn',
378
+ subsystem: 'acp.agent.stderr',
379
+ message: line,
380
+ });
381
+ },
382
+ });
365
383
 
366
384
  const input = Writable.toWeb(child.stdin);
367
385
  const output = Readable.toWeb(child.stdout) as ReadableStream<
@@ -631,39 +649,6 @@ function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
631
649
  return value != null && typeof value === 'object' && !Array.isArray(value);
632
650
  }
633
651
 
634
- function forwardAgentStderr({
635
- child,
636
- turn,
637
- }: {
638
- child: ChildProcessWithoutNullStreams;
639
- turn: BridgeTurn;
640
- }): void {
641
- void (async () => {
642
- child.stderr.setEncoding('utf8');
643
- let pending = '';
644
- for await (const chunk of child.stderr) {
645
- pending += chunk;
646
- const lines = pending.split('\n');
647
- pending = lines.pop() ?? '';
648
- for (const line of lines) {
649
- if (line.length === 0) continue;
650
- turn.bridgeLog({
651
- level: 'warn',
652
- subsystem: 'acp.agent.stderr',
653
- message: line,
654
- });
655
- }
656
- }
657
- if (pending.length > 0) {
658
- turn.bridgeLog({
659
- level: 'warn',
660
- subsystem: 'acp.agent.stderr',
661
- message: pending,
662
- });
663
- }
664
- })();
665
- }
666
-
667
652
  async function readImplementationDescriptor({
668
653
  path,
669
654
  }: {