@salesforce/sfdx-agent-sdk 0.54.0 → 0.56.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 +29 -27
- package/dist/agent-manager.d.ts +52 -3
- package/dist/agent-manager.js +55 -0
- package/dist/errors.d.ts +1 -0
- package/dist/errors.js +1 -0
- package/dist/harness/agent-harness.d.ts +18 -9
- 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.56.0] - 2026-08-27
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
- **agent-sdk**: recoverAgent re-installs a restore-failed agent with thread rehydration @W-24002660@ ([#769](https://github.com/forcedotcom/agentic-dx/pull/769))
|
|
10
|
+
|
|
11
|
+
## [0.55.0] - 2026-08-25
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
- **harness-mastra**: store session context in a harness-owned per-thread store @W-23632686@ ([#765](https://github.com/forcedotcom/agentic-dx/pull/765))
|
|
15
|
+
|
|
6
16
|
## [0.54.0] - 2026-08-25
|
|
7
17
|
|
|
8
18
|
### Chores
|
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
|
|
|
@@ -173,7 +174,7 @@ A single conversation thread.
|
|
|
173
174
|
| `getContextUsage` | `() => ContextUsage` | Snapshot of how much of the model's context window the most recent turn used. |
|
|
174
175
|
| `addMessages` | `(message: string \| Message[]) => Promise<void>` | Append real transcript messages (`user` / `assistant` / `tool`) to the thread **without requesting an agent response** — the write-only half of a turn. The messages persist, appear in `getMessageHistory()`, and replay to the model as prior conversation on the next `chat()`. Use it to seed earlier turns (e.g. file contents as a user message) before the first live prompt; the SDK equivalent of the service's `POST /messages` with `noReply=true`. **Not** `setSessionContext`: this writes _transcript history_ (visible in `getMessageHistory`, additive); `setSessionContext` writes an _out-of-history overlay object_ (never in history, whole-object replace). `'system'` is not a valid role here — system-level state rides `setSessionContext` / `AgentConfig.instructions`. |
|
|
175
176
|
| `addContext` | `(message: string \| Message[]) => Promise<void>` | **Deprecated** — renamed to `addMessages` (identical signature/behavior); delegates to it. The old name read as a sibling of `setSessionContext`, but the two are distinct channels. Will be removed in a future release; migrate to `addMessages`. |
|
|
176
|
-
| `setSessionContext` | `(content: SessionContext) => Promise<void>` | Replace this session's session-context object in full (whole-object set, not a merge). Persisted per-thread and durable across restart; kept out of message history, so it never appears in `getMessageHistory()`.
|
|
177
|
+
| `setSessionContext` | `(content: SessionContext) => Promise<void>` | Replace this session's session-context object in full (whole-object set, not a merge). Persisted per-thread and durable across restart; kept out of message history, so it never appears in `getMessageHistory()`. Rendering the stored object into the model's system-level context on subsequent turns is delivered per harness (live on Mastra; the remaining harnesses land it in their own follow-ups). Delegates to `AgentHarness.setSessionContext` — see that method's JSDoc for the full delivery/durability/isolation contract. |
|
|
177
178
|
| `getSessionContext` | `() => Promise<SessionContext>` | Read this session's current session-context object. Returns `{}` (an empty object) — never `null` or `undefined` — when nothing has been set on this thread yet, so callers never need a null-check. Unrelated to `getContextUsage()`, which reports context-window token occupancy, not the seeded context object. |
|
|
178
179
|
| `subscribe` | `(callback: (event: ChatEvent) => void) => void` | Register a real-time event listener. |
|
|
179
180
|
| `unsubscribe` | `(callback: (event: ChatEvent) => void) => void` | Remove a listener. |
|
|
@@ -757,20 +758,21 @@ totals, subscribe to `chat-stream-completed` telemetry instead.
|
|
|
757
758
|
The SDK throws `AgentSDKError` for predictable not-found and compatibility conditions. Each error has a `type` property
|
|
758
759
|
from `AgentSDKErrorType`:
|
|
759
760
|
|
|
760
|
-
| Type | Thrown By
|
|
761
|
-
| -------------------------------- |
|
|
762
|
-
| `AGENT_NOT_FOUND` | `AgentManager.getAgent()`, `AgentManager.destroyAgent()`
|
|
763
|
-
| `CHAT_SESSION_NOT_FOUND` | `Agent.getChatSession()`, `Agent.destroyChatSession()`, `Agent.cloneChatSession()`, `Agent.compactChatSession()`
|
|
764
|
-
| `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
|
-
| `DISPOSED` | `Agent` and `ChatSession` methods called after the owner has been destroyed
|
|
766
|
-
| `INCOMPATIBLE_HARNESS` | `createAgentManager()` when the factory advertises an unsupported `protocolVersion`, or the constructed harness reports a `protocolVersion` that differs from the factory's
|
|
767
|
-
| `INVALID_MESSAGE_CONTENT` | `ChatSession.chat()` / harness `stream()` when a message part is not valid as input (a `tool-call`/`tool-result` part, or non-base64-string file data)
|
|
768
|
-
| `MCP_SERVER_DISABLED` | `Agent.reconnectMcpServer()` when the named server is configured with `enabled: false`
|
|
769
|
-
| `MCP_SERVER_NOT_FOUND` | `Agent.reconnectMcpServer()` when the server name is not in the agent's `mcpServers` config
|
|
770
|
-
| `MODEL_NOT_SUPPORTED_BY_HARNESS` | `AgentManager.createAgent()` / `Agent.updateAgentConfig()` (G8 pre-flight) when the resolved `ModelConnectivityInfo.providerHint` isn't in the harness's `supportedProviderHints`. Surfaces before any harness work runs (no MCP discovery, no subprocess spawn, no language-model construction) so the consumer can branch cleanly on `err.type` and recover without resource cleanup.
|
|
771
|
-
| `MULTIMODAL_NOT_SUPPORTED` | `ChatSession.chat()` / harness `stream()` when a file fails pre-stream capability validation (unsupported format, too large, or too many files)
|
|
772
|
-
| `NOT_SUPPORTED` | `ApiKeyConnectivityResolver.resolve()` when the consumer-supplied `getApiKey` returns an empty / nullish value. Surfaces locally so the consumer sees "the resolver returned an empty key" rather than chasing a 401 through provider logs after `Authorization: Bearer ` (no key) lands on the wire.
|
|
773
|
-
| `
|
|
761
|
+
| Type | Thrown By |
|
|
762
|
+
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
763
|
+
| `AGENT_NOT_FOUND` | `AgentManager.getAgent()`, `AgentManager.destroyAgent()`, `AgentManager.recoverAgent()` (id neither live nor a restore-failure) |
|
|
764
|
+
| `CHAT_SESSION_NOT_FOUND` | `Agent.getChatSession()`, `Agent.destroyChatSession()`, `Agent.cloneChatSession()`, `Agent.compactChatSession()` |
|
|
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. |
|
|
766
|
+
| `DISPOSED` | `Agent` and `ChatSession` methods called after the owner has been destroyed |
|
|
767
|
+
| `INCOMPATIBLE_HARNESS` | `createAgentManager()` when the factory advertises an unsupported `protocolVersion`, or the constructed harness reports a `protocolVersion` that differs from the factory's |
|
|
768
|
+
| `INVALID_MESSAGE_CONTENT` | `ChatSession.chat()` / harness `stream()` when a message part is not valid as input (a `tool-call`/`tool-result` part, or non-base64-string file data); also `ChatSession.setSessionContext()` / harness `setSessionContext()` when the object exceeds a harness's size / nesting bounds (Mastra: 256 KiB serialized, depth 200). `getSessionContext()` never throws on a corrupt stored slot — it soft-skips to `{}` and logs. |
|
|
769
|
+
| `MCP_SERVER_DISABLED` | `Agent.reconnectMcpServer()` when the named server is configured with `enabled: false` |
|
|
770
|
+
| `MCP_SERVER_NOT_FOUND` | `Agent.reconnectMcpServer()` when the server name is not in the agent's `mcpServers` config |
|
|
771
|
+
| `MODEL_NOT_SUPPORTED_BY_HARNESS` | `AgentManager.createAgent()` / `Agent.updateAgentConfig()` (G8 pre-flight) when the resolved `ModelConnectivityInfo.providerHint` isn't in the harness's `supportedProviderHints`. Surfaces before any harness work runs (no MCP discovery, no subprocess spawn, no language-model construction) so the consumer can branch cleanly on `err.type` and recover without resource cleanup. |
|
|
772
|
+
| `MULTIMODAL_NOT_SUPPORTED` | `ChatSession.chat()` / harness `stream()` when a file fails pre-stream capability validation (unsupported format, too large, or too many files) |
|
|
773
|
+
| `NOT_SUPPORTED` | `ApiKeyConnectivityResolver.resolve()` when the consumer-supplied `getApiKey` returns an empty / nullish value. Surfaces locally so the consumer sees "the resolver returned an empty key" rather than chasing a 401 through provider logs after `Authorization: Bearer ` (no key) lands on the wire. |
|
|
774
|
+
| `THREAD_NOT_FOUND` | `ChatSession.setSessionContext()` / harness `setSessionContext()` when seeding context on a thread that was never created (create-first contract). A `ChatSession` is only reachable via `createChatSession()` → `createThread`, so this indicates a seed-before-create misuse; the harness maps its runtime "thread not found" to this typed error. |
|
|
775
|
+
| `TOOL_CALL_NOT_FOUND` | `ChatSession.approveToolCall()` / `declineToolCall()` / `submitToolResult()` when the supplied `toolCallId` doesn't match any pending tool-call request on the current session. Typically indicates a wrong-id, wrong-session, or already-settled call. Both production harnesses (Mastra, Claude) throw this from their per-turn approval coordinator. |
|
|
774
776
|
|
|
775
777
|
```typescript
|
|
776
778
|
import { AgentSDKError, AgentSDKErrorType } from '@salesforce/sfdx-agent-sdk';
|
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
|
}
|
|
@@ -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
|
package/dist/agent-manager.js
CHANGED
|
@@ -161,6 +161,61 @@ export class DefaultAgentManager {
|
|
|
161
161
|
this.restoreFailures = this.restoreFailures.filter((f) => f.agentId !== agentId);
|
|
162
162
|
return agent;
|
|
163
163
|
}
|
|
164
|
+
async recoverAgent(agentId, options) {
|
|
165
|
+
this.assertNotDisposed();
|
|
166
|
+
// Idempotent: an already-live id is a no-op — return the existing handle rather than
|
|
167
|
+
// constructing a second agent over the same id. (The wedge this method exists to fix
|
|
168
|
+
// only ever leaves a restore-failure placeholder, never a live duplicate.)
|
|
169
|
+
const live = this.agents.get(agentId);
|
|
170
|
+
if (live) {
|
|
171
|
+
return live;
|
|
172
|
+
}
|
|
173
|
+
// Recovery applies only to an id the SDK recorded as a boot-restore failure. An id that
|
|
174
|
+
// is neither live nor a restore-failure is unknown — surface AGENT_NOT_FOUND rather than
|
|
175
|
+
// fabricating an agent. The service's governance-bypass gate depends on this: an
|
|
176
|
+
// SDK-unknown `error` placeholder must not be silently healed.
|
|
177
|
+
const failure = this.restoreFailures.find((f) => f.agentId === agentId);
|
|
178
|
+
if (!failure) {
|
|
179
|
+
throw new AgentSDKError(`No Agent found with id: "${agentId}"`, AgentSDKErrorType.AGENT_NOT_FOUND);
|
|
180
|
+
}
|
|
181
|
+
// Recovery re-establishes an EXISTING identity, so it re-installs from the durable on-disk
|
|
182
|
+
// record — and, unlike `createAgent`, never writes one. Read it fresh (this also carries
|
|
183
|
+
// `consumerMetadata`, which `RestoreFailure` does not). If the file is gone — a concurrent
|
|
184
|
+
// `destroyAgent` removed it during this call, or it was deleted out of band — recovery has
|
|
185
|
+
// no durable basis: throw AGENT_NOT_FOUND rather than construct a live-but-unpersisted
|
|
186
|
+
// "ghost" that would evaporate on the next boot (`list()` finds no file), silently undoing
|
|
187
|
+
// the very identity-continuity guarantee this method exists to provide.
|
|
188
|
+
const record = await this.identityStore.read(agentId);
|
|
189
|
+
if (!record) {
|
|
190
|
+
throw new AgentSDKError(`No Agent found with id: "${agentId}"`, AgentSDKErrorType.AGENT_NOT_FOUND);
|
|
191
|
+
}
|
|
192
|
+
// Re-run the shared install path with thread rehydration: connectivity resolves fresh
|
|
193
|
+
// from `~/.sfdx` (a stale-then-fresh token round-trips) and persisted threads re-attach
|
|
194
|
+
// via `restoreSessions`. A failure here throws WITHOUT reaching the clear below, so the
|
|
195
|
+
// restore-failure entry stays re-armable; `installAgent`'s rollback touches only
|
|
196
|
+
// in-memory/harness state, never the identity store, so the persisted files survive.
|
|
197
|
+
const agent = await this.installAgent(agentId, resolve(record.projectRoot), record.config, {
|
|
198
|
+
...(options?.abortSignal !== undefined ? { abortSignal: options.abortSignal } : {}),
|
|
199
|
+
rehydrateThreads: true,
|
|
200
|
+
consumerMetadata: record.consumerMetadata,
|
|
201
|
+
});
|
|
202
|
+
// TOCTOU guard for the install-window race: a concurrent `destroyAgent` (which, for a
|
|
203
|
+
// restore-failure-only id, splices the failure entry and deletes the file) — or another
|
|
204
|
+
// verb that consumed this id — may have run during `installAgent`'s awaits. If our exact
|
|
205
|
+
// failure entry is gone, that op won: undo the install we just did rather than leave a
|
|
206
|
+
// live agent with no persisted record. `.includes(failure)` matches by object identity,
|
|
207
|
+
// which survives the `.filter(...)` reassignments and `destroyAgent`'s `.splice(...)`.
|
|
208
|
+
// (Concurrent same-id `recoverAgent` calls are the caller's responsibility to serialize;
|
|
209
|
+
// the loser may see the harness's "already registered" error, but state never corrupts.)
|
|
210
|
+
if (!this.restoreFailures.includes(failure)) {
|
|
211
|
+
await this.rollbackInstall(agentId, agent);
|
|
212
|
+
throw new AgentSDKError(`No Agent found with id: "${agentId}"`, AgentSDKErrorType.AGENT_NOT_FOUND);
|
|
213
|
+
}
|
|
214
|
+
// Success clears the now-stale restore-failure entry (mirrors createAgent's clear at the
|
|
215
|
+
// top of this class). The id now answers `getAgent` / `getAgentIds`.
|
|
216
|
+
this.restoreFailures = this.restoreFailures.filter((f) => f.agentId !== agentId);
|
|
217
|
+
return agent;
|
|
218
|
+
}
|
|
164
219
|
/**
|
|
165
220
|
* Shared install path for {@link createAgent} and the boot-time restore
|
|
166
221
|
* loop. Resolves connectivity, calls `harness.createAgent`, constructs
|
package/dist/errors.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export declare const AgentSDKErrorType: {
|
|
|
11
11
|
readonly MODEL_NOT_SUPPORTED_BY_HARNESS: 'MODEL_NOT_SUPPORTED_BY_HARNESS';
|
|
12
12
|
readonly MULTIMODAL_NOT_SUPPORTED: 'MULTIMODAL_NOT_SUPPORTED';
|
|
13
13
|
readonly NOT_SUPPORTED: 'NOT_SUPPORTED';
|
|
14
|
+
readonly THREAD_NOT_FOUND: 'THREAD_NOT_FOUND';
|
|
14
15
|
readonly TOOL_CALL_NOT_FOUND: 'TOOL_CALL_NOT_FOUND';
|
|
15
16
|
};
|
|
16
17
|
export type AgentSDKErrorType = (typeof AgentSDKErrorType)[keyof typeof AgentSDKErrorType];
|
package/dist/errors.js
CHANGED
|
@@ -15,6 +15,7 @@ export const AgentSDKErrorType = {
|
|
|
15
15
|
MODEL_NOT_SUPPORTED_BY_HARNESS: 'MODEL_NOT_SUPPORTED_BY_HARNESS',
|
|
16
16
|
MULTIMODAL_NOT_SUPPORTED: 'MULTIMODAL_NOT_SUPPORTED',
|
|
17
17
|
NOT_SUPPORTED: 'NOT_SUPPORTED',
|
|
18
|
+
THREAD_NOT_FOUND: 'THREAD_NOT_FOUND',
|
|
18
19
|
TOOL_CALL_NOT_FOUND: 'TOOL_CALL_NOT_FOUND',
|
|
19
20
|
};
|
|
20
21
|
export class AgentSDKError extends Error {
|
|
@@ -510,14 +510,15 @@ export interface AgentHarness {
|
|
|
510
510
|
* typically the service layer's read-merge-write over {@link getSessionContext}).
|
|
511
511
|
*
|
|
512
512
|
* The full contract lands in two stages. Persistence is implemented on every
|
|
513
|
-
* harness in the contract-and-persistence milestone (W-23632685
|
|
514
|
-
*
|
|
515
|
-
*
|
|
516
|
-
* W-23632694)
|
|
517
|
-
* assertions, which register only once a
|
|
518
|
-
* render/carry-forward adapter
|
|
519
|
-
*
|
|
520
|
-
* harness
|
|
513
|
+
* harness in the contract-and-persistence milestone (W-23632685). Model
|
|
514
|
+
* delivery and `compactThread` carry-forward are implemented per harness:
|
|
515
|
+
* **Mastra is live (W-23632686)**; Claude (W-23632691) and OpenAI
|
|
516
|
+
* (W-23632694) are pending. They are pinned by the seam-gated
|
|
517
|
+
* `runSessionContextConformance` assertions, which register only once a
|
|
518
|
+
* harness supplies its render/carry-forward adapter (live for Mastra,
|
|
519
|
+
* `it.todo` for the harnesses still pending). The staged bullets below state
|
|
520
|
+
* the contract each harness converges on — not behavior guaranteed live on a
|
|
521
|
+
* harness before its delivery work lands.
|
|
521
522
|
*
|
|
522
523
|
* Live on every harness this milestone:
|
|
523
524
|
* - Persists per-thread, durably (survives harness restart against the same
|
|
@@ -536,7 +537,15 @@ export interface AgentHarness {
|
|
|
536
537
|
* does not exist yet, so the "must not clobber" half is pinned by a
|
|
537
538
|
* seam-gated assertion that goes live alongside it.
|
|
538
539
|
*
|
|
539
|
-
*
|
|
540
|
+
* Enforcement runs ahead on Mastra: it rejects a set on a never-created
|
|
541
|
+
* thread with `THREAD_NOT_FOUND` (create-first) and an oversized / too-deeply
|
|
542
|
+
* nested object with `INVALID_MESSAGE_CONTENT` pre-write. Claude and OpenAI do
|
|
543
|
+
* not enforce these yet (no create-first check, no size/depth cap), so a
|
|
544
|
+
* consumer targeting cross-harness portability should not rely on either being
|
|
545
|
+
* enforced until those harnesses' work lands.
|
|
546
|
+
*
|
|
547
|
+
* Staged per harness (live on Mastra, pending on Claude/OpenAI above;
|
|
548
|
+
* conformance-pinned):
|
|
540
549
|
* - Delivered to the model as system-level context on every subsequent turn
|
|
541
550
|
* on this thread, rendered deterministically (stable key order) so two
|
|
542
551
|
* calls with the same object produce byte-identical rendered text.
|
|
@@ -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.56.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.52.0",
|
|
51
|
+
"@salesforce/sfdx-agent-harness-mastra": "0.55.0",
|
|
52
|
+
"@salesforce/sfdx-agent-harness-openai": "0.21.0",
|
|
53
53
|
"@types/node": "^22.20.1",
|
|
54
54
|
"@vitest/coverage-istanbul": "^4.1.10",
|
|
55
55
|
"@vitest/eslint-plugin": "^1.6.27",
|