@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 CHANGED
@@ -6,16 +6,32 @@ without coupling consumer code to a specific AI framework.
6
6
 
7
7
  ## Quick Start
8
8
 
9
- > **Closed source.** This package is published to npm under the [Salesforce Public Code License](../../LICENSE.txt) and is for use by Salesforce only.
9
+ > **Closed source.** This package is published to npm under the [Salesforce Public Code License](../../LICENSE.txt) and
10
+ > is for use by Salesforce only.
10
11
 
11
12
  ```typescript
12
13
  import { createAgentManager } from '@salesforce/sfdx-agent-sdk';
13
14
  import { MastraHarnessFactory } from '@salesforce/sfdx-agent-harness-mastra';
14
15
 
15
- // 1. Create the manager (validates the storage folder exists)
16
+ // 1. Create the manager. This validates the storage folder, gates the harness's
17
+ // protocol version, and replays any agents the SDK persisted on a prior run
18
+ // (one JSON file per agent under `${storageRootFolder}/agents/`).
16
19
  const manager = await createAgentManager('/path/to/storage', new MastraHarnessFactory());
17
20
 
18
- // 2. Create an agent
21
+ // Bridge SDK logs into your host logger so you observe restore failures + warnings.
22
+ manager.onLog((record) => {
23
+ console[record.level](record.message, record.context);
24
+ });
25
+
26
+ // Boot-time restore failures are queryable as instance state — use them to seed
27
+ // per-agent UI state, not for logging (the SDK already emitted each via onLog).
28
+ for (const failure of manager.getRestoreFailures()) {
29
+ // mark `failure.agentId` as `error` in your application state
30
+ }
31
+
32
+ // 2. Create an agent. The identity triple `{ agentId, projectRoot, config }` is
33
+ // persisted to disk; the next call to `createAgentManager` over the same
34
+ // storage folder will replay this agent automatically.
19
35
  const agent = await manager.createAgent('/path/to/project', {
20
36
  agentId: 'developer-assistant',
21
37
  modelId: 'llmgateway__OpenAIGPT5',
@@ -35,35 +51,83 @@ for await (const event of eventStream) {
35
51
  }
36
52
  }
37
53
 
38
- // 4. Shut down
54
+ // 4. Shut down. The harness is torn down; persisted identity files are NOT
55
+ // removed, so a subsequent `createAgentManager` call restores them.
39
56
  await manager.shutdown();
40
57
  ```
41
58
 
42
59
  ## API Reference
43
60
 
44
- ### `createAgentManager(storageRootFolder, harnessFactory): Promise<AgentManager>`
61
+ ### `createAgentManager<F>(storageRootFolder, harnessFactory, connectivityResolver?): Promise<AgentManager<H>>`
45
62
 
46
63
  Factory function that creates an `AgentManager` backed by the provided `HarnessFactory`. The `storageRootFolder` must be
47
- an existing directory and is used for persistent state (database, memory). The SDK verifies that the constructed harness
48
- uses a supported protocol version before returning the manager.
64
+ an existing directory and is used for persistent state (the harness's runtime data plus the SDK's per-agent identity
65
+ files at `${storageRootFolder}/agents/<id>.json`). The SDK verifies that the constructed harness uses a supported
66
+ protocol version, replays any persisted agents the harness can still serve, and returns the manager. The optional
67
+ `connectivityResolver` overrides the default sf-CLI-based org resolution — used by e2e tests and custom-auth
68
+ deployments; production callers leave it unset.
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
+ ```
49
79
 
50
- ### `AgentManager`
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.
51
83
 
52
- Top-level orchestrator that owns the harness and manages agent lifecycle.
84
+ Restore failures (a persisted record the SDK could not bring back online — e.g. missing project directory, harness
85
+ rejection, thread rehydration failure) are queryable on the returned manager via `getRestoreFailures()`. Soft skips
86
+ inside the persistence directory (corrupt JSON, harness-id mismatch) are silently dropped from the restore pass and emit
87
+ a `warn` on the SDK's log bus.
53
88
 
54
- | Method | Signature | Description |
55
- | -------------- | ------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
56
- | `createAgent` | `(projectRoot: string, config?: AgentConfig & { agentId?: string }, options?: { abortSignal?: AbortSignal }) => Promise<Agent>` | Create and register a new agent. `projectRoot` must be an existing directory. If `agentId` is omitted a UUID is generated. |
57
- | `getAgent` | `(agentId: string) => Agent` | Retrieve an agent by ID. Throws `AgentSDKError` (`AGENT_NOT_FOUND`) if missing. |
58
- | `getAgentIds` | `() => string[]` | List all managed agent IDs. |
59
- | `destroyAgent` | `(agentId: string) => Promise<void>` | Destroy an agent and release its resources. Throws `AgentSDKError` (`AGENT_NOT_FOUND`) if missing. |
60
- | `shutdown` | `() => Promise<void>` | Destroy all agents and shut down the harness. |
61
- | `onTelemetry` | `(callback: TelemetryEventCallback) => Unsubscribe` | Subscribe to telemetry across all managed agents. |
62
- | `onLog` | `(callback: (record: LogRecord) => void) => Unsubscribe` | Subscribe to structured logs across all managed agents. |
89
+ ### `AgentManager<H extends AgentHarness = AgentHarness>`
63
90
 
64
- ### `Agent`
91
+ Top-level orchestrator that owns the harness and manages agent lifecycle. `AgentManager` is an interface; the concrete
92
+ implementation is internal — `createAgentManager` is the only public entry point.
65
93
 
66
- A configured AI agent. Factory for chat sessions.
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. |
110
+
111
+ #### `RestoreFailure`
112
+
113
+ ```typescript
114
+ type RestoreFailure = {
115
+ agentId: string;
116
+ projectRoot: string;
117
+ config: AgentConfig;
118
+ error: unknown;
119
+ };
120
+ ```
121
+
122
+ Returned by `AgentManager.getRestoreFailures()`. Use it to seed `error`-state placeholders in your application; do
123
+ **not** iterate it for logging — the SDK already emitted each failure via `onLog` at `error` level during the restore
124
+ pass, before this function returned.
125
+
126
+ ### `Agent<H extends AgentHarness = AgentHarness>`
127
+
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.
67
131
 
68
132
  | Method | Signature | Description |
69
133
  | ---------------------- | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
@@ -134,16 +198,16 @@ Discriminated union (`event.type`) of streaming events:
134
198
 
135
199
  #### `AgentConfig`
136
200
 
137
- | Field | Type | Description |
138
- | --------------- | ------------------ | -------------------------------------------------------------------- |
139
- | `orgAlias?` | `string` | Salesforce org alias or username. Falls back to project/default org. |
140
- | `modelId?` | `ModelName` | LLM model identifier (e.g. `'llmgateway__OpenAIGPT5'`). |
141
- | `name?` | `string` | Human-readable agent name. |
142
- | `description?` | `string` | Agent purpose description. |
143
- | `instructions?` | `string` | System instructions for the agent. |
144
- | `tools?` | `ToolDefinition[]` | Consumer-executed tool schemas. |
145
- | `mcpServers?` | `MCPConfiguration` | MCP server connections. |
146
- | `skills?` | `string[]` | Each entry is either an individual skill folder (containing `SKILL.md`) or a parent folder containing skill subfolders. Relative and absolute paths supported; forms can be mixed in the same array. |
201
+ | Field | Type | Description |
202
+ | --------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
203
+ | `orgAlias?` | `string` | Salesforce org alias or username. Falls back to project/default org. |
204
+ | `modelId?` | `ModelName` | LLM model identifier (e.g. `'llmgateway__OpenAIGPT5'`). |
205
+ | `name?` | `string` | Human-readable agent name. |
206
+ | `description?` | `string` | Agent purpose description. |
207
+ | `instructions?` | `string` | System instructions for the agent. |
208
+ | `tools?` | `ToolDefinition[]` | Consumer-executed tool schemas. |
209
+ | `mcpServers?` | `MCPConfiguration` | MCP server connections. |
210
+ | `skills?` | `string[]` | Each entry is either an individual skill folder (containing `SKILL.md`) or a parent folder containing skill subfolders. Relative and absolute paths supported; forms can be mixed in the same array. |
147
211
  | `rules?` | `string[]` | Each entry is either an individual `.md` rule file or a directory of `.md` rule files (scanned one level deep, alphabetical, non-`.md` skipped). Bodies are composed verbatim into the agent's effective system prompt; YAML frontmatter is optional and stripped if present. Matches Claude Code's `.claude/rules/*.md` convention. |
148
212
 
149
213
  #### `StreamOptions`
@@ -184,9 +248,53 @@ type MCPRemoteServerConfig = {
184
248
  | -------- | ------------------------------------------------------ | --------------------------------------- |
185
249
  | `name` | `string` | Server identifier. |
186
250
  | `status` | `'connected' \| 'connecting' \| 'disabled' \| 'error'` | Connection state. |
187
- | `tools` | `string[]` | Discovered tool names. |
251
+ | `tools` | [`McpToolInfo[]`](#mcptoolinfo) | Discovered tools (name + metadata). |
188
252
  | `error?` | `string` | Error message when status is `'error'`. |
189
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
+
190
298
  ### Tool Types
191
299
 
192
300
  ```typescript
@@ -296,7 +404,14 @@ const agent = await manager.createAgent('/project', {
296
404
  // Poll until connected
297
405
  const servers = agent.getMcpServerInfo();
298
406
  for (const s of servers) {
299
- 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
+ }
300
415
  }
301
416
  ```
302
417
 
@@ -377,6 +492,84 @@ Returns `true` if the URL matches a Salesforce Hosted MCP Server endpoint (prod,
377
492
  The SDK exposes typed `TelemetryEvent` / `LogRecord` streams at the manager, agent, and session scopes. See
378
493
  [Telemetry & Logs](#telemetry--logs) below for the event catalog, emit contract, and structured-log table.
379
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
+
380
573
  ## Writing a Harness
381
574
 
382
575
  The SDK ships no harness implementation. Consumers pick one by passing a `HarnessFactory` instance to
@@ -388,7 +581,7 @@ Harness authors implement two interfaces and can compose one helper class, all e
388
581
 
389
582
  | Export | Role |
390
583
  | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
391
- | `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`. |
392
585
  | `AgentHarness` | Runtime contract: agent / thread / stream / tool / message lifecycle. Declares its own `harnessId` and `protocolVersion`. |
393
586
  | `SUPPORTED_PROTOCOL_VERSIONS` | Readonly list of harness protocol versions this SDK accepts. `createAgentManager` checks both the factory and the constructed harness. |
394
587
  | `HarnessBusOwner` | Composition helper owning telemetry + log buses with `dispose()` semantics. Reuse it instead of reimplementing bus plumbing. |
@@ -419,10 +612,10 @@ class MyHarness implements AgentHarness {
419
612
  // implement the remaining AgentHarness methods (createAgent, stream, …)
420
613
  }
421
614
 
422
- export class MyHarnessFactory implements HarnessFactory {
615
+ export class MyHarnessFactory implements HarnessFactory<MyHarness> {
423
616
  readonly harnessId = 'my-harness';
424
617
  readonly protocolVersion = 1;
425
- async create(storageRootFolder: string): Promise<AgentHarness> {
618
+ async create(storageRootFolder: string): Promise<MyHarness> {
426
619
  return new MyHarness(storageRootFolder);
427
620
  }
428
621
  }