@getenki/ai-win32-arm64-msvc
v0.5.81
Published
Node.js bindings for Enki's Rust agent runtime.
Maintainers
Readme
@getenki/ai
Node.js bindings for Enki's Rust agent runtime, published as a native package via napi-rs.
Install
npm install @getenki/aiThe 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:
NativeEnkiAgentNativeToolRegistryNativeMultiAgentRuntimeNativeWorkflowRuntimeJsAgentStatusJsMemoryKindJsMemoryModuleJsMemoryEntryJsAgentCardJsAgentRunResultJsExecutionStep
NativeEnkiAgent is the main entrypoint. It can be created in five modes:
new(...)for a plain agentNativeEnkiAgent.withTools(...)NativeEnkiAgent.withToolRegistry(...)NativeEnkiAgent.withMemory(...)NativeEnkiAgent.withToolsAndMemory(...)
NativeToolRegistry supports:
new(...)registerTools(...)clear()toolNames()size
It also supports two loop customization levels:
agenticLoopconstructor arguments for prompt-level loop customizationsetAgentLoopHandler(...)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?: stringsystemPromptPreamble?: stringmodel?: stringmaxIterations?: numberworkspaceHome?: stringagenticLoop?: 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:
idornamedescription- one of
inputSchema,inputSchemaJson,parameters, orparametersJson - either
execute(inputJson, contextJson)or a sharedtoolHandler
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 argumentscontextJson: serialized runtime context withagentDir,workspaceDir, andsessionsDir
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:
toolNameinputJsonagentDirworkspaceDirsessionsDir
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.RecentMessageJsMemoryKind.SummaryJsMemoryKind.EntityJsMemoryKind.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_idrun_idnode_idpromptreasonresponsecreated_atandresolved_at
Two built-in patterns are supported:
human_gatenodes 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 toretry,skip,continue, orfail
The runnable TypeScript example is example/basic-ts/human-intervention-workflow.ts. It demonstrates:
- a
human_gateapproval flow that pauses, resolves, and resumes toapproval.approved = true - a missing-agent failure that pauses for intervention and resumes after a
skipresponse
The runtime interaction loop is:
startJson(...)returns a paused workflow responseinspectJson(runId)exposespending_interventionssubmitInterventionJson(runId, interventionId, response)resolves the interventionresumeJson(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_sumtool - a
get_todaytool - an in-memory
Mapfor 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_topicstool - 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-memoryTypeScript example:
cd example/basic-ts
npm install
npm start
npm run start:multi-agent-tools-memoryThe 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 testUseful scripts:
npm run build: build the native addon in release modenpm run build:debug: build without release optimizationsnpm test: run the AVA test suitenpm run lint: runoxlintnpm run format: run Prettier,cargo fmt, andtaplo format
