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

@leuduan/lagent

v56.0.1

Published

Plugin-based agent execution engine built on @leuduan/cordis

Readme

@leuduan/lagent

A plugin-based agent execution engine built on Cordis. One application owns one append-only journal; conversations are threads in that journal. The host chooses providers, capabilities, credentials and user-interface policy.

The journal is the state. Reopening reconstructs conversation history and unfinished work from recorded facts. Plugins and process resources are mounted again by the application.

Quick start

This creates a chat thread using a host-owned Codex login. Run on Node 22 or newer for SQLite.

import { homedir } from 'node:os'
import { resolve } from 'node:path'
import { Context } from '@leuduan/cordis'
import { core, registerProvider } from '@leuduan/lagent'
import { base } from '@leuduan/lagent/compositions'
import { codexAuthPath, codexProvider } from '@leuduan/lagent/providers'
import { threadDirs } from '@leuduan/lagent/plugins/threadDirs'
import { truncateToolOutput } from '@leuduan/lagent/plugins/truncateToolOutput'
import { SqliteJournalStore } from '@leuduan/lagent/stores/sqlite'

const root = new Context()
const cwd = process.cwd()
const dataDir = resolve('.lagent')

try {
  await root.plugin(core, {
    store: new SqliteJournalStore(`${dataDir}/journal`, {
      lockDirectory: `${dataDir}/locks`,
      locking: { kind: 'posix', busyTimeoutMs: 1_000 },
    }),
    cwd,
    retention: 'unload',
    reactiveTurnLimit: 10,
  })
  // Where each thread keeps files of its own; a host with no file system mounts none.
  await root.plugin(threadDirs, { dataDir })
  registerProvider(
    root,
    codexProvider({
      authPath: codexAuthPath(process.env, homedir()),
      refreshTimeoutMs: 30_000,
      streamIdleTimeoutMs: 300_000,
    }),
    { retry: { maxAttempts: 3, baseDelayMs: 500, maxDelayMs: 10_000 } },
  )
  await root.plugin(base)
  await root.plugin(truncateToolOutput, { spill: true })
  // Nothing runs before this: every plugin a thread needs is mounted.
  await root.threads.start({ servePending: 'none', maxConcurrentRuns: 4 })

  // Every thread is born on a complete route: which model runs is the application's choice. A creation
  // also says where its configuration comes from (`birth.from`) and what an existing id must match (`ifExists`).
  const thread = await root.threads.create('main', {
    id: 'main',
    ifExists: 'match-identity',
    birth: { from: 'defaults', route: { provider: 'codex', model: 'gpt-6-luna', thinking_effort: 'medium' } },
  })
  root.threads.onChunk(({ chunk }) => process.stdout.write(chunk.text_delta))
  // Resolves once this input is answered, or the run responsible for it needs someone.
  const sent = await thread.send('Say hello.', { requestRun: {}, until: 'answered' })
  if (sent.status === 'refused') throw new Error(sent.refusal.message)
  console.log(sent.end)
} finally {
  await root.fiber.dispose()
}

base supplies plumbing and a main agent with no granted tools. For coding, the host registers workspace, command and other capability plugins, then installs codePreset on a thread, or mounts it on the root for an agent selector, to grant them to that thread's agent. Composition and required host configuration are explained in Plugin authoring.

Core model

| Concept | Responsibility | | ---------- | ------------------------------------------------------------------------- | | Root | Shared journal, core services and application-owned plugins. | | Thread | Durable conversation on an agent, with its environment, prompt and route. | | Activation | Reconstructible scope owning a thread's plugins, readiness and resources. | | Origin | The caller a delegated thread sits under, and its call or assignment. | | Fork | A conversation continuing another thread's history at a settled cut. |

Creating or reading a thread does not activate its plugins. Work activates it lazily and waits for readiness. One exclusive operation holds a thread at a time; conflicting work fails busy, and a run waits its turn. The engine runs a thread when a recorded request asks it to and nothing holds it.

Tools and grants are live registrations. Prompts are rendered once after readiness and recorded; plugin guidance uses that same prompt. Rebuild explicitly (thread.configure({ rebuildPrompt: true })) when a conversation should receive new instructions. Dynamic thread facts enter the conversation as sourced notices.

Tool results render as <tool:output success="true"> or <tool:output success="false">, with optional notes in a following <tool:notes> section. Non-text payloads accompany the rendered text.

Reading and driving threads

await thread.send('Another message', { requestRun: {} })
const { state } = root.threads.inspect(thread.id)!
const transcript = await thread.read((view) => view.transcript())

send queues durable input; with requestRun it also asks for a run, which the engine serves once nothing holds the thread, and its outcome names that run (runId). Without it, input waits for the next run and does not keep a running one going. With until: 'answered' (which needs the requestRun) the send waits for that input to end, as the quick start does: end is answered with the first answer accepted after a step claimed it, within the run that claimed it, discarded when it was withdrawn or cleared before that, or held or settled with the state of the run responsible for it when that run will not answer without someone acting. It follows the input itself, through a successor run when its cause landed as a run ended, and later input does not extend the wait; sending the same id again waits again. waitForRun(root.threads, id, runId, until) waits for one exact run, which is what a cleanup that owns that run needs.

The host records what a person did, presents confirmations, failures and background work from threads.inspect and run events, and decides when to start. Threads and recovery explains holds, cancellation, input ordering, forks, read lifetimes and recovery.

The host supplies environment-dependent configuration. Lagent does not choose a workspace, credential file, skill root or subprocess environment from its own process, and refuses a relative core cwd, thread-directories dataDir or thread cwd rather than resolve it against the process directory. Core has no data directory: a host with a file system mounts threadDirs with one, and the plugins that keep files there (commands, and truncateToolOutput with spill: true) stay pending until it is. A host without one, such as a Durable Object, mounts none and gives truncation spill: false. One process holds one journal; incompatible journal formats and extension payloads are refused rather than migrated.

The package's major version is the journal version (JOURNAL_VERSION in src/journal/store.ts): whenever it increases, bump the major version in package.json to match, with minor and patch reset to zero.

Imports

Use the explicit package entry points. The root serves applications, including the processes a host owns (spawnCommand, runToCompletion; a tool never uses them); @leuduan/lagent/plugin serves plugin authors; compositions holds the shipped compositions (base, and codePreset with the policy and prompts it shares); llm and records expose their contracts, the provider contract included; providers holds the shipped providers; stores/sqlite holds the SQLite store and openSqlite, which opens a host's own databases under the same required locking choice (posix, or none for a file system whose POSIX locks fail), the one door that needs node:sqlite; stores/durableObject holds the store over a Cloudflare Durable Object's SQLite storage; plugins/sandbox holds the optional OS sandbox, the confinement handler of ctx.confinement for commands and, through one policy, for file access, whose worker thread is the one module that loads @anthropic-ai/sandbox-runtime; plugins/<name> exposes a feature (plugins/claudeCode loads @anthropic-ai/claude-agent-sdk lazily, in its runtime module only; plugins/mcp imports @modelcontextprotocol/sdk statically, so a host that imports the door installs that optional peer); testing holds test helpers. Each door lists its names; implementation subpaths are private.

In Cloudflare Workers with nodejs_compat, every door imports except plugins/sandbox, which also needs new_module_registry. Features that need a filesystem outside /tmp, processes or node:sqlite fail when used. A Workers composition is core, base and truncateToolOutput with spill: false, plus tools that work there: no threadDirs, commands or sandbox. The Workers tests run one in workerd with pnpm -F @leuduan/lagent run test:workers.

MCP clients identify themselves with the version statically imported from the package metadata, so bundled hosts carry the same version as source and dist/ consumers.

Guides

Start with the documentation map. It distinguishes usage contracts from the design records explaining decisions. Architecture gives the ownership model. The lagentic host is an application example, documented in its guide.

Scripts

Run from the repository root:

pnpm -F @leuduan/lagent run typecheck
pnpm -F @leuduan/lagent run test
pnpm -F @leuduan/lagent run test:related <files>   # tests that reach the files, plus the architecture test
pnpm -F @leuduan/lagent run build

test:related is what the pre-commit hook runs for changed files. It follows import edges only, so it always lists src/architecture.test.ts, which reads the source tree rather than importing it, and forceRerunTriggers in vitest.config.ts names the inputs tests reach without an import (the sandbox worker). The pre-push gate runs the whole suite.