@vantageos/agent-engine
v0.4.0-alpha.0
Published
Reusable Convex component: durable agent engine. Agent = data aggregate (root sheet + child tables), never compiled code. Built on @convex-dev/workflow + @convex-dev/workpool (+ @convex-dev/agent). Eve parity by design.
Maintainers
Readme
@vantageos/agent-engine
A reusable Convex component that
provides a durable agent engine: sessions, delegation, tool approvals,
scheduled "habits", bounded spending "mandates", and long-task survival —
all built on top of
@convex-dev/workflow
and
@convex-dev/workpool.
An agent is a data aggregate, not compiled code. Creating an agent means writing a row (plus child rows for skills, tool approvals, connections, sub-agents, habits, sandbox config) — there is no build step and no redeploy. This mirrors the authoring model pioneered by vercel/eve, whose slot names, contracts and vocabulary this package tracks closely; credit and thanks to that project for the design this component is built in parity with.
Why
Most agent frameworks compile an agent definition into code that has to be
deployed to change. This engine keeps the entire agent definition as
database rows: skills, tool approvals, delegated sub-agents, sandbox
settings, and scheduled work are all mutable at runtime through ordinary
Convex mutations. Combined with @convex-dev/workflow's durable execution,
a single agent session — or a long-running background job — survives
process restarts, Convex's per-action time limits, and redeploys of the
consuming app.
Install
npm install @vantageos/agent-engineWhat ships in the package
The published tarball is dist/ (compiled JS + .d.ts), README.md,
LICENSE. dist/component/testFixtures/** is deliberately excluded
(package.json "files" negation "!dist/component/testFixtures") — it is
test-only demo workflow code used by this repo's own component-packaging
tests, never part of the public exports (./convex.config and
./_generated/component.js), and not a documented consumer API. No file is
published without a stated reason.
Quickstart
Register the component in your app's convex/convex.config.ts:
import { defineApp } from "convex/server";
import agentEngine from "@vantageos/agent-engine/convex.config";
const app = defineApp();
app.use(agentEngine);
export default app;Then call into it from your own Convex functions. Every public function
takes an explicit orgId: string argument — Convex does not propagate the
calling app's ctx.auth identity across a component boundary, so your app
is responsible for resolving the caller's org before calling in:
import { components } from "./_generated/api";
export const createAgent = mutation({
args: { orgId: v.string(), name: v.string(), instructions: v.string() },
handler: async (ctx, args) => {
return ctx.runMutation(components.agentEngine.engine.agents.create, {
orgId: args.orgId,
createdBy: "user_123",
name: args.name,
instructions: args.instructions,
model: "gpt-4o",
});
},
});What's inside
The component's data model (root table agents, plus child tables for
skills, sub-agents, connections, habits, sandbox config, and per-tool
approvals) is deployed under the Convex component boundary your app
registers — it does not touch your own app's tables, and every index is
scoped by orgId first, deny-by-default.
API surface (component/, current release)
components.agentEngine.engine.agents.create({
orgId, createdBy, name, description?, instructions, model,
reasoningEffort?, modelProvider?, maxSubagents?,
maxInputTokensPerSession?,
}) -> Id<"agents">
components.agentEngine.engine.agents.get({ orgId, agentId }) -> agent | null
components.agentEngine.engine.directory.listByOrg({ orgId })
-> { _id, name, status, model, createdAt }[]
components.agentEngine.engine.resolvers.resolveManifest({
orgId, sessionId,
}) -> { agentId, name, instructions, model, toolApprovals[] }
components.agentEngine.engine.sessions.start({
orgId, agentId, createdBy,
}) -> { sessionId: string /* the world runId */, workflowId: string }
// 0.3.0: every `sessionId` below IS that runId (string); an unknown,
// cross-org or non-session id is refused as "Session not found".
components.agentEngine.engine.sessions.sendTurn({
orgId, sessionId, input,
}) -> null
components.agentEngine.engine.sessions.appendReasoning({
orgId, sessionId, chunk,
}) -> null
components.agentEngine.engine.sessions.requestToolCall({
orgId, sessionId, toolName, argsJson,
}) -> { authorized, approvalPending, denied }
components.agentEngine.engine.sessions.completeToolCall({
orgId, sessionId, toolName, resultJson,
}) -> null
components.agentEngine.engine.sessions.completeTurn({
orgId, sessionId,
}) -> null
components.agentEngine.engine.sessions.end({
orgId, sessionId, outcome: "completed"|"cancelled",
}) -> null
components.agentEngine.engine.sessions.listEvents({
orgId, sessionId,
}) -> { _id /* journal eventId */, _creationTime /* event createdAt, ms */,
name, payload?, createdAt }[] (read from the world journal)
components.agentEngine.engine.toolApprovals.respond({
orgId, sessionId, toolName,
decision: "approved"|"denied",
updatePolicy?: "always"|"once"|"never",
}) -> null
components.agentEngine.engine.wakeup.enqueue({
orgId, agentId, mandateScope, cost, reason,
workFunctionHandle, workflowId, awaitedEventName,
}) -> string (action; the world-queue messageId)
components.agentEngine.engine.wakeup.cancel({ orgId, messageId }) -> null
components.agentEngine.engine.wakeup.getStatus({
orgId, messageId,
}) -> { status: "pending" | "delivered" | "failed" }
components.agentEngine.engine.durableJob.start({
orgId, kind, stepFunctionHandle, totalSteps, maxAttemptsPerStep,
}) -> string (the world runId)
components.agentEngine.engine.durableJob.getStatus({ orgId, jobId /* runId */ })
-> { status, cursor, totalSteps, lastError?, abandonedAtStep? }
components.agentEngine.engine.durableJob.cancel({ orgId, jobId }) -> null
components.agentEngine.engine.mandates.set({
orgId, agentId, scope, kind, rate, period, capacity?,
}) -> Id<"mandates">
components.agentEngine.engine.mandates.get({
orgId, agentId, scope,
}) -> mandate | null
components.agentEngine.engine.mandates.spend({
orgId, agentId, scope, cost, reason,
}) -> { ok, approvalId? }
components.agentEngine.engine.mandates.listApprovals({
orgId, agentId,
}) -> approvalRequest[]
components.agentEngine.engine.mandates.resolveApproval({
orgId, approvalId, resolvedBy, status: "approved"|"denied",
}) -> null
components.agentEngine.engine.mandates.usageCount({
orgId, agentId, scope,
}) -> number
components.agentEngine.engine.habits.create({
orgId, agentId, name, cronExpression, prompt,
}) -> Id<"habits">
components.agentEngine.engine.habits.get({ orgId, habitId }) -> habit | null
components.agentEngine.engine.habits.listForAgent({ orgId, agentId }) -> habit[]
components.agentEngine.engine.habits.updateSchedule({
orgId, habitId, cronExpression,
}) -> null
components.agentEngine.engine.habits.disable({ orgId, habitId }) -> null
components.agentEngine.engine.habits.enable({ orgId, habitId }) -> null
components.agentEngine.engine.habits.listRuns({ orgId, habitId }) -> habitRun[]
components.agentEngine.engine.sandboxCallback.startRun({
orgId, workflowId, awaitedEventName, timeoutMs,
}) -> Id<"sandboxRuns">
components.agentEngine.engine.sandboxCallback.attachSandboxId({
orgId, sandboxRunId, sandboxId,
}) -> null
components.agentEngine.engine.sandboxCallback.getStatus({
orgId, sandboxRunId,
}) -> { status, resultSummary?, ... }
components.agentEngine.engine.sandboxCallback.processCallback({
orgId, sandboxRunId, deliveryId, status, resultSummary?,
}) -> { outcome: "woke"|"duplicate" }
components.agentEngine.engine.sandboxCallback.deadLetterMalformed({
orgId?, deliveryId, reason, rawBody,
}) -> null
components.agentEngine.engine.delegation.declareSubagent({
orgId, parentAgentId, childAgentId, toolName, description?,
}) -> Id<"subagents">
components.agentEngine.engine.delegation.listSubagents({
orgId, parentAgentId,
}) -> subagent[]
components.agentEngine.engine.delegation.delegateToSubagent({
orgId, subagentId, ...,
}) -> null (action)
components.agentEngine.engine.delegation.invokeAgentClone({
orgId, agentId, ...,
}) -> null (action; root-only, non-recursive by construction)
components.agentEngine.engine.vpConnection.get({
orgId, agentId,
}) -> vpConnection | null
components.agentEngine.engine.vpConnection.getToggles({
orgId, agentId,
}) -> { memoryEnabled, tasksEnabled, messagesEnabled, notesEnabled }
components.agentEngine.engine.vpConnection.setToggles({
orgId, agentId, memoryEnabled?, tasksEnabled?,
messagesEnabled?, notesEnabled?,
}) -> Id<"vpConnections">
components.agentEngine.engine.vpConnection.invokeVpTool({
orgId, agentId, tool, vpFunctionHandle, argsJson,
}) -> null (action)Concepts
- Sessions — a durable conversation thread, driven by a versioned
@convex-dev/workflowrun. Turns, streamed reasoning, tool-call lifecycle and approval lifecycle are appended as an event log a client replays on reconnect. - Delegation — an agent can declare typed sub-agents (strict isolation: a sub-agent's own skills/connections/approvals are never copied onto the parent) or invoke a non-recursive clone of itself as a built-in tool. Both mechanisms reuse the same pause/wake primitive.
- Mandates — a bounded, weighted spending cap per agent/org/scope. Spending under the cap succeeds; an overrun creates a pending approval request rather than silently failing open or throwing away the audit trail.
- Habits — a per-agent, runtime-editable cron schedule (create, update, disable, enable — all with zero redeploy).
- Long-task survival —
durableJoblets a consumer's task survive past a single Convex action's execution-time ceiling by expressing it as N independently-bounded, resumable steps.
Trust model
Convex does not propagate the calling app's ctx.auth identity across a
component boundary. Every public function in this component takes orgId
(and, where relevant, createdBy) as an explicit argument — your app is
responsible for authenticating its user and resolving orgId before
calling in. Every document lookup by id is checked against the caller's own
orgId before being returned or mutated; a cross-org read or write is
refused the same way a not-found row would be.
Dependency pins
ai is pinned to v6 (6.0.35) in the root package; any other tree that declares ai must use the same major (world-convex declares none today).
Two sub-packages (first-orchestrator/, scheduling-specialist/) keep ai ^7.0.58 because
[email protected] peers ai ^7.0.58 (no eve release peers v6); each runs in its own process with its own
install, sharing no dependency tree with root or world-convex. Everywhere else the v6 pin stands;
scripts/vitestScope.test.ts enforces it.
Requirements
- Convex
^1.42 - A Convex deployment with
@convex-dev/workflow,@convex-dev/workpool,@convex-dev/agent,@convex-dev/rate-limiter,@convex-dev/crons, and@convex-dev/sharded-counterresolvable (all installed automatically as dependencies of this package).
License
FSL-1.1-Apache-2.0 — free for production use; converts to Apache 2.0 after two years, per version.
Credit
Slot names, contracts and authoring vocabulary are deliberately kept in parity with vercel/eve. This package is not affiliated with Vercel; parity is a design choice, not a claim of origin.
