@salesforce/sfdx-agent-sdk 0.4.0 → 0.5.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 +69 -27
- package/dist/agent-manager.d.ts +123 -85
- package/dist/agent-manager.js +147 -110
- package/dist/agent.d.ts +11 -0
- package/dist/agent.js +18 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/internal/agent-identity-store.d.ts +41 -0
- package/dist/internal/agent-identity-store.js +141 -0
- package/package.json +9 -2
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
|
|
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
|
|
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
|
-
//
|
|
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,31 +51,57 @@ 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(storageRootFolder, harnessFactory, connectivityResolver?): Promise<AgentManager>`
|
|
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 (
|
|
48
|
-
|
|
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
|
+
Restore failures (a persisted record the SDK could not bring back online — e.g. missing project directory, harness
|
|
71
|
+
rejection, thread rehydration failure) are queryable on the returned manager via `getRestoreFailures()`. Soft skips
|
|
72
|
+
inside the persistence directory (corrupt JSON, harness-id mismatch) are silently dropped from the restore pass and emit
|
|
73
|
+
a `warn` on the SDK's log bus.
|
|
49
74
|
|
|
50
75
|
### `AgentManager`
|
|
51
76
|
|
|
52
|
-
Top-level orchestrator that owns the harness and manages agent lifecycle.
|
|
77
|
+
Top-level orchestrator that owns the harness and manages agent lifecycle. `AgentManager` is an interface; the concrete
|
|
78
|
+
implementation is internal — `createAgentManager` is the only public entry point.
|
|
79
|
+
|
|
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. |
|
|
90
|
+
|
|
91
|
+
#### `RestoreFailure`
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
type RestoreFailure = {
|
|
95
|
+
agentId: string;
|
|
96
|
+
projectRoot: string;
|
|
97
|
+
config: AgentConfig;
|
|
98
|
+
error: unknown;
|
|
99
|
+
};
|
|
100
|
+
```
|
|
53
101
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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. |
|
|
102
|
+
Returned by `AgentManager.getRestoreFailures()`. Use it to seed `error`-state placeholders in your application; do
|
|
103
|
+
**not** iterate it for logging — the SDK already emitted each failure via `onLog` at `error` level during the restore
|
|
104
|
+
pass, before this function returned.
|
|
63
105
|
|
|
64
106
|
### `Agent`
|
|
65
107
|
|
|
@@ -134,16 +176,16 @@ Discriminated union (`event.type`) of streaming events:
|
|
|
134
176
|
|
|
135
177
|
#### `AgentConfig`
|
|
136
178
|
|
|
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.
|
|
179
|
+
| Field | Type | Description |
|
|
180
|
+
| --------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
181
|
+
| `orgAlias?` | `string` | Salesforce org alias or username. Falls back to project/default org. |
|
|
182
|
+
| `modelId?` | `ModelName` | LLM model identifier (e.g. `'llmgateway__OpenAIGPT5'`). |
|
|
183
|
+
| `name?` | `string` | Human-readable agent name. |
|
|
184
|
+
| `description?` | `string` | Agent purpose description. |
|
|
185
|
+
| `instructions?` | `string` | System instructions for the agent. |
|
|
186
|
+
| `tools?` | `ToolDefinition[]` | Consumer-executed tool schemas. |
|
|
187
|
+
| `mcpServers?` | `MCPConfiguration` | MCP server connections. |
|
|
188
|
+
| `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
189
|
| `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
190
|
|
|
149
191
|
#### `StreamOptions`
|
package/dist/agent-manager.d.ts
CHANGED
|
@@ -5,130 +5,168 @@ import { type AgentConfig } from './harness/harness-config.js';
|
|
|
5
5
|
import { type Agent } from './agent.js';
|
|
6
6
|
import type { TelemetryEventCallback } from './types/telemetry-events.js';
|
|
7
7
|
import { type AgentConnectivityResolver } from './agent-connectivity-resolver.js';
|
|
8
|
+
/**
|
|
9
|
+
* Per-agent failure recorded during the boot-time restore pass.
|
|
10
|
+
*
|
|
11
|
+
* Returned in bulk by {@link AgentManager.getRestoreFailures}. Each entry
|
|
12
|
+
* captures the persisted identity triple plus the underlying error so the
|
|
13
|
+
* consumer can mark its placeholder as `error` and either DELETE or recreate
|
|
14
|
+
* the agent.
|
|
15
|
+
*/
|
|
16
|
+
export type RestoreFailure = {
|
|
17
|
+
agentId: string;
|
|
18
|
+
projectRoot: string;
|
|
19
|
+
config: AgentConfig;
|
|
20
|
+
error: unknown;
|
|
21
|
+
};
|
|
8
22
|
/**
|
|
9
23
|
* Manages the lifecycle of {@link Agent} instances.
|
|
10
24
|
*
|
|
11
|
-
* The
|
|
12
|
-
*
|
|
13
|
-
*
|
|
25
|
+
* The concrete implementation ({@link DefaultAgentManager}) is internal to
|
|
26
|
+
* the SDK and constructed only via {@link createAgentManager}. Consumers
|
|
27
|
+
* program against this interface; they never see the implementation type.
|
|
28
|
+
*
|
|
29
|
+
* ### Boot-time restore
|
|
30
|
+
*
|
|
31
|
+
* `createAgentManager` reads any agents the SDK persisted on a prior run
|
|
32
|
+
* (`${storageRootFolder}/agents/<id>.json`) and replays `installAgent` for
|
|
33
|
+
* each before returning. Failures land in {@link getRestoreFailures}, not
|
|
34
|
+
* thrown. Subscribe to {@link onLog} to bridge SDK log events into your
|
|
35
|
+
* host logger; restore failures are emitted at `error` level and soft skips
|
|
36
|
+
* (corrupt JSON, harness-id mismatch) at `warn`.
|
|
37
|
+
*/
|
|
38
|
+
export interface AgentManager {
|
|
39
|
+
/**
|
|
40
|
+
* Creates a new {@link Agent} with the given configuration and persists
|
|
41
|
+
* its identity triple `{ agentId, projectRoot, config }` so it can be
|
|
42
|
+
* restored on the next boot.
|
|
43
|
+
*
|
|
44
|
+
* @throws If `projectRoot` does not exist or is not a directory.
|
|
45
|
+
* @throws If `config.agentId` is provided and an agent with that ID is
|
|
46
|
+
* already registered (live or in restore-failure state).
|
|
47
|
+
*/
|
|
48
|
+
createAgent(projectRoot: string, config?: AgentConfig & {
|
|
49
|
+
agentId?: string;
|
|
50
|
+
}, options?: {
|
|
51
|
+
abortSignal?: AbortSignal;
|
|
52
|
+
}): Promise<Agent>;
|
|
53
|
+
/**
|
|
54
|
+
* Returns the live {@link Agent} for the given id.
|
|
55
|
+
*
|
|
56
|
+
* @throws `AGENT_NOT_FOUND` if the id is unknown or only present in
|
|
57
|
+
* {@link getRestoreFailures}.
|
|
58
|
+
*/
|
|
59
|
+
getAgent(agentId: string): Agent;
|
|
60
|
+
/**
|
|
61
|
+
* Returns the ids of all live agents (successfully created or
|
|
62
|
+
* restored). Failed-restore agents are not included — query
|
|
63
|
+
* {@link getRestoreFailures} separately.
|
|
64
|
+
*/
|
|
65
|
+
getAgentIds(): string[];
|
|
66
|
+
/**
|
|
67
|
+
* Destroys a managed agent, removes its identity record from disk,
|
|
68
|
+
* and clears any matching entry in {@link getRestoreFailures}.
|
|
69
|
+
*
|
|
70
|
+
* If the id is only in `getRestoreFailures()` (no live agent), the
|
|
71
|
+
* harness is not consulted; the identity file is removed and the
|
|
72
|
+
* failure entry cleared. After this returns, the same id may be
|
|
73
|
+
* passed to {@link createAgent} again.
|
|
74
|
+
*
|
|
75
|
+
* @throws `AGENT_NOT_FOUND` if the id is neither live nor a
|
|
76
|
+
* restore-failure entry.
|
|
77
|
+
*/
|
|
78
|
+
destroyAgent(agentId: string): Promise<void>;
|
|
79
|
+
/** Shuts down the harness and destroys all live agents. */
|
|
80
|
+
shutdown(): Promise<void>;
|
|
81
|
+
/** Subscribe to telemetry events across every managed agent. */
|
|
82
|
+
onTelemetry(callback: TelemetryEventCallback): Unsubscribe;
|
|
83
|
+
/** Subscribe to structured log records across every managed agent. */
|
|
84
|
+
onLog(callback: (record: LogRecord) => void): Unsubscribe;
|
|
85
|
+
/**
|
|
86
|
+
* Returns a snapshot of the boot-time restore failures the SDK has not
|
|
87
|
+
* yet been told to forget. Each entry is cleared on a successful
|
|
88
|
+
* {@link destroyAgent} for the same id; recreating an agent with the
|
|
89
|
+
* same id via {@link createAgent} also clears the entry.
|
|
90
|
+
*/
|
|
91
|
+
getRestoreFailures(): RestoreFailure[];
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Concrete implementation of {@link AgentManager}. **Not exported** from
|
|
95
|
+
* `src/index.ts` — public construction goes through {@link createAgentManager}.
|
|
14
96
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* and calls `createAgent(projectRoot, { agentId: savedId, ...savedConfig })` to rebuild active agent state in memory
|
|
20
|
-
* (like MCP connections and custom tool closures) while reconnecting to the persistent memory data.
|
|
97
|
+
* Construction is asynchronous because {@link init} reads the persistence
|
|
98
|
+
* directory and replays prior agents before the manager becomes useful. The
|
|
99
|
+
* pattern mirrors {@link Workspace.create}: a private constructor for sync
|
|
100
|
+
* field assignment, then a static async builder that calls `init()`.
|
|
21
101
|
*/
|
|
22
|
-
export declare class AgentManager {
|
|
102
|
+
export declare class DefaultAgentManager implements AgentManager {
|
|
23
103
|
private readonly harness;
|
|
24
104
|
private readonly agentIdGenerator;
|
|
25
105
|
private readonly agentConnectivityResolver;
|
|
26
106
|
private readonly clock;
|
|
107
|
+
private readonly identityStore;
|
|
27
108
|
private readonly agents;
|
|
109
|
+
private restoreFailures;
|
|
28
110
|
private readonly telemetryBus;
|
|
29
111
|
private readonly logBus;
|
|
30
112
|
private readonly router;
|
|
31
113
|
private readonly unroutedUnsubs;
|
|
32
114
|
private disposed;
|
|
115
|
+
private constructor();
|
|
33
116
|
/**
|
|
34
|
-
*
|
|
117
|
+
* Module-internal builder used by {@link createAgentManager}. Mirrors
|
|
118
|
+
* {@link Workspace.create}: sync field assignment in the private
|
|
119
|
+
* constructor, then `await init()` to do async startup work
|
|
120
|
+
* (reading the persistence directory + replaying prior agents).
|
|
35
121
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* Production callers should use {@link createAgentManager} instead.
|
|
40
|
-
*
|
|
41
|
-
* @example
|
|
42
|
-
* ```ts
|
|
43
|
-
* // Recommended for production usage (constructs the default connectivity resolver):
|
|
44
|
-
* const manager = await createAgentManager(storageRootFolder, harnessFactory);
|
|
45
|
-
* ```
|
|
46
|
-
*/
|
|
47
|
-
constructor(harness: AgentHarness, agentConnectivityResolver: AgentConnectivityResolver, agentIdGenerator?: UniqueIDGenerator, clock?: Clock, logBus?: LogBus);
|
|
48
|
-
/**
|
|
49
|
-
* Shuts down the harness and destroys all managed agents.
|
|
50
|
-
*
|
|
51
|
-
* @requirements
|
|
52
|
-
* - MUST iterate through all agents in the internal `agents` map and call `agent.destroy()` on each.
|
|
53
|
-
* - MUST clear the internal `agents` map.
|
|
54
|
-
* - MUST delegate to `this.harness.shutdown()` to release harness-level resources.
|
|
122
|
+
* The double-underscore signals "do not call directly" — the constructor
|
|
123
|
+
* is private, so this is the only way to obtain an instance, but
|
|
124
|
+
* consumers should always go through {@link createAgentManager}.
|
|
55
125
|
*/
|
|
126
|
+
static __build(harness: AgentHarness, agentConnectivityResolver: AgentConnectivityResolver, storageRootFolder: string, agentIdGenerator: UniqueIDGenerator, clock: Clock, logBus: LogBus): Promise<DefaultAgentManager>;
|
|
127
|
+
private init;
|
|
56
128
|
shutdown(): Promise<void>;
|
|
57
|
-
/**
|
|
58
|
-
* Creates a new `Agent` with the given configuration.
|
|
59
|
-
*
|
|
60
|
-
* @param projectRoot - Absolute path to the project folder the agent can manipulate files from.
|
|
61
|
-
* Relative paths are resolved against `process.cwd()`. The path must exist and be a directory.
|
|
62
|
-
* @param config - Agent configuration (instructions, tools, model, etc.).
|
|
63
|
-
* @param options - Optional execution options, including abort signals.
|
|
64
|
-
* @returns A new `Agent` ready for chat sessions.
|
|
65
|
-
*
|
|
66
|
-
* @throws If `projectRoot` does not exist or is not a directory.
|
|
67
|
-
*
|
|
68
|
-
* @requirements
|
|
69
|
-
* - MUST resolve `projectRoot` to an absolute, normalized path before use.
|
|
70
|
-
* - MUST throw an Error if the resolved `projectRoot` does not exist or is not a directory.
|
|
71
|
-
* - MUST throw an Error if `config.agentId` is provided and an agent with that ID already exists in the internal `agents` map.
|
|
72
|
-
* - MUST generate a unique ID via `this.agentIdGenerator` if `config.agentId` is omitted.
|
|
73
|
-
* - MUST delegate to `this.harness.createAgent(agentId, projectRoot, llmGatewayClient, config, options)` to register the agent in the harness and allow for cancellation.
|
|
74
|
-
* - MUST instantiate a `DefaultAgent` with the harness and the complete configuration.
|
|
75
|
-
* - MUST store the newly created agent in the internal `agents` map, keyed by its `agentId`.
|
|
76
|
-
* - MUST return the newly created agent.
|
|
77
|
-
*/
|
|
78
129
|
createAgent(projectRoot: string, config?: AgentConfig & {
|
|
79
130
|
agentId?: string;
|
|
80
131
|
}, options?: {
|
|
81
132
|
abortSignal?: AbortSignal;
|
|
82
133
|
}): Promise<Agent>;
|
|
83
134
|
/**
|
|
84
|
-
*
|
|
135
|
+
* Shared install path for {@link createAgent} and the boot-time restore
|
|
136
|
+
* loop. Resolves connectivity, calls `harness.createAgent`, constructs
|
|
137
|
+
* the {@link DefaultAgent}, registers the telemetry slice, emits
|
|
138
|
+
* `agent-created`, and (when restoring) attaches a chat session per
|
|
139
|
+
* persisted thread id.
|
|
85
140
|
*
|
|
86
|
-
* @
|
|
87
|
-
*
|
|
141
|
+
* Returns {@link DefaultAgent} (concrete) rather than {@link Agent}
|
|
142
|
+
* because both call sites are internal — widening to the interface only
|
|
143
|
+
* happens at the public-method return statement.
|
|
88
144
|
*/
|
|
145
|
+
private installAgent;
|
|
146
|
+
private rollbackInstall;
|
|
89
147
|
getAgentIds(): string[];
|
|
90
|
-
/**
|
|
91
|
-
* Retrieves a managed agent by its ID.
|
|
92
|
-
*
|
|
93
|
-
* @param agentId - ID of the agent to retrieve.
|
|
94
|
-
*
|
|
95
|
-
* @requirements
|
|
96
|
-
* - MUST throw an Error if the provided `agentId` is not found in the internal `agents` map.
|
|
97
|
-
* - MUST return the `Agent` instance associated with the given `agentId`.
|
|
98
|
-
*/
|
|
99
148
|
getAgent(agentId: string): Agent;
|
|
100
|
-
/**
|
|
101
|
-
* Destroys a managed agent by its ID, releasing all its resources.
|
|
102
|
-
*
|
|
103
|
-
* @param agentId - ID of the agent to destroy.
|
|
104
|
-
*
|
|
105
|
-
* @requirements
|
|
106
|
-
* - MUST throw an Error if the provided `agentId` is not found in the internal `agents` map.
|
|
107
|
-
* - MUST call `destroy()` on the target agent instance.
|
|
108
|
-
* - MUST remove the agent from the internal `agents` map.
|
|
109
|
-
*/
|
|
110
149
|
destroyAgent(agentId: string): Promise<void>;
|
|
111
|
-
/** Subscribe to telemetry events across every managed agent. Returns an unsubscribe function. */
|
|
112
150
|
onTelemetry(callback: TelemetryEventCallback): Unsubscribe;
|
|
113
|
-
/** Subscribe to structured log records across every managed agent. Returns an unsubscribe function. */
|
|
114
151
|
onLog(callback: (record: LogRecord) => void): Unsubscribe;
|
|
152
|
+
getRestoreFailures(): RestoreFailure[];
|
|
115
153
|
private assertNotDisposed;
|
|
116
154
|
}
|
|
117
155
|
/**
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
* asynchronously, sanity-checks that the constructed harness honors the
|
|
124
|
-
* factory's advertised version, and returns a ready-to-use manager.
|
|
156
|
+
* Public entry point. Validates the storage root, gates the harness's
|
|
157
|
+
* advertised protocol version, constructs the harness, and returns a
|
|
158
|
+
* fully-restored {@link AgentManager}. Boot-time restore runs before this
|
|
159
|
+
* function returns; failures are queryable via
|
|
160
|
+
* {@link AgentManager.getRestoreFailures}.
|
|
125
161
|
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
162
|
+
* The optional `connectivityResolver` overrides the default
|
|
163
|
+
* `DefaultAgentConnectivityResolver` — used by e2e tests and custom-auth
|
|
164
|
+
* deployments where the SDK should not run sf-CLI-based org resolution.
|
|
165
|
+
* Production callers leave it unset.
|
|
128
166
|
*
|
|
129
167
|
* @throws {AgentSDKError} `INCOMPATIBLE_HARNESS` when either the factory or
|
|
130
168
|
* the constructed harness reports a `protocolVersion` outside
|
|
131
169
|
* {@link SUPPORTED_PROTOCOL_VERSIONS}, or when the harness's reported
|
|
132
170
|
* version disagrees with the factory's.
|
|
133
171
|
*/
|
|
134
|
-
export declare function createAgentManager(storageRootFolder: string, harnessFactory: HarnessFactory): Promise<AgentManager>;
|
|
172
|
+
export declare function createAgentManager(storageRootFolder: string, harnessFactory: HarnessFactory, connectivityResolver?: AgentConnectivityResolver): Promise<AgentManager>;
|
package/dist/agent-manager.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Copyright 2026, Salesforce, Inc. All rights reserved.
|
|
3
3
|
* See LICENSE.txt for license terms.
|
|
4
4
|
*/
|
|
5
|
-
import { EventBus, LogBus, RealClock, UUIDGenerator, } from '@salesforce/agentic-common';
|
|
5
|
+
import { EventBus, getErrorMessage, LogBus, RealClock, UUIDGenerator, } from '@salesforce/agentic-common';
|
|
6
6
|
import { resolve } from 'node:path';
|
|
7
7
|
import { stat } from 'node:fs/promises';
|
|
8
8
|
import { SUPPORTED_PROTOCOL_VERSIONS } from './harness/agent-harness.js';
|
|
@@ -10,49 +10,34 @@ import { toHarnessConfig } from './harness/harness-config.js';
|
|
|
10
10
|
import { DefaultAgent } from './agent.js';
|
|
11
11
|
import { AgentSDKError, AgentSDKErrorType } from './errors.js';
|
|
12
12
|
import { TelemetryRouter } from './internal/telemetry-router.js';
|
|
13
|
+
import { AgentIdentityStore } from './internal/agent-identity-store.js';
|
|
13
14
|
import { DefaultAgentConnectivityResolver } from './agent-connectivity-resolver.js';
|
|
14
15
|
/**
|
|
15
|
-
*
|
|
16
|
+
* Concrete implementation of {@link AgentManager}. **Not exported** from
|
|
17
|
+
* `src/index.ts` — public construction goes through {@link createAgentManager}.
|
|
16
18
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* ### Persistence Responsibility
|
|
22
|
-
* Persistence is shared between the harness and the consuming application:
|
|
23
|
-
* - **Engine**: Handles physical data persistence (database storage of threads, messages, and memory state).
|
|
24
|
-
* - **Application**: Handles lifecycle persistence. It manages the discovery and restoration of saved agents,
|
|
25
|
-
* and calls `createAgent(projectRoot, { agentId: savedId, ...savedConfig })` to rebuild active agent state in memory
|
|
26
|
-
* (like MCP connections and custom tool closures) while reconnecting to the persistent memory data.
|
|
19
|
+
* Construction is asynchronous because {@link init} reads the persistence
|
|
20
|
+
* directory and replays prior agents before the manager becomes useful. The
|
|
21
|
+
* pattern mirrors {@link Workspace.create}: a private constructor for sync
|
|
22
|
+
* field assignment, then a static async builder that calls `init()`.
|
|
27
23
|
*/
|
|
28
|
-
export class
|
|
24
|
+
export class DefaultAgentManager {
|
|
29
25
|
harness;
|
|
30
26
|
agentIdGenerator;
|
|
31
27
|
agentConnectivityResolver;
|
|
32
28
|
clock;
|
|
29
|
+
identityStore;
|
|
33
30
|
agents = new Map();
|
|
31
|
+
restoreFailures = [];
|
|
34
32
|
telemetryBus = new EventBus();
|
|
35
33
|
logBus;
|
|
36
34
|
router;
|
|
37
35
|
unroutedUnsubs;
|
|
38
36
|
disposed = false;
|
|
39
|
-
|
|
40
|
-
* Creates a new {@link AgentManager}.
|
|
41
|
-
*
|
|
42
|
-
* This constructor is **exposed for testing only** so unit tests can inject a stubbed
|
|
43
|
-
* {@link AgentHarness} and deterministic ID generator.
|
|
44
|
-
*
|
|
45
|
-
* Production callers should use {@link createAgentManager} instead.
|
|
46
|
-
*
|
|
47
|
-
* @example
|
|
48
|
-
* ```ts
|
|
49
|
-
* // Recommended for production usage (constructs the default connectivity resolver):
|
|
50
|
-
* const manager = await createAgentManager(storageRootFolder, harnessFactory);
|
|
51
|
-
* ```
|
|
52
|
-
*/
|
|
53
|
-
constructor(harness, agentConnectivityResolver, agentIdGenerator = new UUIDGenerator(), clock = new RealClock(), logBus = new LogBus()) {
|
|
37
|
+
constructor(harness, agentConnectivityResolver, identityStore, agentIdGenerator, clock, logBus) {
|
|
54
38
|
this.harness = harness;
|
|
55
39
|
this.agentConnectivityResolver = agentConnectivityResolver;
|
|
40
|
+
this.identityStore = identityStore;
|
|
56
41
|
this.agentIdGenerator = agentIdGenerator;
|
|
57
42
|
this.clock = clock;
|
|
58
43
|
this.logBus = logBus;
|
|
@@ -62,15 +47,47 @@ export class AgentManager {
|
|
|
62
47
|
this.router.unrouted.log.forwardTo(this.logBus),
|
|
63
48
|
];
|
|
64
49
|
}
|
|
65
|
-
// Note: harness construction is async (see createAgentManager), so AgentManager itself no longer has initialize().
|
|
66
50
|
/**
|
|
67
|
-
*
|
|
51
|
+
* Module-internal builder used by {@link createAgentManager}. Mirrors
|
|
52
|
+
* {@link Workspace.create}: sync field assignment in the private
|
|
53
|
+
* constructor, then `await init()` to do async startup work
|
|
54
|
+
* (reading the persistence directory + replaying prior agents).
|
|
68
55
|
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* - MUST delegate to `this.harness.shutdown()` to release harness-level resources.
|
|
56
|
+
* The double-underscore signals "do not call directly" — the constructor
|
|
57
|
+
* is private, so this is the only way to obtain an instance, but
|
|
58
|
+
* consumers should always go through {@link createAgentManager}.
|
|
73
59
|
*/
|
|
60
|
+
static async __build(harness, agentConnectivityResolver, storageRootFolder, agentIdGenerator, clock, logBus) {
|
|
61
|
+
const identityStore = new AgentIdentityStore(storageRootFolder, harness.harnessId, logBus);
|
|
62
|
+
const manager = new DefaultAgentManager(harness, agentConnectivityResolver, identityStore, agentIdGenerator, clock, logBus);
|
|
63
|
+
await manager.init();
|
|
64
|
+
return manager;
|
|
65
|
+
}
|
|
66
|
+
async init() {
|
|
67
|
+
const records = await this.identityStore.list();
|
|
68
|
+
const failures = [];
|
|
69
|
+
await Promise.all(records.map(async (record) => {
|
|
70
|
+
try {
|
|
71
|
+
await this.installAgent(record.agentId, record.projectRoot, record.config, {
|
|
72
|
+
rehydrateThreads: true,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
failures.push({
|
|
77
|
+
agentId: record.agentId,
|
|
78
|
+
projectRoot: record.projectRoot,
|
|
79
|
+
config: record.config,
|
|
80
|
+
error: err,
|
|
81
|
+
});
|
|
82
|
+
this.logBus.error('agent restore failed', err instanceof Error ? err : undefined, {
|
|
83
|
+
agentId: record.agentId,
|
|
84
|
+
projectRoot: record.projectRoot,
|
|
85
|
+
message: getErrorMessage(err),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}));
|
|
89
|
+
this.restoreFailures = failures;
|
|
90
|
+
}
|
|
74
91
|
async shutdown() {
|
|
75
92
|
if (this.disposed) {
|
|
76
93
|
return;
|
|
@@ -88,78 +105,99 @@ export class AgentManager {
|
|
|
88
105
|
this.logBus.dispose();
|
|
89
106
|
this.disposed = true;
|
|
90
107
|
}
|
|
91
|
-
/**
|
|
92
|
-
* Creates a new `Agent` with the given configuration.
|
|
93
|
-
*
|
|
94
|
-
* @param projectRoot - Absolute path to the project folder the agent can manipulate files from.
|
|
95
|
-
* Relative paths are resolved against `process.cwd()`. The path must exist and be a directory.
|
|
96
|
-
* @param config - Agent configuration (instructions, tools, model, etc.).
|
|
97
|
-
* @param options - Optional execution options, including abort signals.
|
|
98
|
-
* @returns A new `Agent` ready for chat sessions.
|
|
99
|
-
*
|
|
100
|
-
* @throws If `projectRoot` does not exist or is not a directory.
|
|
101
|
-
*
|
|
102
|
-
* @requirements
|
|
103
|
-
* - MUST resolve `projectRoot` to an absolute, normalized path before use.
|
|
104
|
-
* - MUST throw an Error if the resolved `projectRoot` does not exist or is not a directory.
|
|
105
|
-
* - MUST throw an Error if `config.agentId` is provided and an agent with that ID already exists in the internal `agents` map.
|
|
106
|
-
* - MUST generate a unique ID via `this.agentIdGenerator` if `config.agentId` is omitted.
|
|
107
|
-
* - MUST delegate to `this.harness.createAgent(agentId, projectRoot, llmGatewayClient, config, options)` to register the agent in the harness and allow for cancellation.
|
|
108
|
-
* - MUST instantiate a `DefaultAgent` with the harness and the complete configuration.
|
|
109
|
-
* - MUST store the newly created agent in the internal `agents` map, keyed by its `agentId`.
|
|
110
|
-
* - MUST return the newly created agent.
|
|
111
|
-
*/
|
|
112
108
|
async createAgent(projectRoot, config = {}, options) {
|
|
113
109
|
this.assertNotDisposed();
|
|
114
110
|
const resolvedProjectRoot = resolve(projectRoot);
|
|
111
|
+
const { agentId: providedAgentId, ...agentConfig } = config;
|
|
112
|
+
const agentId = providedAgentId ?? this.agentIdGenerator.getUniqueId();
|
|
113
|
+
if (this.agents.has(agentId)) {
|
|
114
|
+
throw new Error(`Agent with id "${agentId}" already exists`);
|
|
115
|
+
}
|
|
116
|
+
// installAgent validates projectRoot existence — same path as the restore loop.
|
|
117
|
+
const agent = await this.installAgent(agentId, resolvedProjectRoot, agentConfig, {
|
|
118
|
+
abortSignal: options?.abortSignal,
|
|
119
|
+
});
|
|
120
|
+
// If the disk write fails (disk full, permissions, fs error), the in-memory install
|
|
121
|
+
// is now stale — the harness has the agent, the manager has it in `agents`, but no
|
|
122
|
+
// record on disk means a subsequent restart loses it. Roll back the install so the
|
|
123
|
+
// failure is observable and the id stays reusable, then rethrow.
|
|
124
|
+
try {
|
|
125
|
+
await this.identityStore.write(agentId, resolvedProjectRoot, agentConfig);
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
await this.rollbackInstall(agentId, agent);
|
|
129
|
+
throw err;
|
|
130
|
+
}
|
|
131
|
+
// A successful (re)create clears any prior failed-restore entry for the same id.
|
|
132
|
+
this.restoreFailures = this.restoreFailures.filter((f) => f.agentId !== agentId);
|
|
133
|
+
return agent;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Shared install path for {@link createAgent} and the boot-time restore
|
|
137
|
+
* loop. Resolves connectivity, calls `harness.createAgent`, constructs
|
|
138
|
+
* the {@link DefaultAgent}, registers the telemetry slice, emits
|
|
139
|
+
* `agent-created`, and (when restoring) attaches a chat session per
|
|
140
|
+
* persisted thread id.
|
|
141
|
+
*
|
|
142
|
+
* Returns {@link DefaultAgent} (concrete) rather than {@link Agent}
|
|
143
|
+
* because both call sites are internal — widening to the interface only
|
|
144
|
+
* happens at the public-method return statement.
|
|
145
|
+
*/
|
|
146
|
+
async installAgent(agentId, projectRoot, config, options = {}) {
|
|
115
147
|
let dirStat;
|
|
116
148
|
try {
|
|
117
|
-
dirStat = await stat(
|
|
149
|
+
dirStat = await stat(projectRoot);
|
|
118
150
|
}
|
|
119
151
|
catch {
|
|
120
|
-
throw new Error(`projectRoot does not exist: "${
|
|
152
|
+
throw new Error(`projectRoot does not exist: "${projectRoot}"`);
|
|
121
153
|
}
|
|
122
154
|
if (!dirStat.isDirectory()) {
|
|
123
|
-
throw new Error(`projectRoot is not a directory: "${
|
|
155
|
+
throw new Error(`projectRoot is not a directory: "${projectRoot}"`);
|
|
124
156
|
}
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
if (providedAgentId && this.agents.has(providedAgentId)) {
|
|
128
|
-
throw new Error(`Agent with id "${providedAgentId}" already exists`);
|
|
129
|
-
}
|
|
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,
|
|
160
|
+
const agent = new DefaultAgent(this.harness, agentId, projectRoot, config, runtime.llmGatewayClient, runtime.orgConnection, runtime.orgJwt, this.agentConnectivityResolver, this.router, agentSlice, { telemetry: this.telemetryBus, log: this.logBus }, this.clock, this.agentIdGenerator);
|
|
134
161
|
this.agents.set(agentId, agent);
|
|
135
162
|
this.telemetryBus.emit({
|
|
136
163
|
type: 'agent-created',
|
|
137
164
|
timestamp: this.clock.now(),
|
|
138
165
|
agentId,
|
|
139
|
-
projectRoot
|
|
166
|
+
projectRoot,
|
|
140
167
|
modelName: runtime.llmGatewayClient.getModel().name,
|
|
141
168
|
});
|
|
169
|
+
if (options.rehydrateThreads) {
|
|
170
|
+
// If thread enumeration or session attachment fails, the restore is a full failure
|
|
171
|
+
// (the user would otherwise see an agent marked `ready` whose chat sessions return 404).
|
|
172
|
+
// We must roll back the in-memory installation so the failure surfaces in
|
|
173
|
+
// `restoreFailures` and the id remains reusable via createAgent.
|
|
174
|
+
try {
|
|
175
|
+
const threadIds = await this.harness.getThreadIds(agentId);
|
|
176
|
+
agent.restoreSessions(threadIds);
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
await this.rollbackInstall(agentId, agent);
|
|
180
|
+
throw err;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
142
183
|
return agent;
|
|
143
184
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
185
|
+
async rollbackInstall(agentId, agent) {
|
|
186
|
+
// `agent.destroy()` already calls `harness.destroyAgent(agentId)` internally
|
|
187
|
+
// (see `DefaultAgent.destroy` in `agent.ts`), so we don't need a second harness call.
|
|
188
|
+
try {
|
|
189
|
+
await agent.destroy();
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
// Swallow secondary failure; the original error (passed to caller) is what matters.
|
|
193
|
+
}
|
|
194
|
+
this.router.unregisterAgent(agentId);
|
|
195
|
+
this.agents.delete(agentId);
|
|
196
|
+
}
|
|
150
197
|
getAgentIds() {
|
|
151
198
|
this.assertNotDisposed();
|
|
152
199
|
return Array.from(this.agents.keys());
|
|
153
200
|
}
|
|
154
|
-
/**
|
|
155
|
-
* Retrieves a managed agent by its ID.
|
|
156
|
-
*
|
|
157
|
-
* @param agentId - ID of the agent to retrieve.
|
|
158
|
-
*
|
|
159
|
-
* @requirements
|
|
160
|
-
* - MUST throw an Error if the provided `agentId` is not found in the internal `agents` map.
|
|
161
|
-
* - MUST return the `Agent` instance associated with the given `agentId`.
|
|
162
|
-
*/
|
|
163
201
|
getAgent(agentId) {
|
|
164
202
|
this.assertNotDisposed();
|
|
165
203
|
const agent = this.agents.get(agentId);
|
|
@@ -168,36 +206,35 @@ export class AgentManager {
|
|
|
168
206
|
}
|
|
169
207
|
return agent;
|
|
170
208
|
}
|
|
171
|
-
/**
|
|
172
|
-
* Destroys a managed agent by its ID, releasing all its resources.
|
|
173
|
-
*
|
|
174
|
-
* @param agentId - ID of the agent to destroy.
|
|
175
|
-
*
|
|
176
|
-
* @requirements
|
|
177
|
-
* - MUST throw an Error if the provided `agentId` is not found in the internal `agents` map.
|
|
178
|
-
* - MUST call `destroy()` on the target agent instance.
|
|
179
|
-
* - MUST remove the agent from the internal `agents` map.
|
|
180
|
-
*/
|
|
181
209
|
async destroyAgent(agentId) {
|
|
182
210
|
this.assertNotDisposed();
|
|
183
211
|
const agent = this.agents.get(agentId);
|
|
184
|
-
|
|
212
|
+
const failureIndex = this.restoreFailures.findIndex((f) => f.agentId === agentId);
|
|
213
|
+
if (!agent && failureIndex === -1) {
|
|
185
214
|
throw new AgentSDKError(`No Agent found with id: "${agentId}"`, AgentSDKErrorType.AGENT_NOT_FOUND);
|
|
186
215
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
216
|
+
if (agent) {
|
|
217
|
+
await agent.destroy();
|
|
218
|
+
this.router.unregisterAgent(agentId);
|
|
219
|
+
this.agents.delete(agentId);
|
|
220
|
+
}
|
|
221
|
+
if (failureIndex !== -1) {
|
|
222
|
+
this.restoreFailures.splice(failureIndex, 1);
|
|
223
|
+
}
|
|
224
|
+
await this.identityStore.remove(agentId);
|
|
190
225
|
}
|
|
191
|
-
/** Subscribe to telemetry events across every managed agent. Returns an unsubscribe function. */
|
|
192
226
|
onTelemetry(callback) {
|
|
193
227
|
this.assertNotDisposed();
|
|
194
228
|
return this.telemetryBus.on(callback);
|
|
195
229
|
}
|
|
196
|
-
/** Subscribe to structured log records across every managed agent. Returns an unsubscribe function. */
|
|
197
230
|
onLog(callback) {
|
|
198
231
|
this.assertNotDisposed();
|
|
199
232
|
return this.logBus.on(callback);
|
|
200
233
|
}
|
|
234
|
+
getRestoreFailures() {
|
|
235
|
+
this.assertNotDisposed();
|
|
236
|
+
return [...this.restoreFailures];
|
|
237
|
+
}
|
|
201
238
|
assertNotDisposed() {
|
|
202
239
|
if (this.disposed) {
|
|
203
240
|
throw new AgentSDKError('AgentManager has been shut down.', AgentSDKErrorType.DISPOSED);
|
|
@@ -205,23 +242,23 @@ export class AgentManager {
|
|
|
205
242
|
}
|
|
206
243
|
}
|
|
207
244
|
/**
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
* asynchronously, sanity-checks that the constructed harness honors the
|
|
214
|
-
* factory's advertised version, and returns a ready-to-use manager.
|
|
245
|
+
* Public entry point. Validates the storage root, gates the harness's
|
|
246
|
+
* advertised protocol version, constructs the harness, and returns a
|
|
247
|
+
* fully-restored {@link AgentManager}. Boot-time restore runs before this
|
|
248
|
+
* function returns; failures are queryable via
|
|
249
|
+
* {@link AgentManager.getRestoreFailures}.
|
|
215
250
|
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
251
|
+
* The optional `connectivityResolver` overrides the default
|
|
252
|
+
* `DefaultAgentConnectivityResolver` — used by e2e tests and custom-auth
|
|
253
|
+
* deployments where the SDK should not run sf-CLI-based org resolution.
|
|
254
|
+
* Production callers leave it unset.
|
|
218
255
|
*
|
|
219
256
|
* @throws {AgentSDKError} `INCOMPATIBLE_HARNESS` when either the factory or
|
|
220
257
|
* the constructed harness reports a `protocolVersion` outside
|
|
221
258
|
* {@link SUPPORTED_PROTOCOL_VERSIONS}, or when the harness's reported
|
|
222
259
|
* version disagrees with the factory's.
|
|
223
260
|
*/
|
|
224
|
-
export async function createAgentManager(storageRootFolder, harnessFactory) {
|
|
261
|
+
export async function createAgentManager(storageRootFolder, harnessFactory, connectivityResolver) {
|
|
225
262
|
let stats;
|
|
226
263
|
try {
|
|
227
264
|
stats = await stat(storageRootFolder);
|
|
@@ -255,8 +292,8 @@ export async function createAgentManager(storageRootFolder, harnessFactory) {
|
|
|
255
292
|
`advertised version ${factoryVersion} (SDK supports: ${SUPPORTED_PROTOCOL_VERSIONS.join(', ')}). ` +
|
|
256
293
|
`Update the SDK or harness package.`, AgentSDKErrorType.INCOMPATIBLE_HARNESS);
|
|
257
294
|
}
|
|
258
|
-
const agentConnectivityResolver = new DefaultAgentConnectivityResolver();
|
|
259
|
-
return
|
|
295
|
+
const agentConnectivityResolver = connectivityResolver ?? new DefaultAgentConnectivityResolver();
|
|
296
|
+
return DefaultAgentManager.__build(harness, agentConnectivityResolver, storageRootFolder, new UUIDGenerator(), new RealClock(), new LogBus());
|
|
260
297
|
}
|
|
261
298
|
function isSupportedProtocolVersion(version) {
|
|
262
299
|
return (typeof version === 'number' &&
|
package/dist/agent.d.ts
CHANGED
|
@@ -212,6 +212,17 @@ export declare class DefaultAgent implements Agent {
|
|
|
212
212
|
destroy(): Promise<void>;
|
|
213
213
|
onTelemetry(callback: TelemetryEventCallback): Unsubscribe;
|
|
214
214
|
onLog(callback: (record: LogRecord) => void): Unsubscribe;
|
|
215
|
+
/**
|
|
216
|
+
* Re-attach `DefaultChatSession` wrappers for thread ids that already
|
|
217
|
+
* exist in the harness's persistent store. Called by the SDK during
|
|
218
|
+
* boot-time restore. Idempotent — thread ids that already have a session
|
|
219
|
+
* are skipped.
|
|
220
|
+
*
|
|
221
|
+
* Not on the public {@link Agent} interface because it's only meaningful
|
|
222
|
+
* to {@link DefaultAgentManager}; that's also why `installAgent` returns
|
|
223
|
+
* the concrete `DefaultAgent` type rather than `Agent`.
|
|
224
|
+
*/
|
|
225
|
+
restoreSessions(threadIds: string[]): void;
|
|
215
226
|
private attachSession;
|
|
216
227
|
private detachSession;
|
|
217
228
|
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, {
|
package/dist/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export type { MCPConfiguration, MCPServerConfig, MCPStdioServerConfig, MCPRemote
|
|
|
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';
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ export { McpServerStatus } from './mcp-config.js';
|
|
|
7
7
|
export { ModelName } from '@salesforce/llm-gateway-sdk';
|
|
8
8
|
export { SfApiEnv } from '@salesforce/agentic-common';
|
|
9
9
|
// ── Agent Layer ─────────────────────────────────────────────────────
|
|
10
|
-
export {
|
|
10
|
+
export { createAgentManager } from './agent-manager.js';
|
|
11
11
|
export {} from './agent.js';
|
|
12
12
|
export {} from './chat-session.js';
|
|
13
13
|
export { SUPPORTED_PROTOCOL_VERSIONS } from './harness/agent-harness.js';
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type LogBus } from '@salesforce/agentic-common';
|
|
2
|
+
import type { AgentConfig } from '../harness/harness-config.js';
|
|
3
|
+
export type AgentIdentityRecord = {
|
|
4
|
+
agentId: string;
|
|
5
|
+
projectRoot: string;
|
|
6
|
+
config: AgentConfig;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* SDK-owned persistence for the agent-identity triple
|
|
10
|
+
* `{ agentId, projectRoot, AgentConfig }`. Stored as one JSON file per agent
|
|
11
|
+
* under `${storageRootFolder}/agents/`. Internal to the SDK; not exported.
|
|
12
|
+
*
|
|
13
|
+
* Each record carries the current harness's `harnessId`. On `list()`, records
|
|
14
|
+
* whose `harnessId` does not match the current harness are skipped with a
|
|
15
|
+
* `LogBus.warn` — restoring an agent into the wrong harness is never the
|
|
16
|
+
* right answer.
|
|
17
|
+
*/
|
|
18
|
+
export declare class AgentIdentityStore {
|
|
19
|
+
private readonly storageRootFolder;
|
|
20
|
+
private readonly harnessId;
|
|
21
|
+
private readonly logBus;
|
|
22
|
+
/**
|
|
23
|
+
* Per-agentId queue of in-flight writes. Concurrent `write()` calls for the same agentId
|
|
24
|
+
* chain onto the previous promise so the `writeFile` + `rename` pair runs sequentially.
|
|
25
|
+
*
|
|
26
|
+
* Why this matters: POSIX `rename` atomically overwrites an existing target, but Windows
|
|
27
|
+
* `rename` returns `EPERM` when any handle is open on the target — including a sibling
|
|
28
|
+
* concurrent rename from the same process. Three concurrent writers calling
|
|
29
|
+
* `rename(tmp, 'a.json')` succeed on Linux/macOS and fail on Windows. Serializing per
|
|
30
|
+
* agentId eliminates the race entirely (and removes any need for per-call temp suffixes
|
|
31
|
+
* because at most one write is touching the temp path at a time). Last-writer-wins
|
|
32
|
+
* semantics are preserved.
|
|
33
|
+
*/
|
|
34
|
+
private readonly inflightWrites;
|
|
35
|
+
constructor(storageRootFolder: string, harnessId: string, logBus: LogBus);
|
|
36
|
+
write(agentId: string, projectRoot: string, config: AgentConfig): Promise<void>;
|
|
37
|
+
private writeImmediate;
|
|
38
|
+
remove(agentId: string): Promise<void>;
|
|
39
|
+
list(): Promise<AgentIdentityRecord[]>;
|
|
40
|
+
private dir;
|
|
41
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026, Salesforce, Inc. All rights reserved.
|
|
3
|
+
* See LICENSE.txt for license terms.
|
|
4
|
+
*/
|
|
5
|
+
import { mkdir, readdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { getErrorMessage } from '@salesforce/agentic-common';
|
|
8
|
+
const FILE_VERSION = 1;
|
|
9
|
+
const AGENTS_SUBDIR = 'agents';
|
|
10
|
+
/**
|
|
11
|
+
* SDK-owned persistence for the agent-identity triple
|
|
12
|
+
* `{ agentId, projectRoot, AgentConfig }`. Stored as one JSON file per agent
|
|
13
|
+
* under `${storageRootFolder}/agents/`. Internal to the SDK; not exported.
|
|
14
|
+
*
|
|
15
|
+
* Each record carries the current harness's `harnessId`. On `list()`, records
|
|
16
|
+
* whose `harnessId` does not match the current harness are skipped with a
|
|
17
|
+
* `LogBus.warn` — restoring an agent into the wrong harness is never the
|
|
18
|
+
* right answer.
|
|
19
|
+
*/
|
|
20
|
+
export class AgentIdentityStore {
|
|
21
|
+
storageRootFolder;
|
|
22
|
+
harnessId;
|
|
23
|
+
logBus;
|
|
24
|
+
/**
|
|
25
|
+
* Per-agentId queue of in-flight writes. Concurrent `write()` calls for the same agentId
|
|
26
|
+
* chain onto the previous promise so the `writeFile` + `rename` pair runs sequentially.
|
|
27
|
+
*
|
|
28
|
+
* Why this matters: POSIX `rename` atomically overwrites an existing target, but Windows
|
|
29
|
+
* `rename` returns `EPERM` when any handle is open on the target — including a sibling
|
|
30
|
+
* concurrent rename from the same process. Three concurrent writers calling
|
|
31
|
+
* `rename(tmp, 'a.json')` succeed on Linux/macOS and fail on Windows. Serializing per
|
|
32
|
+
* agentId eliminates the race entirely (and removes any need for per-call temp suffixes
|
|
33
|
+
* because at most one write is touching the temp path at a time). Last-writer-wins
|
|
34
|
+
* semantics are preserved.
|
|
35
|
+
*/
|
|
36
|
+
inflightWrites = new Map();
|
|
37
|
+
constructor(storageRootFolder, harnessId, logBus) {
|
|
38
|
+
this.storageRootFolder = storageRootFolder;
|
|
39
|
+
this.harnessId = harnessId;
|
|
40
|
+
this.logBus = logBus;
|
|
41
|
+
}
|
|
42
|
+
async write(agentId, projectRoot, config) {
|
|
43
|
+
const previous = this.inflightWrites.get(agentId) ?? Promise.resolve();
|
|
44
|
+
// `.catch(() => undefined)` so a previous failure doesn't poison the next caller's await.
|
|
45
|
+
// Each caller still observes its own write's success or failure via the returned promise.
|
|
46
|
+
const next = previous.catch(() => undefined).then(() => this.writeImmediate(agentId, projectRoot, config));
|
|
47
|
+
this.inflightWrites.set(agentId, next);
|
|
48
|
+
try {
|
|
49
|
+
await next;
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
// Only clear the slot if it still points at this write — a newer write may have
|
|
53
|
+
// already chained onto `next` and replaced the entry.
|
|
54
|
+
if (this.inflightWrites.get(agentId) === next) {
|
|
55
|
+
this.inflightWrites.delete(agentId);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async writeImmediate(agentId, projectRoot, config) {
|
|
60
|
+
const dir = this.dir();
|
|
61
|
+
await mkdir(dir, { recursive: true });
|
|
62
|
+
const payload = {
|
|
63
|
+
version: FILE_VERSION,
|
|
64
|
+
harnessId: this.harnessId,
|
|
65
|
+
agentId,
|
|
66
|
+
projectRoot,
|
|
67
|
+
config,
|
|
68
|
+
};
|
|
69
|
+
const target = join(dir, `${agentId}.json`);
|
|
70
|
+
const tmp = `${target}.tmp`;
|
|
71
|
+
await writeFile(tmp, JSON.stringify(payload, null, 2), 'utf8');
|
|
72
|
+
await rename(tmp, target);
|
|
73
|
+
}
|
|
74
|
+
async remove(agentId) {
|
|
75
|
+
// Wait for any in-flight write before removing so we don't race a rename and leave
|
|
76
|
+
// the file behind. Same Windows-EPERM hazard as concurrent writes, plus the obvious
|
|
77
|
+
// last-writer-wins concern (a write completing after a remove would resurrect the file).
|
|
78
|
+
const previous = this.inflightWrites.get(agentId);
|
|
79
|
+
if (previous) {
|
|
80
|
+
await previous.catch(() => undefined);
|
|
81
|
+
}
|
|
82
|
+
await rm(join(this.dir(), `${agentId}.json`), { force: true });
|
|
83
|
+
}
|
|
84
|
+
async list() {
|
|
85
|
+
let entries;
|
|
86
|
+
try {
|
|
87
|
+
entries = await readdir(this.dir());
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
if (err.code === 'ENOENT')
|
|
91
|
+
return [];
|
|
92
|
+
throw err;
|
|
93
|
+
}
|
|
94
|
+
const records = [];
|
|
95
|
+
for (const entry of entries) {
|
|
96
|
+
if (!entry.endsWith('.json'))
|
|
97
|
+
continue;
|
|
98
|
+
const filePath = join(this.dir(), entry);
|
|
99
|
+
let parsed;
|
|
100
|
+
try {
|
|
101
|
+
const raw = await readFile(filePath, 'utf8');
|
|
102
|
+
parsed = JSON.parse(raw);
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
this.logBus.warn('skipping unreadable persisted agent identity file', {
|
|
106
|
+
filePath,
|
|
107
|
+
error: getErrorMessage(err),
|
|
108
|
+
});
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (!parsed.agentId || !parsed.projectRoot || !parsed.config || !parsed.harnessId) {
|
|
112
|
+
// `harnessId` is in the missing-fields gate (not the harness-mismatch gate below)
|
|
113
|
+
// so a record without it produces a "missing required fields" warn rather than
|
|
114
|
+
// a confusing "different harness" warn with `recordHarnessId: undefined`.
|
|
115
|
+
this.logBus.warn('skipping persisted agent identity file with missing required fields', {
|
|
116
|
+
filePath,
|
|
117
|
+
});
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (parsed.harnessId !== this.harnessId) {
|
|
121
|
+
this.logBus.warn('skipping persisted agent identity file from a different harness', {
|
|
122
|
+
filePath,
|
|
123
|
+
agentId: parsed.agentId,
|
|
124
|
+
recordHarnessId: parsed.harnessId,
|
|
125
|
+
currentHarnessId: this.harnessId,
|
|
126
|
+
});
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
records.push({
|
|
130
|
+
agentId: parsed.agentId,
|
|
131
|
+
projectRoot: parsed.projectRoot,
|
|
132
|
+
config: parsed.config,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return records;
|
|
136
|
+
}
|
|
137
|
+
dir() {
|
|
138
|
+
return join(this.storageRootFolder, AGENTS_SUBDIR);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=agent-identity-store.js.map
|
package/package.json
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/sfdx-agent-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Harness-agnostic agentic infrastructure for Salesforce developer experience tooling",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
8
15
|
"scripts": {
|
|
9
16
|
"build": "tsc --build",
|
|
10
17
|
"clean": "tsc --build --clean",
|
|
@@ -33,7 +40,7 @@
|
|
|
33
40
|
},
|
|
34
41
|
"devDependencies": {
|
|
35
42
|
"@eslint/js": "^10.0.1",
|
|
36
|
-
"@salesforce/sfdx-agent-harness-mastra": "0.
|
|
43
|
+
"@salesforce/sfdx-agent-harness-mastra": "0.5.0",
|
|
37
44
|
"@types/node": "^22.19.17",
|
|
38
45
|
"@vitest/coverage-istanbul": "^4.1.5",
|
|
39
46
|
"@vitest/eslint-plugin": "^1.6.16",
|