@vantageos/agent-engine 0.1.0-alpha.6 → 0.1.0-alpha.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +110 -0
- package/README.md +231 -661
- package/dist/component/convex.config.js +8 -9
- package/dist/component/engine/awaitEventTimeout.js +2 -3
- package/dist/component/engine/delegation.js +14 -16
- package/dist/component/engine/directory.js +1 -1
- package/dist/component/engine/durableJob.js +6 -6
- package/dist/component/engine/habits.js +8 -9
- package/dist/component/engine/mandates.js +6 -7
- package/dist/component/engine/sandboxCallback.js +8 -11
- package/dist/component/engine/sessions.js +8 -9
- package/dist/component/engine/toolApprovals.js +1 -1
- package/dist/component/engine/vpConnection.js +6 -9
- package/dist/component/engine/wakeup.js +18 -20
- package/dist/component/engine/workflows.js +4 -7
- package/dist/component/schema.js +42 -67
- package/dist/component/testFixtures/callerSteps.js +1 -1
- package/dist/component/testFixtures/delegationDemoWorkflow.js +2 -3
- package/dist/component/testFixtures/sandboxDemoWorkflow.js +2 -2
- package/dist/component/testFixtures/wakeupDemoWorkflow.js +2 -2
- package/package.json +16 -5
package/README.md
CHANGED
|
@@ -1,424 +1,41 @@
|
|
|
1
1
|
# @vantageos/agent-engine
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
| model provider / API key | not in the Eve repo (env-only) | `agents.modelProvider` + `modelKeys` table | **notre ligne** — BYOK (mvp §2.3), server-only table split from `agents` so list/get can never leak the key |
|
|
35
|
-
| `limits.maxSubagents`, `maxInputTokensPerSession` | `agent-definition.ts:163-207` | `agents.maxSubagents`, `agents.maxInputTokensPerSession` | direct parity |
|
|
36
|
-
| `dynamicSkills` / skills | `manifest.ts:722-725`, `agent-definition.ts:139` | `skills` child table | direct parity |
|
|
37
|
-
| `dynamicTools` / tools | `manifest.ts:722-725` | resolved at read-time from `connections` + `toolApprovals` | direct parity |
|
|
38
|
-
| Tool `approval: Approval<...>` (`always`/`once`/`never`) | `public/definitions/tool.ts:117-125` | `toolApprovals` child table | direct parity |
|
|
39
|
-
| MCP/OpenAPI connections (`defineMcpClientConnection`) | `public/definitions/connections/mcp.ts:23-48` | `connections` child table | direct parity. Our VantagePeers tools (memory, tasks, messages, notes) are wired here as an MCP connection, on by default — **notre ligne**, mvp §2.2 |
|
|
40
|
-
| Declared subagents (named tool, strict isolation) | https://eve.dev/docs/subagents ("a declared subagent inherits nothing from the root's authored slots") | `subagents` child table (`parentAgentId` -> `childAgentId`, recursive) + `engine/delegation.declareSubagent` / `listSubagents` (I6) | direct parity. Isolation is by construction: `declareSubagent` writes ONLY the link + `toolName`; the child's own slots are only ever read through `engine/resolvers.resolveManifest` against a session on `childAgentId` — never copied onto the parent (proven: `engineDelegation.test.ts`, "manifest carries ONLY its own skills") |
|
|
41
|
-
| Subagent tool-name collision -> **build rejected** | https://eve.dev/docs/subagents ("eve rejects the build rather than picking a winner") | `engine/delegation.declareSubagent`'s build-time collision guard (`by_org_parent_tool` index + reserved `"agent"` name + `toolApprovals` lookup) | direct parity — a colliding `declareSubagent` call throws and writes nothing, never picks a silent winner |
|
|
42
|
-
| Built-in `agent` tool — clone of the root, root-only, **NOT recursive** | https://eve.dev/docs/subagents | `engine/delegation.invokeAgentClone`, `agentCloneRuns` child table, `viaAgentTool` recursion guard | direct parity — a clone invocation cannot invoke the `agent` tool again; refused before any row is written or the wake-up pool is touched |
|
|
43
|
-
| Inter-agent delegation mechanism | not modeled explicitly in `vercel/eve`'s own docs | `engine/delegation.delegateToSubagent` / `invokeAgentClone`, BOTH terminate in the SAME public `engine.wakeup.enqueue` (T4/I2) — one pause/wake system, not two (SYNTHESE §4.d) | **notre ligne (mechanism)** — delegation IS a wake-up: A parks on `step.awaitEvent`, B's work is enqueued through `wakeupPool`, `onComplete` -> `sendEvent` wakes A. No second `onComplete`/`sendEvent` path exists anywhere in this engine |
|
|
44
|
-
| `sandbox` | `agent-definition.ts:150-160` | `sandboxConfig` child table (Vercel Sandbox, egress policy, allowlist) | direct parity, I0 §8 |
|
|
45
|
-
| `schedules` / hooks -> "habits" | `manifest.ts:143`, `:726` | `habits` + `habitRuns` child tables (dynamic cron per agent/org, `@convex-dev/crons`, see "Mandates & habits (T4)" below) | **notre ligne (naming + mechanism)** — Eve calls these schedules/hooks; we call them habits (product decision) and back them with runtime-registered crons instead of static config |
|
|
46
|
-
| *(no equivalent)* — bounded per-agent authority | not present in `vercel/eve` | `mandates` + `approvalRequests` child tables (`@convex-dev/rate-limiter` + `@convex-dev/sharded-counter`, see "Mandates & habits (T4)" below) | **notre ligne** — SYNTHESE §2 "la réponse à la peur de lâcher les commandes"; a weighted, data-driven spending cap per org+agent+scope, overrun => human approval request, never a silent workaround |
|
|
47
|
-
| Session/turn resolver boundaries (`ALLOWED_DYNAMIC_INSTRUCTION_EVENTS = {session.started, turn.started}`) | `dynamic-tool-definition.ts:40-46` | `sessionEvents.name` union (`session.started`, `turn.started`, ...) + `engine/resolvers.ts:resolveManifest` | direct parity — same event names, same "reread the aggregate, never cache" contract |
|
|
48
|
-
| VantagePeers tools (memory, tasks, messages, notes) as a connection | not in `vercel/eve` (no equivalent first-party integration) | `vpConnections` child table (4 booleans, ON by default) + `engine/vpConnection.invokeVpTool` (I6, mvp §2.2 "Travail d'équipe") | **notre ligne** — direct access, NOT network: dispatch is a caller-supplied Convex function handle (`createFunctionHandle`, same discipline as `wakeableWork.workId` / `durableJobs.stepFunctionHandle`), never a `fetch()` call. `orgId` is threaded explicitly into the target handle so a VP write always lands in the invoking agent's own org tenant (`engineDelegation.test.ts` proves both the write and the cross-org refusal) |
|
|
49
|
-
| Agent directory, scoped by org | not in `vercel/eve` | `directory.listByOrg` (I6) | **notre ligne** — same `by_org` index discipline as every other list in this engine; reports each agent's subagent/connection counts alongside `agents.list` |
|
|
50
|
-
| ~~`subagents` NOT used for inter-agent collaboration~~ | eve docs / mvp-evevantage.md §2.5 (T4-era decision) | superseded by I6 — see the delegation rows above | **REVISED, T6/I6**: the T4-era README claimed "la collaboration entre agents n'arrive pas par les `subagents` d'eve" (VantagePeers-only). I6 wires `subagents` up for real, by the SAME pause/wake mechanism as everything else in this engine (SYNTHESE §4.d) — VantagePeers connection tools (row above) remain available IN ADDITION, not as a replacement |
|
|
51
|
-
| `outputSchema`, `compaction`, `modelOptions`, `channels` | `agent-definition.ts:124-146,313-330`, `manifest.ts:56-77` | out of scope this lot | left for a later I-cycle, documented as deliberately deferred (mvp §2.5) |
|
|
52
|
-
|
|
53
|
-
## Data aggregate — schema (`convex/schema.ts`)
|
|
54
|
-
|
|
55
|
-
Root sheet: `agents`.
|
|
56
|
-
|
|
57
|
-
Child tables (>= 7, every index starts with `orgId`):
|
|
58
|
-
|
|
59
|
-
1. `skills` — contextual procedures
|
|
60
|
-
2. `subagents` — recursive declared delegation, strict isolation
|
|
61
|
-
3. `connections` — MCP/OpenAPI (VantagePeers tools included)
|
|
62
|
-
4. `habits` (+ `habitRuns`) — dynamic crons (habits, notre ligne, T4)
|
|
63
|
-
5. `sandboxConfig` — Vercel Sandbox settings
|
|
64
|
-
6. `toolApprovals` — `always` / `once` / `never` per tool
|
|
65
|
-
7. `modelKeys` — BYOK, server-only, split from `agents`
|
|
66
|
-
8. `mandates` (+ `approvalRequests`) — bounded authority (notre ligne, T4)
|
|
67
|
-
9. `vpConnections` (+ `vpToolInvocations`) — VantagePeers connection toggles + audit trail (notre ligne, T6/I6)
|
|
68
|
-
|
|
69
|
-
Engine tables: `sessions`, `sessionEvents`, `awaitEventTimeouts`,
|
|
70
|
-
`delegationRuns`, `agentCloneRuns` (I6 — see "Delegation (T6/I6)" below).
|
|
71
|
-
|
|
72
|
-
**Hard rule**: `orgId` is derived server-side from the authenticated
|
|
73
|
-
identity (`convex/lib/auth.ts:requireOrgId`, reads the Clerk `org_id`
|
|
74
|
-
claim) — it is never accepted as a client argument, and it is the FIRST
|
|
75
|
-
field of every `.index(...)` call. Deny by default: cross-org reads return
|
|
76
|
-
`null` / cross-org writes throw, indistinguishable from "not found"
|
|
77
|
-
(`convex/lib/auth.ts:assertSameOrg`).
|
|
78
|
-
|
|
79
|
-
## Engine — durable sessions (`convex/engine/`)
|
|
80
|
-
|
|
81
|
-
- `engine/workflows.ts` — `WorkflowManager` from `@convex-dev/workflow`.
|
|
82
|
-
The session loop is exported as `sessionWorkflowV1`. **Versioned
|
|
83
|
-
workflows / drain-before-deploy discipline** (workflow#35): a breaking
|
|
84
|
-
change to the loop ships as `sessionWorkflowV2`, never an in-place edit
|
|
85
|
-
of `V1` while sessions still reference it.
|
|
86
|
-
- `engine/resolvers.ts` — `resolveManifest`, the dynamic resolver. Rereads
|
|
87
|
-
the full agent aggregate (skills, tool approvals, connections) on every
|
|
88
|
-
call; callers invoke it at `session.started` / `turn.started` boundaries
|
|
89
|
-
only, mirroring eve's `ALLOWED_DYNAMIC_INSTRUCTION_EVENTS`.
|
|
90
|
-
- `engine/sessions.ts` — `start` / `recordTurnStarted`, the mutations that
|
|
91
|
-
write the named `sessionEvents` rows Eve-style.
|
|
92
|
-
- `engine/awaitEventTimeout.ts` — **self-built `awaitEvent` timeout**
|
|
93
|
-
(workflow#177: Convex ships no native timeout for `step.awaitEvent`).
|
|
94
|
-
`schedule` writes an `awaitEventTimeouts` row and schedules an
|
|
95
|
-
`internalMutation` (`fire`) via `ctx.scheduler.runAfter`; if the real
|
|
96
|
-
event arrives first, `cancel` cancels the scheduled function. `fire`
|
|
97
|
-
inserts a blank `sandbox.timeout` `sessionEvents` row so the awaiting
|
|
98
|
-
workflow step wakes up regardless.
|
|
99
|
-
|
|
100
|
-
`ai` is pinned to `6.0.35` (peer dependency of `@convex-dev/agent@0.6.4`,
|
|
101
|
-
agent#79 warns the v5->v6 migration broke other consumers — we track
|
|
102
|
-
majors deliberately, never float). Counters/rollups, when needed, go
|
|
103
|
-
through `@convex-dev/sharded-counter` / `@convex-dev/aggregate` — no
|
|
104
|
-
canary/alpha third-party dependency for a guardrail (I0 §7.4).
|
|
105
|
-
|
|
106
|
-
## Long-task survival (I1) — `engine/durableJob.ts`
|
|
107
|
-
|
|
108
|
-
SYNTHESE.md §5/§7: "le plafond de durée des actions Convex est notre
|
|
109
|
-
plafond aujourd'hui, sur tous nos produits" -- `durableJobs` is the
|
|
110
|
-
GENERIC (shape-agnostic, unlike `sessions`) primitive that lets a
|
|
111
|
-
consumer's task survive past that ceiling by expressing it as N
|
|
112
|
-
independently-bounded steps. It **ENVELOPS** `@convex-dev/workflow` (via
|
|
113
|
-
the same `WorkflowManager` + versioned-workflow discipline as
|
|
114
|
-
`engine/workflows.ts`), it does not reimplement durable execution.
|
|
115
|
-
|
|
116
|
-
Contract (see `convex/engine/durableJob.ts` for the full commentary and
|
|
117
|
-
`examples/vantage-peers-long-task/` for the worked integration a consumer
|
|
118
|
-
reads without needing to know our internals):
|
|
119
|
-
|
|
120
|
-
- `start(kind, stepFunctionHandle, totalSteps, maxAttemptsPerStep)` —
|
|
121
|
-
enqueue a job driven by a step function the CALLER owns (component
|
|
122
|
-
isolation: we invoke it by opaque `FunctionHandle`, we never read the
|
|
123
|
-
caller's own tables).
|
|
124
|
-
- `getStatus(jobId)` — poll `status` / `cursor` / `lastError` /
|
|
125
|
-
`abandonedAtStep`.
|
|
126
|
-
- `cancel(jobId)` — best-effort stop.
|
|
127
|
-
- **Pause at step boundary**: one loop iteration == one workflow step ==
|
|
128
|
-
one durability boundary (`@convex-dev/workflow` persists the journal
|
|
129
|
-
after each step returns).
|
|
130
|
-
- **Deterministic replay**: `runStepInternal` no-ops for any `stepIndex <
|
|
131
|
-
job.cursor`, so a resumed workflow's journal replay never re-runs a
|
|
132
|
-
completed step.
|
|
133
|
-
- **Abandon after N**: the caller's step function is retried up to
|
|
134
|
-
`maxAttemptsPerStep` times inside one workflow step; once exhausted the
|
|
135
|
-
job is marked `abandoned` with `abandonedAtStep` + `lastError`, and no
|
|
136
|
-
further step runs.
|
|
137
|
-
|
|
138
|
-
## Mandates & habits (T4) — NOTRE LIGNE, EveVantage's two product differentiators
|
|
139
|
-
|
|
140
|
-
SYNTHESE.md §2: *"Deux arguments de vente que personne n'a"* — mandates
|
|
141
|
-
("cet agent peut engager jusqu'à X ; au-delà, il me demande") and habits
|
|
142
|
-
("tous les matins, relève les mails, classe, fais le point"). Both are
|
|
143
|
-
**declared divergences from `vercel/eve`** — Eve has no concept of a
|
|
144
|
-
weighted spending cap or a runtime-editable recurrence; this is our line,
|
|
145
|
-
not a silent gap.
|
|
146
|
-
|
|
147
|
-
### Mandates — `convex/mandates.ts` (`mandates`, `approvalRequests` tables)
|
|
148
|
-
|
|
149
|
-
**Notre ligne**: bounded authority per agent+org+scope. A mandate is a
|
|
150
|
-
DATA ROW (`mandates.{kind,rate,period,capacity}`), never a constant in
|
|
151
|
-
code — a client changing their mind is a `mandates.set` mutation, zero
|
|
152
|
-
deploy. `spend(agentId, scope, cost, reason)` is the single choke point
|
|
153
|
-
every costed action must pass through:
|
|
154
|
-
|
|
155
|
-
- **fails-closed**: no mandate row for `orgId+agentId+scope` => throws
|
|
156
|
-
immediately (`Mandate refused: no mandate configured for scope "..."`).
|
|
157
|
-
There is no "unlimited by default" path.
|
|
158
|
-
- **overrun => human approval, never a silent workaround**: when
|
|
159
|
-
`@convex-dev/rate-limiter`'s `limit()` (called with an **inline dynamic
|
|
160
|
-
`config`** built from the DB row — see rate-limiter README "Define a
|
|
161
|
-
rate limit inline / dynamically") returns `ok: false`, `spend` inserts
|
|
162
|
-
an `approvalRequests` row (`status: "pending"`) and returns
|
|
163
|
-
`{ ok: false, approvalId }`. It deliberately does **not** throw on this
|
|
164
|
-
path: Convex mutations are single transactions, and throwing after
|
|
165
|
-
`ctx.db.insert` would roll the insert back along with everything else —
|
|
166
|
-
destroying the very approval record the guard exists to leave behind.
|
|
167
|
-
Callers (the session/workflow engine) are REQUIRED to check `.ok` and
|
|
168
|
-
treat `false` as a hard refusal; the only way authority is restored is
|
|
169
|
-
an explicit human `resolveApproval(approvalId, "approved", resolvedBy)`.
|
|
170
|
-
- **rollups via `@convex-dev/sharded-counter`** (`usageCount` query), one
|
|
171
|
-
bucket per `orgId:agentId:scope` key — I0 §7.4: no canary/alpha
|
|
172
|
-
dependency for a guardrail. `@convex-dev/aggregate` (also I0-approved,
|
|
173
|
-
also official, also no canary) is the natural next step for time-
|
|
174
|
-
windowed billing rollups; not wired in this lot, called out here rather
|
|
175
|
-
than silently left out.
|
|
176
|
-
- `count` passed to `rateLimiter.limit` is a WEIGHTED cost (tokens, euros,
|
|
177
|
-
calls — whatever the scope means), never a bare call counter.
|
|
178
|
-
|
|
179
|
-
### Habits — `convex/habits.ts` (`habits`, `habitRuns` tables)
|
|
180
|
-
|
|
181
|
-
**Notre ligne (naming + mechanism)**: Eve's closest concept is
|
|
182
|
-
`schedules`/`hooks`, statically declared in a compiled agent. Habits are
|
|
183
|
-
runtime DATA: `habits.cronExpression` is a column, and the actual
|
|
184
|
-
recurrence is a **dynamically registered `@convex-dev/crons` job**
|
|
185
|
-
(`Crons.register`/`.delete`, never a static `convex/crons.ts` entry).
|
|
186
|
-
|
|
187
|
-
- `create` writes the habit row and registers its cron in the SAME
|
|
188
|
-
mutation transaction — `@convex-dev/crons` guarantees the cron "is
|
|
189
|
-
guaranteed to exist after the mutation that creates them has run"
|
|
190
|
-
(README), so the very next tick can fire it with zero deploy.
|
|
191
|
-
- `updateSchedule` edits `cronExpression`/`prompt`/`name` at runtime:
|
|
192
|
-
deletes the old cron registration, registers a fresh one from the new
|
|
193
|
-
cadence, still inside one transaction.
|
|
194
|
-
- `disable` is a HARD stop: it deletes the runtime cron registration
|
|
195
|
-
(not a soft `enabled` flag the target function checks) — after it
|
|
196
|
-
resolves there is no scheduled function left that could fire the habit.
|
|
197
|
-
`run` (the cron-invoked internal mutation) also re-checks `enabled` as
|
|
198
|
-
defence in depth for any run already in flight.
|
|
199
|
-
- `enable` re-registers from the habit's own `cronExpression` data.
|
|
200
|
-
- Every tick writes a `habitRuns` proof row — this is what makes "fires
|
|
201
|
-
its first run without a deploy" and "a disabled habit does not fire
|
|
202
|
-
again" PROVABLE in `habits.test.ts` rather than asserted on trust.
|
|
203
|
-
|
|
204
|
-
## Delegation (T6/I6) — `engine/delegation.ts`, `engine/vpConnection.ts`, `directory.ts`
|
|
205
|
-
|
|
206
|
-
Bible: SYNTHESE §4.d — "un agent qui délègue à un autre agent = LE MÊME
|
|
207
|
-
système pause/réveil (awaitEvent/sendEvent) que le pont sandbox — un seul
|
|
208
|
-
système à écrire, pas deux".
|
|
209
|
-
|
|
210
|
-
### (a) Two delegation semantics, both reusing `engine.wakeup.enqueue`
|
|
211
|
-
|
|
212
|
-
- **Declared subagents** (`subagents` table, `engine/delegation.ts`):
|
|
213
|
-
strict isolation — `declareSubagent` writes ONLY the parent->child link +
|
|
214
|
-
the named tool (`toolName`); the child's own aggregate is the sole place
|
|
215
|
-
its skills/connections/toolApprovals ever live (read via
|
|
216
|
-
`engine/resolvers.resolveManifest` against a session on `childAgentId`).
|
|
217
|
-
**Build-time collision guard**: `toolName` must be unique against (1) the
|
|
218
|
-
reserved built-in `"agent"` tool name, (2) every other declared subagent
|
|
219
|
-
of the same parent (`by_org_parent_tool` index), (3) an existing
|
|
220
|
-
`toolApprovals` row on the parent — any collision REJECTS the mutation,
|
|
221
|
-
never a silent "last one wins" (eve.dev/docs/subagents, verbatim).
|
|
222
|
-
- **Built-in `agent` tool** (`invokeAgentClone`, `agentCloneRuns` table):
|
|
223
|
-
a clone of the ROOT, root-only, NOT recursive. `viaAgentTool: true`
|
|
224
|
-
(a recursive attempt) is refused BEFORE any row is written or
|
|
225
|
-
`engine.wakeup.enqueue` is ever called.
|
|
226
|
-
- **The reuse point**: `delegateToSubagent` and `invokeAgentClone` BOTH
|
|
227
|
-
terminate in exactly one call to the SAME public `engine.wakeup.enqueue`
|
|
228
|
-
action T4/I2 already ships (`ctx.runAction(api.engine.wakeup.enqueue,
|
|
229
|
-
...)`) — no bespoke `onComplete`/`sendEvent` path exists in
|
|
230
|
-
`engine/delegation.ts`. `engine/delegationDemoWorkflow.ts` is the
|
|
231
|
-
reference parent-side recipient (same shape as
|
|
232
|
-
`engine/wakeupDemoWorkflow.ts`): it parks on `step.awaitEvent` and
|
|
233
|
-
resumes only when the delegate's work completes.
|
|
234
|
-
|
|
235
|
-
### (b) VantagePeers connection — `engine/vpConnection.ts`
|
|
236
|
-
|
|
237
|
-
Four toggles (`memoryEnabled`, `tasksEnabled`, `messagesEnabled`,
|
|
238
|
-
`notesEnabled`), ON by default (mvp §2.2 "Travail d'équipe", "quatre
|
|
239
|
-
interrupteurs cochés par défaut" — a missing `vpConnections` row means "use
|
|
240
|
-
the defaults"). `invokeVpTool` dispatches via a caller-supplied Convex
|
|
241
|
-
function handle (`createFunctionHandle`, same discipline as
|
|
242
|
-
`wakeableWork.workId` / `durableJobs.stepFunctionHandle`) — **direct
|
|
243
|
-
access, zero network in the hot path**:
|
|
244
|
-
|
|
245
|
-
```
|
|
246
|
-
$ grep -n "fetch(" convex/engine/vpConnection.ts convex/engine/delegation.ts
|
|
247
|
-
(no matches)
|
|
3
|
+
A reusable [Convex component](https://www.convex.dev/components) that
|
|
4
|
+
provides a durable **agent engine**: sessions, delegation, tool approvals,
|
|
5
|
+
scheduled "habits", bounded spending "mandates", and long-task survival —
|
|
6
|
+
all built on top of
|
|
7
|
+
[`@convex-dev/workflow`](https://www.npmjs.com/package/@convex-dev/workflow)
|
|
8
|
+
and
|
|
9
|
+
[`@convex-dev/workpool`](https://www.npmjs.com/package/@convex-dev/workpool).
|
|
10
|
+
|
|
11
|
+
**An agent is a data aggregate, not compiled code.** Creating an agent means
|
|
12
|
+
writing a row (plus child rows for skills, tool approvals, connections,
|
|
13
|
+
sub-agents, habits, sandbox config) — there is no build step and no
|
|
14
|
+
redeploy. This mirrors the authoring model pioneered by
|
|
15
|
+
[vercel/eve](https://github.com/vercel/eve), whose slot names, contracts and
|
|
16
|
+
vocabulary this package tracks closely; credit and thanks to that project for
|
|
17
|
+
the design this component is built in parity with.
|
|
18
|
+
|
|
19
|
+
## Why
|
|
20
|
+
|
|
21
|
+
Most agent frameworks compile an agent definition into code that has to be
|
|
22
|
+
deployed to change. This engine keeps the entire agent definition as
|
|
23
|
+
database rows: skills, tool approvals, delegated sub-agents, sandbox
|
|
24
|
+
settings, and scheduled work are all mutable at runtime through ordinary
|
|
25
|
+
Convex mutations. Combined with `@convex-dev/workflow`'s durable execution,
|
|
26
|
+
a single agent session — or a long-running background job — survives
|
|
27
|
+
process restarts, Convex's per-action time limits, and redeploys of the
|
|
28
|
+
consuming app.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install @vantageos/agent-engine
|
|
248
34
|
```
|
|
249
35
|
|
|
250
|
-
|
|
251
|
-
client-supplied value, never the target's own `ctx.auth`), so a VP tool
|
|
252
|
-
call always writes into the CALLING agent's own org tenant — proven in
|
|
253
|
-
`engineDelegation.test.ts` alongside the cross-org refusal (an agent
|
|
254
|
-
belonging to another org is "not found", same deny-by-default posture as
|
|
255
|
-
everywhere else in this engine).
|
|
256
|
-
|
|
257
|
-
### (c) Agent directory — `directory.ts`
|
|
258
|
-
|
|
259
|
-
`directory.listByOrg` — every agent of the caller's own org (server-derived
|
|
260
|
-
`orgId`, `by_org` index), plus each agent's subagent/connection counts.
|
|
261
|
-
|
|
262
|
-
## `convex/_generated/` freshness -- two gates, not one
|
|
263
|
-
|
|
264
|
-
`convex/_generated/api.d.ts` carries an "AUTOMATICALLY GENERATED" header
|
|
265
|
-
but nothing stops a hand-edit of its *content* (as opposed to adding or
|
|
266
|
-
removing an entire module) from being committed as-is. That gap was
|
|
267
|
-
measured for real on PR #2 (T2-BIS): a manual edit of a single existing
|
|
268
|
-
line (`FunctionReference<any, "public">` flipped to `"internal">`) passed
|
|
269
|
-
the original structural gate with `OK`, because that gate only diffs the
|
|
270
|
-
*list* of modules present under `convex/` against the modules imported by
|
|
271
|
-
`api.d.ts` -- it never recomputes a byte.
|
|
272
|
-
|
|
273
|
-
Two gates now exist, and they check different things:
|
|
274
|
-
|
|
275
|
-
1. **Structural, offline, secret-free** --
|
|
276
|
-
`scripts/check-generated-freshness.mjs` (`npm run check:generated`).
|
|
277
|
-
Runs in CI (`.github/workflows/ci.yml`). Catches a function file
|
|
278
|
-
added/renamed/removed without regenerating bindings. **Does NOT catch a
|
|
279
|
-
hand-edit of existing generated content.**
|
|
280
|
-
2. **Content, requires a live deployment** --
|
|
281
|
-
`scripts/check-generated-content.mjs` (`npm run
|
|
282
|
-
check:generated:content`). Runs `npx convex codegen` against the dev
|
|
283
|
-
deployment for BOTH `convex/_generated/` (the host repo's own dev
|
|
284
|
-
deployment bindings) AND, since 0.1.0-alpha.5, `component/_generated/`
|
|
285
|
-
via `npx convex codegen --component-dir component` -- the checked-in
|
|
286
|
-
file `tsc -p tsconfig.build.json` copies verbatim into the PUBLISHED
|
|
287
|
-
`dist/component/_generated/component.d.ts`, i.e. the exact type a
|
|
288
|
-
consuming app checks `components.agentEngine.*` calls against. Diffs
|
|
289
|
-
each byte-for-byte (`git diff --exit-code`). Catches everything,
|
|
290
|
-
including a single hand-edited character.
|
|
291
|
-
|
|
292
|
-
**`component/_generated/` was NOT covered before 0.1.0-alpha.5** — that
|
|
293
|
-
gap is why `0.1.0-alpha.4` shipped a `component.d.ts` stale against the
|
|
294
|
-
real component schema (missing `orgId`/`createdBy` on
|
|
295
|
-
sessions/agents/durableJob/toolApprovals args), which broke
|
|
296
|
-
`tsc --noEmit` for any consumer calling `sessions.start`/`durableJob.start`
|
|
297
|
-
with `orgId` (Hestia, VP k17ea6x3tht8xa3rxm139t23gx8ajkf3, defaut 1).
|
|
298
|
-
The visibility (`"internal"`) on every entry is, by measured comparison
|
|
299
|
-
against `@convex-dev/rate-limiter`'s own published `component.d.ts`,
|
|
300
|
-
CORRECT and unrelated to that defect — every official Convex component
|
|
301
|
-
types its cross-component `ComponentApi` as `"internal"` regardless of
|
|
302
|
-
whether the underlying function is declared `mutation`/`query` or
|
|
303
|
-
`internalMutation`/`internalQuery`; `ctx.runMutation`'s own signature
|
|
304
|
-
(`convex/server`) accepts `FunctionReference<"mutation", "public" |
|
|
305
|
-
"internal">` either way. The root cause was staleness of the args
|
|
306
|
-
shape, not visibility.
|
|
307
|
-
|
|
308
|
-
**Three-state exit contract** (never two -- a two-state gate cannot
|
|
309
|
-
distinguish "I measured, it's clean" from "I could not measure at
|
|
310
|
-
all", and collapsing those is either a silent fail-open or a red that
|
|
311
|
-
proves nothing; see flotte rule `three-state-verdict.md`):
|
|
312
|
-
|
|
313
|
-
| Exit | Meaning | When |
|
|
314
|
-
| ---- | ------- | ---- |
|
|
315
|
-
| `0` | **PASS** | A real measurement was taken (`npx convex codegen` ran) and `convex/_generated/` is byte-identical to its output. |
|
|
316
|
-
| `1` | **VIOLATION** | A real measurement was taken and it found >=1 byte of divergence -- hand-edited, stale, or generated against a different schema. |
|
|
317
|
-
| `2` | **REFUSAL TO JUDGE** | `npx convex codegen` itself could not run (no live, authenticated Convex deployment reachable -- `CONVEX_DEPLOYMENT` / `.env.local` missing or invalid). No measurement was taken; this is neither a clean bill of health nor a proven violation. |
|
|
318
|
-
|
|
319
|
-
### CI cannot run the content gate -- explicit limit, not a rounding error
|
|
320
|
-
|
|
321
|
-
`npx convex codegen` has no offline mode: it always needs
|
|
322
|
-
`CONVEX_DEPLOYMENT` / a deploy key against a live deployment. This repo
|
|
323
|
-
refuses to store that as a CI secret (plan B, PR #1). **Consequence,
|
|
324
|
-
stated plainly: CI cannot detect a hand-edited `convex/_generated/`
|
|
325
|
-
content change.** CI only runs the structural gate. The content gate is
|
|
326
|
-
the only thing that catches a content-level tamper, and it can only run
|
|
327
|
-
where the secret lives: **locally, pre-commit**, via `.env.local`.
|
|
328
|
-
|
|
329
|
-
To install the pre-commit gate once per clone:
|
|
36
|
+
## Quickstart
|
|
330
37
|
|
|
331
|
-
|
|
332
|
-
git config core.hooksPath scripts/git-hooks
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
This points git at the versioned hook `scripts/git-hooks/pre-commit`
|
|
336
|
-
(plain git hook, not a Claude hook -- Claude hooks under `.claude/hooks/`
|
|
337
|
-
are Pi-owned only). The hook no-ops unless a staged file is under
|
|
338
|
-
`convex/`, and then runs `npm run check:generated:content`, reading its
|
|
339
|
-
exit code explicitly rather than treating it as a boolean:
|
|
340
|
-
|
|
341
|
-
- `0` (PASS) -- commit proceeds.
|
|
342
|
-
- `1` (VIOLATION) -- commit is **blocked**, message names the divergence.
|
|
343
|
-
- `2` (REFUSAL TO JUDGE, e.g. `.env.local` deployment secret missing or
|
|
344
|
-
invalid) -- commit is **also blocked**, with a distinct message that
|
|
345
|
-
says explicitly this is *unverified*, not a proven violation. This is a
|
|
346
|
-
deliberate fail-closed choice: an unverified change to `convex/_generated/`
|
|
347
|
-
content must never slip through silently just because the local
|
|
348
|
-
instrument (a live Convex deployment) was unavailable. The trade-off is
|
|
349
|
-
that a developer without a working `.env.local` cannot commit to
|
|
350
|
-
`convex/**` at all until they fix their local deployment credentials --
|
|
351
|
-
judged the safer failure mode than a fail-open that would let a hand-
|
|
352
|
-
edited, un-regenerated `convex/_generated/` through whenever the secret
|
|
353
|
-
happens to be missing.
|
|
354
|
-
|
|
355
|
-
**If `core.hooksPath` is never configured, this protection does not
|
|
356
|
-
exist on that clone.** Nothing currently makes that configuration
|
|
357
|
-
mandatory or automatic -- that decision (e.g. via a repo bootstrap script
|
|
358
|
-
that all contributors run) is Pi's to make, not this PR's.
|
|
359
|
-
|
|
360
|
-
## Tests (TDD, RED then GREEN)
|
|
361
|
-
|
|
362
|
-
- `convex/agents.test.ts` — org isolation (family 1)
|
|
363
|
-
- `convex/engineResolvers.test.ts` — dynamic resolver, no-redeploy (family 2)
|
|
364
|
-
- `convex/engineAwaitEventTimeout.test.ts` — awaitEvent timeout wakes the
|
|
365
|
-
workflow when the real event never arrives (family 3)
|
|
366
|
-
- `convex/engineDurableJob.test.ts` — I1 long-task survival contract
|
|
367
|
-
(family 4): a 25-step job survives to `completed` via the component; a
|
|
368
|
-
replayed already-completed step is a proven no-op (resume after forced
|
|
369
|
-
interruption); a step that always fails is retried and the job is
|
|
370
|
-
`abandoned` (never touching the next step); a step that fails once then
|
|
371
|
-
succeeds still reaches `completed`; cross-org `getStatus` is denied.
|
|
372
|
-
- `convex/mandates.test.ts` — mandates, three-pole probe: MUST_PASS (spend
|
|
373
|
-
under cap succeeds, rollup counts it), MUST_BLOCK (spend at cap is
|
|
374
|
-
refused, `approvalRequests` row created), MUST_REFUSE (no mandate
|
|
375
|
-
configured => fails-closed, names the missing scope, no approval row
|
|
376
|
-
created) + org isolation + human `resolveApproval`.
|
|
377
|
-
- `convex/habits.test.ts` — habits, three-pole probe: MUST_PASS (a habit
|
|
378
|
-
created at runtime fires its first run with zero deploy), MUST_BLOCK (a
|
|
379
|
-
disabled habit does not fire again after further clock advances),
|
|
380
|
-
MUST_REFUSE (`enable` on an already-enabled habit is a documented
|
|
381
|
-
no-op, not a silent double-registration) + full runtime lifecycle
|
|
382
|
-
(create / updateSchedule / disable / enable, no redeploy at any step).
|
|
383
|
-
- `component/durableJob.componentPackaging.test.ts` — the SAME contract
|
|
384
|
-
(long job survives, abandon-after-N, org isolation, cancel), but run
|
|
385
|
-
against `component/_generated/api` — the exact source tree compiled to
|
|
386
|
-
`dist/component/` and published — proving the migrated, packaged code
|
|
387
|
-
itself, not only the repo-root dev app.
|
|
388
|
-
- `convex/engineWakeup.test.ts` — I2 wake-up primitive (`enqueue` ->
|
|
389
|
-
`onComplete` -> `sendEvent`), three-pole probe + org isolation.
|
|
390
|
-
- `convex/engineSandboxCallback.test.ts` — I5 sandbox bridge (signed
|
|
391
|
-
callback, dedup, timeout, dead-letter).
|
|
392
|
-
- `convex/engineDelegation.test.ts` — I6 delegation, three-pole probe:
|
|
393
|
-
MUST_PASS (a declared subagent's manifest carries only its own skills;
|
|
394
|
-
`declareSubagent` succeeds on a fresh name; `invokeAgentClone` succeeds
|
|
395
|
-
for a first-level call; VP toggles default ON; `invokeVpTool` writes
|
|
396
|
-
into the calling org's tenant; `directory.listByOrg` scoped to the
|
|
397
|
-
caller's org), MUST_BLOCK (a subagent tool-name collision with an
|
|
398
|
-
existing tool OR the reserved `"agent"` name is rejected and writes
|
|
399
|
-
nothing; a recursive `invokeAgentClone` call is refused before any row
|
|
400
|
-
is written; a cross-org `invokeVpTool` call is refused; a disabled VP
|
|
401
|
-
tool is refused; zero `fetch(` calls in the VP connection hot path),
|
|
402
|
-
MUST_REFUSE (every mutation/action above with no identity names
|
|
403
|
-
"Unauthenticated" rather than crashing unreadably).
|
|
404
|
-
- `convex/engineDelegationDemo.test.ts` — the real pause/wake delegation
|
|
405
|
-
demo (A delegates to B via `engine.delegation.delegateToSubagent`, B's
|
|
406
|
-
work completes through `wakeupPool`, A's parked workflow resumes),
|
|
407
|
-
console-logged verbatim (`[i6-demo]` prefix) and kept in its own file
|
|
408
|
-
because it drives `vi.useFakeTimers()` through more event-loop pumps
|
|
409
|
-
than the rest of the suite.
|
|
410
|
-
|
|
411
|
-
Run: `npx vitest run` (or `npm test`).
|
|
412
|
-
|
|
413
|
-
## Packaging & publication (T2-TER, functions since T2-QUATER)
|
|
414
|
-
|
|
415
|
-
`@vantageos/agent-engine` is published to npm under the `alpha` dist-tag
|
|
416
|
-
(`npm view @vantageos/agent-engine version` / `dist-tags`, read from the
|
|
417
|
-
registry after publish, never from `npm publish`'s own exit status). The
|
|
418
|
-
package exposes a resolvable `"./convex.config"` subpath (types + JS,
|
|
419
|
-
built by `npm run build` from `component/convex.config.ts` via
|
|
420
|
-
`tsc -p tsconfig.build.json` into `dist/component/`), so a consumer's
|
|
421
|
-
`convex/convex.config.ts` can do:
|
|
38
|
+
Register the component in your app's `convex/convex.config.ts`:
|
|
422
39
|
|
|
423
40
|
```ts
|
|
424
41
|
import { defineApp } from "convex/server";
|
|
@@ -429,265 +46,218 @@ app.use(agentEngine);
|
|
|
429
46
|
export default app;
|
|
430
47
|
```
|
|
431
48
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
this package makes (`convex-test` states it verbatim: "Auth doesn't
|
|
437
|
-
propagate across component boundaries"). A function inside this component
|
|
438
|
-
therefore can NEVER read `ctx.auth.getUserIdentity()` and expect it to
|
|
439
|
-
reflect the consumer app's end user.
|
|
440
|
-
|
|
441
|
-
**`orgId` is an explicit ARGUMENT on every public function in this
|
|
442
|
-
component.** The calling app is responsible for authenticating its user and
|
|
443
|
-
resolving that user's `orgId` on its OWN side (e.g. reading a Clerk
|
|
444
|
-
org-scoped claim from its own `ctx.auth`) BEFORE calling into this
|
|
445
|
-
component — this component cannot do that step for you, it is a platform
|
|
446
|
-
constraint, not a design preference.
|
|
447
|
-
|
|
448
|
-
What did NOT change: the component still denies by default
|
|
449
|
-
(`lib/auth.ts:requireOrgId` throws on a missing/empty `orgId`), `orgId` is
|
|
450
|
-
still the first field of every index, and every document fetched by `_id`
|
|
451
|
-
is still checked with `lib/auth.ts:assertSameOrg` before being returned or
|
|
452
|
-
mutated — a caller-supplied `orgId` can never read or write another org's
|
|
453
|
-
rows. The isolation boundary moved from "component reads `ctx.auth`"
|
|
454
|
-
(impossible across a component boundary) to "component enforces the
|
|
455
|
-
`orgId` argument the consumer already vouches for"; deny-by-default,
|
|
456
|
-
orgId-first-index, no-cross-org-reads are unchanged.
|
|
457
|
-
|
|
458
|
-
### What's callable, and what isn't (read this before wiring a consumer)
|
|
459
|
-
|
|
460
|
-
`component/` (sibling of `convex/`, NOT nested under it — see "Why
|
|
461
|
-
`component/` and not `convex/component/`" below) hosts the **actual,
|
|
462
|
-
compiled, published functions** for the FULL agent aggregate as of
|
|
463
|
-
0.1.0-alpha.6 (C1-C6, all merged) — not just the component's identity:
|
|
464
|
-
|
|
465
|
-
- `component/schema.ts` — `durableJobs`, `agents`, `toolApprovals`,
|
|
466
|
-
`sessions`, `sessionEvents`, `wakeableWork`, `awaitEventTimeouts`,
|
|
467
|
-
`mandates`, `approvalRequests`, `habits`, `habitRuns`, `sandboxConfig`,
|
|
468
|
-
`sandboxRuns`, `sandboxCallbackDeliveries`, `sandboxCallbackDeadLetters`,
|
|
469
|
-
`subagents`, `delegationRuns`, `agentCloneRuns`, `vpConnections`,
|
|
470
|
-
`vpToolInvocations` (+ test-only fixture tables: `callerStepInvocations`,
|
|
471
|
-
`wakeupDemoResults`, `vpTenantFixture`).
|
|
472
|
-
- `component/engine/durableJob.ts` — `start` / `getStatus` / `cancel`
|
|
473
|
-
(public contract) + `durableJobWorkflowV1` (the versioned pause/resume/
|
|
474
|
-
replay workflow) + its internal step-runner mutations.
|
|
475
|
-
- `component/engine/agents.ts` — `create` / `get` (root-sheet bootstrap,
|
|
476
|
-
now including `description`/`reasoningEffort`/`modelProvider`/
|
|
477
|
-
`maxSubagents`/`maxInputTokensPerSession`; `update`/skills/connections
|
|
478
|
-
CRUD remain out of scope for this pass).
|
|
479
|
-
- `component/engine/directory.ts` — `listByOrg` (agent directory listing).
|
|
480
|
-
- `component/engine/resolvers.ts` — `resolveManifest` (dynamic reread at
|
|
481
|
-
session/turn boundaries; returns `agentId`/`name`/`instructions`/`model`/
|
|
482
|
-
`toolApprovals` — `skills`/`connections` NOT included in this pass).
|
|
483
|
-
- `component/engine/sessions.ts` — the conversation thread contract:
|
|
484
|
-
`start` / `sendTurn` / `appendReasoning` / `requestToolCall` /
|
|
485
|
-
`completeToolCall` / `completeTurn` / `listEvents`, now piloted by a
|
|
486
|
-
versioned `@convex-dev/workflow` run (`sessionWorkflowV1`,
|
|
487
|
-
`component/engine/workflows.ts` — C1.5).
|
|
488
|
-
`sessions.start` returns `{ sessionId, workflowId }`.
|
|
489
|
-
- `component/engine/toolApprovals.ts` — `respond`.
|
|
490
|
-
- `component/engine/wakeup.ts` — `enqueue` / `cancel` / `getStatus` +
|
|
491
|
-
`onWorkComplete` (the wake-without-polling primitive; funds through
|
|
492
|
-
`mandates.spendInternal` before any work enters the pool — C2/C3 seam).
|
|
493
|
-
- `component/engine/awaitEventTimeout.ts` — self-built `step.awaitEvent`
|
|
494
|
-
timeout (workflow#177: Convex has no native timeout for it).
|
|
495
|
-
- `component/engine/mandates.ts` — `set` / `get` / `spend` /
|
|
496
|
-
`listApprovals` / `resolveApproval` / `usageCount` (autorité bornée,
|
|
497
|
-
fails-closed spend gate; "notre ligne").
|
|
498
|
-
- `component/engine/habits.ts` — `create` / `get` / `listForAgent` /
|
|
499
|
-
`updateSchedule` / `disable` / `enable` / `listRuns` (per-agent dynamic
|
|
500
|
-
cron, zero-redeploy lifecycle via `@convex-dev/crons`; "notre ligne").
|
|
501
|
-
- `component/engine/sandboxCallback.ts` — `startRun` / `attachSandboxId` /
|
|
502
|
-
`getStatus` / `deadLetterMalformed` / `processCallback` (the DB-side
|
|
503
|
-
half of the Vercel Sandbox bridge: dedup + wake + timeout, reusing C2's
|
|
504
|
-
`wakeup` mechanism).
|
|
505
|
-
- `component/engine/delegation.ts` — `declareSubagent` / `listSubagents` /
|
|
506
|
-
`delegateToSubagent` / `invokeAgentClone` (inter-agent delegation and the
|
|
507
|
-
built-in `agent` clone tool, both reusing `wakeup.enqueue` — the SAME
|
|
508
|
-
pause/wake mechanism as the sandbox bridge, one system not two).
|
|
509
|
-
- `component/engine/vpConnection.ts` — `get` / `getToggles` / `setToggles`
|
|
510
|
-
/ `invokeVpTool` (the four VantagePeers tools — memory/tasks/messages/
|
|
511
|
-
notes — wired as a connection of the agent aggregate; "notre ligne").
|
|
512
|
-
- `component/lib/auth.ts` — `requireOrgId` / `assertSameOrg`: see "Trust
|
|
513
|
-
model" above.
|
|
514
|
-
|
|
515
|
-
**Callable through the published component today** — every function below
|
|
516
|
-
takes `orgId: v.string()` as an explicit argument (see the source files
|
|
517
|
-
above for the exact `v.*` shapes of the remaining args/returns):
|
|
49
|
+
Then call into it from your own Convex functions. Every public function
|
|
50
|
+
takes an explicit `orgId: string` argument — Convex does not propagate the
|
|
51
|
+
calling app's `ctx.auth` identity across a component boundary, so your app
|
|
52
|
+
is responsible for resolving the caller's org before calling in:
|
|
518
53
|
|
|
54
|
+
```ts
|
|
55
|
+
import { components } from "./_generated/api";
|
|
56
|
+
|
|
57
|
+
export const createAgent = mutation({
|
|
58
|
+
args: { orgId: v.string(), name: v.string(), instructions: v.string() },
|
|
59
|
+
handler: async (ctx, args) => {
|
|
60
|
+
return ctx.runMutation(components.agentEngine.engine.agents.create, {
|
|
61
|
+
orgId: args.orgId,
|
|
62
|
+
createdBy: "user_123",
|
|
63
|
+
name: args.name,
|
|
64
|
+
instructions: args.instructions,
|
|
65
|
+
model: "gpt-4o",
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
});
|
|
519
69
|
```
|
|
520
|
-
components.agentEngine.engine.durableJob.start({ orgId, kind, stepFunctionHandle, totalSteps, maxAttemptsPerStep }) -> Id<"durableJobs">
|
|
521
|
-
components.agentEngine.engine.durableJob.getStatus({ orgId, jobId }) -> { status, cursor, totalSteps, lastError?, abandonedAtStep? }
|
|
522
|
-
components.agentEngine.engine.durableJob.cancel({ orgId, jobId }) -> null
|
|
523
70
|
|
|
524
|
-
|
|
525
|
-
components.agentEngine.engine.agents.get({ orgId, agentId }) -> agent | null
|
|
526
|
-
|
|
527
|
-
components.agentEngine.engine.directory.listByOrg({ orgId }) -> { _id, name, status, model, createdAt }[]
|
|
71
|
+
## What's inside
|
|
528
72
|
|
|
529
|
-
|
|
73
|
+
The component's data model (root table `agents`, plus child tables for
|
|
74
|
+
skills, sub-agents, connections, habits, sandbox config, and per-tool
|
|
75
|
+
approvals) is deployed under the Convex component boundary your app
|
|
76
|
+
registers — it does not touch your own app's tables, and every index is
|
|
77
|
+
scoped by `orgId` first, deny-by-default.
|
|
530
78
|
|
|
531
|
-
|
|
532
|
-
components.agentEngine.engine.sessions.sendTurn({ orgId, sessionId, input }) -> null
|
|
533
|
-
components.agentEngine.engine.sessions.appendReasoning({ orgId, sessionId, chunk }) -> null
|
|
534
|
-
components.agentEngine.engine.sessions.requestToolCall({ orgId, sessionId, toolName, argsJson }) -> { authorized, approvalPending, denied }
|
|
535
|
-
components.agentEngine.engine.sessions.completeToolCall({ orgId, sessionId, toolName, resultJson }) -> null
|
|
536
|
-
components.agentEngine.engine.sessions.completeTurn({ orgId, sessionId }) -> null
|
|
537
|
-
components.agentEngine.engine.sessions.listEvents({ orgId, sessionId }) -> sessionEvent[]
|
|
79
|
+
### API surface (component/, current release)
|
|
538
80
|
|
|
539
|
-
|
|
81
|
+
```
|
|
82
|
+
components.agentEngine.engine.agents.create({
|
|
83
|
+
orgId, createdBy, name, description?, instructions, model,
|
|
84
|
+
reasoningEffort?, modelProvider?, maxSubagents?,
|
|
85
|
+
maxInputTokensPerSession?,
|
|
86
|
+
}) -> Id<"agents">
|
|
87
|
+
components.agentEngine.engine.agents.get({ orgId, agentId }) -> agent | null
|
|
540
88
|
|
|
541
|
-
components.agentEngine.engine.
|
|
89
|
+
components.agentEngine.engine.directory.listByOrg({ orgId })
|
|
90
|
+
-> { _id, name, status, model, createdAt }[]
|
|
91
|
+
|
|
92
|
+
components.agentEngine.engine.resolvers.resolveManifest({
|
|
93
|
+
orgId, sessionId,
|
|
94
|
+
}) -> { agentId, name, instructions, model, toolApprovals[] }
|
|
95
|
+
|
|
96
|
+
components.agentEngine.engine.sessions.start({
|
|
97
|
+
orgId, agentId, createdBy,
|
|
98
|
+
}) -> { sessionId: Id<"sessions">, workflowId: string }
|
|
99
|
+
components.agentEngine.engine.sessions.sendTurn({
|
|
100
|
+
orgId, sessionId, input,
|
|
101
|
+
}) -> null
|
|
102
|
+
components.agentEngine.engine.sessions.appendReasoning({
|
|
103
|
+
orgId, sessionId, chunk,
|
|
104
|
+
}) -> null
|
|
105
|
+
components.agentEngine.engine.sessions.requestToolCall({
|
|
106
|
+
orgId, sessionId, toolName, argsJson,
|
|
107
|
+
}) -> { authorized, approvalPending, denied }
|
|
108
|
+
components.agentEngine.engine.sessions.completeToolCall({
|
|
109
|
+
orgId, sessionId, toolName, resultJson,
|
|
110
|
+
}) -> null
|
|
111
|
+
components.agentEngine.engine.sessions.completeTurn({
|
|
112
|
+
orgId, sessionId,
|
|
113
|
+
}) -> null
|
|
114
|
+
components.agentEngine.engine.sessions.listEvents({
|
|
115
|
+
orgId, sessionId,
|
|
116
|
+
}) -> sessionEvent[]
|
|
117
|
+
|
|
118
|
+
components.agentEngine.engine.toolApprovals.respond({
|
|
119
|
+
orgId, sessionId, toolName,
|
|
120
|
+
decision: "approved"|"denied",
|
|
121
|
+
updatePolicy?: "always"|"once"|"never",
|
|
122
|
+
}) -> null
|
|
123
|
+
|
|
124
|
+
components.agentEngine.engine.wakeup.enqueue({
|
|
125
|
+
orgId, agentId, mandateScope, cost, reason,
|
|
126
|
+
workFunctionHandle, workflowId, awaitedEventName,
|
|
127
|
+
}) -> Id<"wakeableWork"> (action)
|
|
542
128
|
components.agentEngine.engine.wakeup.cancel({ orgId, wakeableWorkId }) -> null
|
|
543
|
-
components.agentEngine.engine.wakeup.getStatus({
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
components.agentEngine.engine.
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
components.agentEngine.engine.
|
|
129
|
+
components.agentEngine.engine.wakeup.getStatus({
|
|
130
|
+
orgId, wakeableWorkId,
|
|
131
|
+
}) -> { status, resultSummary?, ... }
|
|
132
|
+
|
|
133
|
+
components.agentEngine.engine.durableJob.start({
|
|
134
|
+
orgId, kind, stepFunctionHandle, totalSteps, maxAttemptsPerStep,
|
|
135
|
+
}) -> Id<"durableJobs">
|
|
136
|
+
components.agentEngine.engine.durableJob.getStatus({ orgId, jobId })
|
|
137
|
+
-> { status, cursor, totalSteps, lastError?, abandonedAtStep? }
|
|
138
|
+
components.agentEngine.engine.durableJob.cancel({ orgId, jobId }) -> null
|
|
551
139
|
|
|
552
|
-
components.agentEngine.engine.
|
|
140
|
+
components.agentEngine.engine.mandates.set({
|
|
141
|
+
orgId, agentId, scope, kind, rate, period, capacity?,
|
|
142
|
+
}) -> Id<"mandates">
|
|
143
|
+
components.agentEngine.engine.mandates.get({
|
|
144
|
+
orgId, agentId, scope,
|
|
145
|
+
}) -> mandate | null
|
|
146
|
+
components.agentEngine.engine.mandates.spend({
|
|
147
|
+
orgId, agentId, scope, cost, reason,
|
|
148
|
+
}) -> { ok, approvalId? }
|
|
149
|
+
components.agentEngine.engine.mandates.listApprovals({
|
|
150
|
+
orgId, agentId,
|
|
151
|
+
}) -> approvalRequest[]
|
|
152
|
+
components.agentEngine.engine.mandates.resolveApproval({
|
|
153
|
+
orgId, approvalId, resolvedBy, status: "approved"|"denied",
|
|
154
|
+
}) -> null
|
|
155
|
+
components.agentEngine.engine.mandates.usageCount({
|
|
156
|
+
orgId, agentId, scope,
|
|
157
|
+
}) -> number
|
|
158
|
+
|
|
159
|
+
components.agentEngine.engine.habits.create({
|
|
160
|
+
orgId, agentId, name, cronExpression, prompt,
|
|
161
|
+
}) -> Id<"habits">
|
|
553
162
|
components.agentEngine.engine.habits.get({ orgId, habitId }) -> habit | null
|
|
554
163
|
components.agentEngine.engine.habits.listForAgent({ orgId, agentId }) -> habit[]
|
|
555
|
-
components.agentEngine.engine.habits.updateSchedule({
|
|
164
|
+
components.agentEngine.engine.habits.updateSchedule({
|
|
165
|
+
orgId, habitId, cronExpression,
|
|
166
|
+
}) -> null
|
|
556
167
|
components.agentEngine.engine.habits.disable({ orgId, habitId }) -> null
|
|
557
168
|
components.agentEngine.engine.habits.enable({ orgId, habitId }) -> null
|
|
558
169
|
components.agentEngine.engine.habits.listRuns({ orgId, habitId }) -> habitRun[]
|
|
559
170
|
|
|
560
|
-
components.agentEngine.engine.sandboxCallback.startRun({
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
components.agentEngine.engine.sandboxCallback.
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
components.agentEngine.engine.
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
components.agentEngine.engine.
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
components.agentEngine.engine.
|
|
573
|
-
|
|
574
|
-
|
|
171
|
+
components.agentEngine.engine.sandboxCallback.startRun({
|
|
172
|
+
orgId, workflowId, awaitedEventName, timeoutMs,
|
|
173
|
+
}) -> Id<"sandboxRuns">
|
|
174
|
+
components.agentEngine.engine.sandboxCallback.attachSandboxId({
|
|
175
|
+
orgId, sandboxRunId, sandboxId,
|
|
176
|
+
}) -> null
|
|
177
|
+
components.agentEngine.engine.sandboxCallback.getStatus({
|
|
178
|
+
orgId, sandboxRunId,
|
|
179
|
+
}) -> { status, resultSummary?, ... }
|
|
180
|
+
components.agentEngine.engine.sandboxCallback.processCallback({
|
|
181
|
+
orgId, sandboxRunId, deliveryId, status, resultSummary?,
|
|
182
|
+
}) -> { outcome: "woke"|"duplicate" }
|
|
183
|
+
components.agentEngine.engine.sandboxCallback.deadLetterMalformed({
|
|
184
|
+
orgId?, deliveryId, reason, rawBody,
|
|
185
|
+
}) -> null
|
|
186
|
+
|
|
187
|
+
components.agentEngine.engine.delegation.declareSubagent({
|
|
188
|
+
orgId, parentAgentId, childAgentId, toolName, description?,
|
|
189
|
+
}) -> Id<"subagents">
|
|
190
|
+
components.agentEngine.engine.delegation.listSubagents({
|
|
191
|
+
orgId, parentAgentId,
|
|
192
|
+
}) -> subagent[]
|
|
193
|
+
components.agentEngine.engine.delegation.delegateToSubagent({
|
|
194
|
+
orgId, subagentId, ...,
|
|
195
|
+
}) -> null (action)
|
|
196
|
+
components.agentEngine.engine.delegation.invokeAgentClone({
|
|
197
|
+
orgId, agentId, ...,
|
|
198
|
+
}) -> null (action; root-only, non-recursive by construction)
|
|
199
|
+
|
|
200
|
+
components.agentEngine.engine.vpConnection.get({
|
|
201
|
+
orgId, agentId,
|
|
202
|
+
}) -> vpConnection | null
|
|
203
|
+
components.agentEngine.engine.vpConnection.getToggles({
|
|
204
|
+
orgId, agentId,
|
|
205
|
+
}) -> { memoryEnabled, tasksEnabled, messagesEnabled, notesEnabled }
|
|
206
|
+
components.agentEngine.engine.vpConnection.setToggles({
|
|
207
|
+
orgId, agentId, memoryEnabled?, tasksEnabled?,
|
|
208
|
+
messagesEnabled?, notesEnabled?,
|
|
209
|
+
}) -> Id<"vpConnections">
|
|
210
|
+
components.agentEngine.engine.vpConnection.invokeVpTool({
|
|
211
|
+
orgId, agentId, tool, vpFunctionHandle, argsJson,
|
|
212
|
+
}) -> null (action)
|
|
575
213
|
```
|
|
576
214
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
`
|
|
600
|
-
|
|
601
|
-
and
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
`turn.toolCall.completed` for a denied call, i.e. the tool was never run
|
|
627
|
-
and the turn is NOT silently treated as done.
|
|
628
|
-
- **`updatePolicy`** (optional) — persists a NEW standing gate
|
|
629
|
-
(`"always"|"once"|"never"`, eve parity: `eve/tools/approval` helpers) for
|
|
630
|
-
this `(agentId, toolName)` pair going forward, independent of `decision`.
|
|
631
|
-
A human can deny this turn's call and leave the policy alone (ask again
|
|
632
|
-
next time), or approve this turn and simultaneously flip the policy to
|
|
633
|
-
`"always"`/`"never"` so future calls stop pausing. `"never"` still means
|
|
634
|
-
Eve's "never require approval" (auto-authorize) — that policy literal is
|
|
635
|
-
unchanged and is NOT the same thing as a `"denied"` decision.
|
|
636
|
-
|
|
637
|
-
`createdBy` (on `agents.create` / `sessions.start`) is, for the same
|
|
638
|
-
platform reason as `orgId`, no longer read from `ctx.auth.getUserIdentity()`
|
|
639
|
-
— the consumer passes the acting user's own identifier.
|
|
640
|
-
|
|
641
|
-
Verified two ways: (1) every `component/*.componentPackaging.test.ts` file
|
|
642
|
-
(one per migrated module: durableJob, sessions, directory, wakeup,
|
|
643
|
-
awaitEventTimeout, mandates, habits, sandboxCallback, delegation,
|
|
644
|
-
vpConnection) runs `convex-test` directly against `component/_generated/api`
|
|
645
|
-
(the exact source tree `tsc -p tsconfig.build.json` compiles into
|
|
646
|
-
`dist/component/`, which is what `npm pack` ships), so the compiled
|
|
647
|
-
artifact has a real green test suite behind it, not just the repo-root dev
|
|
648
|
-
app's; (2) a from-scratch global consumer probe (install the REAL
|
|
649
|
-
`npm pack` tarball into a bare Convex project outside this repo, register
|
|
650
|
-
the component, call one function from EACH migrated capability from the
|
|
651
|
-
consumer's own function, passing `orgId` explicitly) — see the C7
|
|
652
|
-
completion report for the verbatim transcript and, honestly, whichever
|
|
653
|
-
parts of it required deployment provisioning this repo does not have.
|
|
654
|
-
|
|
655
|
-
Only `skills` and `connections` (the agent's own tool-definition and
|
|
656
|
-
third-party-connection tables) remain genuinely NOT ported — see
|
|
657
|
-
"Deliberately app-level" and the closing paragraph above for the full,
|
|
658
|
-
current boundary.
|
|
659
|
-
|
|
660
|
-
### Why `component/` and not `convex/component/`
|
|
661
|
-
|
|
662
|
-
`npx convex codegen --component-dir <path>` builds a synthetic root app
|
|
663
|
-
that does `import component from "<relative-import-to-path>"`. When
|
|
664
|
-
`<path>` is nested INSIDE the functions directory it resolves from
|
|
665
|
-
(`convex/`), that relative import comes out as a bare specifier with no
|
|
666
|
-
leading `./` or `../` (e.g. `"component/convex.config.js"`), which esbuild
|
|
667
|
-
correctly refuses to resolve as a relative path — not a bug in our code,
|
|
668
|
-
a corner case of running `--component-dir` against a directory nested
|
|
669
|
-
under the default functions directory. Moving the component's source tree
|
|
670
|
-
one level up, to `component/` (a sibling of `convex/`, mirroring how
|
|
671
|
-
`@convex-dev/workflow` itself keeps its component source under `src/component`,
|
|
672
|
-
outside its `example/convex` app directory) makes the generated relative
|
|
673
|
-
import start with `../`, which resolves — measured empirically against
|
|
674
|
-
this exact repo layout, not guessed.
|
|
675
|
-
|
|
676
|
-
## Status
|
|
677
|
-
|
|
678
|
-
T1 (schema + durable session engine, PR #1) merged @
|
|
679
|
-
`9f3401ba63e6e663af95e9e4f5da42bc30b3550b`. I1 (long-task survival
|
|
680
|
-
contract, `engine/durableJob.ts`) — not merged, Eta reviews. T4 (mandates
|
|
681
|
-
+ habits) — **not merged** — Eta reviews, merge on Pi token. T2-TER (npm
|
|
682
|
-
packaging + component identity + publication) through C6 (agents,
|
|
683
|
-
sessions/workflow, directory, wakeup, awaitEventTimeout, mandates, habits,
|
|
684
|
-
sandbox bridge DB-side, delegation, VantagePeers connection — all ported
|
|
685
|
-
under `component/` with their own green `convex-test` suite) — see
|
|
686
|
-
"Packaging & publication" above.
|
|
687
|
-
|
|
688
|
-
**M-COMPLETION C7** (this pass): version bump to `0.1.0-alpha.6`
|
|
689
|
-
republishing the FULL C1-C6 surface + a from-scratch global consumer probe
|
|
690
|
-
against the real `npm pack` tarball proving packaged surface == built
|
|
691
|
-
surface. npm `latest`/`alpha` tags are NOT repointed by this PR — that is
|
|
692
|
-
a post-merge deployment step, Pi token protocol, same as every prior
|
|
693
|
-
publish. See the C7 completion report for the probe transcript.
|
|
215
|
+
### Concepts
|
|
216
|
+
|
|
217
|
+
- **Sessions** — a durable conversation thread, driven by a versioned
|
|
218
|
+
`@convex-dev/workflow` run. Turns, streamed reasoning, tool-call
|
|
219
|
+
lifecycle and approval lifecycle are appended as an event log a client
|
|
220
|
+
replays on reconnect.
|
|
221
|
+
- **Delegation** — an agent can declare typed sub-agents (strict isolation:
|
|
222
|
+
a sub-agent's own skills/connections/approvals are never copied onto the
|
|
223
|
+
parent) or invoke a non-recursive clone of itself as a built-in tool.
|
|
224
|
+
Both mechanisms reuse the same pause/wake primitive.
|
|
225
|
+
- **Mandates** — a bounded, weighted spending cap per agent/org/scope.
|
|
226
|
+
Spending under the cap succeeds; an overrun creates a pending approval
|
|
227
|
+
request rather than silently failing open or throwing away the audit
|
|
228
|
+
trail.
|
|
229
|
+
- **Habits** — a per-agent, runtime-editable cron schedule (create, update,
|
|
230
|
+
disable, enable — all with zero redeploy).
|
|
231
|
+
- **Long-task survival** — `durableJob` lets a consumer's task survive past
|
|
232
|
+
a single Convex action's execution-time ceiling by expressing it as N
|
|
233
|
+
independently-bounded, resumable steps.
|
|
234
|
+
|
|
235
|
+
### Trust model
|
|
236
|
+
|
|
237
|
+
Convex does not propagate the calling app's `ctx.auth` identity across a
|
|
238
|
+
component boundary. Every public function in this component takes `orgId`
|
|
239
|
+
(and, where relevant, `createdBy`) as an explicit argument — your app is
|
|
240
|
+
responsible for authenticating its user and resolving `orgId` before
|
|
241
|
+
calling in. Every document lookup by id is checked against the caller's own
|
|
242
|
+
`orgId` before being returned or mutated; a cross-org read or write is
|
|
243
|
+
refused the same way a not-found row would be.
|
|
244
|
+
|
|
245
|
+
## Requirements
|
|
246
|
+
|
|
247
|
+
- Convex `^1.42`
|
|
248
|
+
- A Convex deployment with `@convex-dev/workflow`, `@convex-dev/workpool`,
|
|
249
|
+
`@convex-dev/agent`, `@convex-dev/rate-limiter`, `@convex-dev/crons`, and
|
|
250
|
+
`@convex-dev/sharded-counter` resolvable (all installed automatically as
|
|
251
|
+
dependencies of this package).
|
|
252
|
+
|
|
253
|
+
## License
|
|
254
|
+
|
|
255
|
+
[FSL-1.1-Apache-2.0](./LICENSE) — free for production use; converts to
|
|
256
|
+
Apache 2.0 after two years, per version.
|
|
257
|
+
|
|
258
|
+
## Credit
|
|
259
|
+
|
|
260
|
+
Slot names, contracts and authoring vocabulary are deliberately kept in
|
|
261
|
+
parity with [vercel/eve](https://github.com/vercel/eve). This package is
|
|
262
|
+
not affiliated with Vercel; parity is a design choice, not a claim of
|
|
263
|
+
origin.
|