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 tool
  • loadAgent() to inspect an installed agent package and its resolved packages
  • loadSkill() to inspect an installed skill package and its resolved tools
  • loadKnowledge() 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 install

This 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.0

That 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.0

That 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.0

That writes:

  • the installed knowledge package under .agentpm/knowledge/<namespace>/<name>/<version>

2) Install the Node SDK#

pnpm add @agentpm/sdk
# or: npm i @agentpm/sdk

3) 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 loader

5) 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 in agent.lock
  • reserved refs (knowledge, memory, profiles) from the lockfile metadata path
  • resolvedTools, which are the exact tool package refs represented in agent.lock
  • resolvedSkills, which are the exact skill package refs represented in agent.lock

Compatibility note:

  • resolvedKnowledge is populated from the modern first-class root.knowledge entries in agent.lock.
  • reserved.knowledge is legacy pass-through metadata from older lockfile shapes. For current installs, treat resolvedKnowledge as authoritative and expect reserved.knowledge to usually be empty.
  • If your workspace still has an older lockfile shape where Knowledge refs only exist under reserved.knowledge, rerun agentpm install to 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") instead

8) 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: 0 on 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 like python3.11.
    • Otherwise: throws Unsupported agent.json.entrypoint.command ....
  • Interpreter on PATH
    • Verifies the interpreter can be resolved (checks PATH, attempts --version).
  • Runtime ↔ entrypoint match (if runtime present)
    • Ensures runtime.type agrees with entrypoint.command (e.g., nodenode).
  • Timeouts
    • Per-call hard cap (timeoutMs, default 120s).
  • Environment merging
    • Merges manifest entrypoint.env + load(opts).env + safe base env.

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 dedicated HOME and TMPDIR.
  • Node memory cap: if the interpreter is Node and no cap is present, the SDK injects --max-old-space-size=256 to prevent runaway memory in long processes.
  • Environment: composed via buildEnv(entry.env, opts.env, HOME, TMPDIR) to keep the tool isolated yet configurable.
Tip

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/python is on PATH. The error message prints the PATH searched.
  • Timeouts
    • Increase timeoutMs in load() or set entrypoint.timeout_ms (SDK uses the per-call value).
  • 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) with entrypoint.command.
  • Local testing / custom layouts
    • Use toolDirOverride in load() to point to a local unpacked tool directory.
  • Modern package loading requires a current agent.lock
    • loadAgent() and loadSkill() read modern lockfile shapes. If the lockfile is missing or too old, rerun agentpm install.
  • Agent installed on disk but not in the lockfile
    • loadAgent() uses the installed agent directory and the agent.lock root 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 the agent.lock root 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.