npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

unigent-sdk

v0.1.5

Published

Cross-harness agent orchestration SDK for TypeScript

Readme

Install

Unigent requires Node.js 24 or newer. The live trace inspector (unigent tui) additionally requires Bun. Published packages are ESM-only.

npm install unigent-sdk zod
npm install -g unigent-cli

The examples below use Zod, but Unigent accepts any Standard Schema validator.

Drop-in Bun scripts

For small automations, a single Bun script is often more comfortable than adding a package.json, lockfile, and local dependencies to a project. Start the file with #!/usr/bin/env bun; Unigent's CLI will use Bun in both normal and TUI mode, with missing-package fallback enabled. Imports are downloaded into Bun's global cache instead of creating project boilerplate.

Copy examples/standalone.ts into a project and launch it through Unigent:

unigent standalone.ts "Write a launch announcement"
unigent tui standalone.ts "Write a launch announcement"

The same file is directly executable when you do not need Unigent's trace transport or TUI:

chmod +x standalone.ts
./standalone.ts "Write a launch announcement"

Pi reads models and authentication from your local Pi configuration. The Claude and Codex adapters use the authenticated official CLIs installed on your machine.

👉 Your agent can invoke such workflow scripts with a bash command, isn't that cool?

Your first agent

import { agent, piAgent } from "unigent-sdk";

const writer = agent({
  name: "writer",
  backend: piAgent(),
  model: "openrouter/deepseek/deepseek-v4-flash",
});

const result = await writer.run("Write one sentence about durable software.");
console.log(result.output);

result also contains token usage, any cost reported by the harness, and the full trace. The run starts immediately, can be aborted, and exposes its events as an async iterable while you await the final result.

See examples/hello.ts for the smallest complete script and examples/pitch.ts for a workflow using most of the API.

Choose what comes back

Every run starts with .run(prompt). The optional second argument controls what comes back: pass a schema for a typed value or done for side-effect-only work.

import { done } from "unigent-sdk";
import { z } from "zod";

const prose = await writer.run("Explain the tradeoff.");

const structured = await writer.run(
  "Choose the strongest title.",
  z.object({ title: z.string(), score: z.number().min(0).max(100) }),
);

await writer.run("Update CHANGELOG.md.", done);
  • Omit the second argument to return prose.
  • Pass a Standard Schema to return its inferred type.
  • Pass done when the work ends with side effects and no prose result is useful.

done is a built-in completion sentinel schema. Passing it as the second argument makes Unigent expose the reserved unigent_return tool with an empty object input schema. The agent is instructed to call that tool after completing the requested side effects. An accepted call returns void and terminates the run before the agent writes a final prose response, saving tokens that the caller would otherwise discard.

Structured output uses the same completion-tool protocol. Instead of the empty sentinel schema, unigent_return accepts a value matching the schema passed to run().

Structured values therefore never need to be scraped from prose: Markdown, preambles, and trailing commentary cannot corrupt the value. If the agent omits the completion call or returns an invalid value, Unigent repairs the turn a bounded number of times and then throws AgentRepairExhaustedError.

Give agents tools

Pass ordinary named functions. Unigent reads their TypeScript signatures and JSDoc from the module named by source, then builds the tool definitions for the harness.

/** Return the number of words in some copy.
 *
 * @promptSnippet Use this before accepting copy with a word limit.
 * @promptGuideline Do not call wordCount repeatedly with unchanged text.
 */
function wordCount(text: string): number {
  return text.trim().split(/\s+/u).length;
}

const editor = agent({
  name: "editor",
  source: import.meta.url,
  backend: piAgent(),
  model: "openrouter/deepseek/deepseek-v4-flash",
  tools: [wordCount],
});

The opening JSDoc prose is the tool description; there is no @description tag. Both prompt tags are optional. @promptSnippet adds a named entry to the system prompt's available-tools section. Each @promptGuideline becomes a standalone bullet in its guidelines section, without an automatic tool-name prefix, so every guideline must name its tool explicitly. This distinction follows Pi's native semantics. Claude Code and Codex do not expose equivalent tool fields, so Unigent renders both sections itself for every harness instead of appending either tag to the tool description. Omit the tags when that extra guidance is not worth its token cost. When TypeScript source is unavailable, the portable tool({...}) helper accepts an explicit Standard Schema input.

For compiled deployments, bake source-derived schemas after compiling the entry:

tsc && unigent bake src/worker.ts

This writes worker.unigent-tools.json beside the compiled worker.js. Production loads that manifest without installing or loading TypeScript; development can continue reflecting directly from a .ts entry when the optional typescript peer is installed.

Only values listed in tools are callable. Deliberate agent failure is also opt-in:

import { fail } from "unigent-sdk";

const reviewer = agent({ ...options, tools: [fail] });

Unigent does not rename ordinary function tools: wordCount is advertised as wordCount. To the agent, unigent_fail is another callable tool. It is special only at the TypeScript boundary: ordinary tool errors are recoverable feedback for the agent, while unigent_fail terminates the run and throws AgentRaisedError to the caller. Adding the fail sentinel opts the agent into that control tool; agents cannot deliberately abort a run unless the developer enables it. User-defined names beginning with unigent_ are rejected because that namespace belongs to protocol tools such as unigent_return and unigent_fail.

Compose agents with functions

An agent becomes a nested agent when you call it from a tool function.

const researcher = agent({
  name: "researcher",
  backend: piAgent(),
  model: "openrouter/deepseek/deepseek-v4-pro",
});

/** Research one question and return a concise finding. */
async function research(question: string): Promise<string> {
  const result = await researcher.run(question);
  return result.output;
}

const author = agent({
  name: "author",
  source: import.meta.url,
  backend: piAgent(),
  model: "openrouter/deepseek/deepseek-v4-flash",
  tools: [research],
});

When the author calls research, the function opens another Unigent run. The nested run keeps its trace parentage, and its usage rolls up into the parent. The surrounding program still decides how many agents run, which work happens in parallel, and where results go next.

Keep a conversation and branch it

Stateless runs are the default. A session keeps the harness conversation for later turns.

const review = reviewer.session();

await review.run(`
Read the repository and learn its architecture, conventions, and current behavior.
Trace the main execution paths and note the patterns that new code should follow.
Do not review or modify anything yet; reply when you have enough context.
`);

const correctnessReview = review.fork();
const maintainabilityReview = review.fork();

const [correctness, maintainability] = await Promise.all([
  correctnessReview.run("Review the codebase only for correctness and edge-case failures."),
  maintainabilityReview.run("Review the codebase only for maintainability and API clarity."),
]);

Both branches reuse the repository context gathered by the first turn instead of paying to rebuild it independently. All three official harnesses support forks. A session can be forked after its first completed turn, but not while a turn is active.

Group a workflow with scopes

A scope groups related runs and owns their cumulative usage, traces, annotations, logs, cancellation, and deadline.

const release = writer.scope("release", {
  duration: "10m",
  retainTraces: 50,
  limits: { budgetUsd: 1 },
});

release.annotate({ pullRequest: 42 });
release.log("drafting release notes");

const draft = await release.scope("draft").run("Draft the notes.");
await release.scope("publish").run(`Publish these notes: ${draft.output}`, done);

console.log(release.usage);
console.log(release.traces);

.with(overrides) returns an immutable agent variation with different configuration, such as a model, backend, system prompt, or per-turn limits. It does not create a new lifecycle or telemetry group; runs still belong to the current scope.

.scope(name, options) creates a named workflow boundary. It can apply the same configuration overrides, but it also owns aggregate usage and traces, annotations and logs, cancellation, and an overall duration deadline. Nested scopes roll their usage and traces into their parent. Inherited limits can only tighten: limits.turnDuration caps each agent turn, while the scope's duration caps the whole grouped workflow. Explicit scopes retain the latest 50 completed root traces by default; set retainTraces to another bound or 0 to disable retention. Each run still returns its complete trace as result.trace.

Run and inspect scripts

The CLI runs an ordinary TypeScript file as a child process. The file needs no TUI-specific code. Everything after the script path is forwarded to the script. A conventional -- separator is also accepted for compatibility with command generators that insert one, but Unigent does not need it: its own option parsing stops at the script path.

unigent examples/pitch.ts "Kebab shop app"
unigent tui examples/pitch.ts "Kebab shop app"

Trace events travel over an isolated channel, leaving the script's stdout and stderr alone. The TUI shows the live run tree, transcript, system prompt, tool calls, usage, errors, and process output.

| Key | Action | | --- | --- | | / | Select a trace row | | Enter | Expand or hide the selected agent's tool calls | | s | Show or hide the system prompt | | o | Switch between activity and process output | | [ / ] | Page through older or newer trace history | | r / Ctrl-R | Rerun or stop | | q | Quit |

The trace retains its complete history in 250-row pages, and the activity pane keeps only its latest 250 entries mounted. Both panes use viewport culling so off-screen content is not painted.

Parse script arguments

args() parses process.argv through any Standard Schema. A scalar schema joins positional arguments into one natural value:

import { args } from "unigent-sdk";
import { z } from "zod";

const idea = await args(z.string().min(1));

The second argument is optional. Add it only when the script needs more useful --help output:

const idea = await args(z.string().min(1), {
  description: "Turn a product idea into an elevator pitch.",
  usage: '"Product idea"',
});

Ask the script to print the generated help without starting an agent run:

$ unigent examples/pitch.ts --help
Turn a product idea into a scored elevator pitch.

Usage: pitch.ts "Product idea"

Options:
  -i  Prompt for missing required arguments.

args() prints this standardized usage for -h or --help. Invalid input prints the schema's validation message and the same usage before exiting. Zod's default messages work without any authored message; customize them only when the default is not clear enough for your script.

Object schemas accept named flags, defaults, repeated arrays, dotted paths, booleans, and --no-* negation:

const input = await args(
  z.object({
    keyword: z.string().describe("The keyword to transform"),
    count: z.number().int().positive().default(1),
    cache: z.boolean().default(true),
  }),
);
unigent task.ts --keyword kebab --count 3 --no-cache

--no-cache sets the cache field to false; --cache sets it to true. The no- prefix is a conventional CLI negation for boolean flags, not part of the schema field name.

$ unigent task.ts --count 3 -i
--keyword (The keyword to transform): kebab

-i prompts for missing required arguments; descriptions are optional. It requires a terminal and is unavailable in TUI or piped execution. Without -i, missing input fails immediately.

Resume finished work with checkpoints

Checkpoints reuse successful stateless runs across reruns and deduplicate identical concurrent calls. Their fingerprint covers the prompt, return schema, harness adapter, model, system prompt, tools, and explicit checkpoint keys.

import { agent, createFileCheckpointStore } from "unigent-sdk";

const defaults = {
  backend: piAgent(),
  checkpoint: createFileCheckpointStore(".unigent/checkpoints.jsonl"),
  limits: { turnDuration: "5m" as const },
};

const writer = agent({ ...defaults, name: "writer", model: "model-a" });
const reviewer = agent({ ...defaults, name: "reviewer", model: "model-b" });

Set checkpointKey when closed-over or ambient behavior changes the run's identity. Use .with({ checkpoint: false }) for a fresh branch. Stateful session turns always run fresh because their result depends on the conversation transcript. A checkpoint replay restores the recorded usage, including costUsd; scope telemetry and budgets therefore describe the logical workflow, not only newly billed backend calls.

Budget enforcement is exact for sequential work. Parallel siblings can each begin below the same remaining budget because their costs are unknown until they settle; a sibling that takes the scope over budget rejects after settlement, but the provider spend has already occurred.

Test without a model

The SDK includes a deterministic harness adapter under its test subpath, so tests need no second Unigent package.

import { agent } from "unigent-sdk";
import { createScriptedBackend } from "unigent-sdk/test";
import { z } from "zod";

const backend = createScriptedBackend([
  {
    toolCalls: [
      { name: "unigent_return", input: { value: { title: "Unigent" } } },
    ],
  },
]);

const subject = agent({ name: "subject", backend, model: "test" });
const result = await subject.run("Name the project.", z.object({ title: z.string() }));

expect(result.output).toEqual({ title: "Unigent" });
expect(backend.requests).toHaveLength(1);

createTestBackend accepts a programmable turn handler. createScriptedBackend consumes declarative turns. Adapter authors can use exerciseBackendContract to run the shared prose, session, and fork contract against a new harness adapter.

Harnesses

Unigent talks to each harness through a small adapter that opens sessions, runs turns, executes Unigent tools, streams events, reports usage, and forks conversations where supported.

import { claudeCli, codexCli, piAgent } from "unigent-sdk";

const pi = piAgent();
const claude = claudeCli();
const codex = codexCli();

All three adapters default to base: "clean", which starts without the machine's prompt, plugins, skills, hooks, or MCP configuration. Authentication, model configuration, and each harness's native tools remain available unless you restrict them. Set base: "machine" to inherit ambient harness configuration.

| Capability | Pi agent SDK | Claude CLI | Codex CLI | | --- | --- | --- | --- | | Prose, structured output, done, fail, tools | Yes | Yes, through ephemeral local MCP | Yes, through ephemeral local MCP | | Cost reporting and budget enforcement | Yes | Yes | No; budget configuration fails fast | | Session continuation and forks | Yes | Yes, through CLI resume/fork flags | Yes; forks copy Codex's persisted session transcript under a new session ID | | Native tools | Exact allowlist; [] disables | --tools allowlist; may retain ToolSearch for MCP | [] disables shell and web search; no exact allowlist | | Plugins | Exact allowlist or disable | Exact installed-plugin allowlist or disable | No Unigent control | | Skills | Exact allowlist or disable | Inherit all with machine base or disable all; no named allowlist | Exact allowlist or disable | | Machine MCP servers and hooks | Pi has no built-in MCP; MCP extensions and hooks follow plugin selection | Inherit all with machine base or disable all; no named allowlist | Clean base ignores user config and rules; machine base inherits | | Permissions | Harness execution is unrestricted | Bypassed by default; permissions: "cli" defers to Claude | Bypassed by default; permissions: "cli" defers to Codex |

Explicit adapter options override the inherited category where the harness can enforce them. Unsupported controls fail during setup instead of becoming silent no-ops.

Development

corepack pnpm install --frozen-lockfile
corepack pnpm run typecheck
corepack pnpm test
corepack pnpm run test:tui
corepack pnpm run test:e2e
corepack pnpm run check:full

The deterministic suite uses no model. test:tui drives a real terminal without a model. test:e2e spends real Pi, Claude CLI, and Codex CLI turns and requires all three local harnesses to be authenticated.

License

MIT © 2026 Gintas Zenevskis