@nodus-ai/equile-preview
v0.4.0-preview.52
Published
TypeScript SDK for running user-owned coding agents through Equile.
Readme
@nodus-ai/equile
Run authenticated Codex, Claude, Grok, Gemini, Cursor, or Factory agents through Equile.
Use this package from trusted server-side code only. Never expose EQUILE_API_KEY in a browser, Git repository, prompt, screenshot, or log.
Install
npm install @nodus-ai/equileNode.js 18 or newer is required.
Required values
export EQUILE_BASE_URL="https://preview-api.equile.tech"
export EQUILE_API_KEY="ndk_..."
export EQUILE_AGENT_ID="..."EQUILE_API_KEYis either scoped to fixed Agent IDs or to one Agent Workspace. A Workspace key dynamically includes Agents added to that Workspace later.EQUILE_AGENT_IDidentifies an Agent already created and authenticated in Equile.- The SDK cannot create Agents or change provider credentials.
Run one task
For a Workspace SDK key, omit agentId. Equile distributes runs across healthy matching Agents in that Workspace:
const { run } = await equile.agentRuns.createAndWait({
provider: "codex",
model: "gpt-5.5",
mode: "goal",
prompt: "Begin the review and write the final report.",
goal: `# Review Goal
## Requirements
- Inspect the complete project.
- Identify the three highest-priority issues.
- Support every finding with concrete evidence.
## Deliverable
Write the completed report to output/report.md.`
});The bundled CLI supports interactive login and persistent local profiles:
npm install -g @nodus-ai/equile@preview
equile login
equile auth status
equile agents listequile login opens a browser, displays a one-time code, and waits for approval.
The resulting revocable Workspace SDK key is stored in
~/.config/equile/config.json with file mode 0600. Use --profile NAME or
EQUILE_PROFILE for multiple accounts and environments. EQUILE_API_KEY and
EQUILE_BASE_URL always take precedence for CI and headless use. equile logout
revokes the key server-side and removes the local profile.
The CLI also accepts direct environment credentials:
export EQUILE_API_KEY="ndk_..."
equile sdk
equile capabilities
equile agents list
equile agents quota --agent "$EQUILE_AGENT_ID" --refresh
equile agents configure --agent "$EQUILE_AGENT_ID" --model gpt-5.5 --reasoning high
equile agents concurrency --agent "$EQUILE_AGENT_ID" --max 12
equile run --provider codex --model gpt-5.5 --instructions "Return exactly: workspace smoke" --wait
equile runs list --status running
equile runs get --run <run-id>
equile runs events --run <run-id>
equile runs cancel --run <run-id>
equile runs artifact --run <run-id> --artifact <artifact-id>
equile runs artifact --run <run-id> --download-all --to ./artifacts
equile runs batch --from-jsonl tasks.jsonl --idempotency-key nightly-review --wait
equile session list # Active Sessions only; use --status destroyed --limit 100 for history
equile session create --agent "$EQUILE_AGENT_ID" --persistent
equile session run --session <session-id> --instructions "Continue the task" --wait
equile session exec --session <session-id> --command "git status --short"
equile session events --session <session-id> --after 0
equile session telemetry --session <session-id>
equile session telemetry-history --session <session-id> --step-sec 60
equile session pause --session <session-id>
equile session resume --session <session-id>
equile session destroy --session <session-id>Workspace keys can still target a specific Agent when deterministic routing is required:
Create run.mjs:
import { createEquile } from "@nodus-ai/equile";
const equile = createEquile({
baseUrl: process.env.EQUILE_BASE_URL,
apiKey: process.env.EQUILE_API_KEY,
requestTimeoutMs: 15 * 60_000
});
const { run } = await equile.agentRuns.createAndWait({
agentId: process.env.EQUILE_AGENT_ID,
instructions: "Review this project and return the three highest-priority issues.",
timeoutSeconds: 1800
});
console.log(run.status); // "completed"
console.log(run.finalOutput); // final assistant responseRun it:
node run.mjsEvery run-returning SDK method uses the same { run } envelope. This includes
create(), wait(), and createAndWait():
const { run } = await equile.agentRuns.create({
agentId: process.env.EQUILE_AGENT_ID,
instructions: "Perform the task."
});
console.log(run.id);Do not read result.id from create(). The ID is result.run.id.
Reliable create-then-wait pattern
Use this pattern for batch jobs and unreliable networks:
const { run } = await equile.agentRuns.create({
agentId,
instructions,
idempotencyKey: `teacher:${teacherId}:review-v1`,
timeoutSeconds: 7200
});
const { run: completed } = await equile.agentRuns.wait(run.id, {
timeoutMs: 10 * 60 * 60 * 1000,
pollIntervalMs: 5000
});equile sdk (also available as equile capabilities) prints the installed SDK
version, every callable client method, feature flags such as Workspace
credential failover and persistent session snapshots, plus the supported CLI
commands. It does not make an authenticated API request, so it also works
before EQUILE_API_KEY is configured.
The Session namespace also exposes diff, ports, git, and
terminalTicket for workspace inspection and terminal clients. The Desktop
Runtime namespace exposes browse, read, ports, attach, reveal, and
terminalTicket in addition to lifecycle methods. These owner-wide methods
require a Workspace-scoped SDK key.
equile workspace build excludes .git from the uploaded Docker context.
Keep generated files, local dependencies, and secrets out of the selected
context directory; the CLI does not reinterpret Docker ignore rules.
Rules:
- Generate a stable
idempotencyKeyfor each logical task. - After
create()returns a run ID, keep waiting on that same ID. - If polling fails, retry
wait(run.id); do not create a new run. - If the create response is lost, call
create()again with the sameidempotencyKey. - Do not log complete prompts or API keys.
The CLI --wait form prints run_id=<id> to stderr before it starts polling.
If the waiting process is interrupted, resume with equile runs wait --run <id>.
Concurrency and queues
Submitting many promises only submits many runs. Actual execution is limited by the Agent's server-side maxConcurrency.
This is Run concurrency, not a sandbox count. Equile may use one sandbox per Run or multiple isolated runtime sessions inside one sandbox. SDK behavior stays the same.
Backend worker capacity and EQUILE_AGENT_RUN_WORKER_CAPACITY only control internal throughput. Postgres enforces maxConcurrency globally across all workers.
await equile.agents.update(agentId, { modelId: "gpt-5.5", reasoningEffort: "high" });
await equile.agents.update(agentId, { modelId: null, reasoningEffort: null }); // Restore provider defaults
await equile.agents.setRunConcurrency(agentId, 160); // Valid range: 1-160
const created = await Promise.all(
tasks.map((task) => equile.agentRuns.create({
agentId,
instructions: task.instructions,
idempotencyKey: task.id
}))
);Runs above the available Agent concurrency remain queued and start automatically when a slot becomes available. Queue time does not consume the run's timeoutSeconds; the execution timer starts when Equile claims the run.
Before starting a replacement batch, inspect existing work instead of blindly creating duplicates:
const queued = await equile.agentRuns.list({ agentId, status: "queued" });
const running = await equile.agentRuns.list({ agentId, status: "running" });
console.log({ queued: queued.runs.length, running: running.runs.length });List and cancel old runs
list() returns at most 100 lightweight run summaries. Use nextCursor to continue.
let cursor;
const queuedRunIds = [];
do {
const page = await equile.agentRuns.list({
agentId,
status: "queued",
cursor,
limit: 100
});
queuedRunIds.push(...page.runs.map((run) => run.id));
cursor = page.nextCursor ?? undefined;
} while (cursor);
await equile.agentRuns.cancelMany(queuedRunIds);Only runs belonging to Agents allowed by the SDK key are visible or cancellable.
Batch runner
createBatch() submits one durable job. The server owns fan-out, status
aggregation, and idempotent child creation; the client only polls one job.id.
Input file:
[
{
"instructions": "Review teacher 123 and return JSON.",
"timeoutSeconds": 7200,
"metadata": { "teacherId": "123" },
"agentId": "<agent-id>"
}
]Run (the command prints run_id=<job-id> to stderr before waiting):
node examples/batch-agent-runs.mjs tasks.json results.jsonlFor application code, use createBatch({ items, idempotencyKey }) followed by
waitBatch(job.id). Repeating the same top-level idempotency key returns the
same job and child run IDs.
File inputs
Pass short-lived HTTPS URLs as task resources:
const { run } = await equile.agentRuns.createAndWait({
agentId,
instructions: "Inspect the attached project.",
resources: [{
url: signedUrl,
path: "input/project.zip",
contentType: "application/zip"
}]
});Never place credentials in resource URLs, filenames, metadata, or instructions.
Send images and other inline files
Small binary payloads (screenshots, photos, PDFs) can be passed directly — no hosting required. Equile stages the bytes privately and the agent reads the file from its workspace:
import { readFile } from "node:fs/promises";
const { run } = await equile.agentRuns.createAndWait({
agentId,
instructions: "Describe what is wrong in the attached screenshot.",
resources: [{
contentBase64: (await readFile("screenshot.png")).toString("base64"),
path: "input/screenshot.png",
contentType: "image/png"
}]
});Sessions accept per-message attachments; the delivered turn references the staged file paths so the agent can open them with its file-reading tools:
await equile.sessions.runAndWait(session.id, "Compare this design against the implementation.", {
attachments: [{ name: "design.png", contentBase64: (await readFile("design.png")).toString("base64") }]
});The CLI reads local files for you:
equile run --agent "$EQUILE_AGENT_ID" --instructions "Describe the screenshot." --attach ./screenshot.png --wait
equile session run --session <session-id> --instructions "Look at this photo." --attach ./photo.jpg --waitLimits: about 7 MB per file (10,000,000 base64 characters), 20 MB of inline content per run, and 4 attachments per session message. Attachment names must look like name.ext (letters, digits, _, -). Inline blobs are deleted when the run is cleaned up; session staging blobs are deleted immediately after the file lands in the workspace.
Goal mode
Goal mode is supported only by Codex Agents:
const { run } = await equile.agentRuns.createAndWait({
agentId,
mode: "goal",
instructions: "Implement and verify the requested change."
});For all other providers, omit mode or use "standard".
Artifacts
Completed runs can return output artifacts:
for (const artifact of run.artifacts) {
const { url, expiresAt } = await equile.agentRuns.getArtifactDownloadUrl(run.id, artifact.id);
console.log(artifact.path, url, expiresAt);
}Artifact download URLs are short-lived. Artifact metadata reports available or expired.
Use downloadArtifacts(run.id, directory) to fetch every available artifact
and preserve its relative path in one call.
Network policy
Runs deny outbound network access by default. Add explicit domains only when a task needs them:
await equile.agentRuns.create({
agentId,
instructions: "Read the approved documentation site.",
networkPolicy: { mode: "allowlist", allowedDomains: ["docs.example.com"] }
});Use noNetwork for an explicit deny request; the runtime still keeps its local model gateway and backend-controlled callbacks available. The effective policy is recorded in the session_started event for audit.
Events and streaming
for await (const event of equile.agentRuns.stream(runId)) {
console.log(event.type, event.payload);
console.log(event.raw?.provider, event.raw?.type, event.raw?.payload);
}Use wait() when only the final result matters. Use stream() when progress events are needed. Both wait for runtime cleanup before finishing.
SDK event methods request full event detail. event.payload is the stable,
normalized client contract; event.raw contains the complete persisted provider
event for rich desktop rendering. Raw payloads are not truncated, but they are
still recursively sanitized before persistence: credentials and internal
infrastructure identifiers are removed or redacted.
Persistent sessions
Agent Runs are temporary and automatically cleaned up. Use Sessions only when the conversation must continue across multiple messages.
A Session is intentionally bound to one Agent identity. Automatic credential failover applies to Workspace-scheduled Agent Runs created with provider and no agentId; it does not silently switch an existing Session to a different Agent. If a fixed Session loses its credential, start a new Workspace-scheduled Run when cross-Agent continuation is required.
const { session } = await equile.sessions.create({ agentId });
const first = await equile.sessions.runAndWait(session.id, "Inspect the repository.");
const second = await equile.sessions.runAndWait(session.id, "Now implement the fix.");
const live = await equile.sessions.telemetry(session.id);
const history = await equile.sessions.telemetryHistory(session.id, { stepSec: 60 });
await equile.sessions.pause(session.id);
await equile.sessions.resume(session.id);
await equile.sessions.destroy(session.id);Persistent sandboxes (Claude only)
By default a sandbox is a single-shot workflow: it is reaped when idle and an
agent run destroys it on completion. Pass persistent: true to keep the
sandbox alive as an ongoing workspace instead. Claude only — its tmux harness
accepts appended messages; other providers reject the flag.
// A session whose sandbox is never idle-reaped (max sandbox lifetime, 24h;
// pause/resume snapshots carry it beyond that):
const { session } = await equile.sessions.create({ agentId, persistent: true });
// An agent run that leaves its workspace running. The prompt drops the
// "deliver files to an output folder" single-shot instructions and treats
// /workspace as an ongoing project. The completed run exposes `sessionId`,
// and follow-up persistent runs on the same agent continue the same live
// workspace:
const { run } = await equile.agentRuns.createAndWait({
agentId,
instructions: "Scaffold the service.",
persistent: true
});
await equile.sessions.runAndWait(run.sessionId, "Now add tests.");Persistent sessions are billed while live; pause(), resume() and
destroy() work as usual.
Errors
SDK failures throw EquileError:
import { EquileError } from "@nodus-ai/equile";
try {
await equile.agentRuns.wait(runId);
} catch (error) {
if (error instanceof EquileError) {
console.error(error.status, error.code, error.retryAt, error.message, error.data);
}
throw error;
}Common statuses:
400: invalid request;401: invalid SDK key;403: SDK key is not allowed to use the Agent;404: run or session not found;409: runtime/auth conflict;504: the local SDK wait deadline elapsed. The remote run may still be active; callget()orwait()again before creating another run.
Agent Run creation also exposes stable business codes through EquileError.code:
workspace_auth_unavailable: every matching Workspace credential is invalid or unavailable;workspace_capacity_unavailable: usable credentials exist, but every matching Agent is at capacity;quota_exhausted: a specifically selected Agent is rate-limited;EquileError.retryAtcontains the reset time when known.
Session APIs expose stable codes for caller-actionable failures:
invalid_session_request,invalid_session_list_request,invalid_session_list_cursor,invalid_session_message,invalid_session_wait_request;session_not_found,session_turn_conflict,session_turn_not_found;session_not_live,session_not_paused,session_checkpoint_unavailable;desktop_runtime_offline,agent_auth_unavailable,workspace_revision_not_ready.
API reference
agents
list()create({ provider, sourceId?, credentialRef?, harness, modelId, displayName?, defaultResourceProfile?, maxConcurrency?, metadata? })— existing OAuth calls may omitsourceId; API connections must provide it.update(agentId, { modelId?, reasoningEffort? })setRunConcurrency(agentId, maxConcurrency)setConcurrency(agentId, maxConcurrency)— compatibility aliasquota(agentId, { refresh? })
agentRuns
create(input)→{ run }wait(runId, waitOptions?)→{ run }createAndWait(input, waitOptions?)→{ run }createBatch({ items, idempotencyKey? })→{ job }getBatch(batchId)/waitBatch(batchId)→{ job, runs }downloadArtifacts(runId, toDir)→{ runId, toDir, files }get(runId)list({ agentId?, status?, cursor?, limit? })wait(runId, options?)events(runId, { after? })→ normalized events plus sanitizedrawprovider payloadsstream(runId, options?)cancel(runId)cancelMany(runIds)getArtifactDownloadUrl(runId, artifactId)
sessions
list({ status?, limit?, cursor? })→{ sessions, nextCursor }; defaults to active Sessions onlycreate({ agentId })get(sessionId)telemetry(sessionId)/telemetryHistory(sessionId, { startMs?, endMs?, stepSec? })diff(sessionId, knownHash?)/ports(sessionId, knownHash?)git(sessionId, { action, message?, path? })terminalTicket(sessionId)run(sessionId, text, { attachments? })execWorkspace(sessionId, command, timeoutSeconds?)runAndWait(sessionId, text, options?)— options may includeattachmentswaitForIdle(sessionId, options)events(sessionId, { after? })→ normalized events plus sanitizedrawprovider payloadsstream(sessionId, options?)pause(sessionId)resume(sessionId)destroy(sessionId)
workspaces (runtime images and revisions)
This namespace manages immutable execution environments and interactive builders. Agent Workspace membership and routing are exposed through the Workspace-scoped key plus agents, agentRuns, and sessions.
These methods use the same Workspace SDK key as Agents, Runs, and Sessions.
create({ name, defaultResourceProfile? })list()/get(workspaceId)/archive(workspaceId)/revisions(workspaceId)startBuilder(workspaceId, { baseRevisionId? })/getBuild(buildId)exec(buildId, command, timeoutSeconds?)listFiles(buildId, path?)/readFile(buildId, path)/writeFile(buildId, path, content)upload(buildId, path, content)/download(buildId, path)publish(buildId)/destroyBuilder(buildId)createContextUpload(workspaceId)buildFromDockerfile(workspaceId, input)/buildFromContext(workspaceId, input)
desktopRuntimes
These methods require a Workspace-scoped SDK key because Desktop Runtimes are owner-wide execution resources rather than properties of one Agent.
list()get(desktopRuntimeId)rename(desktopRuntimeId, name)grants(desktopRuntimeId)grant(desktopRuntimeId, agentId, "full" | "files_only")revokeGrant(desktopRuntimeId, agentId)browse(desktopRuntimeId, { path?, dirsOnly? })/read(desktopRuntimeId, path)ports(desktopRuntimeId, knownHash?)attach(desktopRuntimeId, { name, contentBase64 })reveal(desktopRuntimeId, { path, app? })terminalTicket(desktopRuntimeId)disconnect(desktopRuntimeId); disconnected runtimes are hidden fromlist()andget()
Terminal tickets are short-lived bearer credentials. Pass them directly to the WebSocket client and do not persist or log them.
The same paired computer can serve multiple Agents with independent permissions. files_only grants expose only list, read, and write operations; full grants additionally allow commands, PTYs, local skills/plugins, and local MCP. Desktop daemons use bounded concurrency through EQUILE_DESKTOP_CONCURRENCY (default 4, maximum 16). Runs remain queued with desktopWaiting: true while the computer is offline, so execution timeout and Goal turns do not start until it reconnects.
Checklist for coding agents
Before writing an Equile integration, verify all of these:
- Import from
@nodus-ai/equile, not a repository-relativesdk/distpath. - Read
create()results from{ run }. - Give every logical task a stable
idempotencyKey. - Keep application metadata under your own keys; Equile rejects reserved runtime and failover metadata keys.
- Retry polling against the same run ID.
- Inspect/cancel old queued runs before replacing a failed batch.
- Treat
timeoutSecondsas execution time, not queue time. - Keep API keys and full prompts out of logs.
