@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.
- package/LICENSE +110 -0
- package/README.md +154 -473
- package/dist/component/_generated/api.d.ts +31 -0
- package/dist/component/_generated/component.d.ts +346 -2
- package/dist/component/convex.config.js +18 -0
- package/dist/component/engine/agents.d.ts +10 -0
- package/dist/component/engine/agents.js +21 -0
- package/dist/component/engine/awaitEventTimeout.d.ts +13 -0
- package/dist/component/engine/awaitEventTimeout.js +81 -0
- package/dist/component/engine/delegation.d.ts +75 -0
- package/dist/component/engine/delegation.js +327 -0
- package/dist/component/engine/directory.d.ts +9 -0
- package/dist/component/engine/directory.js +45 -0
- package/dist/component/engine/durableJob.js +6 -6
- package/dist/component/engine/habits.d.ts +70 -0
- package/dist/component/engine/habits.js +218 -0
- package/dist/component/engine/mandates.d.ts +74 -0
- package/dist/component/engine/mandates.js +271 -0
- package/dist/component/engine/sandboxCallback.d.ts +36 -0
- package/dist/component/engine/sandboxCallback.js +259 -0
- package/dist/component/engine/sessions.d.ts +6 -2
- package/dist/component/engine/sessions.js +43 -7
- package/dist/component/engine/toolApprovals.js +1 -1
- package/dist/component/engine/vpConnection.d.ts +69 -0
- package/dist/component/engine/vpConnection.js +218 -0
- package/dist/component/engine/wakeup.d.ts +33 -0
- package/dist/component/engine/wakeup.js +228 -0
- package/dist/component/engine/workflows.d.ts +11 -0
- package/dist/component/engine/workflows.js +84 -0
- package/dist/component/schema.d.ts +365 -4
- package/dist/component/schema.js +345 -18
- package/dist/component/testFixtures/callerSteps.js +1 -1
- package/dist/component/testFixtures/delegationDemoWorkflow.d.ts +18 -0
- package/dist/component/testFixtures/delegationDemoWorkflow.js +75 -0
- package/dist/component/testFixtures/sandboxDemoWorkflow.d.ts +19 -0
- package/dist/component/testFixtures/sandboxDemoWorkflow.js +69 -0
- package/dist/component/testFixtures/vpTool.d.ts +4 -0
- package/dist/component/testFixtures/vpTool.js +25 -0
- package/dist/component/testFixtures/wakeupDemoWorkflow.d.ts +19 -0
- package/dist/component/testFixtures/wakeupDemoWorkflow.js +77 -0
- package/dist/component/testFixtures/wakeupWork.d.ts +8 -0
- package/dist/component/testFixtures/wakeupWork.js +21 -0
- package/package.json +17 -5
package/README.md
CHANGED
|
@@ -1,337 +1,39 @@
|
|
|
1
1
|
# @vantageos/agent-engine
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
| `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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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.
|