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

@vietor/easy-agent-core

v0.6.3

Published

Lightweight AI agent framework — session orchestration, tool system, MCP client/server, skill loader

Readme

@vietor/easy-agent-core

Lightweight AI agent framework — session orchestration, tool system, MCP client/server, skill loader.

npm install @vietor/easy-agent-core

Requires Node.js ≥ 22 (ESM only).


createSession

createSession(options: SessionOptions): Promise<Session>

Factory that wires together the LLM client, tool registry, MCP servers, and skills into a ready-to-use Session instance.

import { createSession } from "@vietor/easy-agent-core";

const session = await createSession({
  systemPrompt: "You are a helpful assistant.",
  llmConfig: {
    baseUrl: "https://api.deepseek.com/v1",
    apiKey: "your-api-key",
    model: "deepseek-v4-flash",
    reasoningEffort: "high",
    wireApi: "completions",
    maxInputTokens: 1_000_000,
  },
  tools: [myCustomTool],
  skills: tryLoadSkills("./skills") ?? [],
  mcpServers: {
    filesystem: { command: "npx", args: ["-y", "@modelcontextprotocol/server-filesystem", "."] },
  },
  builtinTools: { askUser: true },
  clientInfo: { name: "my-app", version: "1.0.0" },
});

SessionOptions

| Property | Type | Default | Description | |---|---|---|---| | systemPrompt | string | (required) | System prompt for the LLM. | | llmConfig | LLMConfig | (required) | LLM endpoint config (OpenAI-compatible or Anthropic; see wireApi). Only baseUrl, apiKey, and model are required; reasoningEffort, wireApi, maxInputTokens, and maxOutputTokens default to "high", "completions", 1_000_000, and 128_000. | | cwd | string | process.cwd() | Working directory used by tools (e.g. path-based tools). | | tools | Tool[] | undefined | Additional tools registered alongside built-ins. | | skills | Skill[] | undefined | Skills loaded from SKILL.md files; invoked via the built-in Skill tool or via session.runSkill() (hosts may map them to slash commands). | | mcpServers | Record<string, MCPServerConfig> | undefined | MCP servers to connect on startup. | | builtinTools | BuiltInToolsOptions \| false | (7 core tools enabled; interactive tools off) | readOnly: true registers only the read-only core tools (FileRead/Glob/Grep/WebFetch); askUser/todoWrite/subAgent enable interactive tools (all off by default); false to disable all built-in tools. | | clientInfo | { name: string; version: string } | { name: "easy-agent-core", version: "0.0.0" } | Client identity sent to MCP servers. | | sessionId | string | randomUUID() | Unique session identifier, used as key for persistence. | | persistence | SessionPersistence | undefined | Persistence backend for save/resume. When set, the session auto-saves after every turn. | | maxTurns | number | 50 | Maximum agent turns (LLM calls with tool calls) per prompt before the run errors out. | | stallThreshold | number | 3 | Stall tolerance: consecutive identical tool-call sets, or consecutive text-only responses while todos are incomplete, before the run is treated as stalled. |

The auto-compaction threshold is not configurable — it's derived internally as 75% of llmConfig.maxInputTokens and exposed via session.compactThreshold.


SYSTEM_PROMPT_BOUNDARY

SYSTEM_PROMPT_BOUNDARY: string

A constant separator that createSession appends between the user-provided systemPrompt and the auto-generated tool-use/behavior guidelines. Also exported so callers can use it when composing their own system prompt from multiple segments:

import { SYSTEM_PROMPT_BOUNDARY } from "@vietor/easy-agent-core";

const systemPrompt = [
  coreInstructions,
  contextRules,
].join(SYSTEM_PROMPT_BOUNDARY);

Session

The main session object. Create one via createSession() — it wires the LLM client, tool registry, and MCP servers, and connects MCP servers listed in mcpServers:

const session = await createSession({ systemPrompt, llmConfig });

Running prompts

| Method | Description | |---|---| | startPrompt(text: string): Promise<SessionPromptResult> | Submit a user message and run the agent loop (LLM → tool calls → LLM) until a final answer or error. Returns a SessionPromptResult with the run status and the final assistant reply. |

Managing conversation

| Method | Description | |---|---| | clear(): void | Reset the conversation and log. | | restore(): Promise<boolean> | Reload persisted messages and todos from the SessionPersistence backend into the session. Returns false (loading nothing) when the backend has no saved state for this session. | | export(): ConversationMessage[] | Return all conversation messages (excluding the system prompt). | | compact(): Promise<RunStatus> | Ask the LLM to summarize the conversation so far, replacing history with a single summary message. Runs through the run loop — streams the summary and can be aborted via abort(). | | abort(): void | Abort the current prompt or compact, cancel pending tool calls, and dismiss unanswered user questions. | | submitAnswer(id: string, answer: string): void | Supply an answer to a pending user question (from the built-in AskUser tool). | | getPendingQuestion(): Extract<StreamEvent, { type: "question" }> \| undefined | Return the first unanswered question, or undefined if none are pending. |

Events

| Method | Description | |---|---| | subscribeEvents(listener: (e: StreamEvent) => void): () => void | Subscribe to structured incremental events (streaming deltas, tool calls, errors, questions, run state). Supports multiple listeners; returns an unsubscribe function. | | flush(): Promise<void> | Resolve once all pending persistence writes for this session have settled. |

StreamEvent

A discriminated union emitted as the session runs.

type StreamEvent =
  | { type: "user"; text: string }
  | { type: "skill"; name: string }
  | { type: "assistant_delta"; text: string }
  | { type: "reasoning_delta"; text: string }
  | { type: "reasoning_clear" }
  | { type: "assistant"; text: string }
  | { type: "tool_start"; id: string; name: string; argsSummary: string; result?: string | null; isError?: boolean; resultSummary?: string }
  | { type: "tool_end"; id: string; result: string; isError?: boolean; resultSummary?: string }
  | { type: "retry"; attempt: number; max: number; reason: string }
  | { type: "error"; text: string }
  | { type: "interrupted" }
  | { type: "question"; id: string; text: string; options: string[]; answer?: string | null }
  | { type: "notice"; text: string }
  | { type: "run_state"; running: boolean; elapsed: number; thinkingElapsed: number; replyElapsed: number; inputTokens: number; outputTokens: number };

| Type | Emitted when | In timeline | |---|---|---| | user | User submits a prompt (startPrompt). | ✓ | | skill | A skill is invoked. | ✓ | | assistant_delta | A streaming token delta from the LLM. | — | | reasoning_delta | A streaming thinking/reasoning token delta (extended thinking). | — | | reasoning_clear | Clears the accumulated reasoning text (e.g. on new tool round). | — | | assistant | A text response segment is flushed (on tool call or completion). | ✓ | | tool_start | A tool call starts. | ✓ | | tool_end | A tool call finishes. | — (merged into its tool_start entry) | | retry | The LLM client retries after a transient API error. | ✓ | | error | An error occurred. | ✓ | | interrupted | The current run was aborted. | ✓ | | question | The AskUser tool poses a question. | ✓ | | notice | session.timelineNotice() is called, or the run auto-compacts context. | ✓ | | run_state | Run state changes: at run start, every second, and at run end (running: false). | — |

Note: subscribeEvents is the primary stream for network/remote consumers (multi-subscriber, incremental). For local React useSyncExternalStore view invalidation use subscribe + getSnapshot.

RunState

interface RunState {
  running: boolean;        // whether a prompt is in progress
  elapsed: number;         // seconds since the current prompt started
  thinkingElapsed: number; // seconds before the first assistant text token (incl. reasoning/tools)
  replyElapsed: number;    // seconds after the first assistant text token (incl. later tool rounds)
  inputTokens: number;     // cumulative input (prompt) tokens for the current run
  outputTokens: number;    // cumulative output (completion) tokens for the current run
}

createRunState(): RunState returns the all-zero, not-running initial value.

Skills & messages

The command system lives in host code. Core exposes the primitives hosts build on:

| Method | Description | |---|---| | runSkill(name: string): Promise<boolean> | Run a skill by name through the agent loop. Returns false (no error emitted) if the name is unknown; throws SessionBusyError if a run is in progress. | | timelineNotice(text: string): void | Append a notice entry to the timeline and emit a notice event. | | timelineError(text: string): void | Append an error entry to the timeline and emit an error event. | | skills: readonly Skill[] | All loaded skills. |

State accessors

| Property | Type | Description | |---|---|---| | model | string | The LLM model name (e.g. "deepseek-v4-flash"). | | reasoningEffort | "high" \| "max" | The configured reasoning effort. | | compactThreshold | number | Estimated-token threshold that triggers auto-compaction. | | mcpServers | readonly MCPServerInfo[] | Status and tool list of connected MCP servers. | | contextTokens | number | Estimated token count of the current conversation. | | running | boolean | Whether a prompt/compact is in progress. Check before issuing a driver call (see Reentrancy). | | localStore | Map<string, unknown> | A local key-value store available to tools and host code during the session. |

Reentrancy

A Session runs one prompt/compact at a time. While a run is in progress, calling a driver method throws SessionBusyError (code === "SESSION_BUSY") so a host can map it to an HTTP 409:

| Driver method | Behavior when busy | |---|---| | startPrompt, compact, runSkill, clear, restore | Throws SessionBusyError. |

These remain callable during a run (they are inputs to the running loop, or read-only/teardown):

| Method | Behavior when busy | |---|---| | abort, submitAnswer | Allowed - control the running loop. | | subscribeEvents, subscribe, getSnapshot, getPendingQuestion, export, flush, dispose, accessors | Allowed. |

import { SessionBusyError } from "@vietor/easy-agent-core";

if (!session.running) {
  try {
    await session.startPrompt(text);
  } catch (e) {
    if (e instanceof SessionBusyError) /* -> HTTP 409 */;
  }
}

The run_state event (running: boolean) also signals run start/end for stream consumers.

Snapshot subscription

| Method | Description | |---|---| | subscribe(listener: () => void): () => void | Subscribe to timeline or todo changes; returns an unsubscribe function. | | getSnapshot(): SessionSnapshot | Current session snapshot ({ timeline, todos }); the reference stays stable until the next change. Designed for useSyncExternalStore. |

Cleanup

| Method | Description | |---|---| | dispose(): void | Kill all MCP server processes and clean up. |


Types

SessionSnapshot

The snapshot returned by session.getSnapshot().

interface SessionSnapshot {
  timeline: readonly StreamEvent[];
  todos: readonly Todo[];
}

SessionPromptResult

Returned by session.startPrompt().

interface SessionPromptResult {
  status: RunStatus;
  reply: string;
}

status indicates how the run ended; reply is the final assistant text (may be partial or empty when status !== "ok"). Error details are delivered via the error event; subscribe to subscribeEvents for the full picture.

RunStatus

type RunStatus = "ok" | "aborted" | "error" | "stalled" | "max_turns";

| Status | Meaning | |---|---| | ok | The run completed with a final assistant reply. | | aborted | The run was aborted via abort(). | | error | The run ended due to an LLM/API error. | | stalled | The agent stalled past stallThreshold: repeated identical tool calls, or repeated text-only responses while todos are incomplete. | | max_turns | The agent exceeded maxTurns. |

Also returned by session.compact() ("ok" on success, "aborted" if aborted, "error" on failure).

Timeline

SessionSnapshot.timeline is readonly StreamEvent[] — the persisted subset of StreamEvent (user, skill, assistant, tool_start, retry, error, interrupted, question, notice). Transient events (assistant_delta, reasoning_delta, reasoning_clear, run_state) are never stored; tool_end is merged into its tool_start entry.

tool_start and question entries carry lifecycle state as pending fields, set null while outstanding and replaced on completion:

| Field | Meaning | |---|---| | result?: string \| null (tool_start) | null while the tool is running; the result text once tool_end arrives, or "aborted" if the run was interrupted. | | isError?: boolean / resultSummary?: string (tool_start) | Set when the tool ended with an error / a condensed summary of the result. | | answer?: string \| null (question) | null until the user answers (via submitAnswer or abort). |

ConversationMessage

The internal message format exchanged with the agent, also returned by session.export().

type ConversationMessage =
  | { role: "system"; content: string }
  | { role: "user"; content: string }
  | { role: "skill"; name: string; content: string }
  | AssistantMessage
  | { role: "tool"; tool_call_id: string; content: string; resultSummary?: string; isError?: boolean };

// AssistantMessage includes optional tool_calls[] for function-calling

LLMConfig

interface LLMConfig {
  baseUrl: string;            // API endpoint (e.g. "https://api.deepseek.com/v1" or "https://api.anthropic.com") — required
  apiKey: string;             // API key — required
  model: string;              // Model name (e.g. "deepseek-v4-flash" or "claude-sonnet-5") — required
  reasoningEffort?: LLMReasoningEffort;  // Reasoning depth; "high" for standard tasks, "max" for deeper reasoning on complex tasks (default: "high")
  wireApi?: LLMWireApi;  // Wire protocol; "completions" (OpenAI Chat Completions), "anthropic" (Anthropic Messages API via the official SDK), or "responses" (OpenAI Responses API via the official SDK) (default: "completions")
  maxInputTokens?: number;    // Context window in tokens; 75% of it is used as the auto-compaction threshold (default: 1,000,000)
  maxOutputTokens?: number;   // Max output tokens per request, capped by the model's output limit (default: 128,000)
}

type LLMReasoningEffort = "high" | "max";

type LLMWireApi = "completions" | "anthropic" | "responses";

Both aliases are exported so hosts can reference them in their own config types.

wireApi selects the request/response protocol the client speaks:

  • "completions" - OpenAI Chat Completions compatible endpoint. reasoningEffort is sent as reasoning_effort; maxOutputTokens is sent as max_tokens.
  • "anthropic" - Anthropic Messages API (via @anthropic-ai/sdk). Point baseUrl at an Anthropic-compatible endpoint and model at a Claude model. maxOutputTokens is sent as max_tokens; reasoningEffort enables extended thinking ("high" = 16k token budget, "max" = 32k, both capped by maxOutputTokens); thinking blocks are preserved across tool-use turns as required by the API.
  • "responses" - OpenAI Responses API (via openai SDK). Tool results round-trip as function_call/function_call_output items; maxOutputTokens is sent as max_output_tokens; reasoningEffort is sent as reasoning.effort, and reasoning summaries are streamed via reasoning.summary_text.

SessionPersistence

Async interface for save/resume. Implement to persist session state between runs (filesystem, database, etc.).

interface SessionPersistence {
  load(sessionId: string): Promise<SessionData | null>;
  saveAll(sessionId: string, state: SessionData): Promise<void>;
  listSessions(): Promise<SessionMeta[]>;
  delete?(sessionId: string): Promise<void>;
}

SessionData is what load/saveAll persist per session:

interface SessionData {
  messages: ConversationMessage[];
  todos: Todo[];
}

SessionMeta is returned by listSessions. Metadata is owned by the implementation: saveAll only persists messages and todos, so implementations update updatedAt on write and set createdAt on first creation without core overwriting a title set elsewhere.

interface SessionMeta {
  id: string;
  title?: string;
  createdAt: number;
  updatedAt: number;
  cwd?: string;
}

Persistence writes are asynchronous and serialized per session: saveAll is queued internally so a run never blocks on storage. Call session.flush() to await any pending write (e.g. before tearing down a session).

Todo

interface Todo {
  content: string;
  status: TodoStatus;
}

type TodoStatus = "pending" | "in_progress" | "completed";

Tool System

Tool

interface Tool {
  name: string;
  readOnly?: boolean;
  description: string;
  parameters: Record<string, unknown>;   // JSON Schema object
  summaryArgs?: string[];                // parameter keys used for display summary
  summarizeArgs?: (args: Record<string, unknown>) => string; // custom summary function
  summarizeResult?(result: TextResult): string; // result summary for timeline display
  execute(args: Record<string, unknown>, ctx: ToolContext): Promise<string | TextResult>;
}
  • readOnly (optional) marks the tool as read-only. Read-only tools are what builtinTools: { readOnly: true } registers, and what the SubAgent tool equips sub-agents with.
  • parameters is passed to the LLM as a JSON Schema to describe the tool's arguments.
  • When the LLM calls a tool, execute receives the parsed arguments and a context object.
  • Return a plain string (equivalent to { content: string }) or a TextResult with an optional isError flag.
  • summaryArgs / summarizeArgs control what appears in the tool log entry's argsSummary field.
  • summarizeResult (optional) returns a short result summary for timeline display. Called after execution with the result; the registry prefixes the wall-clock duration. Falls back to a default summary (byte/line count) when not defined.

ToolContext

interface ToolContext {
  signal?: AbortSignal;  // abort signal for the current run
  cwd: string;           // resolved working directory for path-based tools
}

TextResult

interface TextResult {
  content: string;
  isError?: boolean;
}

ToolSchema

interface ToolSchema {
  type: "function";
  function: {
    name: string;
    description: string;
    parameters: Record<string, unknown>;
  };
}

The format sent to the LLM's tools parameter. Generated automatically from registered Tool objects.

Built-in tools

Core tools (registered by default; builtinTools: { readOnly: true } registers only the read-only subset):

| Tool | Description | |---|---| | FileRead (read-only) | Read files with line numbers. | | Glob (read-only) | File listing by glob pattern. | | Grep (read-only) | Content search with regex. | | WebFetch (read-only) | General-purpose HTTP GET — converts HTML to markdown, returns JSON/XML/text raw. Retries transient failures (network, timeouts, 408/429/5xx) up to 3 attempts. | | Shell | Run shell commands. | | FileWrite | Create or overwrite files. | | FileEdit | Surgical text replacement. |

Interactive tools are off by default and registered only when explicitly enabled via builtinTools (askUser: true, todoWrite: true, subAgent: true):

| Tool | Description | |---|---| | AskUser | Ask the user a question and wait for the answer. | | TodoWrite | Track multi-step task progress; the agent must complete every task before its final reply. | | Skill | Invoke a skill by name; loads its instructions into context. Registered automatically whenever skills are provided. | | SubAgent | Run a nested sub-agent: read-only "explore" investigation or "plan" implementation planning. Sub-agents are equipped with the session's read-only tools (FileRead/Glob/Grep/WebFetch, plus any custom tools marked readOnly). |

builtinTools: false disables all built-in tools.

const session = await createSession({
  systemPrompt: "...",
  llmConfig: { ... },
  builtinTools: { askUser: true, todoWrite: true },
});
// Disable all built-in tools:
// builtinTools: false
// Read-only session (no Shell / FileWrite / FileEdit):
// builtinTools: { readOnly: true }

Custom tools example

import type { Tool } from "@vietor/easy-agent-core";

const greetTool: Tool = {
  name: "greet",
  description: "Greet someone by name",
  parameters: {
    type: "object",
    properties: { name: { type: "string" } },
    required: ["name"],
  },
  async execute(args) {
    return `Hello, ${args.name as string}!`;
  },
};

const session = await createSession({
  systemPrompt: "...",
  llmConfig: { ... },
  tools: [greetTool],
});

Command System

Slash commands are a host-side (UI) concept — the core package no longer ships a command system. Hosts implement their own dispatcher on top of the session primitives: startPrompt(), runSkill(), timelineNotice(), timelineError(), and the skills getter.


Skill System

Skill

interface Skill {
  name: string;
  description?: string;
  prompt: string;
}

Skills are loaded from directories containing a SKILL.md file. They are listed in the system prompt so the agent can invoke them via the built-in Skill tool, and can also be run directly by hosts via session.runSkill(name) (e.g. mapped to slash commands).

tryLoadSkills

tryLoadSkills(path: string): Skill[] | undefined

Load skills from a directory. Each subdirectory containing a SKILL.md file becomes one skill. Returns undefined if the directory doesn't exist or contains no valid skills.

SKILL.md supports front matter:

---
name: my-skill
description: Does something useful
---

Your skill prompt here.

If no name is set in front matter, the directory name is used.

const skills = tryLoadSkills("./my-skills") ?? [];

const session = await createSession({
  systemPrompt: "...",
  llmConfig: { ... },
  skills,
});

MCP (Model Context Protocol)

MCPServerConfig

type MCPServerConfig = StdioServerConfig | RemoteServerConfig;

interface StdioServerConfig {
  type?: "stdio";
  command: string;
  args?: string[];
  env?: Record<string, string>;
  enabled?: boolean;    // set false to skip this server
}

interface RemoteServerConfig {
  type: "http";
  url: string;
  headers?: Record<string, string>;
  enabled?: boolean;
}

MCPServerInfo

Returned by session.mcpServers.

interface MCPServerInfo {
  name: string;
  type: "stdio" | "http";
  status: "pending" | "connected" | "failed" | "disabled";
  tools: string[];
  error?: string;  // connection error message when status is "failed"
}

MCP tools are exposed to the LLM with the prefixed name MCP__<server>__<tool>. Connection timeout is 30 seconds per server.


Utility Functions

tryReadFileText

tryReadFileText(path: string): string | undefined

Read a text file, returning undefined on any failure (missing file, empty content, read error).

const content = tryReadFileText("./config.json");
if (content) {
  const config = JSON.parse(content);
}

htmlToMarkdown

htmlToMarkdown(html: string): string

Convert HTML to Markdown using Turndown. Strips script, style, title, meta, head, noscript, template, link, and base elements.

import { htmlToMarkdown } from "@vietor/easy-agent-core";

const md = htmlToMarkdown("<h1>Hello</h1><p>World</p>");
// "# Hello\n\nWorld"

getTextBytes

getTextBytes(content: string): number

Return the UTF-8 byte length of a string (via Buffer.byteLength).

import { getTextBytes } from "@vietor/easy-agent-core";

const bytes = getTextBytes("Hello");   // 5

formatSeconds

formatSeconds(value: number): string

Format a duration in seconds for display (e.g. 3.2s). Used for tool-result summaries.

import { formatSeconds } from "@vietor/easy-agent-core";

formatSeconds(3.24);   // "3.24s"

formatCompactNumber

formatCompactNumber(value: number): string

Format a number compactly (e.g. 1.2K). Used for byte/line counts in summaries.

import { formatCompactNumber } from "@vietor/easy-agent-core";

formatCompactNumber(1234);   // "1.23K"

ellipsisText

ellipsisText(content: string, length: number, showChars?: boolean): string

Collapse whitespace and truncate text to length characters with a trailing . With showChars, append the total character count when truncated — useful for text that changes size over time.

import { ellipsisText } from "@vietor/easy-agent-core";

ellipsisText("a\nvery   long line", 8);              // "a very l…"
ellipsisText("a very long line here", 8, true);      // "a very l… (21)"

errorMessage

errorMessage(e: unknown): string

Stringify an unknown error for display (e instanceof Error ? e.message : String(e)). Used across core for error events.

import { errorMessage } from "@vietor/easy-agent-core";

errorMessage(new Error("boom"));   // "boom"
errorMessage("oops");              // "oops"

netFetch

netFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>

A drop-in replacement for fetch that automatically routes through an HTTP(S) proxy when configured. Respects the standard environment variables:

  • HTTPS_PROXY / https_proxy — proxy URL for HTTPS requests (preferred)
  • HTTP_PROXY / http_proxy — proxy URL for HTTP requests (fallback)
  • NO_PROXY / no_proxy — comma-separated hostnames/domains to bypass the proxy
import { netFetch } from "@vietor/easy-agent-core";

// Same signature as fetch — automatically uses proxy if env vars are set
const res = await netFetch("https://api.example.com/data");
const data = await res.json();

runProcess

runProcess(cmd: string, args: string[], opts?: { cwd?: string; timeout?: number }, signal?: AbortSignal): Promise<ProcessResult>

Run a subprocess, capturing stdout and stderr (used by the built-in Shell tool). The promise never rejects — spawn failures, timeouts, and output over the 10MB cap are reported via ProcessResult.error. Pass a timeout (ms) or an AbortSignal to kill the process tree.

import { runProcess } from "@vietor/easy-agent-core";

const result = await runProcess("ls", ["-la"], { cwd: "./src" });
if (result.error) console.error(result.error.message);
else console.log(result.stdout, result.status);

ProcessResult

Returned by runProcess.

interface ProcessResult {
  stdout: string;
  stderr: string;
  status: number | null;  // exit code; null when killed (signal, abort, timeout, buffer overflow)
  error?: Error;          // spawn failure, timeout, or output exceeded the 10MB cap
  truncated?: boolean;    // true when output was cut off at the 10MB cap
}

MAX_SUMMARY_LENGTH

MAX_SUMMARY_LENGTH: number (75)

The character cap the tool registry applies when truncating tool results into timeline summaries.

import { MAX_SUMMARY_LENGTH } from "@vietor/easy-agent-core";

const summary = result.length > MAX_SUMMARY_LENGTH ? `${result.slice(0, MAX_SUMMARY_LENGTH)}…` : result;

Full Quick Start

import { createSession, tryLoadSkills } from "@vietor/easy-agent-core";

const session = await createSession({
  systemPrompt: "You are a helpful assistant.",
  llmConfig: {
    baseUrl: "https://api.deepseek.com/v1",
    apiKey: "your-api-key",
    model: "deepseek-v4-flash",
    reasoningEffort: "high",
    wireApi: "completions",
    maxInputTokens: 1_000_000,
  },
  mcpServers: {
    filesystem: { command: "npx", args: ["-y", "@modelcontextprotocol/server-filesystem", "."] },
  },
});

session.subscribeEvents((e) => {
  if (e.type === "assistant_delta") process.stdout.write(e.text);
  else if (e.type === "run_state")
    console.log(`tokens: ${e.inputTokens} prompt / ${e.outputTokens} completion`);
});

const result = await session.startPrompt("What files are in the current directory?");
console.log(result.reply);                // final assistant reply

console.log(session.getSnapshot().timeline);   // full session timeline
console.log(session.export());     // LLM message history
session.dispose();