@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.
@@ -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
- * Manages the lifecycle of {@link Agent} instances.
16
+ * Concrete implementation of {@link AgentManager}. **Not exported** from
17
+ * `src/index.ts` — public construction goes through {@link createAgentManager}.
16
18
  *
17
- * The manager owns an {@link AgentHarness} and coordinates initialization,
18
- * agent creation, and graceful shutdown. The harness implementation is
19
- * abstracted away from downstream code (agents, sessions).
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 AgentManager {
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
- * Shuts down the harness and destroys all managed agents.
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
- * @requirements
70
- * - MUST iterate through all agents in the internal `agents` map and call `agent.destroy()` on each.
71
- * - MUST clear the internal `agents` map.
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(resolvedProjectRoot);
149
+ dirStat = await stat(projectRoot);
118
150
  }
119
151
  catch {
120
- throw new Error(`projectRoot does not exist: "${resolvedProjectRoot}"`);
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: "${resolvedProjectRoot}"`);
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(resolvedProjectRoot, agentConfig);
131
- await this.harness.createAgent(agentId, resolvedProjectRoot, runtime.llmGatewayClient, toHarnessConfig(agentConfig, runtime.orgJwt), options);
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, resolvedProjectRoot, agentConfig, runtime.llmGatewayClient, runtime.orgConnection, runtime.orgJwt, this.agentConnectivityResolver, this.router, agentSlice, { telemetry: this.telemetryBus, log: this.logBus }, this.clock, this.agentIdGenerator);
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: resolvedProjectRoot,
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
- * Returns the IDs of all currently managed agents.
146
- *
147
- * @requirements
148
- * - MUST return an array of all string keys (agent IDs) currently tracked in the internal `agents` map.
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
- if (!agent) {
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
- await agent.destroy();
188
- this.router.unregisterAgent(agentId);
189
- this.agents.delete(agentId);
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
- * Creates an {@link AgentManager} using the provided harness factory.
209
- *
210
- * Use this function in production code. It validates the `storageRootFolder`,
211
- * verifies the factory advertises a supported protocol version (failing fast
212
- * before any expensive harness construction), constructs the harness
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
- * @param storageRootFolder - Existing directory used for agent persistence.
217
- * @param harnessFactory - Factory that constructs the harness implementation.
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 new AgentManager(harness, agentConnectivityResolver, new UUIDGenerator());
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<AgentHarness>;
20
+ create(storageRootFolder: string): Promise<H>;
21
21
  }
@@ -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 { AgentManager, createAgentManager } from './agent-manager.js';
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
+ }