@vantageos/agent-engine 0.1.0-alpha.5 → 0.1.0-alpha.7

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.
Files changed (43) hide show
  1. package/LICENSE +110 -0
  2. package/README.md +154 -473
  3. package/dist/component/_generated/api.d.ts +31 -0
  4. package/dist/component/_generated/component.d.ts +346 -2
  5. package/dist/component/convex.config.js +18 -0
  6. package/dist/component/engine/agents.d.ts +10 -0
  7. package/dist/component/engine/agents.js +21 -0
  8. package/dist/component/engine/awaitEventTimeout.d.ts +13 -0
  9. package/dist/component/engine/awaitEventTimeout.js +81 -0
  10. package/dist/component/engine/delegation.d.ts +75 -0
  11. package/dist/component/engine/delegation.js +327 -0
  12. package/dist/component/engine/directory.d.ts +9 -0
  13. package/dist/component/engine/directory.js +45 -0
  14. package/dist/component/engine/durableJob.js +6 -6
  15. package/dist/component/engine/habits.d.ts +70 -0
  16. package/dist/component/engine/habits.js +218 -0
  17. package/dist/component/engine/mandates.d.ts +74 -0
  18. package/dist/component/engine/mandates.js +271 -0
  19. package/dist/component/engine/sandboxCallback.d.ts +36 -0
  20. package/dist/component/engine/sandboxCallback.js +259 -0
  21. package/dist/component/engine/sessions.d.ts +6 -2
  22. package/dist/component/engine/sessions.js +43 -7
  23. package/dist/component/engine/toolApprovals.js +1 -1
  24. package/dist/component/engine/vpConnection.d.ts +69 -0
  25. package/dist/component/engine/vpConnection.js +218 -0
  26. package/dist/component/engine/wakeup.d.ts +33 -0
  27. package/dist/component/engine/wakeup.js +228 -0
  28. package/dist/component/engine/workflows.d.ts +11 -0
  29. package/dist/component/engine/workflows.js +84 -0
  30. package/dist/component/schema.d.ts +365 -4
  31. package/dist/component/schema.js +345 -18
  32. package/dist/component/testFixtures/callerSteps.js +1 -1
  33. package/dist/component/testFixtures/delegationDemoWorkflow.d.ts +18 -0
  34. package/dist/component/testFixtures/delegationDemoWorkflow.js +75 -0
  35. package/dist/component/testFixtures/sandboxDemoWorkflow.d.ts +19 -0
  36. package/dist/component/testFixtures/sandboxDemoWorkflow.js +69 -0
  37. package/dist/component/testFixtures/vpTool.d.ts +4 -0
  38. package/dist/component/testFixtures/vpTool.js +25 -0
  39. package/dist/component/testFixtures/wakeupDemoWorkflow.d.ts +19 -0
  40. package/dist/component/testFixtures/wakeupDemoWorkflow.js +77 -0
  41. package/dist/component/testFixtures/wakeupWork.d.ts +8 -0
  42. package/dist/component/testFixtures/wakeupWork.js +21 -0
  43. package/package.json +17 -5
package/README.md CHANGED
@@ -1,337 +1,39 @@
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, root-only `agent` tool | https://eve.dev/docs/subagents | `subagents` child table (`parentAgentId` -> `childAgentId`, recursive) | direct parity. **Isolation strict, "inherits nothing from the root's authored slots"** — a subagent row only declares the link + exposed tool name; it never inlines the parent's skills/connections/approvals |
41
- | `sandbox` | `agent-definition.ts:150-160` | `sandboxConfig` child table (Vercel Sandbox, egress policy, allowlist) | direct parity, I0 §8 |
42
- | `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 |
43
- | *(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 |
44
- | 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 |
45
- | `subagents` used for inter-agent collaboration | eve docs | **not used** — collaboration goes through VantagePeers MCP tools instead | **notre ligne** — explicit Laurent decision (mvp-evevantage.md §2.5): "la collaboration entre agents n'arrive pas par les `subagents` d'eve" |
46
- | `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) |
47
-
48
- ## Data aggregate — schema (`convex/schema.ts`)
49
-
50
- Root sheet: `agents`.
51
-
52
- Child tables (>= 7, every index starts with `orgId`):
53
-
54
- 1. `skills` — contextual procedures
55
- 2. `subagents` — recursive declared delegation, strict isolation
56
- 3. `connections` — MCP/OpenAPI (VantagePeers tools included)
57
- 4. `habits` (+ `habitRuns`) — dynamic crons (habits, notre ligne, T4)
58
- 5. `sandboxConfig` — Vercel Sandbox settings
59
- 6. `toolApprovals` — `always` / `once` / `never` per tool
60
- 7. `modelKeys` — BYOK, server-only, split from `agents`
61
- 8. `mandates` (+ `approvalRequests`) — bounded authority (notre ligne, T4)
62
-
63
- Engine tables: `sessions`, `sessionEvents`, `awaitEventTimeouts`.
64
-
65
- **Hard rule**: `orgId` is derived server-side from the authenticated
66
- identity (`convex/lib/auth.ts:requireOrgId`, reads the Clerk `org_id`
67
- claim) — it is never accepted as a client argument, and it is the FIRST
68
- field of every `.index(...)` call. Deny by default: cross-org reads return
69
- `null` / cross-org writes throw, indistinguishable from "not found"
70
- (`convex/lib/auth.ts:assertSameOrg`).
71
-
72
- ## Engine — durable sessions (`convex/engine/`)
73
-
74
- - `engine/workflows.ts` — `WorkflowManager` from `@convex-dev/workflow`.
75
- The session loop is exported as `sessionWorkflowV1`. **Versioned
76
- workflows / drain-before-deploy discipline** (workflow#35): a breaking
77
- change to the loop ships as `sessionWorkflowV2`, never an in-place edit
78
- of `V1` while sessions still reference it.
79
- - `engine/resolvers.ts` — `resolveManifest`, the dynamic resolver. Rereads
80
- the full agent aggregate (skills, tool approvals, connections) on every
81
- call; callers invoke it at `session.started` / `turn.started` boundaries
82
- only, mirroring eve's `ALLOWED_DYNAMIC_INSTRUCTION_EVENTS`.
83
- - `engine/sessions.ts` — `start` / `recordTurnStarted`, the mutations that
84
- write the named `sessionEvents` rows Eve-style.
85
- - `engine/awaitEventTimeout.ts` — **self-built `awaitEvent` timeout**
86
- (workflow#177: Convex ships no native timeout for `step.awaitEvent`).
87
- `schedule` writes an `awaitEventTimeouts` row and schedules an
88
- `internalMutation` (`fire`) via `ctx.scheduler.runAfter`; if the real
89
- event arrives first, `cancel` cancels the scheduled function. `fire`
90
- inserts a blank `sandbox.timeout` `sessionEvents` row so the awaiting
91
- workflow step wakes up regardless.
92
-
93
- `ai` is pinned to `6.0.35` (peer dependency of `@convex-dev/agent@0.6.4`,
94
- agent#79 warns the v5->v6 migration broke other consumers — we track
95
- majors deliberately, never float). Counters/rollups, when needed, go
96
- through `@convex-dev/sharded-counter` / `@convex-dev/aggregate` — no
97
- canary/alpha third-party dependency for a guardrail (I0 §7.4).
98
-
99
- ## Long-task survival (I1) — `engine/durableJob.ts`
100
-
101
- SYNTHESE.md §5/§7: "le plafond de durée des actions Convex est notre
102
- plafond aujourd'hui, sur tous nos produits" -- `durableJobs` is the
103
- GENERIC (shape-agnostic, unlike `sessions`) primitive that lets a
104
- consumer's task survive past that ceiling by expressing it as N
105
- independently-bounded steps. It **ENVELOPS** `@convex-dev/workflow` (via
106
- the same `WorkflowManager` + versioned-workflow discipline as
107
- `engine/workflows.ts`), it does not reimplement durable execution.
108
-
109
- Contract (see `convex/engine/durableJob.ts` for the full commentary and
110
- `examples/vantage-peers-long-task/` for the worked integration a consumer
111
- reads without needing to know our internals):
112
-
113
- - `start(kind, stepFunctionHandle, totalSteps, maxAttemptsPerStep)` —
114
- enqueue a job driven by a step function the CALLER owns (component
115
- isolation: we invoke it by opaque `FunctionHandle`, we never read the
116
- caller's own tables).
117
- - `getStatus(jobId)` — poll `status` / `cursor` / `lastError` /
118
- `abandonedAtStep`.
119
- - `cancel(jobId)` — best-effort stop.
120
- - **Pause at step boundary**: one loop iteration == one workflow step ==
121
- one durability boundary (`@convex-dev/workflow` persists the journal
122
- after each step returns).
123
- - **Deterministic replay**: `runStepInternal` no-ops for any `stepIndex <
124
- job.cursor`, so a resumed workflow's journal replay never re-runs a
125
- completed step.
126
- - **Abandon after N**: the caller's step function is retried up to
127
- `maxAttemptsPerStep` times inside one workflow step; once exhausted the
128
- job is marked `abandoned` with `abandonedAtStep` + `lastError`, and no
129
- further step runs.
130
-
131
- ## Mandates & habits (T4) — NOTRE LIGNE, EveVantage's two product differentiators
132
-
133
- SYNTHESE.md §2: *"Deux arguments de vente que personne n'a"* — mandates
134
- ("cet agent peut engager jusqu'à X ; au-delà, il me demande") and habits
135
- ("tous les matins, relève les mails, classe, fais le point"). Both are
136
- **declared divergences from `vercel/eve`** — Eve has no concept of a
137
- weighted spending cap or a runtime-editable recurrence; this is our line,
138
- not a silent gap.
139
-
140
- ### Mandates — `convex/mandates.ts` (`mandates`, `approvalRequests` tables)
141
-
142
- **Notre ligne**: bounded authority per agent+org+scope. A mandate is a
143
- DATA ROW (`mandates.{kind,rate,period,capacity}`), never a constant in
144
- code — a client changing their mind is a `mandates.set` mutation, zero
145
- deploy. `spend(agentId, scope, cost, reason)` is the single choke point
146
- every costed action must pass through:
147
-
148
- - **fails-closed**: no mandate row for `orgId+agentId+scope` => throws
149
- immediately (`Mandate refused: no mandate configured for scope "..."`).
150
- There is no "unlimited by default" path.
151
- - **overrun => human approval, never a silent workaround**: when
152
- `@convex-dev/rate-limiter`'s `limit()` (called with an **inline dynamic
153
- `config`** built from the DB row — see rate-limiter README "Define a
154
- rate limit inline / dynamically") returns `ok: false`, `spend` inserts
155
- an `approvalRequests` row (`status: "pending"`) and returns
156
- `{ ok: false, approvalId }`. It deliberately does **not** throw on this
157
- path: Convex mutations are single transactions, and throwing after
158
- `ctx.db.insert` would roll the insert back along with everything else —
159
- destroying the very approval record the guard exists to leave behind.
160
- Callers (the session/workflow engine) are REQUIRED to check `.ok` and
161
- treat `false` as a hard refusal; the only way authority is restored is
162
- an explicit human `resolveApproval(approvalId, "approved", resolvedBy)`.
163
- - **rollups via `@convex-dev/sharded-counter`** (`usageCount` query), one
164
- bucket per `orgId:agentId:scope` key — I0 §7.4: no canary/alpha
165
- dependency for a guardrail. `@convex-dev/aggregate` (also I0-approved,
166
- also official, also no canary) is the natural next step for time-
167
- windowed billing rollups; not wired in this lot, called out here rather
168
- than silently left out.
169
- - `count` passed to `rateLimiter.limit` is a WEIGHTED cost (tokens, euros,
170
- calls — whatever the scope means), never a bare call counter.
171
-
172
- ### Habits — `convex/habits.ts` (`habits`, `habitRuns` tables)
173
-
174
- **Notre ligne (naming + mechanism)**: Eve's closest concept is
175
- `schedules`/`hooks`, statically declared in a compiled agent. Habits are
176
- runtime DATA: `habits.cronExpression` is a column, and the actual
177
- recurrence is a **dynamically registered `@convex-dev/crons` job**
178
- (`Crons.register`/`.delete`, never a static `convex/crons.ts` entry).
179
-
180
- - `create` writes the habit row and registers its cron in the SAME
181
- mutation transaction — `@convex-dev/crons` guarantees the cron "is
182
- guaranteed to exist after the mutation that creates them has run"
183
- (README), so the very next tick can fire it with zero deploy.
184
- - `updateSchedule` edits `cronExpression`/`prompt`/`name` at runtime:
185
- deletes the old cron registration, registers a fresh one from the new
186
- cadence, still inside one transaction.
187
- - `disable` is a HARD stop: it deletes the runtime cron registration
188
- (not a soft `enabled` flag the target function checks) — after it
189
- resolves there is no scheduled function left that could fire the habit.
190
- `run` (the cron-invoked internal mutation) also re-checks `enabled` as
191
- defence in depth for any run already in flight.
192
- - `enable` re-registers from the habit's own `cronExpression` data.
193
- - Every tick writes a `habitRuns` proof row — this is what makes "fires
194
- its first run without a deploy" and "a disabled habit does not fire
195
- again" PROVABLE in `habits.test.ts` rather than asserted on trust.
196
-
197
- ## `convex/_generated/` freshness -- two gates, not one
198
-
199
- `convex/_generated/api.d.ts` carries an "AUTOMATICALLY GENERATED" header
200
- but nothing stops a hand-edit of its *content* (as opposed to adding or
201
- removing an entire module) from being committed as-is. That gap was
202
- measured for real on PR #2 (T2-BIS): a manual edit of a single existing
203
- line (`FunctionReference<any, "public">` flipped to `"internal">`) passed
204
- the original structural gate with `OK`, because that gate only diffs the
205
- *list* of modules present under `convex/` against the modules imported by
206
- `api.d.ts` -- it never recomputes a byte.
207
-
208
- Two gates now exist, and they check different things:
209
-
210
- 1. **Structural, offline, secret-free** --
211
- `scripts/check-generated-freshness.mjs` (`npm run check:generated`).
212
- Runs in CI (`.github/workflows/ci.yml`). Catches a function file
213
- added/renamed/removed without regenerating bindings. **Does NOT catch a
214
- hand-edit of existing generated content.**
215
- 2. **Content, requires a live deployment** --
216
- `scripts/check-generated-content.mjs` (`npm run
217
- check:generated:content`). Runs `npx convex codegen` against the dev
218
- deployment for BOTH `convex/_generated/` (the host repo's own dev
219
- deployment bindings) AND, since 0.1.0-alpha.5, `component/_generated/`
220
- via `npx convex codegen --component-dir component` -- the checked-in
221
- file `tsc -p tsconfig.build.json` copies verbatim into the PUBLISHED
222
- `dist/component/_generated/component.d.ts`, i.e. the exact type a
223
- consuming app checks `components.agentEngine.*` calls against. Diffs
224
- each byte-for-byte (`git diff --exit-code`). Catches everything,
225
- including a single hand-edited character.
226
-
227
- **`component/_generated/` was NOT covered before 0.1.0-alpha.5** — that
228
- gap is why `0.1.0-alpha.4` shipped a `component.d.ts` stale against the
229
- real component schema (missing `orgId`/`createdBy` on
230
- sessions/agents/durableJob/toolApprovals args), which broke
231
- `tsc --noEmit` for any consumer calling `sessions.start`/`durableJob.start`
232
- with `orgId` (Hestia, VP k17ea6x3tht8xa3rxm139t23gx8ajkf3, defaut 1).
233
- The visibility (`"internal"`) on every entry is, by measured comparison
234
- against `@convex-dev/rate-limiter`'s own published `component.d.ts`,
235
- CORRECT and unrelated to that defect — every official Convex component
236
- types its cross-component `ComponentApi` as `"internal"` regardless of
237
- whether the underlying function is declared `mutation`/`query` or
238
- `internalMutation`/`internalQuery`; `ctx.runMutation`'s own signature
239
- (`convex/server`) accepts `FunctionReference<"mutation", "public" |
240
- "internal">` either way. The root cause was staleness of the args
241
- shape, not visibility.
242
-
243
- **Three-state exit contract** (never two -- a two-state gate cannot
244
- distinguish "I measured, it's clean" from "I could not measure at
245
- all", and collapsing those is either a silent fail-open or a red that
246
- proves nothing; see flotte rule `three-state-verdict.md`):
247
-
248
- | Exit | Meaning | When |
249
- | ---- | ------- | ---- |
250
- | `0` | **PASS** | A real measurement was taken (`npx convex codegen` ran) and `convex/_generated/` is byte-identical to its output. |
251
- | `1` | **VIOLATION** | A real measurement was taken and it found >=1 byte of divergence -- hand-edited, stale, or generated against a different schema. |
252
- | `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. |
253
-
254
- ### CI cannot run the content gate -- explicit limit, not a rounding error
255
-
256
- `npx convex codegen` has no offline mode: it always needs
257
- `CONVEX_DEPLOYMENT` / a deploy key against a live deployment. This repo
258
- refuses to store that as a CI secret (plan B, PR #1). **Consequence,
259
- stated plainly: CI cannot detect a hand-edited `convex/_generated/`
260
- content change.** CI only runs the structural gate. The content gate is
261
- the only thing that catches a content-level tamper, and it can only run
262
- where the secret lives: **locally, pre-commit**, via `.env.local`.
263
-
264
- To install the pre-commit gate once per clone:
265
-
266
- ```
267
- git config core.hooksPath scripts/git-hooks
3
+ A reusable [Convex component](https://www.convex.dev/components) that provides
4
+ a durable **agent engine**: sessions, delegation, tool approvals, scheduled
5
+ "habits", bounded spending "mandates", and long-task survival — all built on
6
+ top of [`@convex-dev/workflow`](https://www.npmjs.com/package/@convex-dev/workflow)
7
+ and [`@convex-dev/workpool`](https://www.npmjs.com/package/@convex-dev/workpool).
8
+
9
+ **An agent is a data aggregate, not compiled code.** Creating an agent means
10
+ writing a row (plus child rows for skills, tool approvals, connections,
11
+ sub-agents, habits, sandbox config) — there is no build step and no
12
+ redeploy. This mirrors the authoring model pioneered by
13
+ [vercel/eve](https://github.com/vercel/eve), whose slot names, contracts and
14
+ vocabulary this package tracks closely; credit and thanks to that project for
15
+ the design this component is built in parity with.
16
+
17
+ ## Why
18
+
19
+ Most agent frameworks compile an agent definition into code that has to be
20
+ deployed to change. This engine keeps the entire agent definition as
21
+ database rows: skills, tool approvals, delegated sub-agents, sandbox
22
+ settings, and scheduled work are all mutable at runtime through ordinary
23
+ Convex mutations. Combined with `@convex-dev/workflow`'s durable execution,
24
+ a single agent session — or a long-running background job — survives
25
+ process restarts, Convex's per-action time limits, and redeploys of the
26
+ consuming app.
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ npm install @vantageos/agent-engine
268
32
  ```
269
33
 
270
- This points git at the versioned hook `scripts/git-hooks/pre-commit`
271
- (plain git hook, not a Claude hook -- Claude hooks under `.claude/hooks/`
272
- are Pi-owned only). The hook no-ops unless a staged file is under
273
- `convex/`, and then runs `npm run check:generated:content`, reading its
274
- exit code explicitly rather than treating it as a boolean:
275
-
276
- - `0` (PASS) -- commit proceeds.
277
- - `1` (VIOLATION) -- commit is **blocked**, message names the divergence.
278
- - `2` (REFUSAL TO JUDGE, e.g. `.env.local` deployment secret missing or
279
- invalid) -- commit is **also blocked**, with a distinct message that
280
- says explicitly this is *unverified*, not a proven violation. This is a
281
- deliberate fail-closed choice: an unverified change to `convex/_generated/`
282
- content must never slip through silently just because the local
283
- instrument (a live Convex deployment) was unavailable. The trade-off is
284
- that a developer without a working `.env.local` cannot commit to
285
- `convex/**` at all until they fix their local deployment credentials --
286
- judged the safer failure mode than a fail-open that would let a hand-
287
- edited, un-regenerated `convex/_generated/` through whenever the secret
288
- happens to be missing.
289
-
290
- **If `core.hooksPath` is never configured, this protection does not
291
- exist on that clone.** Nothing currently makes that configuration
292
- mandatory or automatic -- that decision (e.g. via a repo bootstrap script
293
- that all contributors run) is Pi's to make, not this PR's.
294
-
295
- ## Tests (TDD, RED then GREEN)
296
-
297
- - `convex/agents.test.ts` — org isolation (family 1)
298
- - `convex/engineResolvers.test.ts` — dynamic resolver, no-redeploy (family 2)
299
- - `convex/engineAwaitEventTimeout.test.ts` — awaitEvent timeout wakes the
300
- workflow when the real event never arrives (family 3)
301
- - `convex/engineDurableJob.test.ts` — I1 long-task survival contract
302
- (family 4): a 25-step job survives to `completed` via the component; a
303
- replayed already-completed step is a proven no-op (resume after forced
304
- interruption); a step that always fails is retried and the job is
305
- `abandoned` (never touching the next step); a step that fails once then
306
- succeeds still reaches `completed`; cross-org `getStatus` is denied.
307
- - `convex/mandates.test.ts` — mandates, three-pole probe: MUST_PASS (spend
308
- under cap succeeds, rollup counts it), MUST_BLOCK (spend at cap is
309
- refused, `approvalRequests` row created), MUST_REFUSE (no mandate
310
- configured => fails-closed, names the missing scope, no approval row
311
- created) + org isolation + human `resolveApproval`.
312
- - `convex/habits.test.ts` — habits, three-pole probe: MUST_PASS (a habit
313
- created at runtime fires its first run with zero deploy), MUST_BLOCK (a
314
- disabled habit does not fire again after further clock advances),
315
- MUST_REFUSE (`enable` on an already-enabled habit is a documented
316
- no-op, not a silent double-registration) + full runtime lifecycle
317
- (create / updateSchedule / disable / enable, no redeploy at any step).
318
- - `component/durableJob.componentPackaging.test.ts` — the SAME contract
319
- (long job survives, abandon-after-N, org isolation, cancel), but run
320
- against `component/_generated/api` — the exact source tree compiled to
321
- `dist/component/` and published — proving the migrated, packaged code
322
- itself, not only the repo-root dev app.
323
-
324
- Run: `npx vitest run` (or `npm test`).
325
-
326
- ## Packaging & publication (T2-TER, functions since T2-QUATER)
327
-
328
- `@vantageos/agent-engine` is published to npm under the `alpha` dist-tag
329
- (`npm view @vantageos/agent-engine version` / `dist-tags`, read from the
330
- registry after publish, never from `npm publish`'s own exit status). The
331
- package exposes a resolvable `"./convex.config"` subpath (types + JS,
332
- built by `npm run build` from `component/convex.config.ts` via
333
- `tsc -p tsconfig.build.json` into `dist/component/`), so a consumer's
334
- `convex/convex.config.ts` can do:
34
+ ## Quickstart
35
+
36
+ Register the component in your app's `convex/convex.config.ts`:
335
37
 
336
38
  ```ts
337
39
  import { defineApp } from "convex/server";
@@ -342,72 +44,47 @@ app.use(agentEngine);
342
44
  export default app;
343
45
  ```
344
46
 
345
- ### Trust model — READ THIS FIRST (CORRECTIF, VP k1741evjzm72rj91tkcbdhpskx8ah35a)
346
-
347
- **Convex does not propagate the calling app's `ctx.auth` identity across a
348
- component boundary.** This is a platform isolation guarantee, not a choice
349
- this package makes (`convex-test` states it verbatim: "Auth doesn't
350
- propagate across component boundaries"). A function inside this component
351
- therefore can NEVER read `ctx.auth.getUserIdentity()` and expect it to
352
- reflect the consumer app's end user.
353
-
354
- **`orgId` is an explicit ARGUMENT on every public function in this
355
- component.** The calling app is responsible for authenticating its user and
356
- resolving that user's `orgId` on its OWN side (e.g. reading a Clerk
357
- org-scoped claim from its own `ctx.auth`) BEFORE calling into this
358
- component — this component cannot do that step for you, it is a platform
359
- constraint, not a design preference.
360
-
361
- What did NOT change: the component still denies by default
362
- (`lib/auth.ts:requireOrgId` throws on a missing/empty `orgId`), `orgId` is
363
- still the first field of every index, and every document fetched by `_id`
364
- is still checked with `lib/auth.ts:assertSameOrg` before being returned or
365
- mutated — a caller-supplied `orgId` can never read or write another org's
366
- rows. The isolation boundary moved from "component reads `ctx.auth`"
367
- (impossible across a component boundary) to "component enforces the
368
- `orgId` argument the consumer already vouches for"; deny-by-default,
369
- orgId-first-index, no-cross-org-reads are unchanged.
370
-
371
- ### What's callable, and what isn't (read this before wiring a consumer)
372
-
373
- `component/` (sibling of `convex/`, NOT nested under it — see "Why
374
- `component/` and not `convex/component/`" below) hosts the **actual,
375
- compiled, published functions** for a real slice of the engine, not just
376
- the component's identity:
377
-
378
- - `component/schema.ts` — `durableJobs`, `agents`, `toolApprovals`,
379
- `sessions`, `sessionEvents` (+ a test-only `callerStepInvocations`
380
- fixture table).
381
- - `component/engine/durableJob.ts` — `start` / `getStatus` / `cancel`
382
- (public contract) + `durableJobWorkflowV1` (the versioned pause/resume/
383
- replay workflow) + its internal step-runner mutations.
384
- - `component/engine/agents.ts` — `create` / `get` (minimal root-sheet
385
- bootstrap; no `update`/`list`/skills/connections in this pass).
386
- - `component/engine/resolvers.ts` — `resolveManifest` (dynamic reread at
387
- session/turn boundaries; returns `agentId`/`name`/`instructions`/`model`/
388
- `toolApprovals` — `skills`/`connections` NOT included in this pass).
389
- - `component/engine/sessions.ts` — the conversation thread contract:
390
- `start` / `sendTurn` / `appendReasoning` / `requestToolCall` /
391
- `completeToolCall` / `completeTurn` / `listEvents`.
392
- - `component/engine/toolApprovals.ts` — `respond`.
393
- - `component/lib/auth.ts` — `requireOrgId` / `assertSameOrg`: see "Trust
394
- model" above.
395
-
396
- **Callable through the published component today** — every function below
397
- takes `orgId: v.string()` as an explicit argument (see the source files
398
- above for the exact `v.*` shapes of the remaining args/returns):
47
+ Then call into it from your own Convex functions. Every public function
48
+ takes an explicit `orgId: string` argument — Convex does not propagate the
49
+ calling app's `ctx.auth` identity across a component boundary, so your app
50
+ is responsible for resolving the caller's org before calling in:
399
51
 
52
+ ```ts
53
+ import { components } from "./_generated/api";
54
+
55
+ export const createAgent = mutation({
56
+ args: { orgId: v.string(), name: v.string(), instructions: v.string() },
57
+ handler: async (ctx, args) => {
58
+ return ctx.runMutation(components.agentEngine.engine.agents.create, {
59
+ orgId: args.orgId,
60
+ createdBy: "user_123",
61
+ name: args.name,
62
+ instructions: args.instructions,
63
+ model: "gpt-4o",
64
+ });
65
+ },
66
+ });
400
67
  ```
401
- components.agentEngine.engine.durableJob.start({ orgId, kind, stepFunctionHandle, totalSteps, maxAttemptsPerStep }) -> Id<"durableJobs">
402
- components.agentEngine.engine.durableJob.getStatus({ orgId, jobId }) -> { status, cursor, totalSteps, lastError?, abandonedAtStep? }
403
- components.agentEngine.engine.durableJob.cancel({ orgId, jobId }) -> null
404
68
 
405
- components.agentEngine.engine.agents.create({ orgId, createdBy, name, instructions, model }) -> Id<"agents">
69
+ ## What's inside
70
+
71
+ The component's data model (root table `agents`, plus child tables for
72
+ skills, sub-agents, connections, habits, sandbox config, and per-tool
73
+ approvals) is deployed under the Convex component boundary your app
74
+ registers — it does not touch your own app's tables, and every index is
75
+ scoped by `orgId` first, deny-by-default.
76
+
77
+ ### API surface (component/, current release)
78
+
79
+ ```
80
+ components.agentEngine.engine.agents.create({ orgId, createdBy, name, description?, instructions, model, reasoningEffort?, modelProvider?, maxSubagents?, maxInputTokensPerSession? }) -> Id<"agents">
406
81
  components.agentEngine.engine.agents.get({ orgId, agentId }) -> agent | null
407
82
 
83
+ components.agentEngine.engine.directory.listByOrg({ orgId }) -> { _id, name, status, model, createdAt }[]
84
+
408
85
  components.agentEngine.engine.resolvers.resolveManifest({ orgId, sessionId }) -> { agentId, name, instructions, model, toolApprovals[] }
409
86
 
410
- components.agentEngine.engine.sessions.start({ orgId, agentId, createdBy }) -> Id<"sessions">
87
+ components.agentEngine.engine.sessions.start({ orgId, agentId, createdBy }) -> { sessionId: Id<"sessions">, workflowId: string }
411
88
  components.agentEngine.engine.sessions.sendTurn({ orgId, sessionId, input }) -> null
412
89
  components.agentEngine.engine.sessions.appendReasoning({ orgId, sessionId, chunk }) -> null
413
90
  components.agentEngine.engine.sessions.requestToolCall({ orgId, sessionId, toolName, argsJson }) -> { authorized, approvalPending, denied }
@@ -416,89 +93,93 @@ components.agentEngine.engine.sessions.completeTurn({ orgId, sessionId }) -> nul
416
93
  components.agentEngine.engine.sessions.listEvents({ orgId, sessionId }) -> sessionEvent[]
417
94
 
418
95
  components.agentEngine.engine.toolApprovals.respond({ orgId, sessionId, toolName, decision: "approved"|"denied", updatePolicy?: "always"|"once"|"never" }) -> null
96
+
97
+ components.agentEngine.engine.wakeup.enqueue({ orgId, agentId, mandateScope, cost, reason, workFunctionHandle, workflowId, awaitedEventName }) -> Id<"wakeableWork"> (action)
98
+ components.agentEngine.engine.wakeup.cancel({ orgId, wakeableWorkId }) -> null
99
+ components.agentEngine.engine.wakeup.getStatus({ orgId, wakeableWorkId }) -> { status, resultSummary?, ... }
100
+
101
+ components.agentEngine.engine.durableJob.start({ orgId, kind, stepFunctionHandle, totalSteps, maxAttemptsPerStep }) -> Id<"durableJobs">
102
+ components.agentEngine.engine.durableJob.getStatus({ orgId, jobId }) -> { status, cursor, totalSteps, lastError?, abandonedAtStep? }
103
+ components.agentEngine.engine.durableJob.cancel({ orgId, jobId }) -> null
104
+
105
+ components.agentEngine.engine.mandates.set({ orgId, agentId, scope, kind, rate, period, capacity? }) -> Id<"mandates">
106
+ components.agentEngine.engine.mandates.get({ orgId, agentId, scope }) -> mandate | null
107
+ components.agentEngine.engine.mandates.spend({ orgId, agentId, scope, cost, reason }) -> { ok, approvalId? }
108
+ components.agentEngine.engine.mandates.listApprovals({ orgId, agentId }) -> approvalRequest[]
109
+ components.agentEngine.engine.mandates.resolveApproval({ orgId, approvalId, resolvedBy, status: "approved"|"denied" }) -> null
110
+ components.agentEngine.engine.mandates.usageCount({ orgId, agentId, scope }) -> number
111
+
112
+ components.agentEngine.engine.habits.create({ orgId, agentId, name, cronExpression, prompt }) -> Id<"habits">
113
+ components.agentEngine.engine.habits.get({ orgId, habitId }) -> habit | null
114
+ components.agentEngine.engine.habits.listForAgent({ orgId, agentId }) -> habit[]
115
+ components.agentEngine.engine.habits.updateSchedule({ orgId, habitId, cronExpression }) -> null
116
+ components.agentEngine.engine.habits.disable({ orgId, habitId }) -> null
117
+ components.agentEngine.engine.habits.enable({ orgId, habitId }) -> null
118
+ components.agentEngine.engine.habits.listRuns({ orgId, habitId }) -> habitRun[]
119
+
120
+ components.agentEngine.engine.sandboxCallback.startRun({ orgId, workflowId, awaitedEventName, timeoutMs }) -> Id<"sandboxRuns">
121
+ components.agentEngine.engine.sandboxCallback.attachSandboxId({ orgId, sandboxRunId, sandboxId }) -> null
122
+ components.agentEngine.engine.sandboxCallback.getStatus({ orgId, sandboxRunId }) -> { status, resultSummary?, ... }
123
+ components.agentEngine.engine.sandboxCallback.processCallback({ orgId, sandboxRunId, deliveryId, status, resultSummary? }) -> { outcome: "woke"|"duplicate" }
124
+ components.agentEngine.engine.sandboxCallback.deadLetterMalformed({ orgId?, deliveryId, reason, rawBody }) -> null
125
+
126
+ components.agentEngine.engine.delegation.declareSubagent({ orgId, parentAgentId, childAgentId, toolName, description? }) -> Id<"subagents">
127
+ components.agentEngine.engine.delegation.listSubagents({ orgId, parentAgentId }) -> subagent[]
128
+ components.agentEngine.engine.delegation.delegateToSubagent({ orgId, subagentId, ... }) -> null (action)
129
+ components.agentEngine.engine.delegation.invokeAgentClone({ orgId, agentId, ... }) -> null (action; root-only, non-recursive by construction)
130
+
131
+ components.agentEngine.engine.vpConnection.get({ orgId, agentId }) -> vpConnection | null
132
+ components.agentEngine.engine.vpConnection.getToggles({ orgId, agentId }) -> { memoryEnabled, tasksEnabled, messagesEnabled, notesEnabled }
133
+ components.agentEngine.engine.vpConnection.setToggles({ orgId, agentId, memoryEnabled?, tasksEnabled?, messagesEnabled?, notesEnabled? }) -> Id<"vpConnections">
134
+ components.agentEngine.engine.vpConnection.invokeVpTool({ orgId, agentId, tool, vpFunctionHandle, argsJson }) -> null (action)
419
135
  ```
420
136
 
421
- ### `toolApprovals.respond` — `decision` vs `updatePolicy` (0.1.0-alpha.5)
422
-
423
- Two DISTINCT axes were conflated pre-alpha.5 (VP
424
- k17ea6x3tht8xa3rxm139t23gx8ajkf3, defaut 2) — a single `approval:
425
- "always"|"once"|"never"` argument was overloaded to mean both "the standing
426
- policy for this tool" AND "the answer to this one pending request", and
427
- there was no literal for "refuse this turn only". Fixed by splitting them:
428
-
429
- - **`decision`** (required) — the response to THIS ONE pending
430
- `tool.approval.requested` event, for THIS turn only. `"approved"` |
431
- `"denied"`. **Eve parity, direct** — vocabulary matches Eve's
432
- `ApprovalStatus` (`packages/eve/src/public/definitions/approval.ts`:
433
- `"approved" | "denied" | "not-applicable" | "user-approval" | ...`); we
434
- did not invent a `deny` literal, we aligned with the one Eve already has.
435
- `"denied"` is a terminal refusal for the turn: `sessions.requestToolCall`
436
- reports back `{ authorized: false, approvalPending: false, denied: true
437
- }` (never re-authorized on retry within the session), and a dedicated
438
- `turn.toolCall.denied` sessionEvent is appended — there is NO
439
- `turn.toolCall.completed` for a denied call, i.e. the tool was never run
440
- and the turn is NOT silently treated as done.
441
- - **`updatePolicy`** (optional) — persists a NEW standing gate
442
- (`"always"|"once"|"never"`, eve parity: `eve/tools/approval` helpers) for
443
- this `(agentId, toolName)` pair going forward, independent of `decision`.
444
- A human can deny this turn's call and leave the policy alone (ask again
445
- next time), or approve this turn and simultaneously flip the policy to
446
- `"always"`/`"never"` so future calls stop pausing. `"never"` still means
447
- Eve's "never require approval" (auto-authorize) — that policy literal is
448
- unchanged and is NOT the same thing as a `"denied"` decision.
449
-
450
- `createdBy` (on `agents.create` / `sessions.start`) is, for the same
451
- platform reason as `orgId`, no longer read from `ctx.auth.getUserIdentity()`
452
- — the consumer passes the acting user's own identifier.
453
-
454
- Verified two ways: (1) `component/durableJob.componentPackaging.test.ts` +
455
- `component/sessions.componentPackaging.test.ts` run `convex-test` directly
456
- against `component/_generated/api` (the exact source tree
457
- `tsc -p tsconfig.build.json` compiles into `dist/component/`, which is
458
- what `npm pack` ships), so the compiled artifact has a real green test
459
- suite behind it, not just the repo-root dev app's; (2) a from-scratch
460
- consumer probe (install the published tarball into a bare `npx convex dev`
461
- project, register the component, call `durableJob.start` + `sessions.start`
462
- from the consumer's own function, passing `orgId` explicitly) — see the
463
- completion report for the verbatim transcript.
464
-
465
- **NOT yet callable through the published component** (still only exists in
466
- this repo's OWN dev/test deployment under `convex/schema.ts` +
467
- `convex/agents.ts` + `convex/skills.ts`, used by `npx convex dev` and the
468
- repo-root `convex-test` suite): `skills`, `connections`, `subagents`,
469
- `habits`, `sandboxConfig`, `modelKeys`, `mandates`, `approvalRequests`,
470
- `awaitEventTimeouts`, `wakeableWork`, `engine/wakeup.ts`,
471
- `engine/workflows.ts` (`sessionWorkflowV1` — this pass's `sessions` is
472
- deliberately NOT tied to a `@convex-dev/workflow` run; see
473
- `component/schema.ts`'s `sessions` table comment for why). Porting each of
474
- those under `component/` without a working component-level `convex-test`
475
- harness behind it would ship unverified code — Day 128 forbids that; it is
476
- deliberately left for the next, separately-scoped migration.
477
-
478
- ### Why `component/` and not `convex/component/`
479
-
480
- `npx convex codegen --component-dir <path>` builds a synthetic root app
481
- that does `import component from "<relative-import-to-path>"`. When
482
- `<path>` is nested INSIDE the functions directory it resolves from
483
- (`convex/`), that relative import comes out as a bare specifier with no
484
- leading `./` or `../` (e.g. `"component/convex.config.js"`), which esbuild
485
- correctly refuses to resolve as a relative path — not a bug in our code,
486
- a corner case of running `--component-dir` against a directory nested
487
- under the default functions directory. Moving the component's source tree
488
- one level up, to `component/` (a sibling of `convex/`, mirroring how
489
- `@convex-dev/workflow` itself keeps its component source under `src/component`,
490
- outside its `example/convex` app directory) makes the generated relative
491
- import start with `../`, which resolves — measured empirically against
492
- this exact repo layout, not guessed.
493
-
494
- ## Status
495
-
496
- T1 (schema + durable session engine, PR #1) merged @
497
- `9f3401ba63e6e663af95e9e4f5da42bc30b3550b`. I1 (long-task survival
498
- contract, `engine/durableJob.ts`) — not merged, Eta reviews. T4 (mandates
499
- + habits) — **not merged** — Eta reviews, merge on Pi token. T2-TER (npm
500
- packaging + component identity + publication) and T2-QUATER (this pass:
501
- migrated `durableJob` + its schema + `lib/auth.ts` under the actual
502
- component boundary, `component/`, with its own `_generated/` and its own
503
- green `convex-test` suite) — see "Packaging & publication" above; **not
504
- merged**.
137
+ ### Concepts
138
+
139
+ - **Sessions** — a durable conversation thread, driven by a versioned
140
+ `@convex-dev/workflow` run. Turns, streamed reasoning, tool-call
141
+ lifecycle and approval lifecycle are appended as an event log a client
142
+ replays on reconnect.
143
+ - **Delegation** — an agent can declare typed sub-agents (strict isolation:
144
+ a sub-agent's own skills/connections/approvals are never copied onto the
145
+ parent) or invoke a non-recursive clone of itself as a built-in tool.
146
+ Both mechanisms reuse the same pause/wake primitive.
147
+ - **Mandates** — a bounded, weighted spending cap per agent/org/scope.
148
+ Spending under the cap succeeds; an overrun creates a pending approval
149
+ request rather than silently failing open or throwing away the audit
150
+ trail.
151
+ - **Habits** — a per-agent, runtime-editable cron schedule (create, update,
152
+ disable, enable — all with zero redeploy).
153
+ - **Long-task survival** — `durableJob` lets a consumer's task survive past
154
+ a single Convex action's execution-time ceiling by expressing it as N
155
+ independently-bounded, resumable steps.
156
+
157
+ ### Trust model
158
+
159
+ Convex does not propagate the calling app's `ctx.auth` identity across a
160
+ component boundary. Every public function in this component takes `orgId`
161
+ (and, where relevant, `createdBy`) as an explicit argument — your app is
162
+ responsible for authenticating its user and resolving `orgId` before
163
+ calling in. Every document lookup by id is checked against the caller's own
164
+ `orgId` before being returned or mutated; a cross-org read or write is
165
+ refused the same way a not-found row would be.
166
+
167
+ ## Requirements
168
+
169
+ - Convex `^1.42`
170
+ - A Convex deployment with `@convex-dev/workflow`, `@convex-dev/workpool`,
171
+ `@convex-dev/agent`, `@convex-dev/rate-limiter`, `@convex-dev/crons`, and
172
+ `@convex-dev/sharded-counter` resolvable (all installed automatically as
173
+ dependencies of this package).
174
+
175
+ ## License
176
+
177
+ [FSL-1.1-Apache-2.0](./LICENSE) — free for production use; converts to
178
+ Apache 2.0 after two years, per version.
179
+
180
+ ## Credit
181
+
182
+ Slot names, contracts and authoring vocabulary are deliberately kept in
183
+ parity with [vercel/eve](https://github.com/vercel/eve). This package is
184
+ not affiliated with Vercel; parity is a design choice, not a claim of
185
+ origin.