@salesforce/sfdx-agent-harness-claude 0.17.0 → 0.18.0

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
@@ -3,6 +3,16 @@
3
3
  All notable changes to `@salesforce/sfdx-agent-harness-claude` are documented in this file.
4
4
  Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
+ ## [0.18.0] - 2026-06-19
7
+
8
+ ### Features
9
+ - **BREAKING** Major refactor to Unify Connectivity via ModelConnectivityInfo @W-22782317 ([#607](https://github.com/forcedotcom/agentic-dx/pull/607))
10
+
11
+ ### Chores
12
+ - **deps-dev**: bump @vitest/eslint-plugin from 1.6.19 to 1.6.20 in the vitest group across 1 directory ([#601](https://github.com/forcedotcom/agentic-dx/pull/601))
13
+ - **deps-dev**: bump eslint from 10.4.1 to 10.5.0 in the eslint group across 1 directory ([#600](https://github.com/forcedotcom/agentic-dx/pull/600))
14
+ - **deps-dev**: bump @types/node from 22.19.20 to 22.19.21 in the dev-dependencies group ([#599](https://github.com/forcedotcom/agentic-dx/pull/599))
15
+
6
16
  ## [0.17.0] - 2026-06-15
7
17
 
8
18
  ### Fixes
package/README.md CHANGED
@@ -15,37 +15,39 @@ npm install @salesforce/sfdx-agent-sdk @salesforce/sfdx-agent-harness-claude
15
15
  ## Quick start
16
16
 
17
17
  ```ts
18
- import { createAgentManager } from '@salesforce/sfdx-agent-sdk';
19
- import { ClaudeHarnessFactory, DirectClaudeTransportResolver } from '@salesforce/sfdx-agent-harness-claude';
18
+ import { createAgentManager, DefaultAgentConnectivityResolver } from '@salesforce/sfdx-agent-sdk';
19
+ import { ClaudeHarnessFactory } from '@salesforce/sfdx-agent-harness-claude';
20
20
 
21
21
  const harnessFactory = new ClaudeHarnessFactory({
22
- resolver: new DirectClaudeTransportResolver(() => process.env.ANTHROPIC_API_KEY!),
23
22
  permissionMode: 'bypassPermissions',
24
23
  });
25
24
 
26
- const manager = await createAgentManager('/path/to/storage', harnessFactory);
25
+ const manager = await createAgentManager('/path/to/storage', harnessFactory, {
26
+ connectivityResolver: new DefaultAgentConnectivityResolver(),
27
+ });
27
28
  ```
28
29
 
30
+ Connectivity (gateway URL, JWT, headers, native model id) is resolved by an `AgentConnectivityResolver` on the SDK and
31
+ handed to the harness as a `ModelConnectivityInfo` bag. The harness factory itself owns harness-only concerns
32
+ (permission mode, tool-approval timeout, bypass list, query defaults). For BYOK / direct-Anthropic / LLMG-Express
33
+ deployments use `ApiKeyConnectivityResolver`; for non-Salesforce hosts implement your own resolver. See the
34
+ [`@salesforce/sfdx-agent-sdk` README](../sfdx-agent-sdk/README.md) for the full resolver surface.
35
+
29
36
  See the [`@salesforce/sfdx-agent-sdk` README](../sfdx-agent-sdk/README.md) for the full consumer-facing API. For
30
37
  internal architecture, see [ARCHITECTURE.md](ARCHITECTURE.md).
31
38
 
32
39
  ## Public API
33
40
 
34
- | Export | Kind | Description |
35
- | ------------------------------------------------ | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
36
- | `ClaudeHarnessFactory` | class | `HarnessFactory` that produces a Claude-backed `AgentHarness` bound to a storage root. |
37
- | `ClaudeHarnessFactoryConfig` | interface | Configuration for `ClaudeHarnessFactory` (`resolver`, `permissionMode`, optional `queryDefaults`, `toolApprovalTimeoutMs`, `bypassApprovalTools`). |
38
- | `ClaudeHarnessPermissionMode` | type | Narrowed permission mode: `'bypassPermissions' \| 'acceptEdits' \| 'plan'`. |
39
- | `ClaudeAgentHarness` | type | Branded `AgentHarness` subtype carrying the Claude-specific config inference. Inferred at `manager.createAgent` automatically — annotation rarely needed. |
40
- | `ClaudeAgentConfig` | type | `AgentConfig` extended with Claude-only fields (`skillSearch`, `toolSearch`). |
41
- | `ClaudeSkillSearchConfig` | type | Shape of `ClaudeAgentConfig.skillSearch` (`topK`). Opts the in-process `skill_bridge` MCP server into search-on-demand mode. |
42
- | `ClaudeToolSearchConfig` | type | Shape of `ClaudeAgentConfig.toolSearch` (`alwaysActive`). Stamps `_meta['anthropic/alwaysLoad']` on matching tools so they bypass Claude tool-search deferral. |
43
- | `ClaudeTransportResolver` | interface | Strategy for resolving transport env, auth, and native model ID for the subprocess. |
44
- | `ClaudeTransportResolverResult` | type | Return type of `ClaudeTransportResolver.resolve()` (`env` + `nativeModelId`). |
45
- | `ClaudeQueryDefaults` | type | Consumer-overridable defaults for the Claude `query()` call (excludes `model`). |
46
- | `DirectClaudeTransportResolver` | class | Resolver for direct Anthropic API access (sets `ANTHROPIC_API_KEY`). Suitable for local dev with a personal API key; not the production path. |
47
- | `SalesforceBedrockClaudeTransportResolver` | class | Resolver that routes the Claude subprocess through the Salesforce LLM Gateway's Bedrock proxy. Canonical production path for Salesforce developer-tooling consumers. |
48
- | `SalesforceBedrockClaudeTransportResolverConfig` | interface | Configuration for `SalesforceBedrockClaudeTransportResolver` (`gatewayBaseUrl`, optional `modelIdMap`, `appContext`, `featureId`, `ideContext`). |
41
+ | Export | Kind | Description |
42
+ | ----------------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
43
+ | `ClaudeHarnessFactory` | class | `HarnessFactory` that produces a Claude-backed `AgentHarness` bound to a storage root. |
44
+ | `ClaudeHarnessFactoryConfig` | interface | Configuration for `ClaudeHarnessFactory` (`permissionMode`, optional `queryDefaults`, `toolApprovalTimeoutMs`, `bypassApprovalTools`). |
45
+ | `ClaudeHarnessPermissionMode` | type | Narrowed permission mode: `'bypassPermissions' \| 'acceptEdits' \| 'plan'`. |
46
+ | `ClaudeAgentHarness` | type | Branded `AgentHarness` subtype carrying the Claude-specific config inference. Inferred at `manager.createAgent` automatically — annotation rarely needed. |
47
+ | `ClaudeAgentConfig` | type | `AgentConfig` extended with Claude-only fields (`skillSearch`, `toolSearch`). |
48
+ | `ClaudeSkillSearchConfig` | type | Shape of `ClaudeAgentConfig.skillSearch` (`topK`). Opts the in-process `skill_bridge` MCP server into search-on-demand mode. |
49
+ | `ClaudeToolSearchConfig` | type | Shape of `ClaudeAgentConfig.toolSearch` (`alwaysActive`). Stamps `_meta['anthropic/alwaysLoad']` on matching tools so they bypass Claude tool-search deferral. |
50
+ | `ClaudeQueryDefaults` | type | Consumer-overridable defaults for the Claude `query()` call (excludes `model`). |
49
51
 
50
52
  ### Permission modes
51
53
 
@@ -201,65 +203,25 @@ afterwards.
201
203
  > (which `break` does inside `for await`) finalizes the harness's source generator before the terminal event can reach
202
204
  > you. Settle in-line and keep iterating; the loop exits naturally when the terminal `finish` arrives.
203
205
 
204
- ### Using the Salesforce Bedrock resolver
205
-
206
- The canonical production path for Salesforce developer-tooling consumers. Routes the Claude subprocess through the
207
- Salesforce LLM Gateway's Bedrock proxy using an org JWT for auth.
208
-
209
- ```ts
210
- import { createAgentManager } from '@salesforce/sfdx-agent-sdk';
211
- import { ClaudeHarnessFactory, SalesforceBedrockClaudeTransportResolver } from '@salesforce/sfdx-agent-harness-claude';
206
+ ### Connectivity (gateway URL, auth, native model id)
212
207
 
213
- const harnessFactory = new ClaudeHarnessFactory({
214
- resolver: new SalesforceBedrockClaudeTransportResolver({
215
- // Pick the gateway URL for your environment — e.g. test:
216
- gatewayBaseUrl: 'https://test.api.salesforce.com/ai/gpt/v1',
217
- // No `modelIdMap` needed in the common case: the LLM Gateway's invoke API
218
- // accepts the gateway-prefixed canonical id (`llmgateway__Bedrock*`) for
219
- // every Anthropic Claude model, and the SDK's `Model.name` is already that
220
- // canonical id, so the resolver's default passthrough is correct. Set the
221
- // map only if your consumer needs to translate to a different shape (e.g.
222
- // a self-hosted Bedrock proxy that expects the bare-native ids).
223
- // Optional: override the `x-client-feature-id` header. Defaults to `orgJwt.getFeatureId()`.
224
- featureId: 'EinsteinGptForDevelopers',
225
- // Optional: getter for the `x-aip-user-ctx-tag` header. Invoked on every `resolve()`;
226
- // returning `undefined` (or omitting the option) drops the header. Use when the tag is
227
- // derived from post-JWT-mint state that changes between turns.
228
- ideContext: () => orgEntitlement.ideUserContext,
229
- }),
230
- permissionMode: 'bypassPermissions',
231
- });
208
+ The Claude harness reads connectivity per-spawn from a `ModelConnectivityInfo` bag the SDK hands it. The bag carries the
209
+ gateway base URL, the native model id (e.g. `anthropic.claude-sonnet-4-6`), the provider hint (`bedrock-anthropic` or
210
+ `anthropic`), and a `getHeaders()` callback the harness invokes once per subprocess spawn. JWT rotation rides on that
211
+ callback — the next `stream()` call reads the rotated JWT through the same closure without rebuilding anything.
232
212
 
233
- const manager = await createAgentManager('/path/to/storage', harnessFactory);
234
- ```
213
+ The SDK ships two bundled resolvers and a documented extension point — see the
214
+ [`@salesforce/sfdx-agent-sdk` README](../sfdx-agent-sdk/README.md):
235
215
 
236
- JWT refresh works the same as elsewhere in the harness: each `resolve()` call eagerly reads `orgJwt.getValue()` /
237
- `orgJwt.getTenantKey()` and freezes the result into the static subprocess headers. `Agent.updateAgentConfig({})` runs
238
- `harness.updateAgent`, the resolver re-reads the fresh JWT on the next `stream()`, and the next subprocess spawns with
239
- the new headers — connected MCP clients are preserved through the rotation.
216
+ - **`DefaultAgentConnectivityResolver`** — Salesforce-org gateway path. Targets the LLM Gateway's pass-through Bedrock
217
+ endpoint (`/invoke-with-response-stream`) using an org JWT.
218
+ - **`ApiKeyConnectivityResolver`** — direct-Anthropic / OpenAI-direct / LLMG-Express. Constructor takes
219
+ `{ getApiKey, baseUrl, providerHint, ... }`.
220
+ - **Custom `AgentConnectivityResolver`** — for deployments neither bundled resolver covers, implement the SDK's
221
+ `AgentConnectivityResolver` interface and pass it to `createAgentManager({ ..., connectivityResolver })`.
240
222
 
241
- ### Using a custom resolver
242
-
243
- For deployment topologies neither built-in resolver covers (alternative gateways, custom auth flows, etc.), implement
244
- the `ClaudeTransportResolver` interface directly.
245
-
246
- ```ts
247
- import { createAgentManager } from '@salesforce/sfdx-agent-sdk';
248
- import { ClaudeHarnessFactory } from '@salesforce/sfdx-agent-harness-claude';
249
- import type { ClaudeTransportResolver } from '@salesforce/sfdx-agent-harness-claude';
250
-
251
- const resolver: ClaudeTransportResolver = {
252
- async resolve(orgJwt, modelId) {
253
- return {
254
- env: { ANTHROPIC_API_KEY: '...' },
255
- nativeModelId: 'anthropic.claude-sonnet-4-6',
256
- };
257
- },
258
- };
259
-
260
- const harnessFactory = new ClaudeHarnessFactory({ resolver, permissionMode: 'bypassPermissions' });
261
- const manager = await createAgentManager('/path/to/storage', harnessFactory);
262
- ```
223
+ The harness factory itself takes no resolver, gateway URL, or auth field — those concerns live entirely on the
224
+ connectivity resolver.
263
225
 
264
226
  ### Per-server MCP reconnect (eager semantics)
265
227
 
@@ -287,7 +249,6 @@ reject.
287
249
 
288
250
  ```ts
289
251
  const harnessFactory = new ClaudeHarnessFactory({
290
- resolver: new DirectClaudeTransportResolver(() => process.env.ANTHROPIC_API_KEY!),
291
252
  permissionMode: 'bypassPermissions',
292
253
  queryDefaults: {
293
254
  maxTurns: 5,
@@ -307,11 +268,10 @@ across harnesses.
307
268
  `ClaudeQueryDefaults` does not strip). It blocks the model from emitting `tool_use` for the named built-ins entirely.
308
269
 
309
270
  ```ts
310
- import { createAgentManager } from '@salesforce/sfdx-agent-sdk';
311
- import { ClaudeHarnessFactory, SalesforceBedrockClaudeTransportResolver } from '@salesforce/sfdx-agent-harness-claude';
271
+ import { createAgentManager, DefaultAgentConnectivityResolver } from '@salesforce/sfdx-agent-sdk';
272
+ import { ClaudeHarnessFactory } from '@salesforce/sfdx-agent-harness-claude';
312
273
 
313
274
  const harnessFactory = new ClaudeHarnessFactory({
314
- resolver: new SalesforceBedrockClaudeTransportResolver({ gatewayBaseUrl: '...' }),
315
275
  permissionMode: 'bypassPermissions',
316
276
  // Suppress the built-in `AskUserQuestion` so the cross-harness consumer
317
277
  // tool below is the only ask-the-user surface the model sees on Claude
@@ -321,7 +281,9 @@ const harnessFactory = new ClaudeHarnessFactory({
321
281
  },
322
282
  });
323
283
 
324
- const manager = await createAgentManager('/path/to/storage', harnessFactory);
284
+ const manager = await createAgentManager('/path/to/storage', harnessFactory, {
285
+ connectivityResolver: new DefaultAgentConnectivityResolver(),
286
+ });
325
287
  const agent = await manager.createAgent(projectRoot, {
326
288
  instructions: '...',
327
289
  tools: [
@@ -360,7 +322,6 @@ additional tool names out of the gate.
360
322
 
361
323
  ```ts
362
324
  const harnessFactory = new ClaudeHarnessFactory({
363
- resolver: new SalesforceBedrockClaudeTransportResolver({ gatewayBaseUrl: '...' }),
364
325
  permissionMode: 'bypassPermissions',
365
326
  // Even when `requireToolApproval: true`, calls to these tools bypass the
366
327
  // PreToolUse hook. They still emit `tool-call` / `tool-result` ChatEvents,
@@ -1,5 +1,4 @@
1
- import type { HarnessFactory } from '@salesforce/sfdx-agent-sdk';
2
- import type { ClaudeTransportResolver } from './claude-transport-resolver.js';
1
+ import type { HarnessFactory, ProviderHint } from '@salesforce/sfdx-agent-sdk';
3
2
  import type { ClaudeQueryDefaults } from './claude-query-options-builder.js';
4
3
  import { type ClaudeAgentHarness } from './claude-harness.js';
5
4
  /**
@@ -13,7 +12,6 @@ import { type ClaudeAgentHarness } from './claude-harness.js';
13
12
  */
14
13
  export type ClaudeHarnessPermissionMode = 'bypassPermissions' | 'acceptEdits' | 'plan';
15
14
  export interface ClaudeHarnessFactoryConfig {
16
- resolver: ClaudeTransportResolver;
17
15
  permissionMode: ClaudeHarnessPermissionMode;
18
16
  queryDefaults?: ClaudeQueryDefaults;
19
17
  /**
@@ -42,6 +40,14 @@ export declare class ClaudeHarnessFactory implements HarnessFactory<ClaudeAgentH
42
40
  private readonly config;
43
41
  readonly harnessId = "claude";
44
42
  readonly protocolVersion = 1;
43
+ /**
44
+ * The Claude CLI binary speaks Anthropic-shaped protocols only — both the
45
+ * Bedrock pass-through (`'bedrock-anthropic'`) and direct Anthropic API
46
+ * (`'anthropic'`). Routing it at a non-Anthropic model is a programming
47
+ * error caught at `createAgent` / `updateAgentConfig` by the manager's
48
+ * G8 validation.
49
+ */
50
+ readonly supportedProviderHints: readonly ProviderHint[];
45
51
  constructor(config: ClaudeHarnessFactoryConfig);
46
52
  create(storageRootFolder: string): Promise<ClaudeAgentHarness>;
47
53
  }
@@ -10,11 +10,19 @@ export class ClaudeHarnessFactory {
10
10
  config;
11
11
  harnessId = 'claude';
12
12
  protocolVersion = 1;
13
+ /**
14
+ * The Claude CLI binary speaks Anthropic-shaped protocols only — both the
15
+ * Bedrock pass-through (`'bedrock-anthropic'`) and direct Anthropic API
16
+ * (`'anthropic'`). Routing it at a non-Anthropic model is a programming
17
+ * error caught at `createAgent` / `updateAgentConfig` by the manager's
18
+ * G8 validation.
19
+ */
20
+ supportedProviderHints = ['anthropic', 'bedrock-anthropic'];
13
21
  constructor(config) {
14
22
  this.config = config;
15
23
  }
16
24
  async create(storageRootFolder) {
17
- const envBuilder = new ClaudeQueryEnvBuilder(this.config.resolver, storageRootFolder);
25
+ const envBuilder = new ClaudeQueryEnvBuilder(storageRootFolder);
18
26
  const permissionDefaults = {
19
27
  permissionMode: this.config.permissionMode,
20
28
  ...(this.config.permissionMode === 'bypassPermissions'
@@ -1 +1 @@
1
- {"version":3,"file":"claude-harness-factory.js","sourceRoot":"","sources":["../src/claude-harness-factory.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,OAAO,EAAE,aAAa,EAA2B,MAAM,qBAAqB,CAAC;AAa7E,MAAM,gCAAgC,GAAG,OAAO,CAAC;AA6BjD,MAAM,OAAO,oBAAoB;IAIA;IAHpB,SAAS,GAAG,QAAQ,CAAC;IACrB,eAAe,GAAG,CAAC,CAAC;IAE7B,YAA6B,MAAkC;QAAlC,WAAM,GAAN,MAAM,CAA4B;IAAG,CAAC;IAEnE,KAAK,CAAC,MAAM,CAAC,iBAAyB;QAClC,MAAM,UAAU,GAAG,IAAI,qBAAqB,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,iBAAiB,CAAC,CAAC;QACtF,MAAM,kBAAkB,GAAG;YACvB,cAAc,EAAE,IAAI,CAAC,MAAM,CAAC,cAAc;YAC1C,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,cAAc,KAAK,mBAAmB;gBAClD,CAAC,CAAC,EAAE,+BAA+B,EAAE,IAAa,EAAE;gBACpD,CAAC,CAAC,EAAE,CAAC;SACZ,CAAC;QACF,iEAAiE;QACjE,iEAAiE;QACjE,iEAAiE;QACjE,0DAA0D;QAC1D,kBAAkB;QAClB,MAAM,cAAc,GAAG,IAAI,yBAAyB,CAChD,EAAE,GAAG,kBAAkB,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,aAAa,EAAE,EACvD,IAAI,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,mBAAmB,IAAI,EAAE,CAAC,CACjD,CAAC;QACF,OAAO,IAAI,aAAa,CACpB,UAAU,EACV,cAAc,EACd,IAAI,CAAC,MAAM,CAAC,qBAAqB,IAAI,gCAAgC,EACrE,iBAAiB,CACpB,CAAC;IACN,CAAC;CACJ"}
1
+ {"version":3,"file":"claude-harness-factory.js","sourceRoot":"","sources":["../src/claude-harness-factory.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,OAAO,EAAE,aAAa,EAA2B,MAAM,qBAAqB,CAAC;AAa7E,MAAM,gCAAgC,GAAG,OAAO,CAAC;AA4BjD,MAAM,OAAO,oBAAoB;IAYA;IAXpB,SAAS,GAAG,QAAQ,CAAC;IACrB,eAAe,GAAG,CAAC,CAAC;IAC7B;;;;;;OAMG;IACM,sBAAsB,GAA4B,CAAC,WAAW,EAAE,mBAAmB,CAAC,CAAC;IAE9F,YAA6B,MAAkC;QAAlC,WAAM,GAAN,MAAM,CAA4B;IAAG,CAAC;IAEnE,KAAK,CAAC,MAAM,CAAC,iBAAyB;QAClC,MAAM,UAAU,GAAG,IAAI,qBAAqB,CAAC,iBAAiB,CAAC,CAAC;QAChE,MAAM,kBAAkB,GAAG;YACvB,cAAc,EAAE,IAAI,CAAC,MAAM,CAAC,cAAc;YAC1C,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,cAAc,KAAK,mBAAmB;gBAClD,CAAC,CAAC,EAAE,+BAA+B,EAAE,IAAa,EAAE;gBACpD,CAAC,CAAC,EAAE,CAAC;SACZ,CAAC;QACF,iEAAiE;QACjE,iEAAiE;QACjE,iEAAiE;QACjE,0DAA0D;QAC1D,kBAAkB;QAClB,MAAM,cAAc,GAAG,IAAI,yBAAyB,CAChD,EAAE,GAAG,kBAAkB,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,aAAa,EAAE,EACvD,IAAI,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,mBAAmB,IAAI,EAAE,CAAC,CACjD,CAAC;QACF,OAAO,IAAI,aAAa,CACpB,UAAU,EACV,cAAc,EACd,IAAI,CAAC,MAAM,CAAC,qBAAqB,IAAI,gCAAgC,EACrE,iBAAiB,CACpB,CAAC;IACN,CAAC;CACJ"}
@@ -1,8 +1,7 @@
1
1
  import { type Clock, type LogRecord, type UniqueIDGenerator, type Unsubscribe } from '@salesforce/agentic-common';
2
- import { type AgentHarness, type ChatStreamResult, type HarnessAgentConfig, type McpServerInfo, type Message, type MessagePart, type StreamOptions, type TelemetryEventCallback, type ToolResultInfo } from '@salesforce/sfdx-agent-sdk';
2
+ import { type AgentHarness, type ChatStreamResult, type HarnessAgentConfig, type McpServerInfo, type Message, type MessagePart, type ModelConnectivityInfo, type StreamOptions, type TelemetryEventCallback, type ToolResultInfo, type WireCommunicationEventCallback } from '@salesforce/sfdx-agent-sdk';
3
3
  import { type AgentHooks, type WithAgentConfig } from '@salesforce/sfdx-agent-sdk/harness';
4
4
  import type { ClaudeAgentConfig } from './claude-config.js';
5
- import { type LLMGatewayClient } from '@salesforce/llm-gateway-sdk';
6
5
  import type { ClaudeQueryEnvBuilder } from './claude-query-env-builder.js';
7
6
  import type { ClaudeQueryOptionsBuilder } from './claude-query-options-builder.js';
8
7
  import { buildMcpClients } from './mcp-client-factory.js';
@@ -71,13 +70,52 @@ export declare class ClaudeHarness implements ClaudeAgentHarness {
71
70
  * collaborators on this constructor.
72
71
  */
73
72
  private readonly mcpClientFactory;
73
+ private readonly storageRootFolder;
74
74
  private shuttingDown;
75
75
  constructor(envBuilder: ClaudeQueryEnvBuilder, optionsBuilder: ClaudeQueryOptionsBuilder, toolApprovalTimeoutMs: number, storageRootFolder: string, idGenerator?: UniqueIDGenerator, clock?: Clock, sessionStore?: DiskBackedSessionStore, mcpClientFactory?: typeof buildMcpClients, threadContextStore?: ThreadContextStore);
76
76
  private coordinatorKey;
77
+ /**
78
+ * Returns a per-thread debug-log file path under the SDK storage root when a
79
+ * wire-communication consumer is subscribed; otherwise `undefined`. Callers pass the
80
+ * returned path as `query({ options: { debugFile } })` so the Claude subprocess writes
81
+ * its debug log to a file we own — and emit a `WireMonitoringNotSupportedEvent`
82
+ * pointing at the path immediately, before the subprocess starts.
83
+ *
84
+ * The path is **per-thread** (not per-turn): turns within the same conversation share
85
+ * a single file, accumulating wire data via the SDK's append-on-write behavior. A
86
+ * banner line written by {@link writeTurnBanner} delimits each turn within the file.
87
+ *
88
+ * Listener-count gating: relies on the wire-communication chain
89
+ * (`harness.wireBus → router slice → manager.wireBus`) being wired with
90
+ * `forwardWhileSubscribed`, so a consumer-less manager leaves
91
+ * `wireBus.listenerCount === 0` and this method returns `undefined` (no file written,
92
+ * no debug overhead in the subprocess).
93
+ */
94
+ private maybeAllocateWireDebugLogPath;
95
+ /**
96
+ * Append a turn-start banner to the per-thread wire-debug log file so consumers
97
+ * grepping the accumulated content can find each turn's boundary. The Claude SDK
98
+ * appends to `debugFile` across `query()` calls with no separator of its own; this
99
+ * banner makes turn boundaries explicit and version-stable.
100
+ */
101
+ private writeTurnBanner;
77
102
  shutdown(): Promise<void>;
78
103
  onTelemetry(callback: TelemetryEventCallback): Unsubscribe;
79
104
  onLog(callback: (record: LogRecord) => void): Unsubscribe;
80
- createAgent(agentId: string, projectRoot: string, llmGatewayClient: LLMGatewayClient, config?: HarnessAgentConfig, options?: {
105
+ /**
106
+ * The Claude harness emits `WireMonitoringNotSupportedEvent` (one per stream) when at
107
+ * least one consumer is subscribed at stream-start. The Claude Agent SDK runs the model
108
+ * client in a subprocess with no programmatic seam for HTTP-level monitoring, so per-call
109
+ * `LlmRequestEvent` / `LlmResponseEvent` (Mastra's contract) are not produced here;
110
+ * instead, the harness writes the subprocess's debug log to a file under the SDK storage
111
+ * root and points at it from the event's `message` field.
112
+ *
113
+ * **Subscribe-before-stream**: subscriptions added mid-stream miss the in-flight
114
+ * subprocess on Claude — the listener-count check happens once at `stream()` start. The
115
+ * next subprocess spawn (next `chat()` call) will pick up the subscriber.
116
+ */
117
+ onWireCommunication(callback: WireCommunicationEventCallback): Unsubscribe;
118
+ createAgent(agentId: string, projectRoot: string, modelConnectivityInfo: ModelConnectivityInfo, config?: HarnessAgentConfig, options?: {
81
119
  abortSignal?: AbortSignal;
82
120
  hooks?: AgentHooks;
83
121
  }): Promise<void>;
@@ -146,7 +184,7 @@ export declare class ClaudeHarness implements ClaudeAgentHarness {
146
184
  * `state.threads` is intentionally untouched — sessions are config-
147
185
  * independent, so an `updateAgent` cannot invalidate them.
148
186
  */
149
- updateAgent(agentId: string, llmGatewayClient: LLMGatewayClient, config?: HarnessAgentConfig, options?: {
187
+ updateAgent(agentId: string, modelConnectivityInfo: ModelConnectivityInfo, config?: HarnessAgentConfig, options?: {
150
188
  abortSignal?: AbortSignal;
151
189
  hooks?: AgentHooks;
152
190
  }): Promise<void>;
@@ -2,11 +2,12 @@
2
2
  * Copyright 2026, Salesforce, Inc. All rights reserved.
3
3
  * See LICENSE.txt for license terms.
4
4
  */
5
- import { realpath } from 'node:fs/promises';
5
+ import { appendFile, mkdir, realpath } from 'node:fs/promises';
6
+ import { join } from 'node:path';
6
7
  import { RealClock, UUIDGenerator, backfillCreatedAt, buildSummaryPrompt, getErrorMessage, isAbortError, } from '@salesforce/agentic-common';
7
8
  import { AgentSDKError, AgentSDKErrorType, McpServerStatus, resolveToolApprovalMode, } from '@salesforce/sfdx-agent-sdk';
8
9
  import { HarnessBusOwner, lowerStreamInput, matchesAlwaysActive, mcpServerConfigEqual, validateAlwaysActiveEntry, } from '@salesforce/sfdx-agent-sdk/harness';
9
- import { validateMultimodalFiles } from '@salesforce/llm-gateway-sdk';
10
+ import { validateMultimodalFiles } from '@salesforce/sfdx-agent-sdk';
10
11
  import { deleteSession, forkSession, getSessionMessages, listSessions, query, } from '@anthropic-ai/claude-agent-sdk';
11
12
  import { ClaudeEventAdapter, mapSessionMessageContent } from './claude-event-adapter.js';
12
13
  import { ClaudeApprovalCoordinator } from './claude-approval-coordinator.js';
@@ -94,6 +95,7 @@ export class ClaudeHarness {
94
95
  * collaborators on this constructor.
95
96
  */
96
97
  mcpClientFactory;
98
+ storageRootFolder;
97
99
  shuttingDown = false;
98
100
  constructor(envBuilder, optionsBuilder, toolApprovalTimeoutMs, storageRootFolder, idGenerator = new UUIDGenerator(), clock = new RealClock(), sessionStore, mcpClientFactory = buildMcpClients, threadContextStore) {
99
101
  this.envBuilder = envBuilder;
@@ -101,6 +103,7 @@ export class ClaudeHarness {
101
103
  this.toolApprovalTimeoutMs = toolApprovalTimeoutMs;
102
104
  this.idGenerator = idGenerator;
103
105
  this.clock = clock;
106
+ this.storageRootFolder = storageRootFolder;
104
107
  // Default to the disk-backed store rooted at the harness's storage
105
108
  // folder. The SDK's `InMemorySessionStore` is only suitable for tests —
106
109
  // it loses every transcript on harness shutdown, breaking restart
@@ -114,6 +117,60 @@ export class ClaudeHarness {
114
117
  coordinatorKey(agentId, threadId) {
115
118
  return `${agentId}::${threadId}`;
116
119
  }
120
+ /**
121
+ * Returns a per-thread debug-log file path under the SDK storage root when a
122
+ * wire-communication consumer is subscribed; otherwise `undefined`. Callers pass the
123
+ * returned path as `query({ options: { debugFile } })` so the Claude subprocess writes
124
+ * its debug log to a file we own — and emit a `WireMonitoringNotSupportedEvent`
125
+ * pointing at the path immediately, before the subprocess starts.
126
+ *
127
+ * The path is **per-thread** (not per-turn): turns within the same conversation share
128
+ * a single file, accumulating wire data via the SDK's append-on-write behavior. A
129
+ * banner line written by {@link writeTurnBanner} delimits each turn within the file.
130
+ *
131
+ * Listener-count gating: relies on the wire-communication chain
132
+ * (`harness.wireBus → router slice → manager.wireBus`) being wired with
133
+ * `forwardWhileSubscribed`, so a consumer-less manager leaves
134
+ * `wireBus.listenerCount === 0` and this method returns `undefined` (no file written,
135
+ * no debug overhead in the subprocess).
136
+ */
137
+ async maybeAllocateWireDebugLogPath(agentId, threadId) {
138
+ if (!this.buses.hasWireCommunicationSubscribers()) {
139
+ return undefined;
140
+ }
141
+ const dir = join(this.storageRootFolder, 'claude-debug-logs');
142
+ try {
143
+ await mkdir(dir, { recursive: true });
144
+ }
145
+ catch (err) {
146
+ this.buses.getLogBus()?.warn?.('Failed to create claude-debug-logs directory', { dir }, err);
147
+ return undefined;
148
+ }
149
+ return join(dir, `${agentId}__${threadId}.log`);
150
+ }
151
+ /**
152
+ * Append a turn-start banner to the per-thread wire-debug log file so consumers
153
+ * grepping the accumulated content can find each turn's boundary. The Claude SDK
154
+ * appends to `debugFile` across `query()` calls with no separator of its own; this
155
+ * banner makes turn boundaries explicit and version-stable.
156
+ */
157
+ async writeTurnBanner(filePath, agentId, threadId) {
158
+ const stamp = this.clock.now().toISOString();
159
+ const banner = '\n' +
160
+ '============================================================\n' +
161
+ `# Turn started: ${stamp}\n` +
162
+ `# Agent: ${agentId}\n` +
163
+ `# Thread: ${threadId}\n` +
164
+ '============================================================\n' +
165
+ '\n';
166
+ try {
167
+ await appendFile(filePath, banner);
168
+ }
169
+ catch (err) {
170
+ // Best-effort — failing to write the banner is not worth aborting a turn for.
171
+ this.buses.getLogBus()?.warn?.('Failed to append wire-debug turn banner', { filePath }, err);
172
+ }
173
+ }
117
174
  async shutdown() {
118
175
  if (this.shuttingDown)
119
176
  return;
@@ -146,7 +203,22 @@ export class ClaudeHarness {
146
203
  onLog(callback) {
147
204
  return this.buses.onLog(callback);
148
205
  }
149
- async createAgent(agentId, projectRoot, llmGatewayClient, config = {}, options) {
206
+ /**
207
+ * The Claude harness emits `WireMonitoringNotSupportedEvent` (one per stream) when at
208
+ * least one consumer is subscribed at stream-start. The Claude Agent SDK runs the model
209
+ * client in a subprocess with no programmatic seam for HTTP-level monitoring, so per-call
210
+ * `LlmRequestEvent` / `LlmResponseEvent` (Mastra's contract) are not produced here;
211
+ * instead, the harness writes the subprocess's debug log to a file under the SDK storage
212
+ * root and points at it from the event's `message` field.
213
+ *
214
+ * **Subscribe-before-stream**: subscriptions added mid-stream miss the in-flight
215
+ * subprocess on Claude — the listener-count check happens once at `stream()` start. The
216
+ * next subprocess spawn (next `chat()` call) will pick up the subscriber.
217
+ */
218
+ onWireCommunication(callback) {
219
+ return this.buses.onWireCommunication(callback);
220
+ }
221
+ async createAgent(agentId, projectRoot, modelConnectivityInfo, config = {}, options) {
150
222
  if (this.agents.has(agentId)) {
151
223
  throw new Error(`Agent with id "${agentId}" is already registered`);
152
224
  }
@@ -274,8 +346,12 @@ export class ClaudeHarness {
274
346
  this.agents.set(agentId, {
275
347
  projectRoot: resolvedProjectRoot,
276
348
  config,
349
+ modelConnectivityInfo,
277
350
  rulesAppend,
278
- model: llmGatewayClient.getModel(),
351
+ // The bag's `model` is the canonical model object the resolver
352
+ // produced (with caps, supportedFormats, customHeaders). Used
353
+ // for pre-flight multimodal validation.
354
+ model: modelConnectivityInfo.model,
279
355
  skillMap,
280
356
  skillSearch,
281
357
  toolSearch,
@@ -506,7 +582,7 @@ export class ClaudeHarness {
506
582
  * `state.threads` is intentionally untouched — sessions are config-
507
583
  * independent, so an `updateAgent` cannot invalidate them.
508
584
  */
509
- async updateAgent(agentId, llmGatewayClient, config = {}, options) {
585
+ async updateAgent(agentId, modelConnectivityInfo, config = {}, options) {
510
586
  const agent = this.agents.get(agentId);
511
587
  if (!agent) {
512
588
  throw new AgentSDKError(`No Agent found with id: "${agentId}"`, AgentSDKErrorType.AGENT_NOT_FOUND);
@@ -561,7 +637,8 @@ export class ClaudeHarness {
561
637
  agent.skillMap = skillMap;
562
638
  agent.skillSearch = skillSearch;
563
639
  agent.toolSearch = toolSearch;
564
- agent.model = llmGatewayClient.getModel();
640
+ agent.modelConnectivityInfo = modelConnectivityInfo;
641
+ agent.model = modelConnectivityInfo.model;
565
642
  agent.config = config;
566
643
  agent.hooks = options?.hooks ?? {};
567
644
  }
@@ -793,10 +870,7 @@ export class ClaudeHarness {
793
870
  * `canUseTool` broker — the task is text-in / text-out.
794
871
  */
795
872
  async summarize(agent, prompt) {
796
- if (!agent.config.orgJwt) {
797
- throw new Error('Agent has no orgJwt configured');
798
- }
799
- const { env, nativeModelId } = await this.envBuilder.build(agent.config.orgJwt, agent.model.name);
873
+ const { env, nativeModelId } = await this.envBuilder.build(agent.modelConnectivityInfo);
800
874
  const summaryConfig = {
801
875
  orgJwt: agent.config.orgJwt,
802
876
  // The role declaration + summarization imperative + the
@@ -955,18 +1029,15 @@ export class ClaudeHarness {
955
1029
  if (!agentState) {
956
1030
  throw new AgentSDKError(`No Agent found with id: "${agentId}"`, AgentSDKErrorType.AGENT_NOT_FOUND);
957
1031
  }
958
- const { projectRoot, config, rulesAppend, model } = agentState;
1032
+ const { projectRoot, config, rulesAppend } = agentState;
959
1033
  const threadState = agentState.threads.get(threadId);
960
- if (!config.orgJwt) {
961
- throw new Error(`Agent "${agentId}" has no orgJwt configured`);
962
- }
963
1034
  // Build (and pre-stream-validate) the query prompt. A plain string passes through; a
964
1035
  // MessagePart[] is validated against the model's capabilities and lowered to a single
965
1036
  // Anthropic user message carrying text / image / document content blocks. Validation runs
966
1037
  // synchronously here so failures reject the `stream()` promise (pre-stream contract) rather
967
1038
  // than surfacing mid-iteration.
968
1039
  const prompt = typeof message === 'string' ? message : this.buildMultimodalPrompt(agentState, message);
969
- const { env, nativeModelId } = await this.envBuilder.build(config.orgJwt, model.name);
1040
+ const { env, nativeModelId } = await this.envBuilder.build(agentState.modelConnectivityInfo);
970
1041
  // Pass the underlying claudeSessionId to the Claude SDK, NOT the
971
1042
  // user-facing threadId. They equal each other for legacy threads
972
1043
  // but diverge after `clearMessages()` (see issue #511 / ThreadState
@@ -1114,6 +1185,49 @@ export class ClaudeHarness {
1114
1185
  claudeSessionId: threadState.claudeSessionId,
1115
1186
  });
1116
1187
  }
1188
+ // Wire-communication monitoring on Claude (subscriber-gated): the Claude Agent SDK runs
1189
+ // the model client in a subprocess with no programmatic HTTP-interception seam, so we
1190
+ // can't emit per-call `LlmRequestEvent` / `LlmResponseEvent` like Mastra does. When a
1191
+ // consumer is subscribed to the wire-communication channel at stream-start, we instead
1192
+ // tell the subprocess to write its debug log to a file under the SDK storage root
1193
+ // (one file per thread — turns within the same conversation accumulate via the SDK's
1194
+ // append behavior) and emit a single `WireMonitoringNotSupportedEvent` per stream
1195
+ // pointing at the file. The listener-count check propagates back through
1196
+ // `forwardWhileSubscribed` (manager bus → router slice → harness bus), so this is
1197
+ // essentially free when nobody is listening.
1198
+ const wireDebugLogPath = await this.maybeAllocateWireDebugLogPath(agentId, threadId);
1199
+ if (wireDebugLogPath !== undefined) {
1200
+ options.debugFile = wireDebugLogPath;
1201
+ // Write a turn-banner so consumers grepping the accumulated file can find each
1202
+ // turn's start. The Claude SDK appends to `debugFile` across `query()` calls
1203
+ // with no separator of its own, so without this banner the boundary between
1204
+ // turns is implicit (the de-facto "first line" of every subprocess is
1205
+ // `[DEBUG] MDM settings load completed in Xms` — easy to miss and SDK-version
1206
+ // dependent).
1207
+ await this.writeTurnBanner(wireDebugLogPath, agentId, threadId);
1208
+ try {
1209
+ this.buses.emitWireCommunication({
1210
+ type: 'wire-monitoring-not-supported',
1211
+ timestamp: this.clock.now(),
1212
+ harnessId: this.harnessId,
1213
+ message: `The Claude harness cannot emit per-call wire-communication events: ` +
1214
+ `the Claude Agent SDK runs the model client in a subprocess with no ` +
1215
+ `programmatic seam for monitoring HTTP traffic. The subprocess's debug ` +
1216
+ `log (which contains the underlying request/response data in the Claude ` +
1217
+ `CLI / Anthropic SDK debug format) is being written to:\n\n` +
1218
+ `${wireDebugLogPath}\n\n` +
1219
+ `One file per thread; subsequent turns of this thread append to the same ` +
1220
+ `file. Each turn starts with a banner line ` +
1221
+ `\`# Turn started: <ISO timestamp>\` so boundaries are greppable. The ` +
1222
+ `file is left in place; the consumer is responsible for retention and ` +
1223
+ `cleanup.`,
1224
+ });
1225
+ }
1226
+ catch (err) {
1227
+ if (!isDisposedError(err))
1228
+ throw err;
1229
+ }
1230
+ }
1117
1231
  const queryResult = query({ prompt, options });
1118
1232
  if (threadState) {
1119
1233
  threadState.hasStreamed = true;