JavaScript
@getenki/ai is the JavaScript package for Enki. It exposes native Node.js bindings built from the Rust runtime with napi-rs.
This package is the current JavaScript surface. We are not publishing a WASM binding in this docs set.
What it exposes
NativeEnkiAgentNativeToolRegistryNativeMultiAgentRuntimeNativeWorkflowRuntimeJsAgentRunResultJsExecutionStepJsAgentStatusJsAgentCardJsMemoryKindJsMemoryModuleJsMemoryEntryJsMultiAgentMember
NativeEnkiAgent can be created in five modes:
new NativeEnkiAgent(...)NativeEnkiAgent.withTools(...)NativeEnkiAgent.withToolRegistry(...)NativeEnkiAgent.withMemory(...)NativeEnkiAgent.withToolsAndMemory(...)
It also supports two loop customization levels:
agenticLoopconstructor arguments for prompt-level loop customizationsetAgentLoopHandler(...)for a JavaScript-defined loop override
For traced runs, the package also exposes:
agent.runWithTrace(sessionId, userMessage)runtime.processWithTrace(agentId, sessionId, userMessage)
Install
npm install @getenki/ai
The package ships prebuilt native binaries for:
- Windows x64 and arm64
- macOS x64 and arm64
- Linux x64 and arm64 using GNU libc
Basic agent
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)
If you need execution steps, use runWithTrace(...) instead of run(...):
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 result = await agent.runWithTrace('session-1', 'Explain what this project does.')
console.log(result.output)
console.log(result.steps)
}
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 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.
Current note: the JavaScript loop handler is synchronous, so if your custom loop needs LLM access, prefer a synchronous API call pattern.
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
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
You can also pass a shared toolHandler to withTools(...) or withToolsAndMemory(...). That callback receives:
toolNameinputJsonagentDirworkspaceDirsessionsDir
Reusable tool registries
Use NativeToolRegistry when you want to define tools once and attach them to one or more agents later.
const { NativeEnkiAgent, NativeToolRegistry } = require('@getenki/ai')
const registry = new NativeToolRegistry()
registry.registerTools(
[
{
name: 'lookup_release_note',
description: 'Return a short release note for a named feature.',
inputSchema: {
type: 'object',
properties: {
feature: { type: 'string' },
},
required: ['feature'],
},
},
],
(toolName, inputJson) => {
const args = inputJson ? JSON.parse(inputJson) : {}
return `${toolName}:${args.feature}`
},
)
const agent = new NativeEnkiAgent(
'Registry Agent',
'Use connected tools when they help.',
'ollama::qwen3.5:latest',
20,
process.cwd(),
)
agent.connectToolRegistry(registry)
NativeToolRegistry supports:
new NativeToolRegistry(tools?, toolHandler?)registry.registerTools(tools, toolHandler?)registry.clear()registry.toolNames()registry.sizeagent.connectToolRegistry(registry)
If you already have a registry prepared, you can also construct the agent with NativeEnkiAgent.withToolRegistry(...).
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:
const { JsMemoryKind, NativeEnkiAgent } = require('@getenki/ai')
const memories = [{ name: 'example-memory' }]
const memoryStore = new Map()
const agent = NativeEnkiAgent.withMemory(
'Memory Agent',
'Answer clearly and keep responses short.',
'ollama::qwen3.5:latest',
20,
process.cwd(),
memories,
(memoryName, sessionId, userMsg, assistantMsg) => {
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, sessionId, query, maxEntries) => {
const key = `${memoryName}:${sessionId}`
const entries = memoryStore.get(key) ?? []
return entries.filter((entry) => entry.content.includes(query)).slice(-maxEntries)
},
(memoryName, sessionId) => {
memoryStore.delete(`${memoryName}:${sessionId}`)
},
() => {},
)
Supported memory kinds:
JsMemoryKind.RecentMessageJsMemoryKind.SummaryJsMemoryKind.EntityJsMemoryKind.Preference
For multi-agent orchestration from Node.js, see JavaScript Multi-Agent.
Multi-agent runtime
NativeMultiAgentRuntime is the Rust-backed registry and delegation runtime exposed to JavaScript.
Current methods:
process(agentId, sessionId, userMessage)processWithTrace(agentId, sessionId, userMessage)registry()discover(capability?, status?)
Development
From crates/bindings/enki-js:
npm install
npm run build
npm test