@salesforce/sfdx-agent-sdk 0.21.0 → 0.22.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +12 -11
  3. package/dist/agent-connectivity-resolver.d.ts +47 -25
  4. package/dist/agent-connectivity-resolver.js +92 -15
  5. package/dist/agent-manager.d.ts +17 -1
  6. package/dist/agent-manager.js +40 -7
  7. package/dist/agent.d.ts +33 -10
  8. package/dist/agent.js +38 -50
  9. package/dist/api-key-connectivity-resolver.d.ts +110 -0
  10. package/dist/api-key-connectivity-resolver.js +114 -0
  11. package/dist/errors.d.ts +1 -0
  12. package/dist/errors.js +1 -0
  13. package/dist/harness/agent-harness.d.ts +27 -5
  14. package/dist/harness/harness-bus-owner.d.ts +17 -0
  15. package/dist/harness/harness-bus-owner.js +27 -0
  16. package/dist/harness/harness-config.d.ts +3 -2
  17. package/dist/harness/harness-factory.d.ts +13 -0
  18. package/dist/harness/stream-input.d.ts +9 -5
  19. package/dist/harness/stream-input.js +12 -14
  20. package/dist/index.d.ts +8 -2
  21. package/dist/index.js +4 -1
  22. package/dist/internal/wire-communication-router.d.ts +43 -0
  23. package/dist/internal/wire-communication-router.js +119 -0
  24. package/dist/mcp-auth.d.ts +1 -1
  25. package/dist/models/claude-opus-4-5.d.ts +11 -0
  26. package/dist/models/claude-opus-4-5.js +21 -0
  27. package/dist/models/claude-opus-4-6.d.ts +11 -0
  28. package/dist/models/claude-opus-4-6.js +22 -0
  29. package/dist/models/claude-opus-4-7.d.ts +11 -0
  30. package/dist/models/claude-opus-4-7.js +22 -0
  31. package/dist/models/claude-sonnet-4-5.d.ts +11 -0
  32. package/dist/models/claude-sonnet-4-5.js +23 -0
  33. package/dist/models/claude-sonnet-4-6.d.ts +11 -0
  34. package/dist/models/claude-sonnet-4-6.js +22 -0
  35. package/dist/models/create-claude-model.d.ts +54 -0
  36. package/dist/models/create-claude-model.js +62 -0
  37. package/dist/models/gpt-5-4.d.ts +11 -0
  38. package/dist/models/gpt-5-4.js +21 -0
  39. package/dist/models/gpt-5-5.d.ts +15 -0
  40. package/dist/models/gpt-5-5.js +24 -0
  41. package/dist/models/gpt-5.d.ts +11 -0
  42. package/dist/models/gpt-5.js +23 -0
  43. package/dist/models/index.d.ts +19 -0
  44. package/dist/models/index.js +49 -0
  45. package/dist/models/model.d.ts +69 -0
  46. package/dist/models/model.js +63 -0
  47. package/dist/models/multimodal.d.ts +35 -0
  48. package/dist/models/multimodal.js +78 -0
  49. package/dist/models/types.d.ts +49 -0
  50. package/dist/models/types.js +18 -0
  51. package/dist/types/index.d.ts +1 -0
  52. package/dist/types/model-connectivity-info.d.ts +87 -0
  53. package/dist/types/model-connectivity-info.js +6 -0
  54. package/dist/types/usage.d.ts +3 -3
  55. package/dist/types/wire-communication-event.d.ts +124 -0
  56. package/dist/types/wire-communication-event.js +6 -0
  57. package/dist/wire-communication-file-writer.d.ts +39 -0
  58. package/dist/wire-communication-file-writer.js +142 -0
  59. package/package.json +7 -8
package/dist/agent.js CHANGED
@@ -5,8 +5,6 @@
5
5
  import { EventBus, LogBus, RealClock, UUIDGenerator, } from '@salesforce/agentic-common';
6
6
  import { toHarnessConfig } from './harness/harness-config.js';
7
7
  import { DefaultChatSession } from './chat-session.js';
8
- import {} from '@salesforce/llm-gateway-sdk';
9
- import { resolveAgentConfigModel } from './agent-connectivity-resolver.js';
10
8
  import { AgentSDKError, AgentSDKErrorType } from './errors.js';
11
9
  /**
12
10
  * Default implementation of {@link Agent} that delegates
@@ -17,10 +15,11 @@ export class DefaultAgent {
17
15
  agentId;
18
16
  projectRoot;
19
17
  config;
20
- llmGatewayClient;
18
+ modelConnectivityInfo;
21
19
  orgConnection;
22
20
  orgJwt;
23
21
  agentConnectivityResolver;
22
+ harnessSupportedProviderHints;
24
23
  hooksForAgent;
25
24
  identityStore;
26
25
  sessions = new Map();
@@ -38,7 +37,9 @@ export class DefaultAgent {
38
37
  * @param agentId - Unique identifier for this agent.
39
38
  * @param projectRoot - Project folder this agent is allowed to operate within.
40
39
  * @param config - Initial agent configuration (instructions, model, tools, etc.).
41
- * @param llmGatewayClient - Authenticated LLM gateway client for the resolved org.
40
+ * @param modelConnectivityInfo - Connectivity bag (model, baseUrl, nativeModelId,
41
+ * providerHint, getHeaders) the harness uses to talk to the LLM. Replaced
42
+ * on every `updateAgentConfig` re-resolve.
42
43
  * @param orgConnection - Authenticated org connection carrying identity and env inference.
43
44
  * @param orgJwt - Self-refreshing JWT for the resolved org (used for MCP auth injection).
44
45
  * @param agentConnectivityResolver - Used to re-resolve org connectivity when the org or model changes.
@@ -52,15 +53,16 @@ export class DefaultAgent {
52
53
  * @param inbound - Router slice delivering harness events routed to this agent (non-session-scoped).
53
54
  * @param parent - Manager's bus pair; this agent forwards its events upward into them.
54
55
  */
55
- constructor(harness, agentId, projectRoot, config, llmGatewayClient, orgConnection, orgJwt, agentConnectivityResolver, hooksForAgent, identityStore, router, inbound, parent, clock = new RealClock(), idGenerator = new UUIDGenerator()) {
56
+ constructor(harness, agentId, projectRoot, config, modelConnectivityInfo, orgConnection, orgJwt, agentConnectivityResolver, harnessSupportedProviderHints, hooksForAgent, identityStore, router, inbound, parent, clock = new RealClock(), idGenerator = new UUIDGenerator()) {
56
57
  this.harness = harness;
57
58
  this.agentId = agentId;
58
59
  this.projectRoot = projectRoot;
59
60
  this.config = config;
60
- this.llmGatewayClient = llmGatewayClient;
61
+ this.modelConnectivityInfo = modelConnectivityInfo;
61
62
  this.orgConnection = orgConnection;
62
63
  this.orgJwt = orgJwt;
63
64
  this.agentConnectivityResolver = agentConnectivityResolver;
65
+ this.harnessSupportedProviderHints = harnessSupportedProviderHints;
64
66
  this.hooksForAgent = hooksForAgent;
65
67
  this.identityStore = identityStore;
66
68
  this.router = router;
@@ -121,29 +123,37 @@ export class DefaultAgent {
121
123
  async updateAgentConfig(config = {}, options) {
122
124
  this.assertNotDisposed();
123
125
  const previousConfig = { ...this.config };
124
- const previousClient = this.llmGatewayClient;
126
+ const previousModelConnectivityInfo = this.modelConnectivityInfo;
125
127
  const previousOrgJwt = this.orgJwt;
126
128
  const nextConfig = { ...this.config, ...config };
127
129
  const orgAliasRequested = Object.prototype.hasOwnProperty.call(config, 'orgAlias');
128
- const previousModel = previousClient.getModel();
129
- const nextModel = resolveAgentConfigModel(nextConfig.modelId);
130
- let nextClient = previousClient;
130
+ const modelIdRequested = Object.prototype.hasOwnProperty.call(config, 'modelId');
131
+ let nextModelConnectivityInfo = previousModelConnectivityInfo;
131
132
  let nextConnection = this.orgConnection;
132
133
  let nextOrgJwt = this.orgJwt;
133
- if (orgAliasRequested) {
134
+ // Any orgAlias OR modelId change re-runs the resolver so the bag
135
+ // (baseUrl, nativeModelId, providerHint, getHeaders) tracks the current
136
+ // model/org. `options.forceResolve` is the consumer escape hatch — a
137
+ // resolver-side state change (BYOK toggle, feature-id flip, rate-limit
138
+ // gate) doesn't surface as an orgAlias/modelId change but still needs
139
+ // the bag refreshed.
140
+ if (orgAliasRequested || modelIdRequested || options?.forceResolve === true) {
134
141
  const runtime = await this.agentConnectivityResolver.resolve(this.projectRoot, nextConfig);
135
- nextClient = runtime.llmGatewayClient;
142
+ // G8 — validate the new providerHint before any harness work runs. Same
143
+ // shape as the manager's installAgent check; modelId changes are the only
144
+ // post-creation path that can flip the harness ↔ model compatibility, so
145
+ // catching it here avoids a half-applied state in the harness.
146
+ const providerHint = runtime.modelConnectivityInfo.providerHint;
147
+ if (!this.harnessSupportedProviderHints.includes(providerHint)) {
148
+ throw new AgentSDKError(`Harness "${this.harness.harnessId}" does not support providerHint "${providerHint}". Supported: ${this.harnessSupportedProviderHints.join(', ')}.`, AgentSDKErrorType.MODEL_NOT_SUPPORTED_BY_HARNESS);
149
+ }
150
+ nextModelConnectivityInfo = runtime.modelConnectivityInfo;
136
151
  nextConnection = runtime.orgConnection;
137
152
  nextOrgJwt = runtime.orgJwt;
138
153
  }
139
- else if (nextModel.name !== previousModel.name) {
140
- // Keep the same authenticated client, but pin the updated model.
141
- // (If modelId is omitted, the resolver pinned the default at creation time.)
142
- nextClient.setModel(nextModel);
143
- }
144
154
  try {
145
155
  const nextHooks = this.hooksForAgent?.(this.agentId, nextConfig) ?? {};
146
- await this.harness.updateAgent(this.agentId, nextClient, toHarnessConfig(nextConfig, nextOrgJwt), {
156
+ await this.harness.updateAgent(this.agentId, nextModelConnectivityInfo, toHarnessConfig(nextConfig, nextOrgJwt), {
147
157
  ...(options?.abortSignal !== undefined ? { abortSignal: options.abortSignal } : {}),
148
158
  hooks: nextHooks,
149
159
  });
@@ -152,43 +162,22 @@ export class DefaultAgent {
152
162
  // previousConfig and disk state remains the pre-update record.
153
163
  await this.identityStore.write(this.agentId, this.projectRoot, nextConfig);
154
164
  this.config = nextConfig;
155
- this.llmGatewayClient = nextClient;
165
+ this.modelConnectivityInfo = nextModelConnectivityInfo;
156
166
  this.orgConnection = nextConnection;
157
167
  this.orgJwt = nextOrgJwt;
158
- // Release the old client only once the swap has succeeded. When the orgAlias is unchanged,
159
- // nextClient === previousClient and we must NOT dispose.
160
- if (nextClient !== previousClient) {
161
- previousClient.dispose();
162
- }
163
168
  }
164
169
  catch (error) {
165
170
  // Best-effort restoration to keep wrapper and harness state aligned.
171
+ // Re-apply the previous config through the same primitive. The harness re-diffs
172
+ // against its current state — if updateAgent partially applied (e.g. some MCP
173
+ // servers were already cycled), reverting via updateAgent restores them too.
166
174
  try {
167
- // Restore client model if we mutated it in-place. We re-pin the live previousModel
168
- // instance (captured above as previousClient.getModel()) rather than re-resolving from
169
- // this.config.modelId, because a JSON-rehydrated config may have a plain object there
170
- // that would round-trip through createClaudeModel and lose the original prototype.
171
- if (nextClient === previousClient) {
172
- previousClient.setModel(previousModel);
173
- }
174
- // Re-apply the previous config through the same primitive. The harness re-diffs
175
- // against its current state — if updateAgent partially applied (e.g. some MCP
176
- // servers were already cycled), reverting via updateAgent restores them too.
177
175
  const previousHooks = this.hooksForAgent?.(this.agentId, previousConfig) ?? {};
178
- await this.harness.updateAgent(this.agentId, previousClient, toHarnessConfig(previousConfig, previousOrgJwt), { hooks: previousHooks });
176
+ await this.harness.updateAgent(this.agentId, previousModelConnectivityInfo, toHarnessConfig(previousConfig, previousOrgJwt), { hooks: previousHooks });
179
177
  }
180
178
  catch {
181
179
  // Ignore restoration errors; rethrow the original failure.
182
180
  }
183
- // A freshly-resolved client we never installed must be released so its auth resources don't leak.
184
- if (nextClient !== previousClient) {
185
- try {
186
- nextClient.dispose();
187
- }
188
- catch {
189
- // Ignore; the original error is the one the caller cares about.
190
- }
191
- }
192
181
  throw error;
193
182
  }
194
183
  }
@@ -294,7 +283,6 @@ export class DefaultAgent {
294
283
  }
295
284
  this.sessions.clear();
296
285
  await this.harness.destroyAgent(this.agentId);
297
- this.llmGatewayClient.dispose();
298
286
  this.telemetryBus.emit({
299
287
  type: 'agent-destroyed',
300
288
  timestamp: this.clock.now(),
@@ -339,11 +327,11 @@ export class DefaultAgent {
339
327
  // Live getter — read at call time so getContextUsage() reflects the
340
328
  // model bound to the agent right now, not the model that was bound
341
329
  // when this session was created. updateAgentConfig() can swap the
342
- // underlying LLMGatewayClient mid-life. Per the SDK's Critical
343
- // Invariant on context-window reachability, every bound model
344
- // exposes a usable `contextWindow`; #507's decoupling work must
345
- // preserve that, so this access is contractually safe.
346
- const getContextWindow = () => this.llmGatewayClient.getModel().contextWindow;
330
+ // ModelConnectivityInfo bag mid-life. Per the SDK's Critical Invariant
331
+ // on context-window reachability, every bound model exposes a usable
332
+ // `contextWindow`; #507's decoupling work must preserve that, so this
333
+ // access is contractually safe.
334
+ const getContextWindow = () => this.modelConnectivityInfo.model.contextWindow;
347
335
  const session = new DefaultChatSession(this.harness, this.agentId, threadId, slice, {
348
336
  telemetry: this.telemetryBus,
349
337
  log: this.logBus,
@@ -0,0 +1,110 @@
1
+ import { Model } from './models/index.js';
2
+ import { type OrgConnectionFactory } from '@salesforce/agentic-common';
3
+ import { type AgentConnectivityResolver, type ResolvedConnectivity } from './agent-connectivity-resolver.js';
4
+ import type { AgentConfig } from './harness/harness-config.js';
5
+ import type { ProviderHint } from './types/model-connectivity-info.js';
6
+ /**
7
+ * Constructor configuration for {@link ApiKeyConnectivityResolver}.
8
+ *
9
+ * Covers every named api-key endpoint the SDK reaches: Anthropic-direct,
10
+ * OpenAI-direct, and LLM-Gateway Express. The differences between them are
11
+ * data, not behavior — `getApiKey`, `baseUrl`, `providerHint`, and (for the
12
+ * direct providers) the model-id translation rule.
13
+ */
14
+ export type ApiKeyConnectivityResolverConfig = {
15
+ /**
16
+ * Returns the API key as a string (or a Promise of one). Re-evaluated on
17
+ * every {@link ApiKeyConnectivityResolver.resolve} call so a rotating key
18
+ * source (env var read at-call, secret-store fetch) lands without
19
+ * reconstructing the resolver.
20
+ */
21
+ getApiKey: () => string | Promise<string>;
22
+ /**
23
+ * Fully-qualified base URL the provider SDK speaks to. The SDK constructs
24
+ * its per-call paths by concatenation onto this value, so include any API
25
+ * version segment the provider requires (e.g. `https://api.openai.com/v1`,
26
+ * not `https://api.openai.com`).
27
+ */
28
+ baseUrl: string;
29
+ /**
30
+ * Provider hint the harness uses to dispatch onto the right pass-through
31
+ * builder. Must be one a harness in the consumer's setup actually
32
+ * supports — `HarnessFactory.supportedProviderHints` is checked
33
+ * pre-flight by the manager.
34
+ */
35
+ providerHint: ProviderHint;
36
+ /**
37
+ * Optional translator from canonical {@link Model.name} to the
38
+ * endpoint-native model id. Direct-provider endpoints want their own
39
+ * naming (`claude-sonnet-4-6` for Anthropic-direct,
40
+ * `gpt-4.1-mini-2025-04-14` for OpenAI-direct), while gateway-shaped
41
+ * endpoints accept the canonical `llmgateway__*` id verbatim. Defaults
42
+ * to `model.name` (gateway-shape).
43
+ */
44
+ nativeModelIdFor?: (model: Model) => string;
45
+ /**
46
+ * Optional connection factory. When supplied, the resolver mints an org
47
+ * connection + JWT alongside the api-key bag — using `config.orgAlias`
48
+ * if set, otherwise the project's target org via
49
+ * `OrgConnectionFactory.createFromTargetOrg`. This is the BYOK shape:
50
+ * api-key authenticates the LLM, org JWT authenticates MCP servers /
51
+ * identity. Without a factory, both `orgConnection` and `orgJwt` are
52
+ * omitted from `ResolvedConnectivity`.
53
+ */
54
+ connectionFactory?: OrgConnectionFactory;
55
+ };
56
+ /**
57
+ * Generic api-key {@link AgentConnectivityResolver}. Covers every named
58
+ * api-key endpoint: Anthropic-direct (`/v1/messages`), OpenAI-direct (Responses
59
+ * API at `/v1/responses`), and OpenAI-compatible Chat Completions
60
+ * (`/v1/chat/completions` — LLM Gateway Express, LiteLLM, vLLM, etc.). The
61
+ * provider-specific differences (URL, model-id translation) are constructor
62
+ * data.
63
+ *
64
+ * **Authentication model.** This resolver always emits
65
+ * `Authorization: Bearer <api-key>` as a generic credential carrier. The
66
+ * harness translates that into the provider-native auth shape at the wire:
67
+ * - Mastra `'bedrock-anthropic'` / `'openai-responses'` / `'openai'` /
68
+ * `'openai-compatible'` paths leave `Authorization: Bearer` untouched —
69
+ * their gateway / SDK accepts it.
70
+ * - Mastra `'anthropic'` (direct Anthropic Messages API) peels the bearer
71
+ * token into `x-api-key` (the Anthropic Messages API auth shape). The
72
+ * harness handles this internally; consumers don't need to write a custom
73
+ * resolver to cover the direct-Anthropic case.
74
+ * - Claude `'bedrock-anthropic'` ships the bearer in `ANTHROPIC_CUSTOM_HEADERS`;
75
+ * `'anthropic'` peels it into `ANTHROPIC_API_KEY`.
76
+ *
77
+ * Per-call freshness: `getApiKey` is invoked inside the closure on every
78
+ * `getHeaders()` call, so a rotating source (BYOK toggle, secret-store fetch,
79
+ * LLMG-Express token refresh) lands without reconstructing the resolver.
80
+ *
81
+ * @example
82
+ * // Anthropic Messages API direct
83
+ * new ApiKeyConnectivityResolver({
84
+ * getApiKey: () => process.env.ANTHROPIC_API_KEY!,
85
+ * baseUrl: 'https://api.anthropic.com',
86
+ * providerHint: 'anthropic',
87
+ * nativeModelIdFor: () => 'claude-sonnet-4-6',
88
+ * });
89
+ *
90
+ * // OpenAI Responses API direct
91
+ * new ApiKeyConnectivityResolver({
92
+ * getApiKey: () => process.env.OPENAI_API_KEY!,
93
+ * baseUrl: 'https://api.openai.com/v1',
94
+ * providerHint: 'openai',
95
+ * nativeModelIdFor: () => 'gpt-4.1-2025-04-14',
96
+ * });
97
+ *
98
+ * // LLM Gateway Express (OpenAI-compatible Chat Completions API)
99
+ * new ApiKeyConnectivityResolver({
100
+ * getApiKey: () => process.env.LLMG_EXPRESS_API_KEY!,
101
+ * baseUrl: 'https://express-internal/v1',
102
+ * providerHint: 'openai-compatible',
103
+ * });
104
+ */
105
+ export declare class ApiKeyConnectivityResolver implements AgentConnectivityResolver {
106
+ private readonly cfg;
107
+ constructor(cfg: ApiKeyConnectivityResolverConfig);
108
+ resolve(projectRoot: string, config: AgentConfig): Promise<ResolvedConnectivity>;
109
+ private maybeResolveOrg;
110
+ }
@@ -0,0 +1,114 @@
1
+ /*
2
+ * Copyright 2026, Salesforce, Inc. All rights reserved.
3
+ * See LICENSE.txt for license terms.
4
+ */
5
+ import { Model } from './models/index.js';
6
+ import { createJWTFromConnection, } from '@salesforce/agentic-common';
7
+ import { resolveAgentConfigModel, } from './agent-connectivity-resolver.js';
8
+ import { AgentSDKError, AgentSDKErrorType } from './errors.js';
9
+ /**
10
+ * Generic api-key {@link AgentConnectivityResolver}. Covers every named
11
+ * api-key endpoint: Anthropic-direct (`/v1/messages`), OpenAI-direct (Responses
12
+ * API at `/v1/responses`), and OpenAI-compatible Chat Completions
13
+ * (`/v1/chat/completions` — LLM Gateway Express, LiteLLM, vLLM, etc.). The
14
+ * provider-specific differences (URL, model-id translation) are constructor
15
+ * data.
16
+ *
17
+ * **Authentication model.** This resolver always emits
18
+ * `Authorization: Bearer <api-key>` as a generic credential carrier. The
19
+ * harness translates that into the provider-native auth shape at the wire:
20
+ * - Mastra `'bedrock-anthropic'` / `'openai-responses'` / `'openai'` /
21
+ * `'openai-compatible'` paths leave `Authorization: Bearer` untouched —
22
+ * their gateway / SDK accepts it.
23
+ * - Mastra `'anthropic'` (direct Anthropic Messages API) peels the bearer
24
+ * token into `x-api-key` (the Anthropic Messages API auth shape). The
25
+ * harness handles this internally; consumers don't need to write a custom
26
+ * resolver to cover the direct-Anthropic case.
27
+ * - Claude `'bedrock-anthropic'` ships the bearer in `ANTHROPIC_CUSTOM_HEADERS`;
28
+ * `'anthropic'` peels it into `ANTHROPIC_API_KEY`.
29
+ *
30
+ * Per-call freshness: `getApiKey` is invoked inside the closure on every
31
+ * `getHeaders()` call, so a rotating source (BYOK toggle, secret-store fetch,
32
+ * LLMG-Express token refresh) lands without reconstructing the resolver.
33
+ *
34
+ * @example
35
+ * // Anthropic Messages API direct
36
+ * new ApiKeyConnectivityResolver({
37
+ * getApiKey: () => process.env.ANTHROPIC_API_KEY!,
38
+ * baseUrl: 'https://api.anthropic.com',
39
+ * providerHint: 'anthropic',
40
+ * nativeModelIdFor: () => 'claude-sonnet-4-6',
41
+ * });
42
+ *
43
+ * // OpenAI Responses API direct
44
+ * new ApiKeyConnectivityResolver({
45
+ * getApiKey: () => process.env.OPENAI_API_KEY!,
46
+ * baseUrl: 'https://api.openai.com/v1',
47
+ * providerHint: 'openai',
48
+ * nativeModelIdFor: () => 'gpt-4.1-2025-04-14',
49
+ * });
50
+ *
51
+ * // LLM Gateway Express (OpenAI-compatible Chat Completions API)
52
+ * new ApiKeyConnectivityResolver({
53
+ * getApiKey: () => process.env.LLMG_EXPRESS_API_KEY!,
54
+ * baseUrl: 'https://express-internal/v1',
55
+ * providerHint: 'openai-compatible',
56
+ * });
57
+ */
58
+ export class ApiKeyConnectivityResolver {
59
+ cfg;
60
+ constructor(cfg) {
61
+ this.cfg = cfg;
62
+ }
63
+ async resolve(projectRoot, config) {
64
+ const model = resolveAgentConfigModel(config.modelId);
65
+ const { orgConnection, orgJwt } = await this.maybeResolveOrg(projectRoot, config);
66
+ const cfg = this.cfg;
67
+ // `||` (not `??`) so a buggy translator returning `''` falls back to `model.name`
68
+ // rather than producing an empty `nativeModelId` on the wire (which the upstream
69
+ // would reject as a confusing 400). All sane model ids are non-empty strings, so
70
+ // collapsing every falsy translator output to the canonical name is correct.
71
+ const nativeModelId = cfg.nativeModelIdFor?.(model) || model.name;
72
+ const modelConnectivityInfo = {
73
+ model,
74
+ baseUrl: cfg.baseUrl,
75
+ nativeModelId,
76
+ providerHint: cfg.providerHint,
77
+ getHeaders: async () => {
78
+ const apiKey = await cfg.getApiKey();
79
+ if (!apiKey) {
80
+ // An empty / nullish api-key produces `Authorization: Bearer ` on the wire
81
+ // and the upstream rejects with a confusing 401. Surface the misconfig
82
+ // locally with a typed error so consumers see "the resolver returned an
83
+ // empty key" rather than chasing a 401 through provider logs.
84
+ throw new AgentSDKError('ApiKeyConnectivityResolver: getApiKey() returned an empty / nullish value. ' +
85
+ 'Check the consumer-supplied source.', AgentSDKErrorType.NOT_SUPPORTED);
86
+ }
87
+ // Order matters: spread `customHeaders` FIRST so resolver-set auth /
88
+ // content-type headers win on conflict. Without this, a `Model` instance
89
+ // built with `customHeaders: { Authorization: '...' }` would silently
90
+ // shadow the resolver's freshly-fetched key and bypass the empty-key
91
+ // guard above. `customHeaders` is for vendor-specific labels
92
+ // (`anthropic-version`, model-tag headers, etc.) — not for auth.
93
+ return {
94
+ ...(model.customHeaders ?? {}),
95
+ Authorization: `Bearer ${apiKey}`,
96
+ 'Content-Type': 'application/json;charset=utf-8',
97
+ };
98
+ },
99
+ };
100
+ return { modelConnectivityInfo, orgConnection, orgJwt };
101
+ }
102
+ async maybeResolveOrg(projectRoot, config) {
103
+ const factory = this.cfg.connectionFactory;
104
+ if (!factory) {
105
+ return {};
106
+ }
107
+ const orgConnection = config.orgAlias !== undefined
108
+ ? await factory.createFromOrgAliasOrUsername(config.orgAlias)
109
+ : await factory.createFromTargetOrg({ projectRoot });
110
+ const orgJwt = await createJWTFromConnection(orgConnection);
111
+ return { orgConnection, orgJwt };
112
+ }
113
+ }
114
+ //# sourceMappingURL=api-key-connectivity-resolver.js.map
package/dist/errors.d.ts CHANGED
@@ -7,6 +7,7 @@ export declare const AgentSDKErrorType: {
7
7
  readonly INVALID_MESSAGE_CONTENT: "INVALID_MESSAGE_CONTENT";
8
8
  readonly MCP_SERVER_DISABLED: "MCP_SERVER_DISABLED";
9
9
  readonly MCP_SERVER_NOT_FOUND: "MCP_SERVER_NOT_FOUND";
10
+ readonly MODEL_NOT_SUPPORTED_BY_HARNESS: "MODEL_NOT_SUPPORTED_BY_HARNESS";
10
11
  readonly MULTIMODAL_NOT_SUPPORTED: "MULTIMODAL_NOT_SUPPORTED";
11
12
  readonly NOT_SUPPORTED: "NOT_SUPPORTED";
12
13
  readonly TOOL_CALL_NOT_FOUND: "TOOL_CALL_NOT_FOUND";
package/dist/errors.js CHANGED
@@ -11,6 +11,7 @@ export const AgentSDKErrorType = {
11
11
  INVALID_MESSAGE_CONTENT: 'INVALID_MESSAGE_CONTENT',
12
12
  MCP_SERVER_DISABLED: 'MCP_SERVER_DISABLED',
13
13
  MCP_SERVER_NOT_FOUND: 'MCP_SERVER_NOT_FOUND',
14
+ MODEL_NOT_SUPPORTED_BY_HARNESS: 'MODEL_NOT_SUPPORTED_BY_HARNESS',
14
15
  MULTIMODAL_NOT_SUPPORTED: 'MULTIMODAL_NOT_SUPPORTED',
15
16
  NOT_SUPPORTED: 'NOT_SUPPORTED',
16
17
  TOOL_CALL_NOT_FOUND: 'TOOL_CALL_NOT_FOUND',
@@ -5,8 +5,9 @@ import type { Message, MessagePart } from '../types/messages.js';
5
5
  import type { TelemetryEventCallback } from '../types/telemetry-events.js';
6
6
  import type { ToolResultInfo } from '../types/tools.js';
7
7
  import type { AgentHooks } from '../types/redaction.js';
8
+ import type { WireCommunicationEventCallback } from '../types/wire-communication-event.js';
8
9
  import type { AgentConfig, HarnessAgentConfig, StreamOptions } from './harness-config.js';
9
- import type { LLMGatewayClient } from '@salesforce/llm-gateway-sdk';
10
+ import type { ModelConnectivityInfo } from '../types/model-connectivity-info.js';
10
11
  export declare const SUPPORTED_PROTOCOL_VERSIONS: readonly [1];
11
12
  /**
12
13
  * Opt-in helper that brands a harness type with the {@link AgentConfig}
@@ -91,6 +92,21 @@ export interface AgentHarness {
91
92
  * thread-scoped code path, so the SDK can route them to the correct subscriber scope.
92
93
  */
93
94
  onLog(callback: (record: LogRecord) => void): Unsubscribe;
95
+ /**
96
+ * Subscribe to wire-communication events emitted by the harness — raw
97
+ * request/response captures of the LLM provider's HTTP traffic. Returns
98
+ * an unsubscribe function. The events may contain user prompts and PII;
99
+ * the channel is opt-in and not flowed to log files by default.
100
+ *
101
+ * Fidelity may differ between harnesses: the Mastra (in-process) path
102
+ * emits one `request` + one `response` event per outbound HTTP call,
103
+ * with the full body and status visible. The Claude (subprocess) path
104
+ * has the seam wired but does not emit yet — emission would parse
105
+ * `ANTHROPIC_LOG=debug` stderr from the CLI. Consumers subscribe once
106
+ * at the `AgentManager` layer; the SDK forwards events upward through
107
+ * the bus hierarchy the same way it does for telemetry and log records.
108
+ */
109
+ onWireCommunication(callback: WireCommunicationEventCallback): Unsubscribe;
94
110
  /**
95
111
  * Create and register a new agent with the given configuration.
96
112
  *
@@ -104,7 +120,10 @@ export interface AgentHarness {
104
120
  *
105
121
  * @param agentId - ID to register the agent under.
106
122
  * @param projectRoot - Project folder the agent is allowed to manipulate files from.
107
- * @param llmGatewayClient - Pre-configured LLM gateway client for this agent.
123
+ * @param modelConnectivityInfo - Connectivity bag the harness uses to talk to the
124
+ * LLM (model, baseUrl, nativeModelId, providerHint, getHeaders). Harnesses MUST
125
+ * call `getHeaders()` per outbound call (Mastra: per HTTP call; Claude: once
126
+ * per subprocess spawn). See {@link ModelConnectivityInfo}.
108
127
  * @param config - Engine-facing agent configuration (org resolution omitted).
109
128
  * @param options - Optional execution options.
110
129
  * - `abortSignal` — caller-side cancellation; harnesses thread it
@@ -121,7 +140,7 @@ export interface AgentHarness {
121
140
  * wrap their hook bodies in `try`/`catch` themselves when they
122
141
  * want a richer fail-closed substitute.
123
142
  */
124
- createAgent(agentId: string, projectRoot: string, llmGatewayClient: LLMGatewayClient, config?: HarnessAgentConfig, options?: {
143
+ createAgent(agentId: string, projectRoot: string, modelConnectivityInfo: ModelConnectivityInfo, config?: HarnessAgentConfig, options?: {
125
144
  abortSignal?: AbortSignal;
126
145
  hooks?: AgentHooks;
127
146
  }): Promise<void>;
@@ -194,7 +213,10 @@ export interface AgentHarness {
194
213
  * `Agent.updateAgentConfig` after this method resolves).
195
214
  *
196
215
  * @param agentId - ID of the agent to update.
197
- * @param llmGatewayClient - LLM gateway client bound to the next config's org / model.
216
+ * @param modelConnectivityInfo - Connectivity bag bound to the next config's
217
+ * org / model (replaces the live bag in the harness's per-agent map; the
218
+ * `getHeaders` closure may close over a fresh JWT or rate-limit gate
219
+ * value). See {@link ModelConnectivityInfo}.
198
220
  * @param config - Engine-facing agent configuration to apply.
199
221
  * @param options - Optional execution options.
200
222
  * - `abortSignal` — caller-side cancellation; harnesses thread it
@@ -208,7 +230,7 @@ export interface AgentHarness {
208
230
  * config (and so a rollback `updateAgent(previousConfig)` restores
209
231
  * the prior hook bag too).
210
232
  */
211
- updateAgent(agentId: string, llmGatewayClient: LLMGatewayClient, config?: HarnessAgentConfig, options?: {
233
+ updateAgent(agentId: string, modelConnectivityInfo: ModelConnectivityInfo, config?: HarnessAgentConfig, options?: {
212
234
  abortSignal?: AbortSignal;
213
235
  hooks?: AgentHooks;
214
236
  }): Promise<void>;
@@ -1,5 +1,6 @@
1
1
  import { LogBus, type LogRecord, type Unsubscribe } from '@salesforce/agentic-common';
2
2
  import type { TelemetryEvent, TelemetryEventCallback } from '../types/telemetry-events.js';
3
+ import type { WireCommunicationEvent, WireCommunicationEventCallback } from '../types/wire-communication-event.js';
3
4
  /**
4
5
  * Composition helper used by `AgentHarness` implementations to own telemetry and log buses.
5
6
  *
@@ -13,6 +14,7 @@ import type { TelemetryEvent, TelemetryEventCallback } from '../types/telemetry-
13
14
  export declare class HarnessBusOwner {
14
15
  private readonly telemetryBus;
15
16
  private readonly logBus;
17
+ private readonly wireBus;
16
18
  private disposed;
17
19
  /**
18
20
  * Returns the log bus so harness implementations can hand it to pure helpers (e.g. message
@@ -23,6 +25,21 @@ export declare class HarnessBusOwner {
23
25
  getLogBus(): LogBus | undefined;
24
26
  onTelemetry(callback: TelemetryEventCallback): Unsubscribe;
25
27
  onLog(callback: (record: LogRecord) => void): Unsubscribe;
28
+ onWireCommunication(callback: WireCommunicationEventCallback): Unsubscribe;
29
+ emitWireCommunication(event: WireCommunicationEvent): void;
30
+ /**
31
+ * Returns `true` when the wire-communication channel has at least one downstream subscriber
32
+ * reaching all the way to a consumer. The bus chain
33
+ * `harness.wireBus → router slice → manager.wireBus → consumer.onWireCommunication`
34
+ * is wired with `forwardWhileSubscribed`, so a consumer-less chain leaves
35
+ * `wireBus.listenerCount === 0` and an expensive harness-side producer (e.g. the Claude
36
+ * subprocess `ANTHROPIC_LOG=debug` parser) can short-circuit before paying any cost.
37
+ *
38
+ * Returns `false` after `dispose()`. Read this lazily — listener-presence transitions
39
+ * happen between calls (subscribe / unsubscribe) and the value reflects current state at
40
+ * call time only.
41
+ */
42
+ hasWireCommunicationSubscribers(): boolean;
26
43
  emitTelemetry(event: TelemetryEvent): void;
27
44
  emitLog(record: LogRecord): void;
28
45
  logDebug(message: string, context?: Record<string, unknown>): void;
@@ -17,6 +17,7 @@ import { AgentSDKError, AgentSDKErrorType } from '../errors.js';
17
17
  export class HarnessBusOwner {
18
18
  telemetryBus = new EventBus();
19
19
  logBus = new LogBus();
20
+ wireBus = new EventBus();
20
21
  disposed = false;
21
22
  /**
22
23
  * Returns the log bus so harness implementations can hand it to pure helpers (e.g. message
@@ -37,6 +38,31 @@ export class HarnessBusOwner {
37
38
  this.assertNotDisposed();
38
39
  return this.logBus.on(callback);
39
40
  }
41
+ onWireCommunication(callback) {
42
+ this.assertNotDisposed();
43
+ return this.wireBus.on(callback);
44
+ }
45
+ emitWireCommunication(event) {
46
+ this.assertNotDisposed();
47
+ this.wireBus.emit(event);
48
+ }
49
+ /**
50
+ * Returns `true` when the wire-communication channel has at least one downstream subscriber
51
+ * reaching all the way to a consumer. The bus chain
52
+ * `harness.wireBus → router slice → manager.wireBus → consumer.onWireCommunication`
53
+ * is wired with `forwardWhileSubscribed`, so a consumer-less chain leaves
54
+ * `wireBus.listenerCount === 0` and an expensive harness-side producer (e.g. the Claude
55
+ * subprocess `ANTHROPIC_LOG=debug` parser) can short-circuit before paying any cost.
56
+ *
57
+ * Returns `false` after `dispose()`. Read this lazily — listener-presence transitions
58
+ * happen between calls (subscribe / unsubscribe) and the value reflects current state at
59
+ * call time only.
60
+ */
61
+ hasWireCommunicationSubscribers() {
62
+ if (this.disposed)
63
+ return false;
64
+ return this.wireBus.listenerCount > 0;
65
+ }
40
66
  emitTelemetry(event) {
41
67
  this.assertNotDisposed();
42
68
  this.telemetryBus.emit(event);
@@ -67,6 +93,7 @@ export class HarnessBusOwner {
67
93
  }
68
94
  this.telemetryBus.dispose();
69
95
  this.logBus.dispose();
96
+ this.wireBus.dispose();
70
97
  this.disposed = true;
71
98
  }
72
99
  assertNotDisposed() {
@@ -1,6 +1,7 @@
1
1
  import type { ToolDefinition } from '../types/tools.js';
2
2
  import type { MCPConfiguration } from '../mcp-config.js';
3
- import type { JSONWebToken, Model, ModelName } from '@salesforce/llm-gateway-sdk';
3
+ import type { JSONWebToken } from '@salesforce/agentic-common';
4
+ import type { Model, ModelName } from '../models/index.js';
4
5
  /**
5
6
  * Configuration for an agent's behavior and capabilities.
6
7
  * This excludes identity; `agentId` is handled separately.
@@ -20,7 +21,7 @@ export type AgentConfig = {
20
21
  * Accepts either a {@link ModelName} enum value (the typical case for in-tree models) or a
21
22
  * pre-built {@link Model} instance. The instance form lets consumers opt into a Claude
22
23
  * variant published on the gateway before the SDK has been updated — see
23
- * `createClaudeModel(gatewayId, overrides)` from `@salesforce/llm-gateway-sdk`.
24
+ * `createClaudeModel(gatewayId, overrides)` re-exported from this package.
24
25
  */
25
26
  modelId?: ModelName | Model;
26
27
  /** Human-readable name for the agent. */
@@ -1,4 +1,5 @@
1
1
  import type { AgentHarness } from './agent-harness.js';
2
+ import type { ProviderHint } from '../types/model-connectivity-info.js';
2
3
  /**
3
4
  * Constructs an {@link AgentHarness} on demand.
4
5
  *
@@ -17,5 +18,17 @@ export interface HarnessFactory<H extends AgentHarness = AgentHarness> {
17
18
  readonly harnessId: string;
18
19
  /** SDK-to-harness protocol version implemented by the harness this factory builds. */
19
20
  readonly protocolVersion: number;
21
+ /**
22
+ * Wire shapes the constructed harness can drive.
23
+ *
24
+ * The manager validates the resolved
25
+ * {@link import('../types/model-connectivity-info.js').ModelConnectivityInfo.providerHint}
26
+ * against this list at `createAgent` and `updateAgentConfig` time, throwing
27
+ * `AgentSDKErrorType.MODEL_NOT_SUPPORTED_BY_HARNESS` before any harness
28
+ * work runs when no factory advertises support.
29
+ *
30
+ * Static per-class — the manager reads it without instantiating the harness.
31
+ */
32
+ readonly supportedProviderHints: readonly ProviderHint[];
20
33
  create(storageRootFolder: string): Promise<H>;
21
34
  }
@@ -1,4 +1,4 @@
1
- import { type MultimodalFile } from '@salesforce/llm-gateway-sdk';
1
+ import type { MultimodalFile } from '../models/index.js';
2
2
  import type { FilePart, ImagePart, MessagePart, TextPart } from '../types/messages.js';
3
3
  /**
4
4
  * The subset of {@link MessagePart} that is valid as user `stream()` input: plain `text` plus the
@@ -15,8 +15,10 @@ export type InputMessagePart = TextPart | ImagePart | FilePart;
15
15
  *
16
16
  * Order of operations:
17
17
  * 1. Collect `image` / `file` parts into a {@link MultimodalFile} list and hand them to
18
- * `validateFiles` (per-model + global gateway caps). An {@link LLMGClientError} is mapped to
19
- * `AgentSDKError(MULTIMODAL_NOT_SUPPORTED)`; any other throw propagates unchanged.
18
+ * `validateFiles` (per-model + global gateway caps). The standard
19
+ * {@link validateMultimodalFiles} helper throws
20
+ * `AgentSDKError(MULTIMODAL_NOT_SUPPORTED)` directly on a cap/format violation; any other
21
+ * throw propagates unchanged.
20
22
  * 2. Lower each part via `mapPart`. A `reasoning` / `tool-call` / `tool-result` part is not valid
21
23
  * stream input and throws `AgentSDKError(INVALID_MESSAGE_CONTENT)` before `mapPart` sees it.
22
24
  *
@@ -25,8 +27,10 @@ export type InputMessagePart = TextPart | ImagePart | FilePart;
25
27
  * failure rejects the harness's `stream()` promise pre-stream rather than mid-iteration.
26
28
  *
27
29
  * @param parts - the user message parts to validate and lower.
28
- * @param validateFiles - per-harness file validation (e.g. `validateMultimodalFiles(files, model)`)
29
- * that throws `LLMGClientError` on a cap/format violation and no-ops for text-only input.
30
+ * @param validateFiles - per-harness file validation (typically
31
+ * `(files) => validateMultimodalFiles(files, model)`) that throws
32
+ * `AgentSDKError(MULTIMODAL_NOT_SUPPORTED)` on a cap/format violation and no-ops for text-only
33
+ * input.
30
34
  * @param mapPart - lowers one validated input part into the harness's content-block shape.
31
35
  * @returns the lowered blocks, in the original part order.
32
36
  */