@agent-core

API & usage

How to CALL agent-core: the model-facing tools, the feature-gated HTTP route surface, and the typed getPackageApi() consumer surface.

This page is about calling agent-core, not authoring it. Three audiences reach the package through three distinct surfaces:

  • a model calls agent-core's native tools;
  • a client (the chat UI, a skill script, an external MCP client) calls its HTTP routes, each gated by a feature the caller must hold;
  • another package calls it in-process through the typed getPackageApi() adapter — never over MCP.

For contributing tools, routes, skills, or UI to agent-core, start at the package overview and the directory contract instead.

1. The model-facing tools

agent-core ships its native tools as tools/*.json schemas. Each is loaded, AJV-validated, and dispatched through the same policy, feature-gate, and approval machinery as any package's tools (see tools for the deep dive on the complete catalog). A model calls a tool by name with arguments matching its schema; below are representative calls and their typical result shapes.

execute — run a sandboxed shell command or activate a skill

{ "action": "shell", "command": "ls data://notes", "reason": "list the notes folder" }

Result: a card with stdout, stderr, exitCode, and the resolved working directory. action: "skill" instead renders a SKILL.md body into context and stages its declared credentials for later shell calls.

delegate — spawn a subagent with its own full agentic loop

{ "task": "Audit the auth routes for missing feature gates", "max_steps": 20 }

Result: the child's final summary text, plus a persisted child transcript openable read-only from the chat UI. The child inherits the conversation's permission profile.

workflow — create and manage scheduled workflows (the Calendar)

{ "action": "schedule", "title": "Daily digest", "instruction": "Summarize new issues", "cron": "0 9 * * 1-5", "timezone": "Europe/Budapest" }

Result: the created/updated WorkflowEntry, a list, or a run's event log depending on action. Requires workflow.read, escalating to workflow.write / workflow.dispatch per action.

web_search — search the public web

{ "query": "MCP stateless transport RC", "mode": "research", "max_results": 8 }

Result: a ranked list, each entry carrying at least { title, url, publishedAt, snippet } (plus content_markdown in research mode with include_content); publishedAt is a normalized YYYY-MM-DD day, absent when no date could be determined. provider picks the strategy — auto (default), native, tavily, brave, searxng — and every one falls back to a keyless engine rather than failing. Gated by core.web. Native search runs on the conversation's own model, so its tokens and the vendor's per-search fee are recorded in the same usage event as the rest of the turn and count against the project's spend limits.

web_fetch — fetch and extract readable content from one URL

{ "url": "https://example.com/article", "max_chars": 20000, "format": "markdown" }

Result: the page's content, up to max_chars, fetched through the hardened DNS-pinned, private-IP-blocking pipeline. HTML, Markdown, JSON, XML, plain text and PDF are all handled. format selects the rendering (markdown, text, raw, json); save_to ("tmp" or a data://… path) stores the result and additionally requires drive.write — with core.execute on top for "tmp", which is the shell substrate's own private scratch directory. Gated by core.web.

ask_question — pause and ask the user explicit questions

{ "question": "Which environment should I deploy to?", "options": [{ "id": "stg", "label": "Staging" }, { "id": "prod", "label": "Production" }] }

Result: the stream blocks until the user answers; the answer(s) return to the model. Use only when the decision genuinely belongs to the user.

todos — maintain the session working-state

{ "plan": { "name": "Refactor", "todos": [{ "id": "schema", "content": "Tighten the tool schema", "status": "in_progress" }] } }

The same plan key also formalizes the session goal and loop bound — in the same call or a later one. The goal records the outcome and acceptance evidence; todos remain the executable steps:

{ "plan": { "goal": "Tighten every tool schema", "done": ["all tools validate", "docs updated"], "loop": { "until": "goal", "maxSteps": 40 } } }

Later, mark satisfied acceptance conditions without resending the locked goal — by the number the prompt lists them under (- [ ] 1. all tools validate) or by their exact text, all-or-nothing, together with any todo progress:

{ "mark": { "met": [1] } }
{ "mark": { "todos": [{ "id": "schema", "status": "completed" }], "met": [2] } }

Without close, keys sent together apply plan, then mark, whatever order they arrive in, and a locked goal fails the whole call before anything is written. With close, mark and close end the current cycle first and plan opens the next, so one call replaces a goal the agent set itself (when nothing is open, the close is skipped and named):

{ "close": "cleared", "plan": { "goal": "Tighten the tool schemas the audit named", "done": ["audit list validates"] } }

A refused step stops the call, and the answer names what already landed. Result: the updated goal, loop and todo list, re-rendered into the system prompt inside one <session> block. The working-state is durable — it persists with the conversation, survives a restart, and is shared across a multi-user conversation. A goal/loop is free to set once per cycle, then LOCKS: mark.met can only flip existing conditions to met, until is locked once set, and maxSteps can only be lowered (and is enforced); the user changes the rest. An explicitly authored self-evolving run (evolve: "agent") is the sole goal-rewrite exception; it still cannot loosen the loop bound. plan.loop's maxSteps folds into the run's step ceiling.

The cycle closes by itself. The tick that meets the last condition (or { "close": "achieved" } on a fully-met or condition-less goal) archives the goal and the current todo list as one versioned entry in the conversation's meta.json (sessionLedger.history[], bounded by the sessionArchiveMaxEntries platform setting), empties both, and answers archived as v<n>; the prompt then carries one Archived: v<n> "…" achieved (3/3 · 5/5 todos) line. A todo list with no goal (or a goal without conditions) closes the same way when every step is completed. { "close": "cleared" } archives an obsolete goal the agent authored itself as cleared (with no goal open, its own todo list); a goal set by the user or a workflow is theirs — the agent can achieve it, never drop it (the prompt marks it [set by user — locked]), except in a self-evolving run, where the marker drops locked and the agent may clear the goal too. The composer's Goal & loop dialog offers the same Achieve / Clear to the user and lists the archived cycles; a workflow's seeded goal re-asserts itself on the next fire after a close.

compact_history — free context-window space

{ "actions": [{ "target": { "tool_use_id": "toolu_abc" }, "action": "remove" }], "reason": "context filling" }

Result: an in-place, reversible, non-destructive trim of earlier context. Reference a tool output by its tool_use_id (the ⟦id⟧ handle shown in the conversation map) or a whole non-tool row by message_seq (#N); each action is remove (replace the output with a tiny marker), summarize (replace it with a condensed replacement note), or truncate (keep the first max_chars). The conversation is never forked and never rewritten — the trim is re-applied to the model's prompt each turn and a user can toggle it off ("Show full"). Use it continuously: after reading or searching a lot, immediately compact the outputs that turned out useless so only what matters stays in context.

connect — manage external connection setup safely

{ "check": { "kind": "mcp", "id": "github" }, "list": { "kind": "mcp" } }

Result: a named handshake outcome followed by the current visible MCP assignments. The same five verbs — remove, add, check, authorize, list — cover MCP, channel, git, provider and source kinds. Every requested kind-specific feature is checked before the first mutation, and successful earlier operations remain explicit if a later runtime operation is refused. Secrets are never tool inputs or outputs: provider/git/channel setup returns a secure human handoff, and an OAuth URL is for the human to open.

execute with background: true — a shell that reports back

{ "command": "pnpm build 2>&1", "background": true }

Result: { "shellId": "nrs-sh-…", "statusUri": "data://…/meta.json", "outputUri": "data://…/output.jsonl", "status": "running" } at once. The conversation continues by itself when the shell ends — the exit code and a bounded head + tail of the output arrive as the platform's own background_report result card, streamed live into the spawner's open browser when there is one — so the agent ends its turn instead of polling. The full log is readable any time with fs_read(outputUri). Listing, reading more and stopping are conversation routes (GET /shells, GET /shells/:id/output?since=, POST /shells/:id/kill), reached through the execute-skill scripts or the card's Kill control; they answer only for shells the caller started — a shell handle cannot be borrowed across callers — and stopping answers only for the agent that started it (403 shell_stop_forbidden from another agent's session).

Messaging another agent's conversation

POST /api/packages/@neuralis/agent-core/conversations/<id>/inbox?agentId=<target>
{ "message": "The survey is in data://…/EVIDENCE.md — please review section 3.",
  "delivery": "settle" }

Result: 202 { "queued": true, "id": "msg_…" }. The message lands in that conversation as a report card naming the sender (the caller's own session — a body from is ignored) and the conversation continues by itself as the sender — a turn under the caller's own identity, never the owner's. The caller needs write access to the conversation and read access to the agent; sending to your own conversation is 400; the brakes answer 429 (inbox_hops_exceeded, inbox_full, inbox_rate_limited). GET …/inbox lists the undelivered messages, each with the delivery it was queued with; POST …/delegates/<runId>/inbox addresses a background subagent run's own inbox. The manage-conversations skill's queue.sh / inbox.sh wrap these.

delivery decides WHEN the receiver sees it. settle (the default) delivers it at the start of the receiver's next turn. immediate asks the turn that is already running to take it at its next step, so a correction reaches an agent while it still changes what that agent does. It is a request, not a guarantee: the platform keeps the message queued for the end of the turn — never drops it — when the running turn belongs to a different user, when its text can leave the platform (mirrored to a chat channel, bridged to one, or produced by a workflow run whose result may be announced to one), when the turn runs under a weaker guard than its conversation is configured with, when it is on its forced wrap-up step, or when it has already taken its ceiling of immediate messages for that turn. A background run's inbox has no mid-turn step at all, so immediate there answers 400 inbox_delivery_unsupported, and an unknown value answers 400 inbox_delivery_invalid.

Reading a transcript incrementally

GET /api/packages/@neuralis/agent-core/conversations/<id>/messages?agentId=<id>&sinceSeq=42

sinceSeq is INCLUSIVE — the response holds the rows from that sequence number onward, so a client that already has row 42 gets it back and replaces it. That is deliberate: when several tools run in parallel their results merge into one existing row, which keeps its sequence number, and an exclusive bound would never return the row that changed. The bound is decimal digits only — anything else (a negative, a fraction, 0x2, 1e3) answers 400 invalid_since_seq rather than being coerced or quietly returning the whole transcript. The same parameter works on …/delegates/<runId>/messages, behind the same read gate.

The workspace uses it with the live conversation notification channel, which is how a turn no browser is driving — a scheduled workflow run, a background subagent reporting back, a message from another agent — appears in an open tab without a reload.

Stopping the turn that is live in a conversation

POST /api/packages/@neuralis/agent-core/conversations/<id>/stop?agentId=<agent>

Result: 200 { "ok": true, "stopped": true }. This reaches a turn nobody is holding a browser connection for — a background report, an inbox message being answered, a workflow run — which is the class the chat composer's own Stop cannot reach. The caller needs write access to the conversation, and then either owns the turn (a report runs as its spawner, a workflow as its creator) or holds admin strength; otherwise 403 stream_stop_forbidden. It is idempotent: nothing running answers "stopped": false. The turn ends cleanly — the partial answer is persisted and nothing offers to resume it — and a workflow turn also closes its run so a restart will not restart it. The manage-conversations skill's stop.sh wraps it.

"Cleanly" now includes the work already in flight. A running container shell command or subagent is stopped along with the turn (a host-plane command is only left behind — the turn stops waiting, the command finishes under its own timeout), an external MCP call is cancelled on the wire, and a tool that cannot be stopped is left to finish with its result dropped — so the conversation is released in seconds rather than waiting out whatever the tool was doing. Two things are deliberately exempt because they are built to outlive the turn: a background shell and a background subagent. Each has its own stop.

Tools over MCP

agent-core's manifest hosts exactly four tools on the platform's MCP endpoint for external clients: web_search, web_fetch, connect, and the connector branch of execute. Local shell execution and skill activation through execute still require stream context. Every other agent-core tool is reached only inside a stream. The complete catalog — including the risk profile of each tool — is on the tools page.

2. The HTTP route surface

Every other client reaches agent-core through REST routes under /api/packages/@neuralis/agent-core/<pattern>. The kernel route dispatcher derives userId, projectId, and the optional agentId from the verified session — never from client-supplied values — and enforces each route's declared feature against the caller's grantedFeatures before the handler runs (deny-by-default; a caller lacking the feature gets 403, no data, no existence oracle).

PatternFeatureWhat it reaches
agent-contextcore.observeAuthorized, read-only agent setup snapshot: stable system text, effective tools, contribution counts, exact UTF-8 bytes and estimated tokens. It creates no conversation; unknown and unauthorized agent ids share a 404.
agents/*core.agentsAgent CRUD, team catalog, batch create, resync, hosted MCP API keys, MCP catalog summary. The core.agents gate is only coarse discoverability; resync is capability-floored on top — ordinary file/config resync needs agent-update capability, identity reset needs full agent-management, and the result is truthful (applied / preview / partial, a partial file/config failure returns 207). See agents.
streamcore.executeStart or continue an agent stream as SSE.
conversations/*core.executeConversation CRUD, messages, the evidence read (GET …/evidence — the machine-extracted record of what the conversation verifiably did: files changed, commands run, background shells, subagent runs, messages received, tool failures; ?kinds= filters, ?limit= caps each kind (default 50, ceiling 200; the body is also bounded by serialized size, and every cut kind is named in truncated), ?afterSeq= narrows the range, and a malformed one answers 400 invalid_evidence_kinds / invalid_evidence_limit / invalid_after_seq rather than quietly answering something else), edit, undo, rewind, retry, generate-title, the summarize family (POST …/summarize with an optional activate, POST …/summarize/cancel to stop, GET …/summarize/status; an answer the validator rejected twice is 422 summary_invalid_output with the validator's reason in detail and the model in modelId, and no model answering at all is 400 summary_model_unavailable), the Show-full / Resume history-mode toggle, the separate compact_history show-full toggle, a live mid-stream permission-profile switch, POST …/stop to end the turn the conversation is running (see above — it reaches a turn no browser is driving), and delegate-run transcripts together with their stop / follow-up controls, plus the conversation inbox (POST …/inbox to queue a message into another agent's conversation or a background run, GET …/inbox to list what is undelivered). Branching is here too: GET …/forks lists a conversation's branches and POST …/fork creates one, the latter additionally requiring core.conversation.fork — a feature with no default grant below the admin tier. List and get carry a response-only active flag while a summary is running. A manual summarize refused mid-stream returns 409 (stream_active / pending_interaction — retry later); a conversation whose pinned summary model cannot be dispatched returns 400 summary_model_unavailable (a configuration error — re-pin the model in the context-window popover); a run that exceeds its total deadline returns 504 summary_timeout; 422 summary_invalid_output means the model answered but not with a valid summary.
interactions/*core.executeList, fetch, answer, approve, deny PendingInteraction records. Approving an MCP-app call is the one route that also executes: it re-checks a matching View lease, fresh write access to the conversation, and a fresh mcp.apps grant before the upstream call runs — a core.execute holder without mcp.apps cannot approve one.
context-windowcore.executeProvider-exact context occupancy for a conversation (behind a uri-policy READ gate).
shells/*core.executeList, read and kill your own background shells — the registry behind the chat Shell tab. Ownership is the stored spawning session, never a caller-supplied value; optional agentId / conversationId only narrow within your own shells. Reading is per user across your agents; killing is per agent — a shell started from another agent's chat answers 403 shell_stop_forbidden.
composer-adoptcore.executeRelocate composer attachments staged before a conversation existed into that conversation's own folder. Both source and destination are rebuilt server-side.
composer-configcore.executeThe two client-side composer clipboard caps (max pasted image bytes, max pasted characters) — a deliberately narrow projection of platform config.
usagecore.observeAgent or project usage events and summary.
codex-usagecore.observeThe caller's Codex (ChatGPT subscription) usage-limit snapshot at their resolved credential scope. One on-demand live refresh per open, falling back to the last stored reading; never polls, and every scope-selecting parameter is ignored.
connectors/*core.observe (read) · core.connectors (every mutation)Connector CRUD, delegated to the connector lifecycle. Listing connectors, reading one's health and reading its generated tools sit at the read tier; creating, configuring, connecting, disconnecting, enabling, disabling and deleting one escalate in-handler to core.connectors — a feature with no default grant below the admin tier. Connectors are project-scoped: a caller reaches its own project's connectors plus the platform-wide ones a package declared, and an id belonging to another project answers the same 404 as an unknown one. Creating requires a resolved project.
files/*core.observeResolved file contributions for the agent, one array per category.
tools/*core.observeTool catalog with the agent selection overlay.
packages/*core.observeAggregated package overview + loader diagnostics.
mcp-servers/*core.observe (GET) · mcp.connect (add/remove, header + env secrets) · mcp.sidecar (activate)List connected MCP servers; add/remove one on the agent (POST / DELETE /:id); activate a dormant stdio server as a managed sidecar (POST /activate); set or revoke your own secret outbound header value (POST / DELETE /header-credential) or the sidecar's env twin (POST / DELETE /env-credential) — always in your own user scope, with no scope parameter. The listing reports header slot names and marks a configured remote server that could not connect as unavailable; it never returns a header or env value.
mcp-oauth/*mcp.connectBegin the OAuth 2.1 authorization for one of your own external MCP servers (and seed a static client id/secret for a server without dynamic registration); the resulting tokens land in your own user scope.
mcp-apps/*mcp.appsThe MCP Apps render surface: serve a ui:// template behind a scope-bound View lease, bridge the app's lease-bound tools/call and resources/read back to the one server that owns it — through the standard policy gate — and keep or release that lease (POST /lease/heartbeat, POST /lease/close). Every call re-runs the access check; the lease is routing and ownership, never a cached authorization.
modelscore.observeAvailable LLM models and the default.
logscore.observeJSONL log reader. system / package are agent-core's platform-global logs (every tenant's events, in the app zone) and escalate in-handler to platform.audit (no default grant); run is one agent's log under the session project and escalates to core.logs (no default grant below the admin tier). Feature checks, never role-name checks.
prompt-polishcore.prompt-polishOne-shot composer draft rewrite.
workflow/*workflow.readWorkflow CRUD and dispatch, the package-shipped template catalog and its install, the calendar range projection, run history and a run's event log, routes that add, change or remove ONE trigger while every other trigger stays as stored, plus an SSE feed of live workflow events. Escalates to workflow.write / workflow.dispatch per action. A plain DELETE archives; the permanent delete is a separate archived-only sub-path, recorded in the audit log. Fire-endpoint trigger tokens and signing secrets are issued here, shown once, and only to a person: an agent's session ticket is refused. An invisible or foreign entry answers 404, never 403 — a private workflow leaves no existence trace.
channels/*workflow.readChannel adapter descriptors, connections, pairing approvals, peer links and workflow bindings; mutations escalate to channels.connect / channels.manage, and the binding surface to workflow.write; creating a connection answers a person only, never an agent's session ticket. Deleting a connection, unlinking a peer and removing a binding are recorded in the audit log. Secrets never appear in a response — connection records hold credential references only.
git-connect/*git.connectList, connect, and disconnect your own git remotes — seed a push token or start the OAuth flow for one of the supported hosts, always into your own user scope; tokens are write-only and never echoed back. See git remotes.
self-credentials/*credentials.selfYour own credential catalog: list what you can see, set a value, remove one — always in your own user scope, with no scope parameter. Values are never echoed. See bring your own keys.
terminal/sources/*terminal.readThe terminal tabs the caller may open, each with its resolved working directory — or, when it is not selectable, the reason.
terminal/sessions/*terminal.readManaged PTY lifecycle — create, resize, exec, read scrollback, stage a pasted image, destroy. terminal.container and terminal.native are re-checked in-handler on the operations that reach those planes; exec runs inside an existing session's sandbox and refuses when there is none, so it never creates an unscoped shell of its own. Staging an image additionally needs core.execute, and reaching another user's session at all (a project-wide listing, an attachment, a destroy) additionally needs terminal.supervise.
terminal/healthterminal.readLive PTY session count and uptime.

The features themselves and their default role grants are documented on the package overview. A skill script calling these routes authenticates with the implicit per-stream session ticket — see the security page; it never forges identity in a header. External MCP clients use the OAuth 2.1 MCP endpoint, not these package routes.

3. The typed getPackageApi() consumer surface

Another package never calls agent-core's HTTP routes and never calls it over MCP. It calls the typed, session-aware adapter:

import { getPackageApi } from '@neuralis/package-system';
import type { AgentCoreApi } from '@neuralis/agent-core';

const api = getPackageApi<AgentCoreApi>('@neuralis/agent-core', session);

This is the in-process path the lifecycle mandates: direct internal APIs for package-to-package calls, MCP reserved for the model and external clients. The returned AgentCoreApi exposes, among others:

MemberWhat it gives the caller
agentStore / agentServiceRead and manage agents.
usageStoreToken and cost usage events and summaries.
verifyAgentAccess(userId, projectId, agentId)Ownership/access check for an agent.
getActiveStreams()Snapshot of currently-running agentic streams.
listMcpServers(session) / listConnectorInstances()Dashboard-grade MCP/connector signal for the session.

The host verifies skill session tickets through the required session-ticket-verifier service registered at init. It returns the real live-stream session; the verifier is separate from the public AgentCoreApi.

agent-core consumes brain-core's filesystem API the exact same way — typed, through getPackageApi(), importing nothing across the boundary.

No internal MCP

Internal MCP callTool between packages is forbidden. If you find yourself wanting to call an agent-core tool from another package, you want the typed API above instead — the tool surface is for the model and external clients only.

See also

  • Native tools — the deep dive on execute, delegate, and the web tools.
  • package-system routes — how route patterns, params, and the feature gate work.
  • package-system lifecycle — the getPackageApi() doctrine and the MCP-internal-forbidden rule.
  • Security — the shell sandbox, URI-policy gating, the session ticket, and the MCP boundary.

On this page