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

@getenki/ai

v0.5.81

Published

Node.js bindings for Enki's Rust agent runtime.

Readme

@getenki/ai

Node.js bindings for Enki's Rust agent runtime, published as a native package via napi-rs.

Install

npm install @getenki/ai

The package ships prebuilt native binaries for:

  • Windows x64 and arm64
  • macOS x64 and arm64
  • Linux x64 and arm64 (GNU libc)

What It Exports

The current package surface is:

  • NativeEnkiAgent
  • NativeToolRegistry
  • NativeMultiAgentRuntime
  • NativeWorkflowRuntime
  • JsAgentStatus
  • JsMemoryKind
  • JsMemoryModule
  • JsMemoryEntry
  • JsAgentCard
  • JsAgentRunResult
  • JsExecutionStep

NativeEnkiAgent is the main entrypoint. It can be created in five modes:

  • new(...) for a plain agent
  • NativeEnkiAgent.withTools(...)
  • NativeEnkiAgent.withToolRegistry(...)
  • NativeEnkiAgent.withMemory(...)
  • NativeEnkiAgent.withToolsAndMemory(...)

NativeToolRegistry supports:

  • new(...)
  • registerTools(...)
  • clear()
  • toolNames()
  • size

It also supports two loop customization levels:

  • agenticLoop constructor arguments for prompt-level loop customization
  • setAgentLoopHandler(...) for a JavaScript-defined loop override

NativeMultiAgentRuntime supports:

  • new(...)
  • process(...)
  • processWithTrace(...)
  • registry(...)
  • discover(...)

NativeWorkflowRuntime supports:

  • new(...)
  • listWorkflowsJson(...)
  • listRunsJson(...)
  • inspectJson(...)
  • startJson(...)
  • resumeJson(...)
  • submitInterventionJson(...)

Basic Agent

Use the constructor when you only need a session-based agent backed by the native runtime.

const { NativeEnkiAgent } = require('@getenki/ai')

async function main() {
  const agent = new NativeEnkiAgent(
    'Assistant',
    'Answer clearly and keep responses short.',
    'ollama::qwen3.5:latest',
    20,
    process.cwd(),
  )

  const output = await agent.run('session-1', 'Explain what this project does.')
  console.log(output)
}

main().catch(console.error)

TypeScript version:

import { NativeEnkiAgent } from '@getenki/ai'

const agent = new NativeEnkiAgent(
  'Assistant',
  'Answer clearly and keep responses short.',
  'ollama::qwen3.5:latest',
  20,
  process.cwd(),
)

const output = await agent.run('session-1', 'Explain what this project does.')
console.log(output)

Constructor arguments:

  • name?: string
  • systemPromptPreamble?: string
  • model?: string
  • maxIterations?: number
  • workspaceHome?: string
  • agenticLoop?: string

If omitted, the runtime falls back to built-in defaults for name, prompt, and max iterations.

Custom Agentic Loops

Use the optional agenticLoop argument when you want to replace the default loop instructions seen by the model but still keep the normal Rust runtime loop:

const { NativeEnkiAgent } = require('@getenki/ai')

const agent = new NativeEnkiAgent(
  'Assistant',
  'Answer clearly and keep responses short.',
  'ollama::qwen3.5:latest',
  20,
  process.cwd(),
  [
    '1. Understand the request.',
    '2. Decide whether a tool is needed.',
    '3. Summarize observations.',
    '4. Return the final answer.',
  ].join('\n'),
)

Use setAgentLoopHandler(...) when you want JavaScript to own the loop itself:

const { NativeEnkiAgent } = require('@getenki/ai')

const agent = new NativeEnkiAgent(
  'Assistant',
  'Answer clearly and keep responses short.',
  'ollama::qwen3.5:latest',
  8,
  process.cwd(),
)

agent.setAgentLoopHandler((requestJson) => {
  const request = JSON.parse(requestJson)
  return JSON.stringify({
    content: `Handled in JavaScript for: ${request.user_message}`,
    steps: [
      {
        index: 1,
        phase: 'Custom',
        kind: 'final',
        detail: 'Returned a final answer from JavaScript',
      },
    ],
  })
})

The handler receives a JSON request containing the current transcript, system prompt, tool catalog, model, iteration limit, and workspace paths.

Use clearAgentLoopHandler() to restore the default runtime loop.

Repository examples:

Tools

Tools can be attached with NativeEnkiAgent.withTools(...). Each tool object must provide:

  • id or name
  • description
  • one of inputSchema, inputSchemaJson, parameters, or parametersJson
  • either execute(inputJson, contextJson) or a shared toolHandler

Example:

const { NativeEnkiAgent } = require('@getenki/ai')

const tools = [
  {
    id: 'calculate_sum',
    description: 'Add two numbers and return a short text result.',
    inputSchema: {
      type: 'object',
      properties: {
        a: { type: 'number' },
        b: { type: 'number' },
      },
      required: ['a', 'b'],
    },
    execute: (inputJson, contextJson) => {
      const args = inputJson ? JSON.parse(inputJson) : {}
      const ctx = contextJson ? JSON.parse(contextJson) : {}
      const result = Number(args.a) + Number(args.b)

      return JSON.stringify({
        result,
        workspaceDir: ctx.workspaceDir,
        text: `${args.a} + ${args.b} = ${result}`,
      })
    },
  },
]

const agent = NativeEnkiAgent.withTools(
  'Tool Agent',
  'Use tools when they help.',
  'ollama::qwen3.5:latest',
  20,
  process.cwd(),
  tools,
  null,
)

Per-tool execute receives:

  • inputJson: serialized tool arguments
  • contextJson: serialized runtime context with agentDir, workspaceDir, and sessionsDir

TypeScript tool example:

import { NativeEnkiAgent } from '@getenki/ai'

type SumArgs = {
  a?: number
  b?: number
}

type ExampleTool = {
  id: string
  description: string
  inputSchema: Record<string, unknown>
  execute: (inputJson: string, contextJson: string) => string
}

const tools: ExampleTool[] = [
  {
    id: 'calculate_sum',
    description: 'Add two numbers and return a short text result.',
    inputSchema: {
      type: 'object',
      properties: {
        a: { type: 'number' },
        b: { type: 'number' },
      },
      required: ['a', 'b'],
    },
    execute: (inputJson: string, contextJson: string): string => {
      const args = inputJson ? (JSON.parse(inputJson) as SumArgs) : {}
      const ctx = contextJson
        ? (JSON.parse(contextJson) as { workspaceDir?: string })
        : {}
      const result = Number(args.a) + Number(args.b)

      return JSON.stringify({
        result,
        workspaceDir: ctx.workspaceDir,
        text: `${args.a} + ${args.b} = ${result}`,
      })
    },
  },
]

const agent = NativeEnkiAgent.withTools(
  'Tool Agent',
  'Use tools when they help.',
  'ollama::qwen3.5:latest',
  20,
  process.cwd(),
  tools,
  null,
)

Instead of putting execute on every tool, you can pass a shared toolHandler as the final argument to withTools(...) or withToolsAndMemory(...). The shared handler receives:

  • toolName
  • inputJson
  • agentDir
  • workspaceDir
  • sessionsDir

Reusable Tool Registries

If you want to manage a shared pool of tools and attach it to multiple agents later, create a NativeToolRegistry and connect it dynamically:

const { NativeEnkiAgent, NativeToolRegistry } = require('@getenki/ai')

const registry = new NativeToolRegistry()
registry.registerTools(
  [
    {
      name: 'lookup_release',
      description: 'Return a canned release note.',
      parametersJson: JSON.stringify({
        type: 'object',
        properties: {
          version: { type: 'string' },
        },
        required: ['version'],
      }),
    },
  ],
  (toolName, inputJson) => JSON.stringify({ toolName, inputJson }),
)

const agent = new NativeEnkiAgent(
  'Registry Agent',
  'Use connected tools when they help.',
  'ollama::qwen3.5:latest',
  20,
  process.cwd(),
)

agent.connectToolRegistry(registry)

You can also construct the agent directly from a registry with NativeEnkiAgent.withToolRegistry(...).

This is the cleanest path when multiple agents should share the same tool catalog or when tools need to be connected after agent construction.

TypeScript uses the same API:

import { NativeEnkiAgent, NativeToolRegistry } from '@getenki/ai'

const registry = new NativeToolRegistry()
registry.registerTools(
  [
    {
      name: 'lookup_release',
      description: 'Return a canned release note.',
      parameters: {
        type: 'object',
        properties: {
          version: { type: 'string' },
        },
        required: ['version'],
      },
    },
  ],
  (toolName: string, inputJson: string) => JSON.stringify({ toolName, inputJson }),
)

const agent = NativeEnkiAgent.withToolRegistry(
  'Registry Agent',
  'Use connected tools when they help.',
  'ollama::qwen3.5:latest',
  20,
  process.cwd(),
  registry,
)

Memory

Memory modules are plain objects:

const memories = [{ name: 'example-memory' }]

When using withMemory(...) or withToolsAndMemory(...), you supply four callbacks:

  • recordHandler(memoryName, sessionId, userMsg, assistantMsg)
  • recallHandler(memoryName, sessionId, query, maxEntries)
  • flushHandler(memoryName, sessionId)
  • consolidateHandler(memoryName, sessionId)

recallHandler must return an array of JsMemoryEntry objects:

type JsMemoryEntry = {
  key: string
  content: string
  kind: JsMemoryKind
  relevance: number
  timestampNs: string
}

Supported memory kinds:

  • JsMemoryKind.RecentMessage
  • JsMemoryKind.Summary
  • JsMemoryKind.Entity
  • JsMemoryKind.Preference

TypeScript memory typing example:

import {
  JsMemoryKind,
  type JsMemoryEntry,
  type JsMemoryModule,
} from '@getenki/ai'

const memories: JsMemoryModule[] = [{ name: 'example-memory' }]
const memoryStore = new Map<string, JsMemoryEntry[]>()

function memoryKey(memoryName: string, sessionId: string): string {
  return `${memoryName}:${sessionId}`
}

function getMemoryEntries(memoryName: string, sessionId: string): JsMemoryEntry[] {
  const key = memoryKey(memoryName, sessionId)
  const existing = memoryStore.get(key)
  if (existing) {
    return existing
  }

  const empty: JsMemoryEntry[] = []
  memoryStore.set(key, empty)
  return empty
}

const recordHandler = (
  memoryName: string,
  sessionId: string,
  userMsg: string,
  assistantMsg: string,
): void => {
  const entries = getMemoryEntries(memoryName, sessionId)
  entries.push({
    key: `entry-${entries.length + 1}`,
    content: `User: ${userMsg}\nAssistant: ${assistantMsg}`,
    kind: JsMemoryKind.RecentMessage,
    relevance: 1,
    timestampNs: `${Date.now() * 1000000}`,
  })
}

Workflow Runtime

Use NativeWorkflowRuntime when you want to register workflow agents plus JSON task and workflow definitions directly from Node.js.

const { NativeEnkiAgent, NativeWorkflowRuntime } = require('@getenki/ai')

const researcher = new NativeEnkiAgent(
  'Researcher',
  'Return short factual notes.',
  'ollama::qwen3.5:latest',
  4,
  './.enki',
)
researcher.configureWorkflow('researcher', ['research'])

const writer = new NativeEnkiAgent(
  'Writer',
  'Turn notes into a concise summary.',
  'ollama::qwen3.5:latest',
  4,
  './.enki',
)
writer.configureWorkflow('writer', ['writing'])

const runtime = new NativeWorkflowRuntime(
  [researcher, writer],
  [
    JSON.stringify({
      id: 'research_topic',
      target: { type: 'capabilities', value: ['research'] },
      prompt: 'Research {{topic}} and return 3 concise bullet points.',
      input_bindings: { topic: 'input.topic' },
    }),
    JSON.stringify({
      id: 'write_summary',
      target: { type: 'agent_id', value: 'writer' },
      prompt: 'Write a short summary for {{topic}} using {{research.content}}',
      input_bindings: {
        topic: 'input.topic',
        research: 'research',
      },
    }),
  ],
  [
    JSON.stringify({
      id: 'research-to-summary',
      name: 'Research To Summary',
      nodes: [
        { id: 'research', kind: 'task', task_id: 'research_topic', output_key: 'research' },
        { id: 'summary', kind: 'task', task_id: 'write_summary', output_key: 'summary' },
      ],
      edges: [{ from: 'research', to: 'summary', transition: { type: 'always' } }],
    }),
  ],
  './.enki',
)

const response = JSON.parse(
  await runtime.startJson(
    JSON.stringify({
      workflow_id: 'research-to-summary',
      input: { topic: 'workflow bindings in enki-js' },
    }),
  ),
)

const persisted = JSON.parse(await runtime.inspectJson(response.run_id))
console.log(persisted.status)

Human Intervention

Workflow runs persist pending interventions as part of the run state, so approvals and failure escalations can pause and resume without moving state into a separate coordinator service.

Each pending intervention includes:

  • workflow_id
  • run_id
  • node_id
  • prompt
  • reason
  • response
  • created_at and resolved_at

Two built-in patterns are supported:

  • human_gate nodes pause immediately and wait for a human response
  • task nodes with failure_policy: "pause_for_intervention" convert a terminal failure into an intervention asking the human to retry, skip, continue, or fail

The runnable TypeScript example is example/basic-ts/human-intervention-workflow.ts. It demonstrates:

  • a human_gate approval flow that pauses, resolves, and resumes to approval.approved = true
  • a missing-agent failure that pauses for intervention and resumes after a skip response

The runtime interaction loop is:

  1. startJson(...) returns a paused workflow response
  2. inspectJson(runId) exposes pending_interventions
  3. submitInterventionJson(runId, interventionId, response) resolves the intervention
  4. resumeJson(runId) continues the persisted run

Tools And Memory Example

The repository examples in example/basic-js/index.js and example/basic-ts/index.ts use NativeEnkiAgent.withToolsAndMemory(...) with:

  • a calculate_sum tool
  • a get_today tool
  • an in-memory Map for session memory storage

There are also richer examples in example/basic-js/multi-agent-tools-memory.js and example/basic-ts/multi-agent-tools-memory.ts. Those examples show:

  • a researcher agent with a custom lookup_example_topics tool
  • a coordinator agent consuming a researcher handoff via its own tool
  • shared in-process memory across both agents
  • progress logging so long-running model calls do not look stalled

Minimal JavaScript version:

const { JsMemoryKind, NativeEnkiAgent } = require('@getenki/ai')

const tools = [
  {
    id: 'get_today',
    description: 'Return the current local date in ISO format.',
    inputSchema: { type: 'object', properties: {} },
    execute: () => JSON.stringify({ today: new Date().toISOString().slice(0, 10) }),
  },
]

const memories = [{ name: 'example-memory' }]
const memoryStore = new Map()

function memoryKey(memoryName, sessionId) {
  return `${memoryName}:${sessionId}`
}

const agent = NativeEnkiAgent.withToolsAndMemory(
  'Basic JS Agent',
  'Answer clearly and keep responses short.',
  'ollama::qwen3.5:latest',
  20,
  process.cwd(),
  tools,
  null,
  memories,
  (memoryName, sessionId, userMsg, assistantMsg) => {
    const key = memoryKey(memoryName, sessionId)
    const entries = memoryStore.get(key) ?? []
    entries.push({
      key: `entry-${entries.length + 1}`,
      content: `User: ${userMsg}\nAssistant: ${assistantMsg}`,
      kind: JsMemoryKind.RecentMessage,
      relevance: 1,
      timestampNs: `${Date.now() * 1000000}`,
    })
    memoryStore.set(key, entries)
  },
  (memoryName, sessionId, query, maxEntries) => {
    const entries = memoryStore.get(memoryKey(memoryName, sessionId)) ?? []
    return entries.filter((entry) => entry.content.includes(query)).slice(-maxEntries)
  },
  (memoryName, sessionId) => {
    memoryStore.delete(memoryKey(memoryName, sessionId))
  },
  () => {},
)

Minimal TypeScript version:

import {
  JsMemoryKind,
  type JsMemoryEntry,
  type JsMemoryModule,
  NativeEnkiAgent,
} from '@getenki/ai'

type ExampleTool = {
  id: string
  description: string
  inputSchema: Record<string, unknown>
  execute: (inputJson: string, contextJson: string) => string
}

const tools: ExampleTool[] = [
  {
    id: 'get_today',
    description: 'Return the current local date in ISO format.',
    inputSchema: { type: 'object', properties: {} },
    execute: (): string =>
      JSON.stringify({ today: new Date().toISOString().slice(0, 10) }),
  },
]

const memories: JsMemoryModule[] = [{ name: 'example-memory' }]
const memoryStore = new Map<string, JsMemoryEntry[]>()

const agent = NativeEnkiAgent.withToolsAndMemory(
  'Basic TS Agent',
  'Answer clearly and keep responses short.',
  'ollama::qwen3.5:latest',
  20,
  process.cwd(),
  tools,
  null,
  memories,
  (memoryName: string, sessionId: string, userMsg: string, assistantMsg: string): void => {
    const key = `${memoryName}:${sessionId}`
    const entries = memoryStore.get(key) ?? []
    entries.push({
      key: `entry-${entries.length + 1}`,
      content: `User: ${userMsg}\nAssistant: ${assistantMsg}`,
      kind: JsMemoryKind.RecentMessage,
      relevance: 1,
      timestampNs: `${Date.now() * 1000000}`,
    })
    memoryStore.set(key, entries)
  },
  (memoryName: string, sessionId: string, query: string, maxEntries: number): JsMemoryEntry[] => {
    const entries = memoryStore.get(`${memoryName}:${sessionId}`) ?? []
    return entries.filter((entry) => entry.content.includes(query)).slice(-maxEntries)
  },
  (memoryName: string, sessionId: string): void => {
    memoryStore.delete(`${memoryName}:${sessionId}`)
  },
  (): void => {},
)

Running The Examples

JavaScript example:

cd example/basic-js
npm install
npm start
npm run start:tool-registry
npm run start:custom-agent-loop
npm run start:react-custom-agent-loop
npm run start:multi-agent-tools-memory

TypeScript example:

cd example/basic-ts
npm install
npm start
npm run start:multi-agent-tools-memory

The checked-in examples currently hardcode ollama::qwen3.5:latest as the model, so make sure that model is available in your local provider before running them.

Development

From crates/bindings/enki-js:

npm install
npm run build
npm test

Useful scripts:

  • npm run build: build the native addon in release mode
  • npm run build:debug: build without release optimizations
  • npm test: run the AVA test suite
  • npm run lint: run oxlint
  • npm run format: run Prettier, cargo fmt, and taplo format