Node SDK#
Install packages with the CLI, load tools, and inspect installed agents, skills, and knowledge packages from Node.
Goal#
Install the Node SDK and use:
load()to call a published toolloadAgent()to inspect an installed agent package and its resolved packagesloadSkill()to inspect an installed skill package and its resolved toolsloadKnowledge()to inspect an installed knowledge package and its mode-specific metadata
1) Install a tool#
From your project with an agent manifest:
# declare and install a specific tool version
agentpm install @zack/summarize@0.1.3
# or: edit agent.json → tools[] then run agentpm installThis resolves and prepares the tool under: .agentpm/tools/<namespace>/<name>/<version>
You can also install a published agent package directly:
agentpm install @zack/support-agent@0.1.0That writes:
- the installed agent under
.agentpm/agents/<namespace>/<name>/<version> - its resolved knowledge packages under
.agentpm/knowledge/<namespace>/<name>/<version> - its resolved skills under
.agentpm/skills/<namespace>/<name>/<version> - its resolved tools under
.agentpm/tools/<namespace>/<name>/<version>
You can also install a published skill package directly:
agentpm install @zack/incident-commander@0.1.0That writes:
- the installed skill under
.agentpm/skills/<namespace>/<name>/<version> - its resolved tools under
.agentpm/tools/<namespace>/<name>/<version>
You can also install a published knowledge package directly:
agentpm install @zack/python-docs@0.1.0That writes:
- the installed knowledge package under
.agentpm/knowledge/<namespace>/<name>/<version>
2) Install the Node SDK#
pnpm add @agentpm/sdk
# or: npm i @agentpm/sdk3) The load() function (core API)#
load() resolves the tool and returns a callable bound to a managed subprocess.
Basic usage#
import { load } from '@agentpm/sdk';
// simplest form: just the spec
const summarize = await load('@zack/summarize@0.1.3');
const result = await summarize({
text: 'Cats are elegant, enigmatic creatures.',
});
console.log(result);Type signature (simplified)#
// without metadata
function load(spec: string, opts?: LoadOptions): Promise<(input: JsonValue) => Promise<JsonValue>>;
// with metadata
function load(spec: string, opts: LoadOptions & { withMeta: true }): Promise<{
func: (input: JsonValue) => Promise<JsonValue>;
meta: ToolMeta;
}>;Options#
type LoadOptions = {
withMeta?: boolean; // include manifest metadata in the return value
timeoutMs?: number; // hard cap per-invoke (default: 120_000 ms)
toolDirOverride?: string; // use a custom tool root (tests/local layouts)
env?: Record<string, string>; // merged into the subprocess environment
};Example with environment and timeout#
const capitalize = await load('@zack/capitalize@0.1.11', {
env: {
OPENAI_API_KEY: process.env.OPENAI_API_KEY!, // passed to the subprocess
},
timeoutMs: 30_000,
});
const out = await capitalize({
text:
'Cats are some of the most amazing creatures—loving, curious, and a little wild at heart.',
doKeywords: true,
doSentiment: true,
doSummary: true,
maxSummaryChars: 200,
});4) Loading with metadata#
Ask load() for manifest data you can hand to your agent runtime (for tool descriptions, IO schemas, etc.).
const { func: cap, meta } = await load('@zack/capitalize@0.1.11', { withMeta: true });
/*
meta: {
name: string;
version: string;
description?: string;
inputs?: JsonValue; // JSON Schema (Draft 2020-12)
outputs?: JsonValue; // JSON Schema (Draft 2020-12)
runtime?: { type: 'node'|'python'; version?: string };
}
*/
const out2 = await cap({ text: 'hi' });Using meta to configure an agent tool (example)#
import { load } from '@agentpm/sdk';
// Load two tools and convert to a generic agent tool spec
const tools = await Promise.all([
load('@zack/capitalize@0.1.11', { withMeta: true }),
load('@zack/summarize@0.1.3', { withMeta: true }),
]);
const agentTools = tools.map(({ func, meta }) => ({
name: meta.name,
description: meta.description ?? 'AgentPM tool',
// helpful: concatenate description + IO shapes for better tool selection
longDescription: `${meta.description ?? ''}\n\nInputs: ${JSON.stringify(meta.inputs)}\nOutputs: ${JSON.stringify(meta.outputs)}`,
// the callable your agent will invoke
call: func,
}));
// agentTools can be registered with your agent runtime’s tool loader5) Loading an installed agent package#
Use loadAgent() when you want metadata for a registry-installed agent package plus the concrete package refs it resolved to at install time.
import { load, loadAgent, loadSkill } from '@agentpm/sdk';
const agent = await loadAgent('@zack/support-agent@0.1.0');
const firstSkill = agent.resolvedSkills[0];
const skill = await loadSkill(`${firstSkill.name}@${firstSkill.version}`);
const firstTool = skill.resolvedTools[0];
const tool = await load(`${firstTool.name}@${firstTool.version}`);What loadAgent() returns:
- the installed agent manifest
- the installed agent root path
resolvedKnowledge, which are the exact knowledge package refs represented inagent.lock- reserved refs (
knowledge,memory,profiles) from the lockfile metadata path resolvedTools, which are the exact tool package refs represented inagent.lockresolvedSkills, which are the exact skill package refs represented inagent.lock
Compatibility note:
resolvedKnowledgeis populated from the modern first-classroot.knowledgeentries inagent.lock.reserved.knowledgeis legacy pass-through metadata from older lockfile shapes. For current installs, treatresolvedKnowledgeas authoritative and expectreserved.knowledgeto usually be empty.- If your workspace still has an older lockfile shape where Knowledge refs only exist under
reserved.knowledge, rerunagentpm installto rewrite the lockfile.
What it does not do:
- it does not execute the agent package
- it does not orchestrate the tools for you
- it does not load the local project
./agent.json
6) Loading an installed skill package#
Use loadSkill() when you want metadata for a registry-installed skill package plus the concrete tool refs it resolved to at install time.
import { loadSkill } from '@agentpm/sdk';
const skill = await loadSkill('@zack/incident-commander@0.1.0');
console.log(skill.entrypointPath);
console.log(skill.entrypointContent);
console.log(skill.references);
console.log(skill.scripts);
console.log(skill.resolvedTools);loadSkill() returns an inspectable Skill object. Skills are not runnable SDK objects by themselves.
7) Loading an installed knowledge package#
Use loadKnowledge() when you want metadata for a registry-installed knowledge package plus the canonical package-relative paths it declares.
import { loadKnowledge } from '@agentpm/sdk';
const knowledge = await loadKnowledge('@zack/python-docs@0.1.0');
console.log(knowledge.knowledge.mode);
console.log(knowledge.documentPaths);
console.log(knowledge.chunksPath);
console.log(knowledge.sourcesPath);
console.log(knowledge.vectorsPath);
console.log(knowledge.indexPaths);loadKnowledge() returns an inspectable Knowledge object. Knowledge packages are not runnable SDK objects by themselves.
load() stays tool-only#
import { load } from '@agentpm/sdk';
await load('@zack/incident-commander@0.1.0');
// throws: use loadSkill("@zack/incident-commander@0.1.0") instead
await load('@zack/python-docs@0.1.0');
// throws: use loadKnowledge("@zack/python-docs@0.1.0") instead8) Execution contract (how tools must behave)#
For a tool to work with the SDK, it must follow this process protocol:
- STDIN: SDK writes one JSON object (tool inputs) to the subprocess stdin.
- STDOUT: Tool writes exactly one JSON object (tool outputs) to stdout (typically the last line).
- STDERR: Any logs/diagnostics go to stderr.
- Exit code:
0on success; non-zero indicates failure.
Example Node tool main (pattern):
async function readStdin(): Promise<string> {
const chunks: Buffer[] = [];
for await (const chunk of process.stdin) chunks.push(chunk as Buffer);
return Buffer.concat(chunks).toString('utf8');
}
async function main() {
try {
const raw = await readStdin();
const input = raw.trim() ? JSON.parse(raw) : {};
// ... do work ...
const out = { ok: true };
// IMPORTANT: single JSON object to stdout
process.stdout.write(JSON.stringify(out));
} catch (e: any) {
console.error(e?.stack || String(e)); // stderr
process.exit(1);
}
}
if (import.meta.url === `file://${process.argv[1]}`) main();9) What load() enforces for safety#
- Interpreter allow-list
- Allowed:
node,nodejs,python,python3, or versioned likepython3.11. - Otherwise: throws
Unsupported agent.json.entrypoint.command ....
- Allowed:
- Interpreter on PATH
- Verifies the interpreter can be resolved (checks
PATH, attempts--version).
- Verifies the interpreter can be resolved (checks
- Runtime ↔ entrypoint match (if runtime present)
- Ensures
runtime.typeagrees withentrypoint.command(e.g.,node↔node).
- Ensures
- Timeouts
- Per-call hard cap (
timeoutMs, default 120s).
- Per-call hard cap (
- Environment merging
- Merges manifest
entrypoint.env+load(opts).env+ safe base env.
- Merges manifest
9) How the subprocess is spawned (high level)#
- Working dir (
cwd):entrypoint.cwd(default.) relative to the tool root. - Isolated run dirs: a unique temp
run/folder with dedicatedHOMEandTMPDIR. - Node memory cap: if the interpreter is Node and no cap is present, the SDK injects
--max-old-space-size=256to prevent runaway memory in long processes. - Environment: composed via
buildEnv(entry.env, opts.env, HOME, TMPDIR)to keep the tool isolated yet configurable.
If your tool needs more memory, you can add your own flag in entrypoint.args (e.g., --max-old-space-size=512 for Node).
10) Troubleshooting#
- Interpreter not found
- Ensure
node/pythonis onPATH. The error message prints the PATH searched.
- Ensure
- Timeouts
- Increase
timeoutMsinload()or setentrypoint.timeout_ms(SDK uses the per-call value).
- Increase
- Non-JSON output
- Make sure only one valid JSON object is written to stdout; logs go to stderr.
- Runtime mismatch
- Align
runtime.type(node/python) withentrypoint.command.
- Align
- Local testing / custom layouts
- Use
toolDirOverrideinload()to point to a local unpacked tool directory.
- Use
- Modern package loading requires a current
agent.lockloadAgent()andloadSkill()read modern lockfile shapes. If the lockfile is missing or too old, rerunagentpm install.
- Agent installed on disk but not in the lockfile
loadAgent()uses the installed agent directory and theagent.lockroot entry together. If the lockfile is missing the agent root, reinstall it.
- Skill installed on disk but not in the lockfile
loadSkill()uses the installed skill directory and theagent.lockroot entry together. If the lockfile is missing the skill root, reinstall it.
11) Best practices#
- Define strong IO schemas so agents construct correct arguments.
- Keep logs off stdout; stdout is reserved for the final JSON.
- Set sensible timeouts (
entrypoint.timeout_ms) and document required env vars in your tool README/manifest descriptions.