@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/README.md CHANGED
@@ -1,424 +1,41 @@
1
1
  # @vantageos/agent-engine
2
2
 
3
- Reusable Convex component (pattern `@convex-dev/*`, house precedent
4
- `vantage-doc-forge`): durable agent engine. **An agent is a DATA
5
- AGGREGATE** — root sheet + child tables (skills, recursive subagents,
6
- connections, habits, sandbox config, per-tool approvals) — **never compiled
7
- code**. Instant creation, zero deployment. Built on `@convex-dev/workflow`
8
- + `@convex-dev/workpool` (+ `@convex-dev/agent` for threads/context).
9
-
10
- Consumers, in order: VantagePeers (Sigma, I1 long-task survival),
11
- EveVantage (Hestia), VantageCRM (Theta).
12
-
13
- Bible: `elpiarthera/ElPi-Corp` (private) pinned @ `60b69bfc` —
14
- `analysis/evevantage/SYNTHESE.md` §4/§7,
15
- `analysis/evevantage/i0-research-convex-components.md`,
16
- `analysis/evevantage/mvp-evevantage.md` §2. Upstream reference:
17
- https://github.com/vercel/eve (reasoning source, never an opaque
18
- dependency).
19
-
20
- ## EVE PARITY BY DESIGN
21
-
22
- Non-negotiable rule (SYNTHESE §4.e-ter): slots, contracts and vocabulary
23
- mirror `vercel/eve`. Every deliberate divergence is marked **"notre
24
- ligne"** below, in this table and in the code comments next to the field.
25
-
26
- | Eve slot / concept | Source (vercel/eve) | Our table / contract | Notes |
27
- |---|---|---|---|
28
- | Authored agent = a folder, `compileAgent` | `packages/eve/src/compiler/compile-from-memory.ts:86-87` | `agents` root row | **notre ligne** — we never compile; the row IS the agent |
29
- | `name` derived from folder path (no `name` field) | `agent-definition.ts:281-284` | `agents.name` | **notre ligne** — required field, since we have no folder |
30
- | `description` | `agent-definition.ts:291` | `agents.description` | direct parity |
31
- | `instructions` (`instructions.md`) | `shared/instructions-definition.ts:1-18` | `agents.instructions` | direct parity |
32
- | `model` (required) | `agent-definition.ts:304` | `agents.model` | direct parity |
33
- | reasoning effort | `agent-definition.ts:315-319` | `agents.reasoningEffort` | direct parity |
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
- `orgId` is threaded explicitly into the target handle's args (never a
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
- ### Trust model — READ THIS FIRST (CORRECTIF, VP k1741evjzm72rj91tkcbdhpskx8ah35a)
433
-
434
- **Convex does not propagate the calling app's `ctx.auth` identity across a
435
- component boundary.** This is a platform isolation guarantee, not a choice
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
- components.agentEngine.engine.agents.create({ orgId, createdBy, name, description?, instructions, model, reasoningEffort?, modelProvider?, maxSubagents?, maxInputTokensPerSession? }) -> Id<"agents">
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
- components.agentEngine.engine.resolvers.resolveManifest({ orgId, sessionId }) -> { agentId, name, instructions, model, toolApprovals[] }
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
- components.agentEngine.engine.sessions.start({ orgId, agentId, createdBy }) -> { sessionId: Id<"sessions">, workflowId: string }
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
- components.agentEngine.engine.toolApprovals.respond({ orgId, sessionId, toolName, decision: "approved"|"denied", updatePolicy?: "always"|"once"|"never" }) -> null
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.wakeup.enqueue({ orgId, agentId, mandateScope, cost, reason, workFunctionHandle, workflowId, awaitedEventName }) -> Id<"wakeableWork"> (action)
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({ orgId, wakeableWorkId }) -> { status, resultSummary?, ... }
544
-
545
- components.agentEngine.engine.mandates.set({ orgId, agentId, scope, kind, rate, period, capacity? }) -> Id<"mandates">
546
- components.agentEngine.engine.mandates.get({ orgId, agentId, scope }) -> mandate | null
547
- components.agentEngine.engine.mandates.spend({ orgId, agentId, scope, cost, reason }) -> { ok, approvalId? }
548
- components.agentEngine.engine.mandates.listApprovals({ orgId, agentId }) -> approvalRequest[]
549
- components.agentEngine.engine.mandates.resolveApproval({ orgId, approvalId, resolvedBy, status: "approved"|"denied" }) -> null
550
- components.agentEngine.engine.mandates.usageCount({ orgId, agentId, scope }) -> number
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.habits.create({ orgId, agentId, name, cronExpression, prompt }) -> Id<"habits">
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({ orgId, habitId, cronExpression }) -> null
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({ orgId, workflowId, awaitedEventName, timeoutMs }) -> Id<"sandboxRuns">
561
- components.agentEngine.engine.sandboxCallback.attachSandboxId({ orgId, sandboxRunId, sandboxId }) -> null
562
- components.agentEngine.engine.sandboxCallback.getStatus({ orgId, sandboxRunId }) -> { status, resultSummary?, ... }
563
- components.agentEngine.engine.sandboxCallback.processCallback({ orgId, sandboxRunId, deliveryId, status, resultSummary? }) -> { outcome: "woke"|"duplicate" } (called ONLY after the app-level HTTP route has verified the HMAC signature — see "notre ligne" below)
564
- components.agentEngine.engine.sandboxCallback.deadLetterMalformed({ orgId?, deliveryId, reason, rawBody }) -> null
565
-
566
- components.agentEngine.engine.delegation.declareSubagent({ orgId, parentAgentId, childAgentId, toolName, description? }) -> Id<"subagents">
567
- components.agentEngine.engine.delegation.listSubagents({ orgId, parentAgentId }) -> subagent[]
568
- components.agentEngine.engine.delegation.delegateToSubagent({ orgId, subagentId, ... }) -> null (action; reuses `wakeup.enqueue` end to end)
569
- components.agentEngine.engine.delegation.invokeAgentClone({ orgId, agentId, ... }) -> null (action; the built-in `agent` clone tool, root-only, non-recursive by construction)
570
-
571
- components.agentEngine.engine.vpConnection.get({ orgId, agentId }) -> vpConnection | null
572
- components.agentEngine.engine.vpConnection.getToggles({ orgId, agentId }) -> { memoryEnabled, tasksEnabled, messagesEnabled, notesEnabled }
573
- components.agentEngine.engine.vpConnection.setToggles({ orgId, agentId, memoryEnabled?, tasksEnabled?, messagesEnabled?, notesEnabled? }) -> Id<"vpConnections">
574
- components.agentEngine.engine.vpConnection.invokeVpTool({ orgId, agentId, tool, vpFunctionHandle, argsJson }) -> null (action; direct access via `createFunctionHandle`, never `fetch()`)
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
- **Deliberately app-level, NOT ported into the component boundary** — this
578
- is a measured platform boundary (C5 recherche-d'abord), not an unfinished
579
- migration:
580
-
581
- - The public HTTP route (`/sandbox/callback`) that reads
582
- `SANDBOX_CALLBACK_SECRET` and verifies the HMAC signature over the raw
583
- request body. No official `@convex-dev/*` component ships an `http.ts`
584
- or reads `process.env` directly (measured: zero hits across all 7
585
- installed `@convex-dev/*` component dist trees) — a component cannot
586
- own this. It stays in the consumer app's own `convex/http.ts`, which
587
- calls `sandboxCallback.processCallback` / `deadLetterMalformed` ONLY
588
- after the signature has already checked out.
589
- - The `"use node"` action that actually talks to `@vercel/sandbox`
590
- (`Sandbox.create()` / `sandbox.runCommand()`). The Convex CLI's own
591
- component bundler hard-crashes on any `"use node"` file found inside a
592
- child component directory ("use node directive is not supported in
593
- components"). It stays app-level and calls INTO this component's public
594
- `sandboxCallback.startRun` / `attachSandboxId` (and `mandates.spend`)
595
- instead.
596
-
597
- Everything else that was previously listed as "not yet callable" —
598
- `skills`/`subagents`/`habits`/`sandboxConfig`/`mandates`/
599
- `approvalRequests`/`awaitEventTimeouts`/`wakeableWork`/`engine/wakeup.ts`/
600
- `engine/workflows.ts`/`vpConnections` — is now hosted under `component/`
601
- and callable as listed above (C1 through C6, this pass). `skills` and
602
- `connections` (the agent's own tool-definition and third-party-connection
603
- tables, distinct from the `subagents`/`vpConnections` tables above) remain
604
- the one genuinely deferred slice: porting them needs their own
605
- component-level `convex-test` harness first (Day 128: never ship
606
- unverified), and is left for the next, separately-scoped migration.
607
-
608
- ### `toolApprovals.respond` — `decision` vs `updatePolicy` (0.1.0-alpha.5)
609
-
610
- Two DISTINCT axes were conflated pre-alpha.5 (VP
611
- k17ea6x3tht8xa3rxm139t23gx8ajkf3, defaut 2) — a single `approval:
612
- "always"|"once"|"never"` argument was overloaded to mean both "the standing
613
- policy for this tool" AND "the answer to this one pending request", and
614
- there was no literal for "refuse this turn only". Fixed by splitting them:
615
-
616
- - **`decision`** (required) — the response to THIS ONE pending
617
- `tool.approval.requested` event, for THIS turn only. `"approved"` |
618
- `"denied"`. **Eve parity, direct** — vocabulary matches Eve's
619
- `ApprovalStatus` (`packages/eve/src/public/definitions/approval.ts`:
620
- `"approved" | "denied" | "not-applicable" | "user-approval" | ...`); we
621
- did not invent a `deny` literal, we aligned with the one Eve already has.
622
- `"denied"` is a terminal refusal for the turn: `sessions.requestToolCall`
623
- reports back `{ authorized: false, approvalPending: false, denied: true
624
- }` (never re-authorized on retry within the session), and a dedicated
625
- `turn.toolCall.denied` sessionEvent is appended — there is NO
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.