@salesforce/sfdx-agent-sdk 0.20.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.
- package/CHANGELOG.md +15 -0
- package/README.md +12 -11
- package/dist/agent-connectivity-resolver.d.ts +47 -25
- package/dist/agent-connectivity-resolver.js +92 -15
- package/dist/agent-manager.d.ts +17 -1
- package/dist/agent-manager.js +40 -7
- package/dist/agent.d.ts +33 -10
- package/dist/agent.js +38 -50
- package/dist/api-key-connectivity-resolver.d.ts +110 -0
- package/dist/api-key-connectivity-resolver.js +114 -0
- package/dist/errors.d.ts +2 -0
- package/dist/errors.js +2 -0
- package/dist/harness/agent-harness.d.ts +27 -5
- package/dist/harness/harness-bus-owner.d.ts +17 -0
- package/dist/harness/harness-bus-owner.js +27 -0
- package/dist/harness/harness-config.d.ts +3 -2
- package/dist/harness/harness-factory.d.ts +13 -0
- package/dist/harness/stream-input.d.ts +9 -5
- package/dist/harness/stream-input.js +12 -14
- package/dist/index.d.ts +8 -2
- package/dist/index.js +4 -1
- package/dist/internal/wire-communication-router.d.ts +43 -0
- package/dist/internal/wire-communication-router.js +119 -0
- package/dist/mcp-auth.d.ts +1 -1
- package/dist/models/claude-opus-4-5.d.ts +11 -0
- package/dist/models/claude-opus-4-5.js +21 -0
- package/dist/models/claude-opus-4-6.d.ts +11 -0
- package/dist/models/claude-opus-4-6.js +22 -0
- package/dist/models/claude-opus-4-7.d.ts +11 -0
- package/dist/models/claude-opus-4-7.js +22 -0
- package/dist/models/claude-sonnet-4-5.d.ts +11 -0
- package/dist/models/claude-sonnet-4-5.js +23 -0
- package/dist/models/claude-sonnet-4-6.d.ts +11 -0
- package/dist/models/claude-sonnet-4-6.js +22 -0
- package/dist/models/create-claude-model.d.ts +54 -0
- package/dist/models/create-claude-model.js +62 -0
- package/dist/models/gpt-5-4.d.ts +11 -0
- package/dist/models/gpt-5-4.js +21 -0
- package/dist/models/gpt-5-5.d.ts +15 -0
- package/dist/models/gpt-5-5.js +24 -0
- package/dist/models/gpt-5.d.ts +11 -0
- package/dist/models/gpt-5.js +23 -0
- package/dist/models/index.d.ts +19 -0
- package/dist/models/index.js +49 -0
- package/dist/models/model.d.ts +69 -0
- package/dist/models/model.js +63 -0
- package/dist/models/multimodal.d.ts +35 -0
- package/dist/models/multimodal.js +78 -0
- package/dist/models/types.d.ts +49 -0
- package/dist/models/types.js +18 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/model-connectivity-info.d.ts +87 -0
- package/dist/types/model-connectivity-info.js +6 -0
- package/dist/types/usage.d.ts +3 -3
- package/dist/types/wire-communication-event.d.ts +124 -0
- package/dist/types/wire-communication-event.js +6 -0
- package/dist/wire-communication-file-writer.d.ts +39 -0
- package/dist/wire-communication-file-writer.js +142 -0
- 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
|
-
|
|
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
|
|
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,
|
|
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.
|
|
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
|
|
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
|
|
129
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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,
|
|
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
|
-
//
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
//
|
|
346
|
-
const getContextWindow = () => this.
|
|
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,8 +7,10 @@ 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";
|
|
13
|
+
readonly TOOL_CALL_NOT_FOUND: "TOOL_CALL_NOT_FOUND";
|
|
12
14
|
};
|
|
13
15
|
export type AgentSDKErrorType = (typeof AgentSDKErrorType)[keyof typeof AgentSDKErrorType];
|
|
14
16
|
export declare class AgentSDKError extends Error {
|
package/dist/errors.js
CHANGED
|
@@ -11,8 +11,10 @@ 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',
|
|
17
|
+
TOOL_CALL_NOT_FOUND: 'TOOL_CALL_NOT_FOUND',
|
|
16
18
|
};
|
|
17
19
|
export class AgentSDKError extends Error {
|
|
18
20
|
type;
|
|
@@ -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 {
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
}
|