@salesforce/sfdx-agent-sdk 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +228 -35
- package/dist/agent-manager.d.ts +155 -87
- package/dist/agent-manager.js +161 -110
- package/dist/agent.d.ts +15 -0
- package/dist/agent.js +18 -0
- package/dist/harness/agent-harness.d.ts +45 -1
- package/dist/harness/harness-config.d.ts +10 -0
- package/dist/harness/harness-config.js +10 -0
- package/dist/harness/harness-factory.d.ts +2 -2
- package/dist/harness/index.d.ts +1 -1
- package/dist/index.d.ts +3 -3
- 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/dist/mcp-config.d.ts +64 -1
- package/dist/types/usage.d.ts +3 -1
- package/package.json +18 -11
|
@@ -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/dist/mcp-config.d.ts
CHANGED
|
@@ -43,10 +43,73 @@ export declare enum McpServerStatus {
|
|
|
43
43
|
Disabled = "disabled",
|
|
44
44
|
Error = "error"
|
|
45
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Behavioral / UI-presentation hints for an MCP-discovered tool.
|
|
48
|
+
*
|
|
49
|
+
* Mirrors the MCP protocol's `Tool.annotations` shape
|
|
50
|
+
* (https://spec.modelcontextprotocol.io/specification/server/tools/#tool-annotations)
|
|
51
|
+
* without importing `@modelcontextprotocol/sdk`, keeping this package
|
|
52
|
+
* harness-runtime-free. Each field is optional because MCP servers populate
|
|
53
|
+
* annotations à la carte; absence means "the server did not declare this
|
|
54
|
+
* hint," not "false."
|
|
55
|
+
*/
|
|
56
|
+
export type McpToolAnnotations = {
|
|
57
|
+
/** Human-readable label suitable for UI display (vs. the machine `name`). */
|
|
58
|
+
title?: string;
|
|
59
|
+
/** When `true`, the tool only reads data and has no side effects. */
|
|
60
|
+
readOnlyHint?: boolean;
|
|
61
|
+
/** When `true`, the tool may perform destructive updates to its environment. */
|
|
62
|
+
destructiveHint?: boolean;
|
|
63
|
+
/** When `true`, repeated calls with the same arguments have no additional effect. */
|
|
64
|
+
idempotentHint?: boolean;
|
|
65
|
+
/** When `true`, the tool may interact with an open world of external entities (e.g. the public web). */
|
|
66
|
+
openWorldHint?: boolean;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Runtime metadata for a single MCP-discovered tool.
|
|
70
|
+
*
|
|
71
|
+
* The optional fields are populated when the underlying harness can supply
|
|
72
|
+
* them from its MCP client. A harness whose MCP runtime does not expose a
|
|
73
|
+
* given field leaves it `undefined` — consumers must treat every field
|
|
74
|
+
* except `name` as optional.
|
|
75
|
+
*
|
|
76
|
+
* **Why no `outputSchema` field?** The MCP protocol's `tools/list` response
|
|
77
|
+
* carries an optional `outputSchema` per tool, and a maximally honest mirror
|
|
78
|
+
* of that protocol shape would expose it here. We deliberately do not, because
|
|
79
|
+
* neither harness today can populate it: Mastra's `@mastra/mcp` strips
|
|
80
|
+
* `outputSchema` from each wrapped tool before the harness sees it (a
|
|
81
|
+
* deliberate choice in Mastra's MCP client to keep `CallToolResult`
|
|
82
|
+
* validation correct — passing the schema to `createTool` would cause Zod
|
|
83
|
+
* to strip unrecognized keys from the envelope), and the Claude Agent SDK's
|
|
84
|
+
* MCP status surface omits the field entirely. Adding `outputSchema?` to the
|
|
85
|
+
* SDK contract today would mean shipping a field no harness fills — exactly
|
|
86
|
+
* the "field a consumer should ignore" anti-pattern called out in this
|
|
87
|
+
* package's design principles. If a future harness gains access to
|
|
88
|
+
* `outputSchema` (or one of the existing harnesses adds it), expanding the
|
|
89
|
+
* contract is a non-breaking additive change at that point.
|
|
90
|
+
*/
|
|
91
|
+
export type McpToolInfo = {
|
|
92
|
+
/** Tool name as exposed to the LLM, including any harness-applied namespacing. */
|
|
93
|
+
name: string;
|
|
94
|
+
/** Human-readable description of what the tool does. */
|
|
95
|
+
description?: string;
|
|
96
|
+
/**
|
|
97
|
+
* Tool input parameters as a **JSON Schema** object (the MCP wire format).
|
|
98
|
+
*
|
|
99
|
+
* This is a plain JSON Schema, not a Zod schema. Consumers that want a
|
|
100
|
+
* Zod schema at runtime can convert with a library such as
|
|
101
|
+
* `json-schema-to-zod`; consumers that want runtime validation can feed
|
|
102
|
+
* it to AJV. Typed as `Record<string, unknown>` so this package incurs
|
|
103
|
+
* no `zod` or `@types/json-schema` dependency.
|
|
104
|
+
*/
|
|
105
|
+
inputSchema?: Record<string, unknown>;
|
|
106
|
+
/** Behavioral / UI-presentation hints declared by the MCP server. */
|
|
107
|
+
annotations?: McpToolAnnotations;
|
|
108
|
+
};
|
|
46
109
|
/** Runtime status of a configured MCP server, including its discovered tools. */
|
|
47
110
|
export type McpServerInfo = {
|
|
48
111
|
name: string;
|
|
49
112
|
status: McpServerStatus;
|
|
50
|
-
tools:
|
|
113
|
+
tools: McpToolInfo[];
|
|
51
114
|
error?: string;
|
|
52
115
|
};
|
package/dist/types/usage.d.ts
CHANGED
|
@@ -16,7 +16,9 @@ export type UsageMetadata = {
|
|
|
16
16
|
};
|
|
17
17
|
/**
|
|
18
18
|
* Reason the model stopped generating.
|
|
19
|
-
* Aligned with AI SDK
|
|
19
|
+
* Aligned with AI SDK V3's unified finish-reason set; harnesses normalize provider-specific
|
|
20
|
+
* shapes (e.g. V3's `LanguageModelV3FinishReason` object with `{ unified, raw }`) down to this
|
|
21
|
+
* union so SDK consumers see a stable string regardless of the underlying AI SDK version.
|
|
20
22
|
*
|
|
21
23
|
* - `stop` — The model finished generating naturally (complete response).
|
|
22
24
|
* - `length` — The model hit the maximum output token limit; the response was truncated mid-generation.
|
package/package.json
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/sfdx-agent-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Harness-agnostic agentic infrastructure for Salesforce developer experience tooling",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
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",
|
|
@@ -28,28 +35,28 @@
|
|
|
28
35
|
"LICENSE.txt"
|
|
29
36
|
],
|
|
30
37
|
"dependencies": {
|
|
31
|
-
"@salesforce/agentic-common": "0.
|
|
32
|
-
"@salesforce/llm-gateway-sdk": "0.
|
|
38
|
+
"@salesforce/agentic-common": "0.4.0",
|
|
39
|
+
"@salesforce/llm-gateway-sdk": "0.4.0"
|
|
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.6.0",
|
|
37
44
|
"@types/node": "^22.19.17",
|
|
38
|
-
"@vitest/coverage-istanbul": "^4.1.
|
|
39
|
-
"@vitest/eslint-plugin": "^1.6.
|
|
40
|
-
"eslint": "^10.
|
|
45
|
+
"@vitest/coverage-istanbul": "^4.1.7",
|
|
46
|
+
"@vitest/eslint-plugin": "^1.6.17",
|
|
47
|
+
"eslint": "^10.4.0",
|
|
41
48
|
"eslint-config-prettier": "^10.1.8",
|
|
42
49
|
"eslint-import-resolver-typescript": "^4.4.4",
|
|
43
50
|
"eslint-plugin-import": "^2.32.0",
|
|
44
51
|
"eslint-plugin-n": "^18.0.1",
|
|
45
52
|
"globals": "^17.6.0",
|
|
46
|
-
"lint-staged": "^17.0.
|
|
53
|
+
"lint-staged": "^17.0.5",
|
|
47
54
|
"prettier": "^3.8.3",
|
|
48
55
|
"rimraf": "^6.1.3",
|
|
49
|
-
"tsx": "^4.
|
|
56
|
+
"tsx": "^4.22.3",
|
|
50
57
|
"typescript": "^6.0.3",
|
|
51
|
-
"typescript-eslint": "^8.59.
|
|
52
|
-
"vitest": "^4.1.
|
|
58
|
+
"typescript-eslint": "^8.59.4",
|
|
59
|
+
"vitest": "^4.1.7"
|
|
53
60
|
},
|
|
54
61
|
"engines": {
|
|
55
62
|
"node": ">=22.19.0"
|