@salesforce/sfdx-agent-sdk 0.55.0 → 0.57.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/CHANGELOG.md +10 -0
- package/README.md +14 -13
- package/dist/agent-manager.d.ts +77 -4
- package/dist/agent-manager.js +80 -7
- package/dist/internal/agent-identity-store.d.ts +18 -0
- package/dist/internal/agent-identity-store.js +74 -26
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
All notable changes to `@salesforce/sfdx-agent-sdk` are documented in this file.
|
|
4
4
|
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
5
|
|
|
6
|
+
## [0.57.0] - 2026-08-27
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
- **agent-sdk**: let createAgentManager observe boot-restore logs via onBootLog @W-23992664@ ([#770](https://github.com/forcedotcom/agentic-dx/pull/770))
|
|
10
|
+
|
|
11
|
+
## [0.56.0] - 2026-08-27
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
- **agent-sdk**: recoverAgent re-installs a restore-failed agent with thread rehydration @W-24002660@ ([#769](https://github.com/forcedotcom/agentic-dx/pull/769))
|
|
15
|
+
|
|
6
16
|
## [0.55.0] - 2026-08-25
|
|
7
17
|
|
|
8
18
|
### Features
|
package/README.md
CHANGED
|
@@ -102,18 +102,19 @@ through a typed `manager.extensions` slot. `createAgentManager` infers `H` from
|
|
|
102
102
|
explicitly. The `createAgent` config parameter narrows automatically when the harness brands itself with
|
|
103
103
|
`WithAgentConfig` — see "Harness Extensibility" below.
|
|
104
104
|
|
|
105
|
-
| Property / Method | Signature | Description
|
|
106
|
-
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
107
|
-
| `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.
|
|
108
|
-
| `createAgent` | `(projectRoot: string, config?: ConfigOf<H> & { agentId?: string }, options?: { abortSignal?: AbortSignal; consumerMetadata?: JsonValue }) => 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>`. `options.consumerMetadata` seeds the agent's opaque consumer metadata in the same persistence write as the config (create and initial metadata land together); it is never forwarded to the harness.
|
|
109
|
-
| `
|
|
110
|
-
| `
|
|
111
|
-
| `
|
|
112
|
-
| `
|
|
113
|
-
| `
|
|
114
|
-
| `
|
|
115
|
-
| `
|
|
116
|
-
| `
|
|
105
|
+
| Property / Method | Signature | Description |
|
|
106
|
+
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
107
|
+
| `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. |
|
|
108
|
+
| `createAgent` | `(projectRoot: string, config?: ConfigOf<H> & { agentId?: string }, options?: { abortSignal?: AbortSignal; consumerMetadata?: JsonValue }) => 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>`. `options.consumerMetadata` seeds the agent's opaque consumer metadata in the same persistence write as the config (create and initial metadata land together); it is never forwarded to the harness. |
|
|
109
|
+
| `recoverAgent` | `(agentId: string, options?: { abortSignal?: AbortSignal }) => Promise<Agent<H>>` | Re-install an already-persisted agent whose boot-time restore failed, under the **same** id, resolving connectivity fresh from disk and **rehydrating its chat threads** so a follow-up turn resumes prior context. Idempotent for a live id (no-op returning the existing agent). Success clears the `getRestoreFailures()` entry; failure leaves it intact and re-armable with the persisted files untouched. Throws `AgentSDKError` (`AGENT_NOT_FOUND`) for an id that is neither live nor a restore-failure — it never fabricates an agent. Unlike `createAgent`, this re-establishes an existing identity rather than provisioning a new one. |
|
|
110
|
+
| `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()`. |
|
|
111
|
+
| `getAgentIds` | `() => string[]` | List all live agent IDs (successful + successfully restored). Failed-restore agents are not included — query `getRestoreFailures()` separately. |
|
|
112
|
+
| `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). |
|
|
113
|
+
| `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. |
|
|
114
|
+
| `onTelemetry` | `(callback: TelemetryEventCallback) => Unsubscribe` | Subscribe to telemetry across all managed agents. |
|
|
115
|
+
| `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. |
|
|
116
|
+
| `onWireCommunication` | `(callback: WireCommunicationEventCallback) => Unsubscribe` | Subscribe to wire-level communication events from the harness. Opt-in diagnostic channel that surfaces outbound LLM requests, responses, and harness-specific monitoring metadata. Subscriber-gated end-to-end — harnesses pay no cost when nobody listens. See "Wire-Communication Events" below for the event-shape catalog and the harness-asymmetric coverage (Mastra emits per-call request/response pairs; Claude emits a per-stream pointer to a debug log file). |
|
|
117
|
+
| `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. |
|
|
117
118
|
|
|
118
119
|
#### `RestoreFailure`
|
|
119
120
|
|
|
@@ -759,7 +760,7 @@ from `AgentSDKErrorType`:
|
|
|
759
760
|
|
|
760
761
|
| Type | Thrown By |
|
|
761
762
|
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
762
|
-
| `AGENT_NOT_FOUND` | `AgentManager.getAgent()`, `AgentManager.destroyAgent()`
|
|
763
|
+
| `AGENT_NOT_FOUND` | `AgentManager.getAgent()`, `AgentManager.destroyAgent()`, `AgentManager.recoverAgent()` (id neither live nor a restore-failure) |
|
|
763
764
|
| `CHAT_SESSION_NOT_FOUND` | `Agent.getChatSession()`, `Agent.destroyChatSession()`, `Agent.cloneChatSession()`, `Agent.compactChatSession()` |
|
|
764
765
|
| `COMPACTION_FAILED` | `Agent.compactChatSession()` when the harness's underlying summarization call rejects. The original error is attached as `cause`; the source session is left intact. |
|
|
765
766
|
| `DISPOSED` | `Agent` and `ChatSession` methods called after the owner has been destroyed |
|
package/dist/agent-manager.d.ts
CHANGED
|
@@ -56,8 +56,13 @@ export interface AgentManager<H extends AgentHarness = AgentHarness> {
|
|
|
56
56
|
* and initial metadata land together. It is never forwarded to the harness.
|
|
57
57
|
*
|
|
58
58
|
* @throws If `projectRoot` does not exist or is not a directory.
|
|
59
|
-
* @throws If `config.agentId` is provided and
|
|
60
|
-
* already
|
|
59
|
+
* @throws If `config.agentId` is provided and a **live** agent with that id
|
|
60
|
+
* already exists. An id that is only present in {@link getRestoreFailures}
|
|
61
|
+
* is **not** rejected: `createAgent` re-installs it cleanly as a brand-new
|
|
62
|
+
* identity, overwriting the persisted record and clearing the failure entry
|
|
63
|
+
* — but it does **not** rehydrate the agent's prior chat threads. To recover
|
|
64
|
+
* a restore-failed id *with* its conversation history, use
|
|
65
|
+
* {@link recoverAgent} instead.
|
|
61
66
|
*/
|
|
62
67
|
createAgent(projectRoot: string, config?: ConfigOf<H> & {
|
|
63
68
|
agentId?: string;
|
|
@@ -65,6 +70,46 @@ export interface AgentManager<H extends AgentHarness = AgentHarness> {
|
|
|
65
70
|
abortSignal?: AbortSignal;
|
|
66
71
|
consumerMetadata?: JsonValue;
|
|
67
72
|
}): Promise<Agent>;
|
|
73
|
+
/**
|
|
74
|
+
* Re-installs an already-persisted agent whose boot-time restore failed,
|
|
75
|
+
* returning a live {@link Agent}.
|
|
76
|
+
*
|
|
77
|
+
* Where {@link createAgent} provisions a **brand-new** identity, this
|
|
78
|
+
* re-establishes an **existing** one: it re-runs the same restore/construct
|
|
79
|
+
* path boot uses — resolving connectivity fresh from disk and **rehydrating
|
|
80
|
+
* the agent's chat threads** — so a follow-up turn resumes prior conversation
|
|
81
|
+
* context. Use it to recover an id surfaced by {@link getRestoreFailures}
|
|
82
|
+
* (e.g. one wedged after an org session token expired) under the **same** id,
|
|
83
|
+
* without minting a new agent id and breaking the caller's project→agent
|
|
84
|
+
* binding.
|
|
85
|
+
*
|
|
86
|
+
* - **Idempotent for a live id.** If the id is already live, this is a no-op
|
|
87
|
+
* that returns the existing {@link Agent} — no second construction.
|
|
88
|
+
* - **Success clears the restore-failure entry.** The id then appears in
|
|
89
|
+
* {@link getAgentIds} and is gone from {@link getRestoreFailures}.
|
|
90
|
+
* - **Failure is non-destructive.** If the re-install fails (e.g. auth is
|
|
91
|
+
* still stale), the {@link getRestoreFailures} entry stays intact and
|
|
92
|
+
* re-armable and the persisted identity/thread files are untouched — call
|
|
93
|
+
* again once fresh auth lands.
|
|
94
|
+
*
|
|
95
|
+
* Recovery re-installs from the agent's durable on-disk record and never
|
|
96
|
+
* writes one; if that record is gone, recovery has no basis and throws
|
|
97
|
+
* (see below) rather than construct an unpersisted agent. Concurrent
|
|
98
|
+
* same-id calls (recover racing recover, or recover racing
|
|
99
|
+
* {@link destroyAgent} / {@link createAgent}) are the caller's
|
|
100
|
+
* responsibility to serialize: state never corrupts and a destroy always
|
|
101
|
+
* wins, but a losing concurrent recover may reject rather than return the
|
|
102
|
+
* live agent.
|
|
103
|
+
*
|
|
104
|
+
* @throws `AGENT_NOT_FOUND` if the id is unknown to the SDK — neither live
|
|
105
|
+
* nor present in {@link getRestoreFailures} — or if its durable identity
|
|
106
|
+
* record is missing (e.g. a concurrent {@link destroyAgent} removed it).
|
|
107
|
+
* The method never fabricates an agent for an unknown id (the caller
|
|
108
|
+
* relies on this to gate recovery).
|
|
109
|
+
*/
|
|
110
|
+
recoverAgent(agentId: string, options?: {
|
|
111
|
+
abortSignal?: AbortSignal;
|
|
112
|
+
}): Promise<Agent>;
|
|
68
113
|
/**
|
|
69
114
|
* Returns the live {@link Agent} for the given id.
|
|
70
115
|
*
|
|
@@ -126,7 +171,8 @@ export interface AgentManager<H extends AgentHarness = AgentHarness> {
|
|
|
126
171
|
* Returns a snapshot of the boot-time restore failures the SDK has not
|
|
127
172
|
* yet been told to forget. Each entry is cleared on a successful
|
|
128
173
|
* {@link destroyAgent} for the same id; recreating an agent with the
|
|
129
|
-
* same id via {@link createAgent}
|
|
174
|
+
* same id via {@link createAgent}, or recovering it via
|
|
175
|
+
* {@link recoverAgent}, also clears the entry.
|
|
130
176
|
*/
|
|
131
177
|
getRestoreFailures(): RestoreFailure[];
|
|
132
178
|
}
|
|
@@ -167,7 +213,7 @@ export declare class DefaultAgentManager<H extends AgentHarness = AgentHarness>
|
|
|
167
213
|
* is private, so this is the only way to obtain an instance, but
|
|
168
214
|
* consumers should always go through {@link createAgentManager}.
|
|
169
215
|
*/
|
|
170
|
-
static __build<H extends AgentHarness>(harness: H, harnessSupportedProviderHints: readonly ProviderHint[], agentConnectivityResolver: AgentConnectivityResolver, resolvers: AgentRuntimeResolvers, storageRootFolder: string, agentIdGenerator: UniqueIDGenerator, clock: Clock, logBus: LogBus): Promise<DefaultAgentManager<H>>;
|
|
216
|
+
static __build<H extends AgentHarness>(harness: H, harnessSupportedProviderHints: readonly ProviderHint[], agentConnectivityResolver: AgentConnectivityResolver, resolvers: AgentRuntimeResolvers, storageRootFolder: string, agentIdGenerator: UniqueIDGenerator, clock: Clock, logBus: LogBus, onBootLog?: (record: LogRecord) => void): Promise<DefaultAgentManager<H>>;
|
|
171
217
|
private init;
|
|
172
218
|
shutdown(): Promise<void>;
|
|
173
219
|
createAgent(projectRoot: string, config?: ConfigOf<H> & {
|
|
@@ -176,6 +222,9 @@ export declare class DefaultAgentManager<H extends AgentHarness = AgentHarness>
|
|
|
176
222
|
abortSignal?: AbortSignal;
|
|
177
223
|
consumerMetadata?: JsonValue;
|
|
178
224
|
}): Promise<Agent>;
|
|
225
|
+
recoverAgent(agentId: string, options?: {
|
|
226
|
+
abortSignal?: AbortSignal;
|
|
227
|
+
}): Promise<Agent>;
|
|
179
228
|
/**
|
|
180
229
|
* Shared install path for {@link createAgent} and the boot-time restore
|
|
181
230
|
* loop. Resolves connectivity, calls `harness.createAgent`, constructs
|
|
@@ -226,6 +275,10 @@ export declare class DefaultAgentManager<H extends AgentHarness = AgentHarness>
|
|
|
226
275
|
* resolved bag threads through to `AgentHarness.createAgent`'s
|
|
227
276
|
* `options.hooks` and reaches the harness's native seam (Claude
|
|
228
277
|
* `PostToolUse`, Mastra `processInputStep`).
|
|
278
|
+
* - `onBootLog` — observes logs emitted *during construction only* (boot-time
|
|
279
|
+
* restore failures + identity-store warnings), then detaches. It exists
|
|
280
|
+
* because {@link AgentManager.onLog} can't be attached until this function
|
|
281
|
+
* returns — too late for those. See the `onBootLog` option doc below.
|
|
229
282
|
*
|
|
230
283
|
* @throws {AgentSDKError} `INCOMPATIBLE_HARNESS` when either the factory or
|
|
231
284
|
* the constructed harness reports a `protocolVersion` outside
|
|
@@ -242,4 +295,24 @@ export declare function createAgentManager<H extends AgentHarness = AgentHarness
|
|
|
242
295
|
* third-party remote MCP servers.
|
|
243
296
|
*/
|
|
244
297
|
mcpAuthProviderResolver?: McpAuthProviderResolver;
|
|
298
|
+
/**
|
|
299
|
+
* Observes log records emitted *during construction* — and only then.
|
|
300
|
+
* Boot-time restore runs inside this function (before it returns) and
|
|
301
|
+
* emits an `agent restore failed` record for every persisted agent that
|
|
302
|
+
* fails to replay, plus `AgentIdentityStore` warnings (corrupt record
|
|
303
|
+
* JSON, harness-id mismatch). Those are dropped today:
|
|
304
|
+
* {@link AgentManager.onLog} can only be attached *after* this function
|
|
305
|
+
* returns, and `LogBus` has no replay, so a late subscriber never sees
|
|
306
|
+
* the construction-time records.
|
|
307
|
+
*
|
|
308
|
+
* This callback is subscribed before the restore pass and **detached as
|
|
309
|
+
* soon as construction finishes**, so it observes exactly the pre-return
|
|
310
|
+
* window and never overlaps {@link AgentManager.onLog}. A host that
|
|
311
|
+
* bridges SDK logs to its own logger wires `onBootLog` for the boot
|
|
312
|
+
* window and `manager.onLog` for the runtime window — two disjoint
|
|
313
|
+
* sources into the same sink, no double-logging. Unlike
|
|
314
|
+
* `manager.onLog`, this returns no `Unsubscribe`; its lifetime is fixed
|
|
315
|
+
* to construction.
|
|
316
|
+
*/
|
|
317
|
+
onBootLog?: (record: LogRecord) => void;
|
|
245
318
|
}): Promise<AgentManager<H>>;
|
package/dist/agent-manager.js
CHANGED
|
@@ -76,11 +76,25 @@ export class DefaultAgentManager {
|
|
|
76
76
|
* is private, so this is the only way to obtain an instance, but
|
|
77
77
|
* consumers should always go through {@link createAgentManager}.
|
|
78
78
|
*/
|
|
79
|
-
static async __build(harness, harnessSupportedProviderHints, agentConnectivityResolver, resolvers, storageRootFolder, agentIdGenerator, clock, logBus) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
static async __build(harness, harnessSupportedProviderHints, agentConnectivityResolver, resolvers, storageRootFolder, agentIdGenerator, clock, logBus, onBootLog) {
|
|
80
|
+
// Subscribe `onBootLog` FIRST — before constructing anything that takes `logBus` — so it
|
|
81
|
+
// observes the ENTIRE construction window (identity-store reads + boot-time restore inside
|
|
82
|
+
// `init()`), then detach in `finally`. Keep it at the top: a caller can't reach
|
|
83
|
+
// `manager.onLog()` until this method returns, by which point construction has finished
|
|
84
|
+
// emitting and `LogBus` has no replay — so those records would otherwise be lost. Because
|
|
85
|
+
// the window closes exactly when construction does, `onBootLog` never overlaps a later
|
|
86
|
+
// `manager.onLog()`. (Nothing emits on `logBus` from a constructor today, but subscribing
|
|
87
|
+
// first means it stays correct if that ever changes.)
|
|
88
|
+
const unsubscribeBootLog = onBootLog ? logBus.on(onBootLog) : undefined;
|
|
89
|
+
try {
|
|
90
|
+
const identityStore = new AgentIdentityStore(storageRootFolder, harness.harnessId, logBus);
|
|
91
|
+
const manager = new DefaultAgentManager(harness, harnessSupportedProviderHints, agentConnectivityResolver, resolvers, identityStore, agentIdGenerator, clock, logBus);
|
|
92
|
+
await manager.init();
|
|
93
|
+
return manager;
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
unsubscribeBootLog?.();
|
|
97
|
+
}
|
|
84
98
|
}
|
|
85
99
|
async init() {
|
|
86
100
|
const records = await this.identityStore.list();
|
|
@@ -161,6 +175,61 @@ export class DefaultAgentManager {
|
|
|
161
175
|
this.restoreFailures = this.restoreFailures.filter((f) => f.agentId !== agentId);
|
|
162
176
|
return agent;
|
|
163
177
|
}
|
|
178
|
+
async recoverAgent(agentId, options) {
|
|
179
|
+
this.assertNotDisposed();
|
|
180
|
+
// Idempotent: an already-live id is a no-op — return the existing handle rather than
|
|
181
|
+
// constructing a second agent over the same id. (The wedge this method exists to fix
|
|
182
|
+
// only ever leaves a restore-failure placeholder, never a live duplicate.)
|
|
183
|
+
const live = this.agents.get(agentId);
|
|
184
|
+
if (live) {
|
|
185
|
+
return live;
|
|
186
|
+
}
|
|
187
|
+
// Recovery applies only to an id the SDK recorded as a boot-restore failure. An id that
|
|
188
|
+
// is neither live nor a restore-failure is unknown — surface AGENT_NOT_FOUND rather than
|
|
189
|
+
// fabricating an agent. The service's governance-bypass gate depends on this: an
|
|
190
|
+
// SDK-unknown `error` placeholder must not be silently healed.
|
|
191
|
+
const failure = this.restoreFailures.find((f) => f.agentId === agentId);
|
|
192
|
+
if (!failure) {
|
|
193
|
+
throw new AgentSDKError(`No Agent found with id: "${agentId}"`, AgentSDKErrorType.AGENT_NOT_FOUND);
|
|
194
|
+
}
|
|
195
|
+
// Recovery re-establishes an EXISTING identity, so it re-installs from the durable on-disk
|
|
196
|
+
// record — and, unlike `createAgent`, never writes one. Read it fresh (this also carries
|
|
197
|
+
// `consumerMetadata`, which `RestoreFailure` does not). If the file is gone — a concurrent
|
|
198
|
+
// `destroyAgent` removed it during this call, or it was deleted out of band — recovery has
|
|
199
|
+
// no durable basis: throw AGENT_NOT_FOUND rather than construct a live-but-unpersisted
|
|
200
|
+
// "ghost" that would evaporate on the next boot (`list()` finds no file), silently undoing
|
|
201
|
+
// the very identity-continuity guarantee this method exists to provide.
|
|
202
|
+
const record = await this.identityStore.read(agentId);
|
|
203
|
+
if (!record) {
|
|
204
|
+
throw new AgentSDKError(`No Agent found with id: "${agentId}"`, AgentSDKErrorType.AGENT_NOT_FOUND);
|
|
205
|
+
}
|
|
206
|
+
// Re-run the shared install path with thread rehydration: connectivity resolves fresh
|
|
207
|
+
// from `~/.sfdx` (a stale-then-fresh token round-trips) and persisted threads re-attach
|
|
208
|
+
// via `restoreSessions`. A failure here throws WITHOUT reaching the clear below, so the
|
|
209
|
+
// restore-failure entry stays re-armable; `installAgent`'s rollback touches only
|
|
210
|
+
// in-memory/harness state, never the identity store, so the persisted files survive.
|
|
211
|
+
const agent = await this.installAgent(agentId, resolve(record.projectRoot), record.config, {
|
|
212
|
+
...(options?.abortSignal !== undefined ? { abortSignal: options.abortSignal } : {}),
|
|
213
|
+
rehydrateThreads: true,
|
|
214
|
+
consumerMetadata: record.consumerMetadata,
|
|
215
|
+
});
|
|
216
|
+
// TOCTOU guard for the install-window race: a concurrent `destroyAgent` (which, for a
|
|
217
|
+
// restore-failure-only id, splices the failure entry and deletes the file) — or another
|
|
218
|
+
// verb that consumed this id — may have run during `installAgent`'s awaits. If our exact
|
|
219
|
+
// failure entry is gone, that op won: undo the install we just did rather than leave a
|
|
220
|
+
// live agent with no persisted record. `.includes(failure)` matches by object identity,
|
|
221
|
+
// which survives the `.filter(...)` reassignments and `destroyAgent`'s `.splice(...)`.
|
|
222
|
+
// (Concurrent same-id `recoverAgent` calls are the caller's responsibility to serialize;
|
|
223
|
+
// the loser may see the harness's "already registered" error, but state never corrupts.)
|
|
224
|
+
if (!this.restoreFailures.includes(failure)) {
|
|
225
|
+
await this.rollbackInstall(agentId, agent);
|
|
226
|
+
throw new AgentSDKError(`No Agent found with id: "${agentId}"`, AgentSDKErrorType.AGENT_NOT_FOUND);
|
|
227
|
+
}
|
|
228
|
+
// Success clears the now-stale restore-failure entry (mirrors createAgent's clear at the
|
|
229
|
+
// top of this class). The id now answers `getAgent` / `getAgentIds`.
|
|
230
|
+
this.restoreFailures = this.restoreFailures.filter((f) => f.agentId !== agentId);
|
|
231
|
+
return agent;
|
|
232
|
+
}
|
|
164
233
|
/**
|
|
165
234
|
* Shared install path for {@link createAgent} and the boot-time restore
|
|
166
235
|
* loop. Resolves connectivity, calls `harness.createAgent`, constructs
|
|
@@ -331,6 +400,10 @@ export class DefaultAgentManager {
|
|
|
331
400
|
* resolved bag threads through to `AgentHarness.createAgent`'s
|
|
332
401
|
* `options.hooks` and reaches the harness's native seam (Claude
|
|
333
402
|
* `PostToolUse`, Mastra `processInputStep`).
|
|
403
|
+
* - `onBootLog` — observes logs emitted *during construction only* (boot-time
|
|
404
|
+
* restore failures + identity-store warnings), then detaches. It exists
|
|
405
|
+
* because {@link AgentManager.onLog} can't be attached until this function
|
|
406
|
+
* returns — too late for those. See the `onBootLog` option doc below.
|
|
334
407
|
*
|
|
335
408
|
* @throws {AgentSDKError} `INCOMPATIBLE_HARNESS` when either the factory or
|
|
336
409
|
* the constructed harness reports a `protocolVersion` outside
|
|
@@ -373,9 +446,9 @@ export async function createAgentManager(storageRootFolder, harnessFactory, opti
|
|
|
373
446
|
}
|
|
374
447
|
const agentConnectivityResolver = options?.connectivityResolver ?? new DefaultAgentConnectivityResolver();
|
|
375
448
|
const clock = new RealClock();
|
|
376
|
-
return DefaultAgentManager.__build(harness, harnessFactory.supportedProviderHints, agentConnectivityResolver, { hooksForAgent: options?.hooksForAgent, mcpAuthProviderResolver: options?.mcpAuthProviderResolver }, storageRootFolder, new UUIDGenerator(), clock,
|
|
377
449
|
// The manager's root log bus shares the manager clock and self-reads the process environment context.
|
|
378
|
-
new LogBus(clock)
|
|
450
|
+
const logBus = new LogBus(clock);
|
|
451
|
+
return DefaultAgentManager.__build(harness, harnessFactory.supportedProviderHints, agentConnectivityResolver, { hooksForAgent: options?.hooksForAgent, mcpAuthProviderResolver: options?.mcpAuthProviderResolver }, storageRootFolder, new UUIDGenerator(), clock, logBus, options?.onBootLog);
|
|
379
452
|
}
|
|
380
453
|
function isSupportedProtocolVersion(version) {
|
|
381
454
|
return (typeof version === 'number' &&
|
|
@@ -39,5 +39,23 @@ export declare class AgentIdentityStore {
|
|
|
39
39
|
private writeImmediate;
|
|
40
40
|
remove(agentId: string): Promise<void>;
|
|
41
41
|
list(): Promise<AgentIdentityRecord[]>;
|
|
42
|
+
/**
|
|
43
|
+
* Reads a single persisted record by agent id, applying the same
|
|
44
|
+
* parse/validate/harness-match gate as {@link list}. Returns `undefined` if
|
|
45
|
+
* the file is absent, unreadable, corrupt, missing required fields, or was
|
|
46
|
+
* written by a different harness (each soft skip warns via `LogBus`, matching
|
|
47
|
+
* `list()`).
|
|
48
|
+
*
|
|
49
|
+
* Used by `AgentManager.recoverAgent` to reconstruct a restore-failed agent
|
|
50
|
+
* from durable on-disk state — including `consumerMetadata`, which
|
|
51
|
+
* `RestoreFailure` does not carry.
|
|
52
|
+
*/
|
|
53
|
+
read(agentId: string): Promise<AgentIdentityRecord | undefined>;
|
|
54
|
+
/**
|
|
55
|
+
* Shared parse + field-validation + harness-match gate for {@link list} and
|
|
56
|
+
* {@link read}. Returns the validated record, or `undefined` (with a
|
|
57
|
+
* `LogBus.warn`) for a corrupt / incomplete / foreign-harness file.
|
|
58
|
+
*/
|
|
59
|
+
private parseAndValidate;
|
|
42
60
|
private dir;
|
|
43
61
|
}
|
|
@@ -99,10 +99,9 @@ export class AgentIdentityStore {
|
|
|
99
99
|
if (!entry.endsWith('.json'))
|
|
100
100
|
continue;
|
|
101
101
|
const filePath = join(this.dir(), entry);
|
|
102
|
-
let
|
|
102
|
+
let raw;
|
|
103
103
|
try {
|
|
104
|
-
|
|
105
|
-
parsed = JSON.parse(raw);
|
|
104
|
+
raw = await readFile(filePath, 'utf8');
|
|
106
105
|
}
|
|
107
106
|
catch (err) {
|
|
108
107
|
this.logBus.warn('skipping unreadable persisted agent identity file', {
|
|
@@ -111,32 +110,81 @@ export class AgentIdentityStore {
|
|
|
111
110
|
});
|
|
112
111
|
continue;
|
|
113
112
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
113
|
+
const record = this.parseAndValidate(filePath, raw);
|
|
114
|
+
if (record)
|
|
115
|
+
records.push(record);
|
|
116
|
+
}
|
|
117
|
+
return records;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Reads a single persisted record by agent id, applying the same
|
|
121
|
+
* parse/validate/harness-match gate as {@link list}. Returns `undefined` if
|
|
122
|
+
* the file is absent, unreadable, corrupt, missing required fields, or was
|
|
123
|
+
* written by a different harness (each soft skip warns via `LogBus`, matching
|
|
124
|
+
* `list()`).
|
|
125
|
+
*
|
|
126
|
+
* Used by `AgentManager.recoverAgent` to reconstruct a restore-failed agent
|
|
127
|
+
* from durable on-disk state — including `consumerMetadata`, which
|
|
128
|
+
* `RestoreFailure` does not carry.
|
|
129
|
+
*/
|
|
130
|
+
async read(agentId) {
|
|
131
|
+
const filePath = join(this.dir(), `${agentId}.json`);
|
|
132
|
+
let raw;
|
|
133
|
+
try {
|
|
134
|
+
raw = await readFile(filePath, 'utf8');
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
if (err.code === 'ENOENT')
|
|
138
|
+
return undefined;
|
|
139
|
+
this.logBus.warn('skipping unreadable persisted agent identity file', {
|
|
140
|
+
filePath,
|
|
141
|
+
error: getErrorMessage(err),
|
|
142
|
+
});
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
return this.parseAndValidate(filePath, raw);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Shared parse + field-validation + harness-match gate for {@link list} and
|
|
149
|
+
* {@link read}. Returns the validated record, or `undefined` (with a
|
|
150
|
+
* `LogBus.warn`) for a corrupt / incomplete / foreign-harness file.
|
|
151
|
+
*/
|
|
152
|
+
parseAndValidate(filePath, raw) {
|
|
153
|
+
let parsed;
|
|
154
|
+
try {
|
|
155
|
+
parsed = JSON.parse(raw);
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
this.logBus.warn('skipping unreadable persisted agent identity file', {
|
|
159
|
+
filePath,
|
|
160
|
+
error: getErrorMessage(err),
|
|
161
|
+
});
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
if (!parsed.agentId || !parsed.projectRoot || !parsed.config || !parsed.harnessId) {
|
|
165
|
+
// `harnessId` is in the missing-fields gate (not the harness-mismatch gate below)
|
|
166
|
+
// so a record without it produces a "missing required fields" warn rather than
|
|
167
|
+
// a confusing "different harness" warn with `recordHarnessId: undefined`.
|
|
168
|
+
this.logBus.warn('skipping persisted agent identity file with missing required fields', {
|
|
169
|
+
filePath,
|
|
170
|
+
});
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
if (parsed.harnessId !== this.harnessId) {
|
|
174
|
+
this.logBus.warn('skipping persisted agent identity file from a different harness', {
|
|
175
|
+
filePath,
|
|
133
176
|
agentId: parsed.agentId,
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
...(parsed.consumerMetadata !== undefined ? { consumerMetadata: parsed.consumerMetadata } : {}),
|
|
177
|
+
recordHarnessId: parsed.harnessId,
|
|
178
|
+
currentHarnessId: this.harnessId,
|
|
137
179
|
});
|
|
180
|
+
return undefined;
|
|
138
181
|
}
|
|
139
|
-
return
|
|
182
|
+
return {
|
|
183
|
+
agentId: parsed.agentId,
|
|
184
|
+
projectRoot: parsed.projectRoot,
|
|
185
|
+
config: parsed.config,
|
|
186
|
+
...(parsed.consumerMetadata !== undefined ? { consumerMetadata: parsed.consumerMetadata } : {}),
|
|
187
|
+
};
|
|
140
188
|
}
|
|
141
189
|
dir() {
|
|
142
190
|
return join(this.storageRootFolder, AGENTS_SUBDIR);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/sfdx-agent-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.57.0",
|
|
4
4
|
"description": "Harness-agnostic agentic infrastructure for Salesforce developer experience tooling",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -47,9 +47,9 @@
|
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@eslint/js": "^10.0.1",
|
|
50
|
-
"@salesforce/sfdx-agent-harness-claude": "0.
|
|
51
|
-
"@salesforce/sfdx-agent-harness-mastra": "0.
|
|
52
|
-
"@salesforce/sfdx-agent-harness-openai": "0.
|
|
50
|
+
"@salesforce/sfdx-agent-harness-claude": "0.53.0",
|
|
51
|
+
"@salesforce/sfdx-agent-harness-mastra": "0.56.0",
|
|
52
|
+
"@salesforce/sfdx-agent-harness-openai": "0.22.0",
|
|
53
53
|
"@types/node": "^22.20.1",
|
|
54
54
|
"@vitest/coverage-istanbul": "^4.1.10",
|
|
55
55
|
"@vitest/eslint-plugin": "^1.6.27",
|