Node SDK#

Install a tool with the CLI, install the SDK in your Node app, load tools, and call them.

Goal#

Install the Node SDK and use load() to call a published tool from your app.

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>

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) 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();

6) 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.

7) 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).

8) 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.

9) 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.