docore — The AI Engine¶
@doable/docore is the package that turns "we have an AI provider" into "we have a multi-tenant AI agent platform". It lives at packages/docore/.
This page is a tour of the public API. Internal details (event-mapper field names, persistence formats) are not stable across versions.
Concepts¶
- Engine (
DoCoreEngine) — top-level handle. One per process. Owns the worker pool and event bus. - Session — one logical conversation:
(workspaceId, projectId, userId). Backed by a Copilot SDK session. - Worker process — an OS process that runs one or more sessions. Spawned by the isolator using the strongest available backend (nsjail / systemd / Job Object / direct).
- Tools — JSON-schema callable functions exposed to the model. Built-in (
read_file,write_file,shell, …) plus integration tools loaded dynamically. - Policy — allow/deny rules applied to every tool call.
- Event bus — typed pub/sub for everything happening inside a session.
Getting an engine¶
import { DoCoreEngine } from "@doable/docore";
const engine = new DoCoreEngine({
cliPath: process.env.COPILOT_CLI_PATH,
cliUrl: process.env.COPILOT_CLI_URL,
defaultModel: process.env.COPILOT_DEFAULT_MODEL,
maxWorkers: 4,
isolation: { backend: "auto" }, // 'nsjail' | 'systemd' | 'jobobject' | 'direct'
});
await engine.start();
Opening a session¶
const session = await engine.openSession({
sessionId: "ws-123/proj-abc/user-42",
cwd: "/var/doable/projects/proj-abc",
systemPrompt: BUILD_MODE_PROMPT,
tools: await loadTools({ workspace, project, user }),
permissionHandler: createPolicySandbox({ policy }),
});
The session is persistent: it survives restarts because Copilot serializes its state under ~/.copilot/session-state/<sessionId>/.
Sending a message¶
for await (const event of session.send({ text: "Add a contact form" })) {
switch (event.type) {
case "assistant.message_delta":
process.stdout.write(event.delta);
break;
case "tool.call":
console.log("→", event.tool, event.args);
break;
case "tool.result":
console.log("←", event.result);
break;
case "session.compaction_start":
// model is summarizing context to fit
break;
case "session.task_complete":
console.log("done");
break;
}
}
The full event union lives in packages/docore/src/events.ts — every Copilot SDK event is mapped to a DoCoreEvent so callers don't depend on the upstream wire format.
Pools & per-user accounting¶
In a SaaS deployment, you don't want one user to monopolize all worker processes. Use DoCorePool + DoCoreUserManager:
import { DoCorePool, DoCoreUserManager } from "@doable/docore";
const pool = new DoCorePool({ maxWorkers: 8 });
const users = new DoCoreUserManager(pool, {
perUserLimit: 2, // max concurrent sessions per user
perUserMemoryMB: 300,
});
// Acquire a worker for the user; queues if at limit.
const worker = await users.acquire({ userId, sessionId });
try {
// ...use worker.session
} finally {
worker.release();
}
Sandboxing tool calls¶
import { createPolicySandbox, PolicyStore } from "@doable/docore";
const policy = new PolicyStore({
persistence: new FilePersistence("./policies.json"),
defaults: { ...POLICY_DEFAULTS },
});
const sandbox = createPolicySandbox({
policy,
audit: (entry) => recordAudit(entry),
});
const session = await engine.openSession({
// ...
permissionHandler: sandbox,
});
createPolicySandbox returns a function the Copilot SDK calls before every tool execution. Decisions are allow, deny, or ask (latter is forwarded to the UI as a permission prompt).
Process isolation¶
The ProcessIsolator picks a backend automatically:
import { ProcessIsolator, NsjailBackend, SystemdBackend, JobObjectBackend, DirectBackend } from "@doable/docore";
const isolator = new ProcessIsolator({
backend: process.platform === "linux"
? new SystemdBackend({ memoryMax: "300M", cpuQuota: "50%" })
: process.platform === "win32"
? new JobObjectBackend({ memoryLimit: 300 * 1024 * 1024 })
: new DirectBackend(),
});
Most callers never touch this directly — DoCoreEngine wires it up from its own options.
Tracing¶
docore includes a tiny OpenTelemetry-style tracer (tracer.ts). Plug your own sink:
import { Tracer } from "@doable/docore";
const tracer = new Tracer({
sink: (span) => myOtelExporter.export(span),
});
Public exports cheat-sheet¶
| Export | Use |
|---|---|
DoCoreEngine, DoCoreEngineOptions |
Top-level |
DoCorePool, DoCoreUserManager, WorkerPool |
Concurrency / fairness |
EventBus, DoCoreEvent, DoCoreEventOf<T> |
Event handling |
mapSdkEvent |
Convert raw Copilot SDK event → DoCoreEvent |
createPolicySandbox, createSandboxedPermissionHandler |
Tool-call gatekeeper |
PolicyStore, PolicyAdmin, FilePersistence, MemoryPersistence |
Persisted policy |
POLICY_DEFAULTS, DEFAULT_SAFE_COMMANDS, DEFAULT_DANGEROUS_COMMANDS, DEFAULT_TRAVERSAL_PATTERNS, DEFAULT_URL_ALLOWLIST |
Out-of-the-box rules |
ProcessIsolator, NsjailBackend, SystemdBackend, JobObjectBackend, DirectBackend |
OS isolation |
Tracer, noopTracer |
Observability |
DoCoreServer |
Optional standalone HTTP server mode |
The full type list is in packages/docore/src/index.ts.