@salesforce/sfdx-agent-sdk 0.5.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 CHANGED
@@ -58,7 +58,7 @@ await manager.shutdown();
58
58
 
59
59
  ## API Reference
60
60
 
61
- ### `createAgentManager(storageRootFolder, harnessFactory, connectivityResolver?): Promise<AgentManager>`
61
+ ### `createAgentManager<F>(storageRootFolder, harnessFactory, connectivityResolver?): Promise<AgentManager<H>>`
62
62
 
63
63
  Factory function that creates an `AgentManager` backed by the provided `HarnessFactory`. The `storageRootFolder` must be
64
64
  an existing directory and is used for persistent state (the harness's runtime data plus the SDK's per-agent identity
@@ -67,26 +67,46 @@ protocol version, replays any persisted agents the harness can still serve, and
67
67
  `connectivityResolver` overrides the default sf-CLI-based org resolution — used by e2e tests and custom-auth
68
68
  deployments; production callers leave it unset.
69
69
 
70
+ The harness type `H` is **inferred from the factory's `create()` return type**, so consumers don't pass an explicit type
71
+ argument:
72
+
73
+ ```typescript
74
+ import { MastraHarnessFactory } from '@salesforce/sfdx-agent-harness-mastra';
75
+
76
+ const manager = await createAgentManager(storageRoot, new MastraHarnessFactory());
77
+ // ^? AgentManager<MastraAgentHarness>
78
+ ```
79
+
80
+ When the factory's `create()` is typed as `Promise<AgentHarness>` (the default), the manager is
81
+ `AgentManager<AgentHarness>` and behaves exactly as before. Harness packages that ship a branded subtype (e.g.
82
+ `MastraAgentHarness`) lift consumers into that subtype automatically — see "Harness Extensibility" below.
83
+
70
84
  Restore failures (a persisted record the SDK could not bring back online — e.g. missing project directory, harness
71
85
  rejection, thread rehydration failure) are queryable on the returned manager via `getRestoreFailures()`. Soft skips
72
86
  inside the persistence directory (corrupt JSON, harness-id mismatch) are silently dropped from the restore pass and emit
73
87
  a `warn` on the SDK's log bus.
74
88
 
75
- ### `AgentManager`
89
+ ### `AgentManager<H extends AgentHarness = AgentHarness>`
76
90
 
77
91
  Top-level orchestrator that owns the harness and manages agent lifecycle. `AgentManager` is an interface; the concrete
78
92
  implementation is internal — `createAgentManager` is the only public entry point.
79
93
 
80
- | Method | Signature | Description |
81
- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
82
- | `createAgent` | `(projectRoot: string, config?: AgentConfig & { agentId?: string }, options?: { abortSignal?: AbortSignal }) => Promise<Agent>` | Create and register a new agent and persist its identity triple. `projectRoot` must be an existing directory. If `agentId` is omitted a UUID is generated. |
83
- | `getAgent` | `(agentId: string) => Agent` | Retrieve a live agent by ID. Throws `AgentSDKError` (`AGENT_NOT_FOUND`) for unknown ids and for ids that are only present in `getRestoreFailures()`. |
84
- | `getAgentIds` | `() => string[]` | List all live agent IDs (successful + successfully restored). Failed-restore agents are not included — query `getRestoreFailures()` separately. |
85
- | `destroyAgent` | `(agentId: string) => Promise<void>` | Destroy an agent, remove its identity record from disk, and clear any matching `getRestoreFailures()` entry. Failed-restore-only ids are accepted (no harness call made). |
86
- | `shutdown` | `() => Promise<void>` | Destroy all live agents and shut down the harness. Identity files survive (that's the whole point) — restart `createAgentManager` over the same root to bring them back. |
87
- | `onTelemetry` | `(callback: TelemetryEventCallback) => Unsubscribe` | Subscribe to telemetry across all managed agents. |
88
- | `onLog` | `(callback: (record: LogRecord) => void) => Unsubscribe` | Subscribe to structured logs across all managed agents. Bridge this into your host logger to observe restore-failure events + soft-skip warnings. |
89
- | `getRestoreFailures` | `() => RestoreFailure[]` | Snapshot of agents the SDK could not restore on this boot. Each entry carries the persisted `{ agentId, projectRoot, config }` plus the underlying error. |
94
+ The optional `H` type parameter (default `AgentHarness`) lets harness-aware consumers reach harness-specific features
95
+ through a typed `manager.extensions` slot. `createAgentManager` infers `H` from the factory; you usually don't write it
96
+ explicitly. The `createAgent` config parameter narrows automatically when the harness brands itself with
97
+ `WithAgentConfig` see "Harness Extensibility" below.
98
+
99
+ | Property / Method | Signature | Description |
100
+ | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
101
+ | `extensions` | `H['extensions']` | Harness-specific extensions namespace (read-only). Re-exposes the harness's `extensions` slot typed off `H`. Per-agent accessors take the agent id as their first argument. The SDK never reads or interprets this — see "Harness Extensibility" below. |
102
+ | `createAgent` | `(projectRoot: string, config?: ConfigOf<H> & { agentId?: string }, options?: { abortSignal?: AbortSignal }) => Promise<Agent<H>>` | Create and register a new agent and persist its identity triple. `projectRoot` must be an existing directory. If `agentId` is omitted a UUID is generated. The config type is inferred from the harness — see `ConfigOf<H>`. |
103
+ | `getAgent` | `(agentId: string) => Agent<H>` | Retrieve a live agent by ID. Throws `AgentSDKError` (`AGENT_NOT_FOUND`) for unknown ids and for ids that are only present in `getRestoreFailures()`. |
104
+ | `getAgentIds` | `() => string[]` | List all live agent IDs (successful + successfully restored). Failed-restore agents are not included — query `getRestoreFailures()` separately. |
105
+ | `destroyAgent` | `(agentId: string) => Promise<void>` | Destroy an agent, remove its identity record from disk, and clear any matching `getRestoreFailures()` entry. Failed-restore-only ids are accepted (no harness call made). |
106
+ | `shutdown` | `() => Promise<void>` | Destroy all live agents and shut down the harness. Identity files survive (that's the whole point) — restart `createAgentManager` over the same root to bring them back. |
107
+ | `onTelemetry` | `(callback: TelemetryEventCallback) => Unsubscribe` | Subscribe to telemetry across all managed agents. |
108
+ | `onLog` | `(callback: (record: LogRecord) => void) => Unsubscribe` | Subscribe to structured logs across all managed agents. Bridge this into your host logger to observe restore-failure events + soft-skip warnings. |
109
+ | `getRestoreFailures` | `() => RestoreFailure[]` | Snapshot of agents the SDK could not restore on this boot. Each entry carries the persisted `{ agentId, projectRoot, config }` plus the underlying error. |
90
110
 
91
111
  #### `RestoreFailure`
92
112
 
@@ -103,9 +123,11 @@ Returned by `AgentManager.getRestoreFailures()`. Use it to seed `error`-state pl
103
123
  **not** iterate it for logging — the SDK already emitted each failure via `onLog` at `error` level during the restore
104
124
  pass, before this function returned.
105
125
 
106
- ### `Agent`
126
+ ### `Agent<H extends AgentHarness = AgentHarness>`
107
127
 
108
- A configured AI agent. Factory for chat sessions.
128
+ A configured AI agent. Factory for chat sessions. The optional `H` type parameter is currently informational on `Agent`
129
+ — harness-specific features are reached through `manager.extensions`, not `agent.extensions`. The default `AgentHarness`
130
+ keeps unparameterized call sites working.
109
131
 
110
132
  | Method | Signature | Description |
111
133
  | ---------------------- | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
@@ -226,9 +248,53 @@ type MCPRemoteServerConfig = {
226
248
  | -------- | ------------------------------------------------------ | --------------------------------------- |
227
249
  | `name` | `string` | Server identifier. |
228
250
  | `status` | `'connected' \| 'connecting' \| 'disabled' \| 'error'` | Connection state. |
229
- | `tools` | `string[]` | Discovered tool names. |
251
+ | `tools` | [`McpToolInfo[]`](#mcptoolinfo) | Discovered tools (name + metadata). |
230
252
  | `error?` | `string` | Error message when status is `'error'`. |
231
253
 
254
+ #### `McpToolInfo`
255
+
256
+ Runtime metadata for a single MCP-discovered tool. Optional fields are populated when the underlying harness can supply
257
+ them from its MCP client; harnesses whose runtime does not expose a given field leave it `undefined`. Consumers must
258
+ treat every field except `name` as optional.
259
+
260
+ | Field | Type | Description |
261
+ | -------------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
262
+ | `name` | `string` | Tool name as exposed to the LLM, including any harness-applied namespacing. |
263
+ | `description?` | `string` | Human-readable description of what the tool does. |
264
+ | `inputSchema?` | `Record<string, unknown>` | Tool input parameters as a [**JSON Schema**](https://json-schema.org/) object (the MCP wire format). |
265
+ | `annotations?` | [`McpToolAnnotations`](#mcptoolannotations) | Behavioral / UI-presentation hints declared by the MCP server. |
266
+
267
+ **`inputSchema` is a JSON Schema object, not a Zod schema.** It is typed as `Record<string, unknown>` so this package
268
+ incurs no `zod` or `@types/json-schema` dependency. If you need a Zod schema at runtime, convert with a library such as
269
+ [`json-schema-to-zod`](https://www.npmjs.com/package/json-schema-to-zod); for runtime validation, feed the schema to a
270
+ JSON Schema validator such as [AJV](https://ajv.js.org/).
271
+
272
+ The exact set of keys present on `inputSchema` depends on the harness's normalization step. The Mastra harness, for
273
+ example, reaches consumers after passing through `@mastra/schema-compat`'s converter, which adds a `$schema` annotation
274
+ (typically `http://json-schema.org/draft-07/schema#`) and `additionalProperties: false` even when the source MCP server
275
+ did not declare them. Both are valid JSON Schema annotations and are forwarded untouched.
276
+
277
+ **Why no `outputSchema` field?** The MCP protocol carries an optional `outputSchema` per tool, but neither shipped
278
+ harness can supply it: Mastra's `@mastra/mcp` strips it from each wrapped tool before the harness sees it (to keep
279
+ `CallToolResult` validation correct), and the Claude Agent SDK's MCP status surface omits the field entirely. We
280
+ deliberately keep it off the SDK contract rather than ship an always-`undefined` field consumers would have to ignore;
281
+ adding it later if a harness gains the data is non-breaking.
282
+
283
+ #### `McpToolAnnotations`
284
+
285
+ Mirrors the
286
+ [MCP protocol's `Tool.annotations` shape](https://spec.modelcontextprotocol.io/specification/server/tools/#tool-annotations).
287
+ Each field is optional because MCP servers populate annotations à la carte; absence means "the server did not declare
288
+ this hint," not "false."
289
+
290
+ | Field | Type | Description |
291
+ | ------------------ | --------- | ------------------------------------------------------------------------------ |
292
+ | `title?` | `string` | Human-readable label suitable for UI display (vs. the machine `name`). |
293
+ | `readOnlyHint?` | `boolean` | When `true`, the tool only reads data and has no side effects. |
294
+ | `destructiveHint?` | `boolean` | When `true`, the tool may perform destructive updates to its environment. |
295
+ | `idempotentHint?` | `boolean` | When `true`, repeated calls with the same arguments have no additional effect. |
296
+ | `openWorldHint?` | `boolean` | When `true`, the tool may interact with an open world of external entities. |
297
+
232
298
  ### Tool Types
233
299
 
234
300
  ```typescript
@@ -338,7 +404,14 @@ const agent = await manager.createAgent('/project', {
338
404
  // Poll until connected
339
405
  const servers = agent.getMcpServerInfo();
340
406
  for (const s of servers) {
341
- console.log(`${s.name}: ${s.status}, tools: ${s.tools.join(', ')}`);
407
+ const toolNames = s.tools.map((t) => t.name).join(', ');
408
+ console.log(`${s.name}: ${s.status}, tools: ${toolNames}`);
409
+
410
+ // Each tool also carries description / inputSchema / annotations when available.
411
+ for (const tool of s.tools) {
412
+ if (tool.description) console.log(` ${tool.name} — ${tool.description}`);
413
+ // tool.inputSchema is a JSON Schema object; convert with json-schema-to-zod if you need Zod.
414
+ }
342
415
  }
343
416
  ```
344
417
 
@@ -419,6 +492,84 @@ Returns `true` if the URL matches a Salesforce Hosted MCP Server endpoint (prod,
419
492
  The SDK exposes typed `TelemetryEvent` / `LogRecord` streams at the manager, agent, and session scopes. See
420
493
  [Telemetry & Logs](#telemetry--logs) below for the event catalog, emit contract, and structured-log table.
421
494
 
495
+ ### Harness Extensibility
496
+
497
+ The `AgentHarness` contract is the SDK's lowest-common-denominator surface — every harness implements it, and the SDK
498
+ consumes only that. Features a single harness has but the contract can't generalize over (Mastra workflows, Mastra's
499
+ `ToolSearchProcessor`, structured working memory, etc.) live on a typed `extensions` slot the harness package owns. The
500
+ SDK never reads or interprets `extensions`; it just threads the type through.
501
+
502
+ For most consumers, the inference machinery means **you never write the type parameters**:
503
+
504
+ ```typescript
505
+ import { createAgentManager } from '@salesforce/sfdx-agent-sdk';
506
+ import { MastraHarnessFactory } from '@salesforce/sfdx-agent-harness-mastra';
507
+
508
+ const manager = await createAgentManager(storageRoot, new MastraHarnessFactory());
509
+ // ^? AgentManager<MastraAgentHarness> (inferred from the factory)
510
+
511
+ const agent = await manager.createAgent(projectRoot, {
512
+ instructions: '...',
513
+ toolSearch: { pool: '*' }, // ← Mastra-specific config; autocompletes via WithAgentConfig
514
+ });
515
+
516
+ const processor = manager.extensions.mastra.getToolSearchProcessor(agent.getId());
517
+ // ^? ToolSearchHandle | undefined
518
+ ```
519
+
520
+ Per-agent accessors (like `getToolSearchProcessor` above) take the agent id as their first argument; orchestrator-level
521
+ accessors (e.g. workflow registries) don't. Everything lives on `manager.extensions` regardless of scope — the SDK chose
522
+ the harness-scoped shape for simplicity over a per-feature scope split. See `ARCHITECTURE.md` → "Harness Extensibility"
523
+ for the rationale.
524
+
525
+ #### `AgentHarness.extensions: Record<string, unknown>`
526
+
527
+ Passthrough slot on the harness contract. Concrete harnesses narrow it on their branded subtype (e.g.
528
+ `MastraAgentHarness` declares `extensions: { mastra: { ... } }`). The SDK never reads this; it surfaces it as
529
+ `AgentManager.extensions` typed off the harness type.
530
+
531
+ #### `WithAgentConfig<H extends AgentHarness, C extends AgentConfig>`
532
+
533
+ Opt-in helper for harnesses that accept extra `AgentConfig` fields. Wrap your branded harness type with this helper and
534
+ `AgentManager.createAgent`'s config parameter narrows to `C` automatically:
535
+
536
+ ```typescript
537
+ import type { AgentHarness, WithAgentConfig, AgentConfig } from '@salesforce/sfdx-agent-sdk';
538
+
539
+ type MyAgentConfig = AgentConfig & { custom?: { ... } };
540
+
541
+ type MyAgentHarness = WithAgentConfig<
542
+ AgentHarness & {
543
+ readonly harnessId: 'my-harness';
544
+ readonly extensions: { 'my-harness': { ... } };
545
+ },
546
+ MyAgentConfig
547
+ >;
548
+ ```
549
+
550
+ The brand is type-only; it never exists at runtime. Harnesses that don't accept extra config fields leave
551
+ `WithAgentConfig` unused — the base `AgentHarness` interface stays untouched.
552
+
553
+ #### `ConfigOf<H extends AgentHarness>`
554
+
555
+ Resolves the `AgentConfig` subtype declared by a harness via `WithAgentConfig`. Falls back to plain `AgentConfig` when
556
+ the harness wasn't branded. Used internally by `AgentManager.createAgent`'s parameter type; harness-package authors
557
+ don't usually reference it directly.
558
+
559
+ #### Decision rule: cross-harness contract vs. extension
560
+
561
+ When deciding where a new harness-package feature should live:
562
+
563
+ | Question | Answer → Where it goes |
564
+ | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
565
+ | Could a second harness implement this with similar semantics? | Likely cross-harness contract |
566
+ | Is this an operational handle on a harness-internal runtime object (e.g. processor, workspace)? | Extension namespace |
567
+ | Does this require harness-specific config that doesn't generalize? | Extension (extra fields on a `WithAgentConfig`-branded type) |
568
+ | Is the polyfill cheap and faithful across harnesses? | Cross-harness contract with a polyfilled default |
569
+
570
+ Extensions are the default for "Mastra has a thing nothing else has." See `ARCHITECTURE.md` → "Harness Extensibility"
571
+ for the full design including the `WithAgentConfig` rationale.
572
+
422
573
  ## Writing a Harness
423
574
 
424
575
  The SDK ships no harness implementation. Consumers pick one by passing a `HarnessFactory` instance to
@@ -430,7 +581,7 @@ Harness authors implement two interfaces and can compose one helper class, all e
430
581
 
431
582
  | Export | Role |
432
583
  | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
433
- | `HarnessFactory` | Construct an `AgentHarness` bound to a storage root. Declares `harnessId` and `protocolVersion`. |
584
+ | `HarnessFactory<H>` | Construct a harness of type `H` bound to a storage root. Declares `harnessId` and `protocolVersion`. Default `H = AgentHarness`. |
434
585
  | `AgentHarness` | Runtime contract: agent / thread / stream / tool / message lifecycle. Declares its own `harnessId` and `protocolVersion`. |
435
586
  | `SUPPORTED_PROTOCOL_VERSIONS` | Readonly list of harness protocol versions this SDK accepts. `createAgentManager` checks both the factory and the constructed harness. |
436
587
  | `HarnessBusOwner` | Composition helper owning telemetry + log buses with `dispose()` semantics. Reuse it instead of reimplementing bus plumbing. |
@@ -461,10 +612,10 @@ class MyHarness implements AgentHarness {
461
612
  // implement the remaining AgentHarness methods (createAgent, stream, …)
462
613
  }
463
614
 
464
- export class MyHarnessFactory implements HarnessFactory {
615
+ export class MyHarnessFactory implements HarnessFactory<MyHarness> {
465
616
  readonly harnessId = 'my-harness';
466
617
  readonly protocolVersion = 1;
467
- async create(storageRootFolder: string): Promise<AgentHarness> {
618
+ async create(storageRootFolder: string): Promise<MyHarness> {
468
619
  return new MyHarness(storageRootFolder);
469
620
  }
470
621
  }
@@ -1,5 +1,5 @@
1
1
  import { type Clock, LogBus, type LogRecord, type Unsubscribe, type UniqueIDGenerator } from '@salesforce/agentic-common';
2
- import { type AgentHarness } from './harness/agent-harness.js';
2
+ import { type AgentHarness, type ConfigOf } from './harness/agent-harness.js';
3
3
  import type { HarnessFactory } from './harness/harness-factory.js';
4
4
  import { type AgentConfig } from './harness/harness-config.js';
5
5
  import { type Agent } from './agent.js';
@@ -35,17 +35,23 @@ export type RestoreFailure = {
35
35
  * host logger; restore failures are emitted at `error` level and soft skips
36
36
  * (corrupt JSON, harness-id mismatch) at `warn`.
37
37
  */
38
- export interface AgentManager {
38
+ export interface AgentManager<H extends AgentHarness = AgentHarness> {
39
39
  /**
40
40
  * Creates a new {@link Agent} with the given configuration and persists
41
41
  * its identity triple `{ agentId, projectRoot, config }` so it can be
42
42
  * restored on the next boot.
43
43
  *
44
+ * The config type is inferred from the harness type `H`. Harnesses that
45
+ * declare a subtype via `__agentConfig` (e.g. `MastraAgentHarness`
46
+ * declares `MastraAgentConfig`) get their extra fields autocompleted
47
+ * without an explicit generic at the call site. Extra fields thread
48
+ * through opaquely to the harness, which narrows what it cares about.
49
+ *
44
50
  * @throws If `projectRoot` does not exist or is not a directory.
45
51
  * @throws If `config.agentId` is provided and an agent with that ID is
46
52
  * already registered (live or in restore-failure state).
47
53
  */
48
- createAgent(projectRoot: string, config?: AgentConfig & {
54
+ createAgent(projectRoot: string, config?: ConfigOf<H> & {
49
55
  agentId?: string;
50
56
  }, options?: {
51
57
  abortSignal?: AbortSignal;
@@ -78,6 +84,21 @@ export interface AgentManager {
78
84
  destroyAgent(agentId: string): Promise<void>;
79
85
  /** Shuts down the harness and destroys all live agents. */
80
86
  shutdown(): Promise<void>;
87
+ /**
88
+ * Harness-specific extensions namespace, typed off the harness subtype `H`.
89
+ *
90
+ * For the default `AgentHarness`, this is the opaque `Record<string, unknown>`
91
+ * declared on the contract — no IDE help. Consumers parameterize the manager
92
+ * over a harness-specific subtype (e.g. `MastraAgentHarness`) to get a typed
93
+ * slot like `manager.extensions.mastra.getToolSearchProcessor(agentId)`.
94
+ *
95
+ * Per-agent extension accessors take the agent id as their first argument;
96
+ * orchestrator-level accessors (e.g. workflow registries) don't.
97
+ *
98
+ * The SDK never reads or interprets this object; it is a passthrough surface
99
+ * owned by the harness package.
100
+ */
101
+ readonly extensions: H['extensions'];
81
102
  /** Subscribe to telemetry events across every managed agent. */
82
103
  onTelemetry(callback: TelemetryEventCallback): Unsubscribe;
83
104
  /** Subscribe to structured log records across every managed agent. */
@@ -99,7 +120,7 @@ export interface AgentManager {
99
120
  * pattern mirrors {@link Workspace.create}: a private constructor for sync
100
121
  * field assignment, then a static async builder that calls `init()`.
101
122
  */
102
- export declare class DefaultAgentManager implements AgentManager {
123
+ export declare class DefaultAgentManager<H extends AgentHarness = AgentHarness> implements AgentManager<H> {
103
124
  private readonly harness;
104
125
  private readonly agentIdGenerator;
105
126
  private readonly agentConnectivityResolver;
@@ -123,10 +144,10 @@ export declare class DefaultAgentManager implements AgentManager {
123
144
  * is private, so this is the only way to obtain an instance, but
124
145
  * consumers should always go through {@link createAgentManager}.
125
146
  */
126
- static __build(harness: AgentHarness, agentConnectivityResolver: AgentConnectivityResolver, storageRootFolder: string, agentIdGenerator: UniqueIDGenerator, clock: Clock, logBus: LogBus): Promise<DefaultAgentManager>;
147
+ static __build<H extends AgentHarness>(harness: H, agentConnectivityResolver: AgentConnectivityResolver, storageRootFolder: string, agentIdGenerator: UniqueIDGenerator, clock: Clock, logBus: LogBus): Promise<DefaultAgentManager<H>>;
127
148
  private init;
128
149
  shutdown(): Promise<void>;
129
- createAgent(projectRoot: string, config?: AgentConfig & {
150
+ createAgent(projectRoot: string, config?: ConfigOf<H> & {
130
151
  agentId?: string;
131
152
  }, options?: {
132
153
  abortSignal?: AbortSignal;
@@ -150,6 +171,15 @@ export declare class DefaultAgentManager implements AgentManager {
150
171
  onTelemetry(callback: TelemetryEventCallback): Unsubscribe;
151
172
  onLog(callback: (record: LogRecord) => void): Unsubscribe;
152
173
  getRestoreFailures(): RestoreFailure[];
174
+ /**
175
+ * Re-exposes `harness.extensions` typed off `H`. Read-only; the SDK never
176
+ * leaks the harness reference itself, only its declared extensions surface.
177
+ * `assertNotDisposed` is intentionally NOT called here — the extensions
178
+ * object is reachable on the harness, and reading it after shutdown is the
179
+ * harness's prerogative to handle (its own disposal mechanics may have
180
+ * already torn down the underlying state).
181
+ */
182
+ get extensions(): H['extensions'];
153
183
  private assertNotDisposed;
154
184
  }
155
185
  /**
@@ -169,4 +199,4 @@ export declare class DefaultAgentManager implements AgentManager {
169
199
  * {@link SUPPORTED_PROTOCOL_VERSIONS}, or when the harness's reported
170
200
  * version disagrees with the factory's.
171
201
  */
172
- export declare function createAgentManager(storageRootFolder: string, harnessFactory: HarnessFactory, connectivityResolver?: AgentConnectivityResolver): Promise<AgentManager>;
202
+ export declare function createAgentManager<H extends AgentHarness = AgentHarness>(storageRootFolder: string, harnessFactory: HarnessFactory<H>, connectivityResolver?: AgentConnectivityResolver): Promise<AgentManager<H>>;
@@ -235,6 +235,20 @@ export class DefaultAgentManager {
235
235
  this.assertNotDisposed();
236
236
  return [...this.restoreFailures];
237
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
+ }
238
252
  assertNotDisposed() {
239
253
  if (this.disposed) {
240
254
  throw new AgentSDKError('AgentManager has been shut down.', AgentSDKErrorType.DISPOSED);
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. */
@@ -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,7 +4,7 @@ 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';
@@ -12,7 +12,7 @@ export { type AgentManager, type RestoreFailure, createAgentManager } from './ag
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';
@@ -43,10 +43,73 @@ export declare enum McpServerStatus {
43
43
  Disabled = "disabled",
44
44
  Error = "error"
45
45
  }
46
+ /**
47
+ * Behavioral / UI-presentation hints for an MCP-discovered tool.
48
+ *
49
+ * Mirrors the MCP protocol's `Tool.annotations` shape
50
+ * (https://spec.modelcontextprotocol.io/specification/server/tools/#tool-annotations)
51
+ * without importing `@modelcontextprotocol/sdk`, keeping this package
52
+ * harness-runtime-free. Each field is optional because MCP servers populate
53
+ * annotations à la carte; absence means "the server did not declare this
54
+ * hint," not "false."
55
+ */
56
+ export type McpToolAnnotations = {
57
+ /** Human-readable label suitable for UI display (vs. the machine `name`). */
58
+ title?: string;
59
+ /** When `true`, the tool only reads data and has no side effects. */
60
+ readOnlyHint?: boolean;
61
+ /** When `true`, the tool may perform destructive updates to its environment. */
62
+ destructiveHint?: boolean;
63
+ /** When `true`, repeated calls with the same arguments have no additional effect. */
64
+ idempotentHint?: boolean;
65
+ /** When `true`, the tool may interact with an open world of external entities (e.g. the public web). */
66
+ openWorldHint?: boolean;
67
+ };
68
+ /**
69
+ * Runtime metadata for a single MCP-discovered tool.
70
+ *
71
+ * The optional fields are populated when the underlying harness can supply
72
+ * them from its MCP client. A harness whose MCP runtime does not expose a
73
+ * given field leaves it `undefined` — consumers must treat every field
74
+ * except `name` as optional.
75
+ *
76
+ * **Why no `outputSchema` field?** The MCP protocol's `tools/list` response
77
+ * carries an optional `outputSchema` per tool, and a maximally honest mirror
78
+ * of that protocol shape would expose it here. We deliberately do not, because
79
+ * neither harness today can populate it: Mastra's `@mastra/mcp` strips
80
+ * `outputSchema` from each wrapped tool before the harness sees it (a
81
+ * deliberate choice in Mastra's MCP client to keep `CallToolResult`
82
+ * validation correct — passing the schema to `createTool` would cause Zod
83
+ * to strip unrecognized keys from the envelope), and the Claude Agent SDK's
84
+ * MCP status surface omits the field entirely. Adding `outputSchema?` to the
85
+ * SDK contract today would mean shipping a field no harness fills — exactly
86
+ * the "field a consumer should ignore" anti-pattern called out in this
87
+ * package's design principles. If a future harness gains access to
88
+ * `outputSchema` (or one of the existing harnesses adds it), expanding the
89
+ * contract is a non-breaking additive change at that point.
90
+ */
91
+ export type McpToolInfo = {
92
+ /** Tool name as exposed to the LLM, including any harness-applied namespacing. */
93
+ name: string;
94
+ /** Human-readable description of what the tool does. */
95
+ description?: string;
96
+ /**
97
+ * Tool input parameters as a **JSON Schema** object (the MCP wire format).
98
+ *
99
+ * This is a plain JSON Schema, not a Zod schema. Consumers that want a
100
+ * Zod schema at runtime can convert with a library such as
101
+ * `json-schema-to-zod`; consumers that want runtime validation can feed
102
+ * it to AJV. Typed as `Record<string, unknown>` so this package incurs
103
+ * no `zod` or `@types/json-schema` dependency.
104
+ */
105
+ inputSchema?: Record<string, unknown>;
106
+ /** Behavioral / UI-presentation hints declared by the MCP server. */
107
+ annotations?: McpToolAnnotations;
108
+ };
46
109
  /** Runtime status of a configured MCP server, including its discovered tools. */
47
110
  export type McpServerInfo = {
48
111
  name: string;
49
112
  status: McpServerStatus;
50
- tools: string[];
113
+ tools: McpToolInfo[];
51
114
  error?: string;
52
115
  };
@@ -16,7 +16,9 @@ export type UsageMetadata = {
16
16
  };
17
17
  /**
18
18
  * Reason the model stopped generating.
19
- * Aligned with AI SDK `LanguageModelV2FinishReason`.
19
+ * Aligned with AI SDK V3's unified finish-reason set; harnesses normalize provider-specific
20
+ * shapes (e.g. V3's `LanguageModelV3FinishReason` object with `{ unified, raw }`) down to this
21
+ * union so SDK consumers see a stable string regardless of the underlying AI SDK version.
20
22
  *
21
23
  * - `stop` — The model finished generating naturally (complete response).
22
24
  * - `length` — The model hit the maximum output token limit; the response was truncated mid-generation.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/sfdx-agent-sdk",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Harness-agnostic agentic infrastructure for Salesforce developer experience tooling",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -35,28 +35,28 @@
35
35
  "LICENSE.txt"
36
36
  ],
37
37
  "dependencies": {
38
- "@salesforce/agentic-common": "0.3.0",
39
- "@salesforce/llm-gateway-sdk": "0.3.0"
38
+ "@salesforce/agentic-common": "0.4.0",
39
+ "@salesforce/llm-gateway-sdk": "0.4.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@eslint/js": "^10.0.1",
43
- "@salesforce/sfdx-agent-harness-mastra": "0.5.0",
43
+ "@salesforce/sfdx-agent-harness-mastra": "0.6.0",
44
44
  "@types/node": "^22.19.17",
45
- "@vitest/coverage-istanbul": "^4.1.5",
46
- "@vitest/eslint-plugin": "^1.6.16",
47
- "eslint": "^10.2.1",
45
+ "@vitest/coverage-istanbul": "^4.1.7",
46
+ "@vitest/eslint-plugin": "^1.6.17",
47
+ "eslint": "^10.4.0",
48
48
  "eslint-config-prettier": "^10.1.8",
49
49
  "eslint-import-resolver-typescript": "^4.4.4",
50
50
  "eslint-plugin-import": "^2.32.0",
51
51
  "eslint-plugin-n": "^18.0.1",
52
52
  "globals": "^17.6.0",
53
- "lint-staged": "^17.0.4",
53
+ "lint-staged": "^17.0.5",
54
54
  "prettier": "^3.8.3",
55
55
  "rimraf": "^6.1.3",
56
- "tsx": "^4.21.0",
56
+ "tsx": "^4.22.3",
57
57
  "typescript": "^6.0.3",
58
- "typescript-eslint": "^8.59.1",
59
- "vitest": "^4.1.5"
58
+ "typescript-eslint": "^8.59.4",
59
+ "vitest": "^4.1.7"
60
60
  },
61
61
  "engines": {
62
62
  "node": ">=22.19.0"