@salesforce/sfdx-agent-sdk 0.4.0 → 0.6.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/README.md +228 -35
- package/dist/agent-manager.d.ts +155 -87
- package/dist/agent-manager.js +161 -110
- package/dist/agent.d.ts +15 -0
- package/dist/agent.js +18 -0
- package/dist/harness/agent-harness.d.ts +45 -1
- package/dist/harness/harness-config.d.ts +10 -0
- package/dist/harness/harness-config.js +10 -0
- package/dist/harness/harness-factory.d.ts +2 -2
- package/dist/harness/index.d.ts +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/internal/agent-identity-store.d.ts +41 -0
- package/dist/internal/agent-identity-store.js +141 -0
- package/dist/mcp-config.d.ts +64 -1
- package/dist/types/usage.d.ts +3 -1
- package/package.json +18 -11
package/dist/agent-manager.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Copyright 2026, Salesforce, Inc. All rights reserved.
|
|
3
3
|
* See LICENSE.txt for license terms.
|
|
4
4
|
*/
|
|
5
|
-
import { EventBus, LogBus, RealClock, UUIDGenerator, } from '@salesforce/agentic-common';
|
|
5
|
+
import { EventBus, getErrorMessage, LogBus, RealClock, UUIDGenerator, } from '@salesforce/agentic-common';
|
|
6
6
|
import { resolve } from 'node:path';
|
|
7
7
|
import { stat } from 'node:fs/promises';
|
|
8
8
|
import { SUPPORTED_PROTOCOL_VERSIONS } from './harness/agent-harness.js';
|
|
@@ -10,49 +10,34 @@ import { toHarnessConfig } from './harness/harness-config.js';
|
|
|
10
10
|
import { DefaultAgent } from './agent.js';
|
|
11
11
|
import { AgentSDKError, AgentSDKErrorType } from './errors.js';
|
|
12
12
|
import { TelemetryRouter } from './internal/telemetry-router.js';
|
|
13
|
+
import { AgentIdentityStore } from './internal/agent-identity-store.js';
|
|
13
14
|
import { DefaultAgentConnectivityResolver } from './agent-connectivity-resolver.js';
|
|
14
15
|
/**
|
|
15
|
-
*
|
|
16
|
+
* Concrete implementation of {@link AgentManager}. **Not exported** from
|
|
17
|
+
* `src/index.ts` — public construction goes through {@link createAgentManager}.
|
|
16
18
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* ### Persistence Responsibility
|
|
22
|
-
* Persistence is shared between the harness and the consuming application:
|
|
23
|
-
* - **Engine**: Handles physical data persistence (database storage of threads, messages, and memory state).
|
|
24
|
-
* - **Application**: Handles lifecycle persistence. It manages the discovery and restoration of saved agents,
|
|
25
|
-
* and calls `createAgent(projectRoot, { agentId: savedId, ...savedConfig })` to rebuild active agent state in memory
|
|
26
|
-
* (like MCP connections and custom tool closures) while reconnecting to the persistent memory data.
|
|
19
|
+
* Construction is asynchronous because {@link init} reads the persistence
|
|
20
|
+
* directory and replays prior agents before the manager becomes useful. The
|
|
21
|
+
* pattern mirrors {@link Workspace.create}: a private constructor for sync
|
|
22
|
+
* field assignment, then a static async builder that calls `init()`.
|
|
27
23
|
*/
|
|
28
|
-
export class
|
|
24
|
+
export class DefaultAgentManager {
|
|
29
25
|
harness;
|
|
30
26
|
agentIdGenerator;
|
|
31
27
|
agentConnectivityResolver;
|
|
32
28
|
clock;
|
|
29
|
+
identityStore;
|
|
33
30
|
agents = new Map();
|
|
31
|
+
restoreFailures = [];
|
|
34
32
|
telemetryBus = new EventBus();
|
|
35
33
|
logBus;
|
|
36
34
|
router;
|
|
37
35
|
unroutedUnsubs;
|
|
38
36
|
disposed = false;
|
|
39
|
-
|
|
40
|
-
* Creates a new {@link AgentManager}.
|
|
41
|
-
*
|
|
42
|
-
* This constructor is **exposed for testing only** so unit tests can inject a stubbed
|
|
43
|
-
* {@link AgentHarness} and deterministic ID generator.
|
|
44
|
-
*
|
|
45
|
-
* Production callers should use {@link createAgentManager} instead.
|
|
46
|
-
*
|
|
47
|
-
* @example
|
|
48
|
-
* ```ts
|
|
49
|
-
* // Recommended for production usage (constructs the default connectivity resolver):
|
|
50
|
-
* const manager = await createAgentManager(storageRootFolder, harnessFactory);
|
|
51
|
-
* ```
|
|
52
|
-
*/
|
|
53
|
-
constructor(harness, agentConnectivityResolver, agentIdGenerator = new UUIDGenerator(), clock = new RealClock(), logBus = new LogBus()) {
|
|
37
|
+
constructor(harness, agentConnectivityResolver, identityStore, agentIdGenerator, clock, logBus) {
|
|
54
38
|
this.harness = harness;
|
|
55
39
|
this.agentConnectivityResolver = agentConnectivityResolver;
|
|
40
|
+
this.identityStore = identityStore;
|
|
56
41
|
this.agentIdGenerator = agentIdGenerator;
|
|
57
42
|
this.clock = clock;
|
|
58
43
|
this.logBus = logBus;
|
|
@@ -62,15 +47,47 @@ export class AgentManager {
|
|
|
62
47
|
this.router.unrouted.log.forwardTo(this.logBus),
|
|
63
48
|
];
|
|
64
49
|
}
|
|
65
|
-
// Note: harness construction is async (see createAgentManager), so AgentManager itself no longer has initialize().
|
|
66
50
|
/**
|
|
67
|
-
*
|
|
51
|
+
* Module-internal builder used by {@link createAgentManager}. Mirrors
|
|
52
|
+
* {@link Workspace.create}: sync field assignment in the private
|
|
53
|
+
* constructor, then `await init()` to do async startup work
|
|
54
|
+
* (reading the persistence directory + replaying prior agents).
|
|
68
55
|
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* - MUST delegate to `this.harness.shutdown()` to release harness-level resources.
|
|
56
|
+
* The double-underscore signals "do not call directly" — the constructor
|
|
57
|
+
* is private, so this is the only way to obtain an instance, but
|
|
58
|
+
* consumers should always go through {@link createAgentManager}.
|
|
73
59
|
*/
|
|
60
|
+
static async __build(harness, agentConnectivityResolver, storageRootFolder, agentIdGenerator, clock, logBus) {
|
|
61
|
+
const identityStore = new AgentIdentityStore(storageRootFolder, harness.harnessId, logBus);
|
|
62
|
+
const manager = new DefaultAgentManager(harness, agentConnectivityResolver, identityStore, agentIdGenerator, clock, logBus);
|
|
63
|
+
await manager.init();
|
|
64
|
+
return manager;
|
|
65
|
+
}
|
|
66
|
+
async init() {
|
|
67
|
+
const records = await this.identityStore.list();
|
|
68
|
+
const failures = [];
|
|
69
|
+
await Promise.all(records.map(async (record) => {
|
|
70
|
+
try {
|
|
71
|
+
await this.installAgent(record.agentId, record.projectRoot, record.config, {
|
|
72
|
+
rehydrateThreads: true,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
failures.push({
|
|
77
|
+
agentId: record.agentId,
|
|
78
|
+
projectRoot: record.projectRoot,
|
|
79
|
+
config: record.config,
|
|
80
|
+
error: err,
|
|
81
|
+
});
|
|
82
|
+
this.logBus.error('agent restore failed', err instanceof Error ? err : undefined, {
|
|
83
|
+
agentId: record.agentId,
|
|
84
|
+
projectRoot: record.projectRoot,
|
|
85
|
+
message: getErrorMessage(err),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}));
|
|
89
|
+
this.restoreFailures = failures;
|
|
90
|
+
}
|
|
74
91
|
async shutdown() {
|
|
75
92
|
if (this.disposed) {
|
|
76
93
|
return;
|
|
@@ -88,78 +105,99 @@ export class AgentManager {
|
|
|
88
105
|
this.logBus.dispose();
|
|
89
106
|
this.disposed = true;
|
|
90
107
|
}
|
|
91
|
-
/**
|
|
92
|
-
* Creates a new `Agent` with the given configuration.
|
|
93
|
-
*
|
|
94
|
-
* @param projectRoot - Absolute path to the project folder the agent can manipulate files from.
|
|
95
|
-
* Relative paths are resolved against `process.cwd()`. The path must exist and be a directory.
|
|
96
|
-
* @param config - Agent configuration (instructions, tools, model, etc.).
|
|
97
|
-
* @param options - Optional execution options, including abort signals.
|
|
98
|
-
* @returns A new `Agent` ready for chat sessions.
|
|
99
|
-
*
|
|
100
|
-
* @throws If `projectRoot` does not exist or is not a directory.
|
|
101
|
-
*
|
|
102
|
-
* @requirements
|
|
103
|
-
* - MUST resolve `projectRoot` to an absolute, normalized path before use.
|
|
104
|
-
* - MUST throw an Error if the resolved `projectRoot` does not exist or is not a directory.
|
|
105
|
-
* - MUST throw an Error if `config.agentId` is provided and an agent with that ID already exists in the internal `agents` map.
|
|
106
|
-
* - MUST generate a unique ID via `this.agentIdGenerator` if `config.agentId` is omitted.
|
|
107
|
-
* - MUST delegate to `this.harness.createAgent(agentId, projectRoot, llmGatewayClient, config, options)` to register the agent in the harness and allow for cancellation.
|
|
108
|
-
* - MUST instantiate a `DefaultAgent` with the harness and the complete configuration.
|
|
109
|
-
* - MUST store the newly created agent in the internal `agents` map, keyed by its `agentId`.
|
|
110
|
-
* - MUST return the newly created agent.
|
|
111
|
-
*/
|
|
112
108
|
async createAgent(projectRoot, config = {}, options) {
|
|
113
109
|
this.assertNotDisposed();
|
|
114
110
|
const resolvedProjectRoot = resolve(projectRoot);
|
|
111
|
+
const { agentId: providedAgentId, ...agentConfig } = config;
|
|
112
|
+
const agentId = providedAgentId ?? this.agentIdGenerator.getUniqueId();
|
|
113
|
+
if (this.agents.has(agentId)) {
|
|
114
|
+
throw new Error(`Agent with id "${agentId}" already exists`);
|
|
115
|
+
}
|
|
116
|
+
// installAgent validates projectRoot existence — same path as the restore loop.
|
|
117
|
+
const agent = await this.installAgent(agentId, resolvedProjectRoot, agentConfig, {
|
|
118
|
+
abortSignal: options?.abortSignal,
|
|
119
|
+
});
|
|
120
|
+
// If the disk write fails (disk full, permissions, fs error), the in-memory install
|
|
121
|
+
// is now stale — the harness has the agent, the manager has it in `agents`, but no
|
|
122
|
+
// record on disk means a subsequent restart loses it. Roll back the install so the
|
|
123
|
+
// failure is observable and the id stays reusable, then rethrow.
|
|
124
|
+
try {
|
|
125
|
+
await this.identityStore.write(agentId, resolvedProjectRoot, agentConfig);
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
await this.rollbackInstall(agentId, agent);
|
|
129
|
+
throw err;
|
|
130
|
+
}
|
|
131
|
+
// A successful (re)create clears any prior failed-restore entry for the same id.
|
|
132
|
+
this.restoreFailures = this.restoreFailures.filter((f) => f.agentId !== agentId);
|
|
133
|
+
return agent;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Shared install path for {@link createAgent} and the boot-time restore
|
|
137
|
+
* loop. Resolves connectivity, calls `harness.createAgent`, constructs
|
|
138
|
+
* the {@link DefaultAgent}, registers the telemetry slice, emits
|
|
139
|
+
* `agent-created`, and (when restoring) attaches a chat session per
|
|
140
|
+
* persisted thread id.
|
|
141
|
+
*
|
|
142
|
+
* Returns {@link DefaultAgent} (concrete) rather than {@link Agent}
|
|
143
|
+
* because both call sites are internal — widening to the interface only
|
|
144
|
+
* happens at the public-method return statement.
|
|
145
|
+
*/
|
|
146
|
+
async installAgent(agentId, projectRoot, config, options = {}) {
|
|
115
147
|
let dirStat;
|
|
116
148
|
try {
|
|
117
|
-
dirStat = await stat(
|
|
149
|
+
dirStat = await stat(projectRoot);
|
|
118
150
|
}
|
|
119
151
|
catch {
|
|
120
|
-
throw new Error(`projectRoot does not exist: "${
|
|
152
|
+
throw new Error(`projectRoot does not exist: "${projectRoot}"`);
|
|
121
153
|
}
|
|
122
154
|
if (!dirStat.isDirectory()) {
|
|
123
|
-
throw new Error(`projectRoot is not a directory: "${
|
|
124
|
-
}
|
|
125
|
-
const { agentId: providedAgentId, ...agentConfig } = config;
|
|
126
|
-
const agentId = providedAgentId ?? this.agentIdGenerator.getUniqueId();
|
|
127
|
-
if (providedAgentId && this.agents.has(providedAgentId)) {
|
|
128
|
-
throw new Error(`Agent with id "${providedAgentId}" already exists`);
|
|
155
|
+
throw new Error(`projectRoot is not a directory: "${projectRoot}"`);
|
|
129
156
|
}
|
|
130
|
-
const runtime = await this.agentConnectivityResolver.resolve(
|
|
131
|
-
await this.harness.createAgent(agentId,
|
|
157
|
+
const runtime = await this.agentConnectivityResolver.resolve(projectRoot, config);
|
|
158
|
+
await this.harness.createAgent(agentId, projectRoot, runtime.llmGatewayClient, toHarnessConfig(config, runtime.orgJwt), options.abortSignal !== undefined ? { abortSignal: options.abortSignal } : undefined);
|
|
132
159
|
const agentSlice = this.router.registerAgent(agentId);
|
|
133
|
-
const agent = new DefaultAgent(this.harness, agentId,
|
|
160
|
+
const agent = new DefaultAgent(this.harness, agentId, projectRoot, config, runtime.llmGatewayClient, runtime.orgConnection, runtime.orgJwt, this.agentConnectivityResolver, this.router, agentSlice, { telemetry: this.telemetryBus, log: this.logBus }, this.clock, this.agentIdGenerator);
|
|
134
161
|
this.agents.set(agentId, agent);
|
|
135
162
|
this.telemetryBus.emit({
|
|
136
163
|
type: 'agent-created',
|
|
137
164
|
timestamp: this.clock.now(),
|
|
138
165
|
agentId,
|
|
139
|
-
projectRoot
|
|
166
|
+
projectRoot,
|
|
140
167
|
modelName: runtime.llmGatewayClient.getModel().name,
|
|
141
168
|
});
|
|
169
|
+
if (options.rehydrateThreads) {
|
|
170
|
+
// If thread enumeration or session attachment fails, the restore is a full failure
|
|
171
|
+
// (the user would otherwise see an agent marked `ready` whose chat sessions return 404).
|
|
172
|
+
// We must roll back the in-memory installation so the failure surfaces in
|
|
173
|
+
// `restoreFailures` and the id remains reusable via createAgent.
|
|
174
|
+
try {
|
|
175
|
+
const threadIds = await this.harness.getThreadIds(agentId);
|
|
176
|
+
agent.restoreSessions(threadIds);
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
await this.rollbackInstall(agentId, agent);
|
|
180
|
+
throw err;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
142
183
|
return agent;
|
|
143
184
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
185
|
+
async rollbackInstall(agentId, agent) {
|
|
186
|
+
// `agent.destroy()` already calls `harness.destroyAgent(agentId)` internally
|
|
187
|
+
// (see `DefaultAgent.destroy` in `agent.ts`), so we don't need a second harness call.
|
|
188
|
+
try {
|
|
189
|
+
await agent.destroy();
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
// Swallow secondary failure; the original error (passed to caller) is what matters.
|
|
193
|
+
}
|
|
194
|
+
this.router.unregisterAgent(agentId);
|
|
195
|
+
this.agents.delete(agentId);
|
|
196
|
+
}
|
|
150
197
|
getAgentIds() {
|
|
151
198
|
this.assertNotDisposed();
|
|
152
199
|
return Array.from(this.agents.keys());
|
|
153
200
|
}
|
|
154
|
-
/**
|
|
155
|
-
* Retrieves a managed agent by its ID.
|
|
156
|
-
*
|
|
157
|
-
* @param agentId - ID of the agent to retrieve.
|
|
158
|
-
*
|
|
159
|
-
* @requirements
|
|
160
|
-
* - MUST throw an Error if the provided `agentId` is not found in the internal `agents` map.
|
|
161
|
-
* - MUST return the `Agent` instance associated with the given `agentId`.
|
|
162
|
-
*/
|
|
163
201
|
getAgent(agentId) {
|
|
164
202
|
this.assertNotDisposed();
|
|
165
203
|
const agent = this.agents.get(agentId);
|
|
@@ -168,36 +206,49 @@ export class AgentManager {
|
|
|
168
206
|
}
|
|
169
207
|
return agent;
|
|
170
208
|
}
|
|
171
|
-
/**
|
|
172
|
-
* Destroys a managed agent by its ID, releasing all its resources.
|
|
173
|
-
*
|
|
174
|
-
* @param agentId - ID of the agent to destroy.
|
|
175
|
-
*
|
|
176
|
-
* @requirements
|
|
177
|
-
* - MUST throw an Error if the provided `agentId` is not found in the internal `agents` map.
|
|
178
|
-
* - MUST call `destroy()` on the target agent instance.
|
|
179
|
-
* - MUST remove the agent from the internal `agents` map.
|
|
180
|
-
*/
|
|
181
209
|
async destroyAgent(agentId) {
|
|
182
210
|
this.assertNotDisposed();
|
|
183
211
|
const agent = this.agents.get(agentId);
|
|
184
|
-
|
|
212
|
+
const failureIndex = this.restoreFailures.findIndex((f) => f.agentId === agentId);
|
|
213
|
+
if (!agent && failureIndex === -1) {
|
|
185
214
|
throw new AgentSDKError(`No Agent found with id: "${agentId}"`, AgentSDKErrorType.AGENT_NOT_FOUND);
|
|
186
215
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
216
|
+
if (agent) {
|
|
217
|
+
await agent.destroy();
|
|
218
|
+
this.router.unregisterAgent(agentId);
|
|
219
|
+
this.agents.delete(agentId);
|
|
220
|
+
}
|
|
221
|
+
if (failureIndex !== -1) {
|
|
222
|
+
this.restoreFailures.splice(failureIndex, 1);
|
|
223
|
+
}
|
|
224
|
+
await this.identityStore.remove(agentId);
|
|
190
225
|
}
|
|
191
|
-
/** Subscribe to telemetry events across every managed agent. Returns an unsubscribe function. */
|
|
192
226
|
onTelemetry(callback) {
|
|
193
227
|
this.assertNotDisposed();
|
|
194
228
|
return this.telemetryBus.on(callback);
|
|
195
229
|
}
|
|
196
|
-
/** Subscribe to structured log records across every managed agent. Returns an unsubscribe function. */
|
|
197
230
|
onLog(callback) {
|
|
198
231
|
this.assertNotDisposed();
|
|
199
232
|
return this.logBus.on(callback);
|
|
200
233
|
}
|
|
234
|
+
getRestoreFailures() {
|
|
235
|
+
this.assertNotDisposed();
|
|
236
|
+
return [...this.restoreFailures];
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Re-exposes `harness.extensions` typed off `H`. Read-only; the SDK never
|
|
240
|
+
* leaks the harness reference itself, only its declared extensions surface.
|
|
241
|
+
* `assertNotDisposed` is intentionally NOT called here — the extensions
|
|
242
|
+
* object is reachable on the harness, and reading it after shutdown is the
|
|
243
|
+
* harness's prerogative to handle (its own disposal mechanics may have
|
|
244
|
+
* already torn down the underlying state).
|
|
245
|
+
*/
|
|
246
|
+
get extensions() {
|
|
247
|
+
// The harness reference is constrained to `H`, so `harness.extensions`
|
|
248
|
+
// is structurally `H['extensions']` — but TS widens the property access
|
|
249
|
+
// through the constraint, requiring this cast.
|
|
250
|
+
return this.harness.extensions;
|
|
251
|
+
}
|
|
201
252
|
assertNotDisposed() {
|
|
202
253
|
if (this.disposed) {
|
|
203
254
|
throw new AgentSDKError('AgentManager has been shut down.', AgentSDKErrorType.DISPOSED);
|
|
@@ -205,23 +256,23 @@ export class AgentManager {
|
|
|
205
256
|
}
|
|
206
257
|
}
|
|
207
258
|
/**
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
* asynchronously, sanity-checks that the constructed harness honors the
|
|
214
|
-
* factory's advertised version, and returns a ready-to-use manager.
|
|
259
|
+
* Public entry point. Validates the storage root, gates the harness's
|
|
260
|
+
* advertised protocol version, constructs the harness, and returns a
|
|
261
|
+
* fully-restored {@link AgentManager}. Boot-time restore runs before this
|
|
262
|
+
* function returns; failures are queryable via
|
|
263
|
+
* {@link AgentManager.getRestoreFailures}.
|
|
215
264
|
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
265
|
+
* The optional `connectivityResolver` overrides the default
|
|
266
|
+
* `DefaultAgentConnectivityResolver` — used by e2e tests and custom-auth
|
|
267
|
+
* deployments where the SDK should not run sf-CLI-based org resolution.
|
|
268
|
+
* Production callers leave it unset.
|
|
218
269
|
*
|
|
219
270
|
* @throws {AgentSDKError} `INCOMPATIBLE_HARNESS` when either the factory or
|
|
220
271
|
* the constructed harness reports a `protocolVersion` outside
|
|
221
272
|
* {@link SUPPORTED_PROTOCOL_VERSIONS}, or when the harness's reported
|
|
222
273
|
* version disagrees with the factory's.
|
|
223
274
|
*/
|
|
224
|
-
export async function createAgentManager(storageRootFolder, harnessFactory) {
|
|
275
|
+
export async function createAgentManager(storageRootFolder, harnessFactory, connectivityResolver) {
|
|
225
276
|
let stats;
|
|
226
277
|
try {
|
|
227
278
|
stats = await stat(storageRootFolder);
|
|
@@ -255,8 +306,8 @@ export async function createAgentManager(storageRootFolder, harnessFactory) {
|
|
|
255
306
|
`advertised version ${factoryVersion} (SDK supports: ${SUPPORTED_PROTOCOL_VERSIONS.join(', ')}). ` +
|
|
256
307
|
`Update the SDK or harness package.`, AgentSDKErrorType.INCOMPATIBLE_HARNESS);
|
|
257
308
|
}
|
|
258
|
-
const agentConnectivityResolver = new DefaultAgentConnectivityResolver();
|
|
259
|
-
return
|
|
309
|
+
const agentConnectivityResolver = connectivityResolver ?? new DefaultAgentConnectivityResolver();
|
|
310
|
+
return DefaultAgentManager.__build(harness, agentConnectivityResolver, storageRootFolder, new UUIDGenerator(), new RealClock(), new LogBus());
|
|
260
311
|
}
|
|
261
312
|
function isSupportedProtocolVersion(version) {
|
|
262
313
|
return (typeof version === 'number' &&
|
package/dist/agent.d.ts
CHANGED
|
@@ -20,6 +20,10 @@ export type AgentParentBuses = {
|
|
|
20
20
|
* Each `Agent` wraps a single agent in the underlying harness. It
|
|
21
21
|
* provides agent-level operations (configuration, MCP, lifecycle) and serves
|
|
22
22
|
* as the factory for {@link ChatSession} instances.
|
|
23
|
+
*
|
|
24
|
+
* Harness-specific features (tool-search processor handles, workflows, etc.)
|
|
25
|
+
* are reached through {@link AgentManager.extensions}, not `Agent`. Per-agent
|
|
26
|
+
* accessors take the agent id as their first argument.
|
|
23
27
|
*/
|
|
24
28
|
export interface Agent {
|
|
25
29
|
/** Returns the unique agent identifier. */
|
|
@@ -212,6 +216,17 @@ export declare class DefaultAgent implements Agent {
|
|
|
212
216
|
destroy(): Promise<void>;
|
|
213
217
|
onTelemetry(callback: TelemetryEventCallback): Unsubscribe;
|
|
214
218
|
onLog(callback: (record: LogRecord) => void): Unsubscribe;
|
|
219
|
+
/**
|
|
220
|
+
* Re-attach `DefaultChatSession` wrappers for thread ids that already
|
|
221
|
+
* exist in the harness's persistent store. Called by the SDK during
|
|
222
|
+
* boot-time restore. Idempotent — thread ids that already have a session
|
|
223
|
+
* are skipped.
|
|
224
|
+
*
|
|
225
|
+
* Not on the public {@link Agent} interface because it's only meaningful
|
|
226
|
+
* to {@link DefaultAgentManager}; that's also why `installAgent` returns
|
|
227
|
+
* the concrete `DefaultAgent` type rather than `Agent`.
|
|
228
|
+
*/
|
|
229
|
+
restoreSessions(threadIds: string[]): void;
|
|
215
230
|
private attachSession;
|
|
216
231
|
private detachSession;
|
|
217
232
|
private assertNotDisposed;
|
package/dist/agent.js
CHANGED
|
@@ -279,6 +279,24 @@ export class DefaultAgent {
|
|
|
279
279
|
this.assertNotDisposed();
|
|
280
280
|
return this.logBus.on(callback);
|
|
281
281
|
}
|
|
282
|
+
/**
|
|
283
|
+
* Re-attach `DefaultChatSession` wrappers for thread ids that already
|
|
284
|
+
* exist in the harness's persistent store. Called by the SDK during
|
|
285
|
+
* boot-time restore. Idempotent — thread ids that already have a session
|
|
286
|
+
* are skipped.
|
|
287
|
+
*
|
|
288
|
+
* Not on the public {@link Agent} interface because it's only meaningful
|
|
289
|
+
* to {@link DefaultAgentManager}; that's also why `installAgent` returns
|
|
290
|
+
* the concrete `DefaultAgent` type rather than `Agent`.
|
|
291
|
+
*/
|
|
292
|
+
restoreSessions(threadIds) {
|
|
293
|
+
this.assertNotDisposed();
|
|
294
|
+
for (const threadId of threadIds) {
|
|
295
|
+
if (!this.sessions.has(threadId)) {
|
|
296
|
+
this.attachSession(threadId);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
282
300
|
attachSession(threadId) {
|
|
283
301
|
const slice = this.router.registerSession(threadId);
|
|
284
302
|
const session = new DefaultChatSession(this.harness, this.agentId, threadId, slice, {
|
|
@@ -4,9 +4,42 @@ import type { ChatStreamResult } from '../types/events.js';
|
|
|
4
4
|
import type { Message } from '../types/messages.js';
|
|
5
5
|
import type { TelemetryEventCallback } from '../types/telemetry-events.js';
|
|
6
6
|
import type { ToolResultInfo } from '../types/tools.js';
|
|
7
|
-
import type { HarnessAgentConfig, StreamOptions } from './harness-config.js';
|
|
7
|
+
import type { AgentConfig, HarnessAgentConfig, StreamOptions } from './harness-config.js';
|
|
8
8
|
import type { LLMGatewayClient } from '@salesforce/llm-gateway-sdk';
|
|
9
9
|
export declare const SUPPORTED_PROTOCOL_VERSIONS: readonly [1];
|
|
10
|
+
/**
|
|
11
|
+
* Opt-in helper that brands a harness type with the {@link AgentConfig}
|
|
12
|
+
* subtype it expects. Harness authors wrap their branded subtype with
|
|
13
|
+
* this helper when they want consumers to get the harness-specific
|
|
14
|
+
* config shape inferred at the `createAgent` call site:
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* type MastraAgentHarness = WithAgentConfig<
|
|
18
|
+
* AgentHarness & {
|
|
19
|
+
* readonly harnessId: 'mastra';
|
|
20
|
+
* readonly extensions: { mastra: { ... } };
|
|
21
|
+
* },
|
|
22
|
+
* MastraAgentConfig
|
|
23
|
+
* >;
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* The helper attaches an optional, type-only `__agentConfig` slot that
|
|
27
|
+
* never exists at runtime. {@link ConfigOf} reads it. Harnesses that
|
|
28
|
+
* don't need extra config fields don't use the helper, and the base
|
|
29
|
+
* `AgentHarness` interface stays free of the phantom.
|
|
30
|
+
*/
|
|
31
|
+
export type WithAgentConfig<H extends AgentHarness, C extends AgentConfig> = H & {
|
|
32
|
+
readonly __agentConfig?: C;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Resolves the {@link AgentConfig} subtype declared by a harness via the
|
|
36
|
+
* {@link WithAgentConfig} helper. Falls back to `AgentConfig` when the
|
|
37
|
+
* harness wasn't branded. Used by `AgentManager.createAgent` so consumers
|
|
38
|
+
* don't have to pass `<MastraAgentConfig>` at the call site.
|
|
39
|
+
*/
|
|
40
|
+
export type ConfigOf<H extends AgentHarness> = H extends {
|
|
41
|
+
readonly __agentConfig?: infer C;
|
|
42
|
+
} ? unknown extends C ? AgentConfig : C : AgentConfig;
|
|
10
43
|
/**
|
|
11
44
|
* Harness-agnostic interface abstracting the agentic runtime.
|
|
12
45
|
*
|
|
@@ -25,6 +58,17 @@ export interface AgentHarness {
|
|
|
25
58
|
readonly harnessId: string;
|
|
26
59
|
/** Version of the SDK-to-harness protocol implemented by this harness. */
|
|
27
60
|
readonly protocolVersion: number;
|
|
61
|
+
/**
|
|
62
|
+
* Harness-specific extensions namespace.
|
|
63
|
+
*
|
|
64
|
+
* Concrete harnesses narrow this to a typed shape (e.g.
|
|
65
|
+
* `MastraAgentHarness` declares `extensions.mastra.getToolSearchProcessor`).
|
|
66
|
+
* The SDK core never reads or interprets this object; it is a passthrough
|
|
67
|
+
* surface owned by the harness package. Consumers reach harness-specific
|
|
68
|
+
* features through `manager.extensions` (typed off the harness subtype);
|
|
69
|
+
* per-agent accessors take the agent id as their first argument.
|
|
70
|
+
*/
|
|
71
|
+
readonly extensions: Record<string, unknown>;
|
|
28
72
|
/**
|
|
29
73
|
* Shut down the harness gracefully, releasing all resources.
|
|
30
74
|
* Disconnects MCP servers, closes storage connections, and
|
|
@@ -73,6 +73,16 @@ export type HarnessAgentConfig = Omit<AgentConfig, 'orgAlias'> & {
|
|
|
73
73
|
* Strips `orgAlias` since org resolution is handled above the harness and attaches
|
|
74
74
|
* the resolved `orgJwt` so harness implementations can use {@link resolveMcpServerHeaders}
|
|
75
75
|
* to inject auth for Salesforce Hosted MCP Servers.
|
|
76
|
+
*
|
|
77
|
+
* **Contract — extra fields survive the round-trip.** Harness packages that brand
|
|
78
|
+
* their harness type via {@link WithAgentConfig} extend `AgentConfig` with extra
|
|
79
|
+
* fields (e.g. `MastraAgentConfig.toolSearch`). Those fields are not declared on
|
|
80
|
+
* `AgentConfig` or `HarnessAgentConfig`; they ride through the spread below and the
|
|
81
|
+
* harness reads them at its own boundary via runtime narrowing
|
|
82
|
+
* (`(config as { toolSearch? }).toolSearch`). The spread-pass-through is part of
|
|
83
|
+
* the contract — refactoring to `pick` known fields would silently drop those
|
|
84
|
+
* fields and break harness extensibility. There is a regression test in
|
|
85
|
+
* `test/harness/harness-config.test.ts` that asserts unknown fields survive.
|
|
76
86
|
*/
|
|
77
87
|
export declare function toHarnessConfig(config: AgentConfig, orgJwt?: JSONWebToken): HarnessAgentConfig;
|
|
78
88
|
/**
|
|
@@ -9,6 +9,16 @@
|
|
|
9
9
|
* Strips `orgAlias` since org resolution is handled above the harness and attaches
|
|
10
10
|
* the resolved `orgJwt` so harness implementations can use {@link resolveMcpServerHeaders}
|
|
11
11
|
* to inject auth for Salesforce Hosted MCP Servers.
|
|
12
|
+
*
|
|
13
|
+
* **Contract — extra fields survive the round-trip.** Harness packages that brand
|
|
14
|
+
* their harness type via {@link WithAgentConfig} extend `AgentConfig` with extra
|
|
15
|
+
* fields (e.g. `MastraAgentConfig.toolSearch`). Those fields are not declared on
|
|
16
|
+
* `AgentConfig` or `HarnessAgentConfig`; they ride through the spread below and the
|
|
17
|
+
* harness reads them at its own boundary via runtime narrowing
|
|
18
|
+
* (`(config as { toolSearch? }).toolSearch`). The spread-pass-through is part of
|
|
19
|
+
* the contract — refactoring to `pick` known fields would silently drop those
|
|
20
|
+
* fields and break harness extensibility. There is a regression test in
|
|
21
|
+
* `test/harness/harness-config.test.ts` that asserts unknown fields survive.
|
|
12
22
|
*/
|
|
13
23
|
export function toHarnessConfig(config, orgJwt) {
|
|
14
24
|
const { orgAlias: _, ...rest } = config;
|
|
@@ -12,10 +12,10 @@ import type { AgentHarness } from './agent-harness.js';
|
|
|
12
12
|
* guards against a factory that lies about its version or a harness package
|
|
13
13
|
* whose runtime drifts from its packaging metadata.
|
|
14
14
|
*/
|
|
15
|
-
export interface HarnessFactory {
|
|
15
|
+
export interface HarnessFactory<H extends AgentHarness = AgentHarness> {
|
|
16
16
|
/** Unique identifier for the harness type this factory builds (e.g., `'mastra'`). */
|
|
17
17
|
readonly harnessId: string;
|
|
18
18
|
/** SDK-to-harness protocol version implemented by the harness this factory builds. */
|
|
19
19
|
readonly protocolVersion: number;
|
|
20
|
-
create(storageRootFolder: string): Promise<
|
|
20
|
+
create(storageRootFolder: string): Promise<H>;
|
|
21
21
|
}
|
package/dist/harness/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { AgentHarness } from './agent-harness.js';
|
|
1
|
+
export type { AgentHarness, WithAgentConfig, ConfigOf } from './agent-harness.js';
|
|
2
2
|
export type { HarnessFactory } from './harness-factory.js';
|
|
3
3
|
export type { AgentConfig, StreamOptions } from './harness-config.js';
|
|
4
4
|
export { toHarnessConfig, DEFAULT_MAX_STEPS } from './harness-config.js';
|
package/dist/index.d.ts
CHANGED
|
@@ -4,15 +4,15 @@ export type { ToolDefinition, ToolCallInfo, ToolResultInfo } from './types/tools
|
|
|
4
4
|
export type { FinishReason, UsageMetadata } from './types/usage.js';
|
|
5
5
|
export type { AgentConfig, HarnessAgentConfig, StreamOptions } from './harness/harness-config.js';
|
|
6
6
|
export { DEFAULT_MAX_STEPS } from './harness/harness-config.js';
|
|
7
|
-
export type { MCPConfiguration, MCPServerConfig, MCPStdioServerConfig, MCPRemoteServerConfig, McpServerInfo, } from './mcp-config.js';
|
|
7
|
+
export type { MCPConfiguration, MCPServerConfig, MCPStdioServerConfig, MCPRemoteServerConfig, McpServerInfo, McpToolInfo, McpToolAnnotations, } from './mcp-config.js';
|
|
8
8
|
export { McpServerStatus } from './mcp-config.js';
|
|
9
9
|
export { ModelName } from '@salesforce/llm-gateway-sdk';
|
|
10
10
|
export { SfApiEnv } from '@salesforce/agentic-common';
|
|
11
|
-
export { AgentManager, createAgentManager } from './agent-manager.js';
|
|
11
|
+
export { type AgentManager, type RestoreFailure, createAgentManager } from './agent-manager.js';
|
|
12
12
|
export { type Agent } from './agent.js';
|
|
13
13
|
export { type ChatSession, type ChatOptions } from './chat-session.js';
|
|
14
14
|
export type { AgentConnectivityResolver, ResolvedConnectivity } from './agent-connectivity-resolver.js';
|
|
15
|
-
export type { AgentHarness, HarnessFactory } from './harness/index.js';
|
|
15
|
+
export type { AgentHarness, HarnessFactory, WithAgentConfig, ConfigOf } from './harness/index.js';
|
|
16
16
|
export { SUPPORTED_PROTOCOL_VERSIONS } from './harness/agent-harness.js';
|
|
17
17
|
export { HarnessBusOwner } from './harness/harness-bus-owner.js';
|
|
18
18
|
export { AgentSDKError, AgentSDKErrorType } from './errors.js';
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ export { McpServerStatus } from './mcp-config.js';
|
|
|
7
7
|
export { ModelName } from '@salesforce/llm-gateway-sdk';
|
|
8
8
|
export { SfApiEnv } from '@salesforce/agentic-common';
|
|
9
9
|
// ── Agent Layer ─────────────────────────────────────────────────────
|
|
10
|
-
export {
|
|
10
|
+
export { createAgentManager } from './agent-manager.js';
|
|
11
11
|
export {} from './agent.js';
|
|
12
12
|
export {} from './chat-session.js';
|
|
13
13
|
export { SUPPORTED_PROTOCOL_VERSIONS } from './harness/agent-harness.js';
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type LogBus } from '@salesforce/agentic-common';
|
|
2
|
+
import type { AgentConfig } from '../harness/harness-config.js';
|
|
3
|
+
export type AgentIdentityRecord = {
|
|
4
|
+
agentId: string;
|
|
5
|
+
projectRoot: string;
|
|
6
|
+
config: AgentConfig;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* SDK-owned persistence for the agent-identity triple
|
|
10
|
+
* `{ agentId, projectRoot, AgentConfig }`. Stored as one JSON file per agent
|
|
11
|
+
* under `${storageRootFolder}/agents/`. Internal to the SDK; not exported.
|
|
12
|
+
*
|
|
13
|
+
* Each record carries the current harness's `harnessId`. On `list()`, records
|
|
14
|
+
* whose `harnessId` does not match the current harness are skipped with a
|
|
15
|
+
* `LogBus.warn` — restoring an agent into the wrong harness is never the
|
|
16
|
+
* right answer.
|
|
17
|
+
*/
|
|
18
|
+
export declare class AgentIdentityStore {
|
|
19
|
+
private readonly storageRootFolder;
|
|
20
|
+
private readonly harnessId;
|
|
21
|
+
private readonly logBus;
|
|
22
|
+
/**
|
|
23
|
+
* Per-agentId queue of in-flight writes. Concurrent `write()` calls for the same agentId
|
|
24
|
+
* chain onto the previous promise so the `writeFile` + `rename` pair runs sequentially.
|
|
25
|
+
*
|
|
26
|
+
* Why this matters: POSIX `rename` atomically overwrites an existing target, but Windows
|
|
27
|
+
* `rename` returns `EPERM` when any handle is open on the target — including a sibling
|
|
28
|
+
* concurrent rename from the same process. Three concurrent writers calling
|
|
29
|
+
* `rename(tmp, 'a.json')` succeed on Linux/macOS and fail on Windows. Serializing per
|
|
30
|
+
* agentId eliminates the race entirely (and removes any need for per-call temp suffixes
|
|
31
|
+
* because at most one write is touching the temp path at a time). Last-writer-wins
|
|
32
|
+
* semantics are preserved.
|
|
33
|
+
*/
|
|
34
|
+
private readonly inflightWrites;
|
|
35
|
+
constructor(storageRootFolder: string, harnessId: string, logBus: LogBus);
|
|
36
|
+
write(agentId: string, projectRoot: string, config: AgentConfig): Promise<void>;
|
|
37
|
+
private writeImmediate;
|
|
38
|
+
remove(agentId: string): Promise<void>;
|
|
39
|
+
list(): Promise<AgentIdentityRecord[]>;
|
|
40
|
+
private dir;
|
|
41
|
+
}
|