@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 +10 -0
- package/README.md +41 -80
- package/dist/claude-harness-factory.d.ts +9 -3
- package/dist/claude-harness-factory.js +9 -1
- package/dist/claude-harness-factory.js.map +1 -1
- package/dist/claude-harness.d.ts +42 -4
- package/dist/claude-harness.js +129 -15
- package/dist/claude-harness.js.map +1 -1
- package/dist/claude-query-env-builder.d.ts +22 -7
- package/dist/claude-query-env-builder.js +116 -9
- package/dist/claude-query-env-builder.js.map +1 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp-client-factory.d.ts +1 -1
- package/dist/test/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -8
- package/dist/claude-transport-resolver.d.ts +0 -21
- package/dist/claude-transport-resolver.js +0 -6
- package/dist/claude-transport-resolver.js.map +0 -1
- package/dist/direct-claude-transport-resolver.d.ts +0 -13
- package/dist/direct-claude-transport-resolver.js +0 -23
- package/dist/direct-claude-transport-resolver.js.map +0 -1
- package/dist/salesforce-bedrock-transport-resolver.d.ts +0 -104
- package/dist/salesforce-bedrock-transport-resolver.js +0 -74
- package/dist/salesforce-bedrock-transport-resolver.js.map +0 -1
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
|
|
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
|
|
35
|
-
|
|
|
36
|
-
| `ClaudeHarnessFactory`
|
|
37
|
-
| `ClaudeHarnessFactoryConfig`
|
|
38
|
-
| `ClaudeHarnessPermissionMode`
|
|
39
|
-
| `ClaudeAgentHarness`
|
|
40
|
-
| `ClaudeAgentConfig`
|
|
41
|
-
| `ClaudeSkillSearchConfig`
|
|
42
|
-
| `ClaudeToolSearchConfig`
|
|
43
|
-
| `
|
|
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
|
-
###
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
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
|
-
|
|
237
|
-
`
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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;
|
|
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"}
|
package/dist/claude-harness.d.ts
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
187
|
+
updateAgent(agentId: string, modelConnectivityInfo: ModelConnectivityInfo, config?: HarnessAgentConfig, options?: {
|
|
150
188
|
abortSignal?: AbortSignal;
|
|
151
189
|
hooks?: AgentHooks;
|
|
152
190
|
}): Promise<void>;
|
package/dist/claude-harness.js
CHANGED
|
@@ -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/
|
|
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
|
-
|
|
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
|
|
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,
|
|
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.
|
|
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
|
-
|
|
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
|
|
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(
|
|
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;
|