@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.
@@ -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
@@ -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: string[];
113
+ tools: McpToolInfo[];
51
114
  error?: string;
52
115
  };
@@ -16,7 +16,9 @@ export type UsageMetadata = {
16
16
  };
17
17
  /**
18
18
  * Reason the model stopped generating.
19
- * Aligned with AI SDK `LanguageModelV2FinishReason`.
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.4.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.3.0",
32
- "@salesforce/llm-gateway-sdk": "0.3.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.4.0",
43
+ "@salesforce/sfdx-agent-harness-mastra": "0.6.0",
37
44
  "@types/node": "^22.19.17",
38
- "@vitest/coverage-istanbul": "^4.1.5",
39
- "@vitest/eslint-plugin": "^1.6.16",
40
- "eslint": "^10.2.1",
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.4",
53
+ "lint-staged": "^17.0.5",
47
54
  "prettier": "^3.8.3",
48
55
  "rimraf": "^6.1.3",
49
- "tsx": "^4.21.0",
56
+ "tsx": "^4.22.3",
50
57
  "typescript": "^6.0.3",
51
- "typescript-eslint": "^8.59.1",
52
- "vitest": "^4.1.5"
58
+ "typescript-eslint": "^8.59.4",
59
+ "vitest": "^4.1.7"
53
60
  },
54
61
  "engines": {
55
62
  "node": ">=22.19.0"