@vantageos/agent-engine 0.1.0-alpha.2 → 0.1.0-alpha.4
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 +179 -37
- package/dist/component/_generated/api.d.ts +8 -0
- package/dist/component/_generated/component.d.ts +80 -0
- package/dist/component/engine/agents.d.ts +22 -0
- package/dist/component/engine/agents.js +69 -0
- package/dist/component/engine/durableJob.d.ts +3 -0
- package/dist/component/engine/durableJob.js +9 -8
- package/dist/component/engine/resolvers.d.ts +13 -0
- package/dist/component/engine/resolvers.js +50 -0
- package/dist/component/engine/sessions.d.ts +44 -0
- package/dist/component/engine/sessions.js +239 -0
- package/dist/component/engine/toolApprovals.d.ts +6 -0
- package/dist/component/engine/toolApprovals.js +52 -0
- package/dist/component/lib/auth.d.ts +1 -2
- package/dist/component/lib/auth.js +28 -15
- package/dist/component/schema.d.ts +75 -0
- package/dist/component/schema.js +85 -11
- package/dist/component/testFixtures/callerSteps.js +1 -1
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -39,7 +39,8 @@ ligne"** below, in this table and in the code comments next to the field.
|
|
|
39
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
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
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` child
|
|
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 |
|
|
43
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 |
|
|
44
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" |
|
|
45
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) |
|
|
@@ -53,10 +54,11 @@ Child tables (>= 7, every index starts with `orgId`):
|
|
|
53
54
|
1. `skills` — contextual procedures
|
|
54
55
|
2. `subagents` — recursive declared delegation, strict isolation
|
|
55
56
|
3. `connections` — MCP/OpenAPI (VantagePeers tools included)
|
|
56
|
-
4. `habits` — dynamic crons (habits, notre ligne
|
|
57
|
+
4. `habits` (+ `habitRuns`) — dynamic crons (habits, notre ligne, T4)
|
|
57
58
|
5. `sandboxConfig` — Vercel Sandbox settings
|
|
58
59
|
6. `toolApprovals` — `always` / `once` / `never` per tool
|
|
59
60
|
7. `modelKeys` — BYOK, server-only, split from `agents`
|
|
61
|
+
8. `mandates` (+ `approvalRequests`) — bounded authority (notre ligne, T4)
|
|
60
62
|
|
|
61
63
|
Engine tables: `sessions`, `sessionEvents`, `awaitEventTimeouts`.
|
|
62
64
|
|
|
@@ -126,6 +128,72 @@ reads without needing to know our internals):
|
|
|
126
128
|
job is marked `abandoned` with `abandonedAtStep` + `lastError`, and no
|
|
127
129
|
further step runs.
|
|
128
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
|
+
|
|
129
197
|
## `convex/_generated/` freshness -- two gates, not one
|
|
130
198
|
|
|
131
199
|
`convex/_generated/api.d.ts` carries an "AUTOMATICALLY GENERATED" header
|
|
@@ -215,6 +283,17 @@ that all contributors run) is Pi's to make, not this PR's.
|
|
|
215
283
|
interruption); a step that always fails is retried and the job is
|
|
216
284
|
`abandoned` (never touching the next step); a step that fails once then
|
|
217
285
|
succeeds still reaches `completed`; cross-org `getStatus` is denied.
|
|
286
|
+
- `convex/mandates.test.ts` — mandates, three-pole probe: MUST_PASS (spend
|
|
287
|
+
under cap succeeds, rollup counts it), MUST_BLOCK (spend at cap is
|
|
288
|
+
refused, `approvalRequests` row created), MUST_REFUSE (no mandate
|
|
289
|
+
configured => fails-closed, names the missing scope, no approval row
|
|
290
|
+
created) + org isolation + human `resolveApproval`.
|
|
291
|
+
- `convex/habits.test.ts` — habits, three-pole probe: MUST_PASS (a habit
|
|
292
|
+
created at runtime fires its first run with zero deploy), MUST_BLOCK (a
|
|
293
|
+
disabled habit does not fire again after further clock advances),
|
|
294
|
+
MUST_REFUSE (`enable` on an already-enabled habit is a documented
|
|
295
|
+
no-op, not a silent double-registration) + full runtime lifecycle
|
|
296
|
+
(create / updateSchedule / disable / enable, no redeploy at any step).
|
|
218
297
|
- `component/durableJob.componentPackaging.test.ts` — the SAME contract
|
|
219
298
|
(long job survives, abandon-after-N, org isolation, cancel), but run
|
|
220
299
|
against `component/_generated/api` — the exact source tree compiled to
|
|
@@ -242,47 +321,109 @@ app.use(agentEngine);
|
|
|
242
321
|
export default app;
|
|
243
322
|
```
|
|
244
323
|
|
|
324
|
+
### Trust model — READ THIS FIRST (CORRECTIF, VP k1741evjzm72rj91tkcbdhpskx8ah35a)
|
|
325
|
+
|
|
326
|
+
**Convex does not propagate the calling app's `ctx.auth` identity across a
|
|
327
|
+
component boundary.** This is a platform isolation guarantee, not a choice
|
|
328
|
+
this package makes (`convex-test` states it verbatim: "Auth doesn't
|
|
329
|
+
propagate across component boundaries"). A function inside this component
|
|
330
|
+
therefore can NEVER read `ctx.auth.getUserIdentity()` and expect it to
|
|
331
|
+
reflect the consumer app's end user.
|
|
332
|
+
|
|
333
|
+
**`orgId` is an explicit ARGUMENT on every public function in this
|
|
334
|
+
component.** The calling app is responsible for authenticating its user and
|
|
335
|
+
resolving that user's `orgId` on its OWN side (e.g. reading a Clerk
|
|
336
|
+
org-scoped claim from its own `ctx.auth`) BEFORE calling into this
|
|
337
|
+
component — this component cannot do that step for you, it is a platform
|
|
338
|
+
constraint, not a design preference.
|
|
339
|
+
|
|
340
|
+
What did NOT change: the component still denies by default
|
|
341
|
+
(`lib/auth.ts:requireOrgId` throws on a missing/empty `orgId`), `orgId` is
|
|
342
|
+
still the first field of every index, and every document fetched by `_id`
|
|
343
|
+
is still checked with `lib/auth.ts:assertSameOrg` before being returned or
|
|
344
|
+
mutated — a caller-supplied `orgId` can never read or write another org's
|
|
345
|
+
rows. The isolation boundary moved from "component reads `ctx.auth`"
|
|
346
|
+
(impossible across a component boundary) to "component enforces the
|
|
347
|
+
`orgId` argument the consumer already vouches for"; deny-by-default,
|
|
348
|
+
orgId-first-index, no-cross-org-reads are unchanged.
|
|
349
|
+
|
|
245
350
|
### What's callable, and what isn't (read this before wiring a consumer)
|
|
246
351
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
352
|
+
`component/` (sibling of `convex/`, NOT nested under it — see "Why
|
|
353
|
+
`component/` and not `convex/component/`" below) hosts the **actual,
|
|
354
|
+
compiled, published functions** for a real slice of the engine, not just
|
|
355
|
+
the component's identity:
|
|
251
356
|
|
|
252
|
-
- `component/schema.ts` —
|
|
253
|
-
`
|
|
357
|
+
- `component/schema.ts` — `durableJobs`, `agents`, `toolApprovals`,
|
|
358
|
+
`sessions`, `sessionEvents` (+ a test-only `callerStepInvocations`
|
|
359
|
+
fixture table).
|
|
254
360
|
- `component/engine/durableJob.ts` — `start` / `getStatus` / `cancel`
|
|
255
361
|
(public contract) + `durableJobWorkflowV1` (the versioned pause/resume/
|
|
256
362
|
replay workflow) + its internal step-runner mutations.
|
|
257
|
-
- `component/
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
`
|
|
263
|
-
|
|
264
|
-
`
|
|
265
|
-
|
|
363
|
+
- `component/engine/agents.ts` — `create` / `get` (minimal root-sheet
|
|
364
|
+
bootstrap; no `update`/`list`/skills/connections in this pass).
|
|
365
|
+
- `component/engine/resolvers.ts` — `resolveManifest` (dynamic reread at
|
|
366
|
+
session/turn boundaries; returns `agentId`/`name`/`instructions`/`model`/
|
|
367
|
+
`toolApprovals` — `skills`/`connections` NOT included in this pass).
|
|
368
|
+
- `component/engine/sessions.ts` — the conversation thread contract:
|
|
369
|
+
`start` / `sendTurn` / `appendReasoning` / `requestToolCall` /
|
|
370
|
+
`completeToolCall` / `completeTurn` / `listEvents`.
|
|
371
|
+
- `component/engine/toolApprovals.ts` — `respond`.
|
|
372
|
+
- `component/lib/auth.ts` — `requireOrgId` / `assertSameOrg`: see "Trust
|
|
373
|
+
model" above.
|
|
374
|
+
|
|
375
|
+
**Callable through the published component today** — every function below
|
|
376
|
+
takes `orgId: v.string()` as an explicit argument (see the source files
|
|
377
|
+
above for the exact `v.*` shapes of the remaining args/returns):
|
|
378
|
+
|
|
379
|
+
```
|
|
380
|
+
components.agentEngine.engine.durableJob.start({ orgId, kind, stepFunctionHandle, totalSteps, maxAttemptsPerStep }) -> Id<"durableJobs">
|
|
381
|
+
components.agentEngine.engine.durableJob.getStatus({ orgId, jobId }) -> { status, cursor, totalSteps, lastError?, abandonedAtStep? }
|
|
382
|
+
components.agentEngine.engine.durableJob.cancel({ orgId, jobId }) -> null
|
|
383
|
+
|
|
384
|
+
components.agentEngine.engine.agents.create({ orgId, createdBy, name, instructions, model }) -> Id<"agents">
|
|
385
|
+
components.agentEngine.engine.agents.get({ orgId, agentId }) -> agent | null
|
|
386
|
+
|
|
387
|
+
components.agentEngine.engine.resolvers.resolveManifest({ orgId, sessionId }) -> { agentId, name, instructions, model, toolApprovals[] }
|
|
388
|
+
|
|
389
|
+
components.agentEngine.engine.sessions.start({ orgId, agentId, createdBy }) -> Id<"sessions">
|
|
390
|
+
components.agentEngine.engine.sessions.sendTurn({ orgId, sessionId, input }) -> null
|
|
391
|
+
components.agentEngine.engine.sessions.appendReasoning({ orgId, sessionId, chunk }) -> null
|
|
392
|
+
components.agentEngine.engine.sessions.requestToolCall({ orgId, sessionId, toolName, argsJson }) -> { authorized, approvalPending }
|
|
393
|
+
components.agentEngine.engine.sessions.completeToolCall({ orgId, sessionId, toolName, resultJson }) -> null
|
|
394
|
+
components.agentEngine.engine.sessions.completeTurn({ orgId, sessionId }) -> null
|
|
395
|
+
components.agentEngine.engine.sessions.listEvents({ orgId, sessionId }) -> sessionEvent[]
|
|
396
|
+
|
|
397
|
+
components.agentEngine.engine.toolApprovals.respond({ orgId, sessionId, toolName, approval: "always"|"once"|"never" }) -> null
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
`createdBy` (on `agents.create` / `sessions.start`) is, for the same
|
|
401
|
+
platform reason as `orgId`, no longer read from `ctx.auth.getUserIdentity()`
|
|
402
|
+
— the consumer passes the acting user's own identifier.
|
|
403
|
+
|
|
404
|
+
Verified two ways: (1) `component/durableJob.componentPackaging.test.ts` +
|
|
405
|
+
`component/sessions.componentPackaging.test.ts` run `convex-test` directly
|
|
406
|
+
against `component/_generated/api` (the exact source tree
|
|
266
407
|
`tsc -p tsconfig.build.json` compiles into `dist/component/`, which is
|
|
267
408
|
what `npm pack` ships), so the compiled artifact has a real green test
|
|
268
409
|
suite behind it, not just the repo-root dev app's; (2) a from-scratch
|
|
269
|
-
consumer probe (install the published tarball into a bare `npx convex
|
|
270
|
-
|
|
271
|
-
|
|
410
|
+
consumer probe (install the published tarball into a bare `npx convex dev`
|
|
411
|
+
project, register the component, call `durableJob.start` + `sessions.start`
|
|
412
|
+
from the consumer's own function, passing `orgId` explicitly) — see the
|
|
272
413
|
completion report for the verbatim transcript.
|
|
273
414
|
|
|
274
|
-
**NOT yet callable through the published component** (still only exists
|
|
275
|
-
|
|
415
|
+
**NOT yet callable through the published component** (still only exists in
|
|
416
|
+
this repo's OWN dev/test deployment under `convex/schema.ts` +
|
|
276
417
|
`convex/agents.ts` + `convex/skills.ts`, used by `npx convex dev` and the
|
|
277
|
-
repo-root `convex-test` suite):
|
|
278
|
-
`
|
|
279
|
-
`
|
|
280
|
-
— `sessions
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
working component-level `convex-test`
|
|
284
|
-
unverified code — Day 128 forbids that; it is
|
|
285
|
-
next, separately-scoped migration.
|
|
418
|
+
repo-root `convex-test` suite): `skills`, `connections`, `subagents`,
|
|
419
|
+
`habits`, `sandboxConfig`, `modelKeys`, `mandates`, `approvalRequests`,
|
|
420
|
+
`awaitEventTimeouts`, `wakeableWork`, `engine/wakeup.ts`,
|
|
421
|
+
`engine/workflows.ts` (`sessionWorkflowV1` — this pass's `sessions` is
|
|
422
|
+
deliberately NOT tied to a `@convex-dev/workflow` run; see
|
|
423
|
+
`component/schema.ts`'s `sessions` table comment for why). Porting each of
|
|
424
|
+
those under `component/` without a working component-level `convex-test`
|
|
425
|
+
harness behind it would ship unverified code — Day 128 forbids that; it is
|
|
426
|
+
deliberately left for the next, separately-scoped migration.
|
|
286
427
|
|
|
287
428
|
### Why `component/` and not `convex/component/`
|
|
288
429
|
|
|
@@ -304,9 +445,10 @@ this exact repo layout, not guessed.
|
|
|
304
445
|
|
|
305
446
|
T1 (schema + durable session engine, PR #1) merged @
|
|
306
447
|
`9f3401ba63e6e663af95e9e4f5da42bc30b3550b`. I1 (long-task survival
|
|
307
|
-
contract, `engine/durableJob.ts`) — not merged, Eta reviews
|
|
308
|
-
token. T2-TER (npm
|
|
309
|
-
T2-QUATER (this pass:
|
|
310
|
-
|
|
311
|
-
`
|
|
312
|
-
publication" above; **not
|
|
448
|
+
contract, `engine/durableJob.ts`) — not merged, Eta reviews. T4 (mandates
|
|
449
|
+
+ habits) — **not merged** — Eta reviews, merge on Pi token. T2-TER (npm
|
|
450
|
+
packaging + component identity + publication) and T2-QUATER (this pass:
|
|
451
|
+
migrated `durableJob` + its schema + `lib/auth.ts` under the actual
|
|
452
|
+
component boundary, `component/`, with its own `_generated/` and its own
|
|
453
|
+
green `convex-test` suite) — see "Packaging & publication" above; **not
|
|
454
|
+
merged**.
|
|
@@ -6,12 +6,20 @@
|
|
|
6
6
|
* To regenerate, run `npx convex dev`.
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
9
|
+
import type * as engine_agents from "../engine/agents.js";
|
|
9
10
|
import type * as engine_durableJob from "../engine/durableJob.js";
|
|
11
|
+
import type * as engine_resolvers from "../engine/resolvers.js";
|
|
12
|
+
import type * as engine_sessions from "../engine/sessions.js";
|
|
13
|
+
import type * as engine_toolApprovals from "../engine/toolApprovals.js";
|
|
10
14
|
import type * as lib_auth from "../lib/auth.js";
|
|
11
15
|
import type * as testFixtures_callerSteps from "../testFixtures/callerSteps.js";
|
|
12
16
|
import type { ApiFromModules, FilterApi, FunctionReference } from "convex/server";
|
|
13
17
|
declare const fullApi: ApiFromModules<{
|
|
18
|
+
"engine/agents": typeof engine_agents;
|
|
14
19
|
"engine/durableJob": typeof engine_durableJob;
|
|
20
|
+
"engine/resolvers": typeof engine_resolvers;
|
|
21
|
+
"engine/sessions": typeof engine_sessions;
|
|
22
|
+
"engine/toolApprovals": typeof engine_toolApprovals;
|
|
15
23
|
"lib/auth": typeof lib_auth;
|
|
16
24
|
"testFixtures/callerSteps": typeof testFixtures_callerSteps;
|
|
17
25
|
}>;
|
|
@@ -20,6 +20,27 @@ import type { FunctionReference } from "convex/server";
|
|
|
20
20
|
*/
|
|
21
21
|
export type ComponentApi<Name extends string | undefined = string | undefined> = {
|
|
22
22
|
engine: {
|
|
23
|
+
agents: {
|
|
24
|
+
create: FunctionReference<"mutation", "internal", {
|
|
25
|
+
instructions: string;
|
|
26
|
+
model: string;
|
|
27
|
+
name: string;
|
|
28
|
+
}, string, Name>;
|
|
29
|
+
get: FunctionReference<"query", "internal", {
|
|
30
|
+
agentId: string;
|
|
31
|
+
}, {
|
|
32
|
+
_creationTime: number;
|
|
33
|
+
_id: string;
|
|
34
|
+
createdAt: number;
|
|
35
|
+
createdBy: string;
|
|
36
|
+
instructions: string;
|
|
37
|
+
model: string;
|
|
38
|
+
name: string;
|
|
39
|
+
orgId: string;
|
|
40
|
+
status: "draft" | "live" | "archived";
|
|
41
|
+
updatedAt: number;
|
|
42
|
+
} | null, Name>;
|
|
43
|
+
};
|
|
23
44
|
durableJob: {
|
|
24
45
|
cancel: FunctionReference<"mutation", "internal", {
|
|
25
46
|
jobId: string;
|
|
@@ -40,5 +61,64 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
40
61
|
totalSteps: number;
|
|
41
62
|
}, string, Name>;
|
|
42
63
|
};
|
|
64
|
+
resolvers: {
|
|
65
|
+
resolveManifest: FunctionReference<"query", "internal", {
|
|
66
|
+
sessionId: string;
|
|
67
|
+
}, {
|
|
68
|
+
agentId: string;
|
|
69
|
+
instructions: string;
|
|
70
|
+
model: string;
|
|
71
|
+
name: string;
|
|
72
|
+
toolApprovals: Array<{
|
|
73
|
+
approval: "always" | "once" | "never";
|
|
74
|
+
toolName: string;
|
|
75
|
+
}>;
|
|
76
|
+
}, Name>;
|
|
77
|
+
};
|
|
78
|
+
sessions: {
|
|
79
|
+
appendReasoning: FunctionReference<"mutation", "internal", {
|
|
80
|
+
chunk: string;
|
|
81
|
+
sessionId: string;
|
|
82
|
+
}, null, Name>;
|
|
83
|
+
completeToolCall: FunctionReference<"mutation", "internal", {
|
|
84
|
+
resultJson: string;
|
|
85
|
+
sessionId: string;
|
|
86
|
+
toolName: string;
|
|
87
|
+
}, null, Name>;
|
|
88
|
+
completeTurn: FunctionReference<"mutation", "internal", {
|
|
89
|
+
sessionId: string;
|
|
90
|
+
}, null, Name>;
|
|
91
|
+
listEvents: FunctionReference<"query", "internal", {
|
|
92
|
+
sessionId: string;
|
|
93
|
+
}, Array<{
|
|
94
|
+
_creationTime: number;
|
|
95
|
+
_id: string;
|
|
96
|
+
createdAt: number;
|
|
97
|
+
name: "session.started" | "turn.started" | "turn.reasoning" | "turn.toolCall.requested" | "turn.toolCall.completed" | "turn.completed" | "tool.approval.requested" | "tool.approval.resolved";
|
|
98
|
+
payload?: string;
|
|
99
|
+
}>, Name>;
|
|
100
|
+
requestToolCall: FunctionReference<"mutation", "internal", {
|
|
101
|
+
argsJson: string;
|
|
102
|
+
sessionId: string;
|
|
103
|
+
toolName: string;
|
|
104
|
+
}, {
|
|
105
|
+
approvalPending: boolean;
|
|
106
|
+
authorized: boolean;
|
|
107
|
+
}, Name>;
|
|
108
|
+
sendTurn: FunctionReference<"mutation", "internal", {
|
|
109
|
+
input: string;
|
|
110
|
+
sessionId: string;
|
|
111
|
+
}, null, Name>;
|
|
112
|
+
start: FunctionReference<"mutation", "internal", {
|
|
113
|
+
agentId: string;
|
|
114
|
+
}, string, Name>;
|
|
115
|
+
};
|
|
116
|
+
toolApprovals: {
|
|
117
|
+
respond: FunctionReference<"mutation", "internal", {
|
|
118
|
+
approval: "always" | "once" | "never";
|
|
119
|
+
sessionId: string;
|
|
120
|
+
toolName: string;
|
|
121
|
+
}, null, Name>;
|
|
122
|
+
};
|
|
43
123
|
};
|
|
44
124
|
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare const create: import("convex/server").RegisteredMutation<"public", {
|
|
2
|
+
name: string;
|
|
3
|
+
orgId: string;
|
|
4
|
+
instructions: string;
|
|
5
|
+
model: string;
|
|
6
|
+
createdBy: string;
|
|
7
|
+
}, Promise<import("convex/values").GenericId<"agents">>>;
|
|
8
|
+
export declare const get: import("convex/server").RegisteredQuery<"public", {
|
|
9
|
+
orgId: string;
|
|
10
|
+
agentId: import("convex/values").GenericId<"agents">;
|
|
11
|
+
}, Promise<{
|
|
12
|
+
_id: import("convex/values").GenericId<"agents">;
|
|
13
|
+
_creationTime: number;
|
|
14
|
+
name: string;
|
|
15
|
+
orgId: string;
|
|
16
|
+
status: "draft" | "live" | "archived";
|
|
17
|
+
createdAt: number;
|
|
18
|
+
updatedAt: number;
|
|
19
|
+
instructions: string;
|
|
20
|
+
model: string;
|
|
21
|
+
createdBy: string;
|
|
22
|
+
} | null>>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { mutation, query } from "../_generated/server.js";
|
|
3
|
+
import { requireOrgId } from "../lib/auth.js";
|
|
4
|
+
// engine/agents — MINIMAL root-sheet bootstrap for the sessions surface
|
|
5
|
+
// (T2-QUINQUIES). `sessions.start` needs an `agentId` it can read INSIDE
|
|
6
|
+
// the component boundary (component isolation: it cannot reach into a
|
|
7
|
+
// consumer app's own `agents` table). This is deliberately narrow: only
|
|
8
|
+
// `create`/`get`, no `update`/`list`/skills/connections — the full agent
|
|
9
|
+
// aggregate CRUD stays out of this component boundary for this pass (see
|
|
10
|
+
// component/schema.ts SCOPE note).
|
|
11
|
+
const agentDoc = v.object({
|
|
12
|
+
_id: v.id("agents"),
|
|
13
|
+
_creationTime: v.number(),
|
|
14
|
+
orgId: v.string(),
|
|
15
|
+
name: v.string(),
|
|
16
|
+
instructions: v.string(),
|
|
17
|
+
model: v.string(),
|
|
18
|
+
status: v.union(v.literal("draft"), v.literal("live"), v.literal("archived")),
|
|
19
|
+
createdBy: v.string(),
|
|
20
|
+
createdAt: v.number(),
|
|
21
|
+
updatedAt: v.number(),
|
|
22
|
+
});
|
|
23
|
+
export const create = mutation({
|
|
24
|
+
args: {
|
|
25
|
+
orgId: v.string(),
|
|
26
|
+
createdBy: v.string(),
|
|
27
|
+
name: v.string(),
|
|
28
|
+
instructions: v.string(),
|
|
29
|
+
model: v.string(),
|
|
30
|
+
},
|
|
31
|
+
returns: v.id("agents"),
|
|
32
|
+
handler: async (ctx, args) => {
|
|
33
|
+
const orgId = requireOrgId(args.orgId);
|
|
34
|
+
if (args.createdBy.trim().length === 0) {
|
|
35
|
+
throw new Error("Validation: createdBy must not be empty");
|
|
36
|
+
}
|
|
37
|
+
if (args.name.trim().length === 0) {
|
|
38
|
+
throw new Error("Validation: name must not be empty");
|
|
39
|
+
}
|
|
40
|
+
if (args.instructions.trim().length === 0) {
|
|
41
|
+
throw new Error("Validation: instructions must not be empty");
|
|
42
|
+
}
|
|
43
|
+
if (args.model.trim().length === 0) {
|
|
44
|
+
throw new Error("Validation: model must not be empty");
|
|
45
|
+
}
|
|
46
|
+
const now = Date.now();
|
|
47
|
+
return await ctx.db.insert("agents", {
|
|
48
|
+
orgId,
|
|
49
|
+
name: args.name,
|
|
50
|
+
instructions: args.instructions,
|
|
51
|
+
model: args.model,
|
|
52
|
+
status: "live",
|
|
53
|
+
createdBy: args.createdBy,
|
|
54
|
+
createdAt: now,
|
|
55
|
+
updatedAt: now,
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
export const get = query({
|
|
60
|
+
args: { orgId: v.string(), agentId: v.id("agents") },
|
|
61
|
+
returns: v.union(agentDoc, v.null()),
|
|
62
|
+
handler: async (ctx, args) => {
|
|
63
|
+
const orgId = requireOrgId(args.orgId);
|
|
64
|
+
const doc = await ctx.db.get(args.agentId);
|
|
65
|
+
if (!doc || doc.orgId !== orgId)
|
|
66
|
+
return null;
|
|
67
|
+
return doc;
|
|
68
|
+
},
|
|
69
|
+
});
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { WorkflowManager, type WorkflowId } from "@convex-dev/workflow";
|
|
2
2
|
export declare const workflow: WorkflowManager;
|
|
3
3
|
export declare const start: import("convex/server").RegisteredMutation<"public", {
|
|
4
|
+
orgId: string;
|
|
4
5
|
stepFunctionHandle: string;
|
|
5
6
|
totalSteps: number;
|
|
6
7
|
maxAttemptsPerStep: number;
|
|
7
8
|
kind: string;
|
|
8
9
|
}, Promise<import("convex/values").GenericId<"durableJobs">>>;
|
|
9
10
|
export declare const getStatus: import("convex/server").RegisteredQuery<"public", {
|
|
11
|
+
orgId: string;
|
|
10
12
|
jobId: import("convex/values").GenericId<"durableJobs">;
|
|
11
13
|
}, Promise<{
|
|
12
14
|
status: "running" | "completed" | "abandoned" | "cancelled";
|
|
@@ -16,6 +18,7 @@ export declare const getStatus: import("convex/server").RegisteredQuery<"public"
|
|
|
16
18
|
abandonedAtStep: number | undefined;
|
|
17
19
|
}>>;
|
|
18
20
|
export declare const cancel: import("convex/server").RegisteredMutation<"public", {
|
|
21
|
+
orgId: string;
|
|
19
22
|
jobId: import("convex/values").GenericId<"durableJobs">;
|
|
20
23
|
}, Promise<null>>;
|
|
21
24
|
export declare const getJobInternal: import("convex/server").RegisteredQuery<"internal", {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { v } from "convex/values";
|
|
2
2
|
import { WorkflowManager } from "@convex-dev/workflow";
|
|
3
|
-
import { components, internal } from "../_generated/api";
|
|
4
|
-
import { internalMutation, internalQuery, mutation, query, } from "../_generated/server";
|
|
5
|
-
import { assertSameOrg, requireOrgId } from "../lib/auth";
|
|
3
|
+
import { components, internal } from "../_generated/api.js";
|
|
4
|
+
import { internalMutation, internalQuery, mutation, query, } from "../_generated/server.js";
|
|
5
|
+
import { assertSameOrg, requireOrgId } from "../lib/auth.js";
|
|
6
6
|
// durableJobs — THE SURVIVAL CONTRACT (I1, SYNTHESE.md §5/§7): "mise en
|
|
7
7
|
// pause à la frontière d'étape, rejeu déterministe, reprise, abandon après
|
|
8
8
|
// N échecs." This ENVELOPS `@convex-dev/workflow` -- it does not reimplement
|
|
@@ -50,6 +50,7 @@ export const workflow = new WorkflowManager(components.workflow);
|
|
|
50
50
|
// ---------------------------------------------------------------------
|
|
51
51
|
export const start = mutation({
|
|
52
52
|
args: {
|
|
53
|
+
orgId: v.string(),
|
|
53
54
|
kind: v.string(),
|
|
54
55
|
stepFunctionHandle: v.string(),
|
|
55
56
|
totalSteps: v.number(),
|
|
@@ -57,7 +58,7 @@ export const start = mutation({
|
|
|
57
58
|
},
|
|
58
59
|
returns: v.id("durableJobs"),
|
|
59
60
|
handler: async (ctx, args) => {
|
|
60
|
-
const orgId =
|
|
61
|
+
const orgId = requireOrgId(args.orgId);
|
|
61
62
|
if (!Number.isInteger(args.totalSteps) || args.totalSteps <= 0) {
|
|
62
63
|
throw new Error("Validation: totalSteps must be a positive integer");
|
|
63
64
|
}
|
|
@@ -87,7 +88,7 @@ export const start = mutation({
|
|
|
87
88
|
},
|
|
88
89
|
});
|
|
89
90
|
export const getStatus = query({
|
|
90
|
-
args: { jobId: v.id("durableJobs") },
|
|
91
|
+
args: { orgId: v.string(), jobId: v.id("durableJobs") },
|
|
91
92
|
returns: v.object({
|
|
92
93
|
status: v.union(v.literal("running"), v.literal("completed"), v.literal("abandoned"), v.literal("cancelled")),
|
|
93
94
|
cursor: v.number(),
|
|
@@ -96,7 +97,7 @@ export const getStatus = query({
|
|
|
96
97
|
abandonedAtStep: v.optional(v.number()),
|
|
97
98
|
}),
|
|
98
99
|
handler: async (ctx, args) => {
|
|
99
|
-
const orgId =
|
|
100
|
+
const orgId = requireOrgId(args.orgId);
|
|
100
101
|
const job = await ctx.db.get(args.jobId);
|
|
101
102
|
assertSameOrg(orgId, job, "Job not found");
|
|
102
103
|
return {
|
|
@@ -109,10 +110,10 @@ export const getStatus = query({
|
|
|
109
110
|
},
|
|
110
111
|
});
|
|
111
112
|
export const cancel = mutation({
|
|
112
|
-
args: { jobId: v.id("durableJobs") },
|
|
113
|
+
args: { orgId: v.string(), jobId: v.id("durableJobs") },
|
|
113
114
|
returns: v.null(),
|
|
114
115
|
handler: async (ctx, args) => {
|
|
115
|
-
const orgId =
|
|
116
|
+
const orgId = requireOrgId(args.orgId);
|
|
116
117
|
const job = await ctx.db.get(args.jobId);
|
|
117
118
|
assertSameOrg(orgId, job, "Job not found");
|
|
118
119
|
if (job.status !== "running")
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const resolveManifest: import("convex/server").RegisteredQuery<"public", {
|
|
2
|
+
orgId: string;
|
|
3
|
+
sessionId: import("convex/values").GenericId<"sessions">;
|
|
4
|
+
}, Promise<{
|
|
5
|
+
agentId: import("convex/values").GenericId<"agents">;
|
|
6
|
+
name: string;
|
|
7
|
+
instructions: string;
|
|
8
|
+
model: string;
|
|
9
|
+
toolApprovals: {
|
|
10
|
+
toolName: string;
|
|
11
|
+
approval: "always" | "once" | "never";
|
|
12
|
+
}[];
|
|
13
|
+
}>>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { query } from "../_generated/server.js";
|
|
3
|
+
import { assertSameOrg, requireOrgId } from "../lib/auth.js";
|
|
4
|
+
// resolveManifest — the dynamic resolver (eve parity:
|
|
5
|
+
// `dynamicInstructions`/`dynamicSkills`/`dynamicTools`,
|
|
6
|
+
// packages/eve/src/compiler/manifest.ts:722-725). Produced FRESH from the
|
|
7
|
+
// aggregate on every call, never compiled/cached — an agent edited between
|
|
8
|
+
// sessions is visible on the very next `session.started`/`turn.started`
|
|
9
|
+
// call with zero redeploy. `sessions.start` and `sessions.sendTurn` both
|
|
10
|
+
// call this at their respective boundaries (ALLOWED_DYNAMIC_INSTRUCTION_
|
|
11
|
+
// EVENTS, dynamic-tool-definition.ts:40-46).
|
|
12
|
+
//
|
|
13
|
+
// T2-QUINQUIES scope note: `toolApprovals` is included (in scope per
|
|
14
|
+
// brief). `skills`/`connections` are NOT (out of scope for this pass — see
|
|
15
|
+
// component/schema.ts SCOPE note); this resolver returns only what the
|
|
16
|
+
// component boundary currently owns.
|
|
17
|
+
export const resolveManifest = query({
|
|
18
|
+
args: { orgId: v.string(), sessionId: v.id("sessions") },
|
|
19
|
+
returns: v.object({
|
|
20
|
+
agentId: v.id("agents"),
|
|
21
|
+
name: v.string(),
|
|
22
|
+
instructions: v.string(),
|
|
23
|
+
model: v.string(),
|
|
24
|
+
toolApprovals: v.array(v.object({
|
|
25
|
+
toolName: v.string(),
|
|
26
|
+
approval: v.union(v.literal("always"), v.literal("once"), v.literal("never")),
|
|
27
|
+
})),
|
|
28
|
+
}),
|
|
29
|
+
handler: async (ctx, args) => {
|
|
30
|
+
const orgId = requireOrgId(args.orgId);
|
|
31
|
+
const session = await ctx.db.get(args.sessionId);
|
|
32
|
+
assertSameOrg(orgId, session, "Session not found");
|
|
33
|
+
const agent = await ctx.db.get(session.agentId);
|
|
34
|
+
assertSameOrg(orgId, agent, "Agent not found");
|
|
35
|
+
const toolApprovals = await ctx.db
|
|
36
|
+
.query("toolApprovals")
|
|
37
|
+
.withIndex("by_org_agent", (q) => q.eq("orgId", orgId).eq("agentId", session.agentId))
|
|
38
|
+
.collect();
|
|
39
|
+
return {
|
|
40
|
+
agentId: agent._id,
|
|
41
|
+
name: agent.name,
|
|
42
|
+
instructions: agent.instructions,
|
|
43
|
+
model: agent.model,
|
|
44
|
+
toolApprovals: toolApprovals.map((t) => ({
|
|
45
|
+
toolName: t.toolName,
|
|
46
|
+
approval: t.approval,
|
|
47
|
+
})),
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export declare const start: import("convex/server").RegisteredMutation<"public", {
|
|
2
|
+
orgId: string;
|
|
3
|
+
createdBy: string;
|
|
4
|
+
agentId: import("convex/values").GenericId<"agents">;
|
|
5
|
+
}, Promise<import("convex/values").GenericId<"sessions">>>;
|
|
6
|
+
export declare const sendTurn: import("convex/server").RegisteredMutation<"public", {
|
|
7
|
+
orgId: string;
|
|
8
|
+
sessionId: import("convex/values").GenericId<"sessions">;
|
|
9
|
+
input: string;
|
|
10
|
+
}, Promise<null>>;
|
|
11
|
+
export declare const appendReasoning: import("convex/server").RegisteredMutation<"public", {
|
|
12
|
+
orgId: string;
|
|
13
|
+
sessionId: import("convex/values").GenericId<"sessions">;
|
|
14
|
+
chunk: string;
|
|
15
|
+
}, Promise<null>>;
|
|
16
|
+
export declare const requestToolCall: import("convex/server").RegisteredMutation<"public", {
|
|
17
|
+
orgId: string;
|
|
18
|
+
toolName: string;
|
|
19
|
+
sessionId: import("convex/values").GenericId<"sessions">;
|
|
20
|
+
argsJson: string;
|
|
21
|
+
}, Promise<{
|
|
22
|
+
authorized: boolean;
|
|
23
|
+
approvalPending: boolean;
|
|
24
|
+
}>>;
|
|
25
|
+
export declare const completeToolCall: import("convex/server").RegisteredMutation<"public", {
|
|
26
|
+
orgId: string;
|
|
27
|
+
toolName: string;
|
|
28
|
+
sessionId: import("convex/values").GenericId<"sessions">;
|
|
29
|
+
resultJson: string;
|
|
30
|
+
}, Promise<null>>;
|
|
31
|
+
export declare const completeTurn: import("convex/server").RegisteredMutation<"public", {
|
|
32
|
+
orgId: string;
|
|
33
|
+
sessionId: import("convex/values").GenericId<"sessions">;
|
|
34
|
+
}, Promise<null>>;
|
|
35
|
+
export declare const listEvents: import("convex/server").RegisteredQuery<"public", {
|
|
36
|
+
orgId: string;
|
|
37
|
+
sessionId: import("convex/values").GenericId<"sessions">;
|
|
38
|
+
}, Promise<{
|
|
39
|
+
_id: import("convex/values").GenericId<"sessionEvents">;
|
|
40
|
+
_creationTime: number;
|
|
41
|
+
name: "session.started" | "turn.started" | "turn.reasoning" | "turn.toolCall.requested" | "turn.toolCall.completed" | "turn.completed" | "tool.approval.requested" | "tool.approval.resolved";
|
|
42
|
+
payload: string | undefined;
|
|
43
|
+
createdAt: number;
|
|
44
|
+
}[]>>;
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { api } from "../_generated/api.js";
|
|
3
|
+
import { mutation, query } from "../_generated/server.js";
|
|
4
|
+
import { assertSameOrg, requireOrgId } from "../lib/auth.js";
|
|
5
|
+
// engine/sessions — THE FIL DE CONVERSATION contract (T2-QUINQUIES). This
|
|
6
|
+
// is what unblocks EveVantage V4: open a session, send a turn, stream
|
|
7
|
+
// reasoning/tool calls, gate on tool approval, replay the thread on
|
|
8
|
+
// reconnect. See component/schema.ts for the event log this is built on
|
|
9
|
+
// (`sessionEvents` IS the streaming transport via Convex reactivity — no
|
|
10
|
+
// separate channel).
|
|
11
|
+
//
|
|
12
|
+
// Named events mirror eve's session/turn boundaries
|
|
13
|
+
// (dynamic-tool-definition.ts:40-46): `session.started`, `turn.started`.
|
|
14
|
+
// `resolvers.resolveManifest` (engine/resolvers.ts) is reread at BOTH of
|
|
15
|
+
// those boundaries — never mid-turn — so an agent edit is visible at the
|
|
16
|
+
// very next turn with zero redeploy.
|
|
17
|
+
async function insertEvent(ctx, args) {
|
|
18
|
+
await ctx.db.insert("sessionEvents", {
|
|
19
|
+
orgId: args.orgId,
|
|
20
|
+
sessionId: args.sessionId,
|
|
21
|
+
name: args.name,
|
|
22
|
+
payload: args.payload,
|
|
23
|
+
createdAt: Date.now(),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
// start — opens a session (session.started boundary), rereading the
|
|
27
|
+
// manifest immediately so a caller can act on it before the first turn.
|
|
28
|
+
export const start = mutation({
|
|
29
|
+
args: { orgId: v.string(), agentId: v.id("agents"), createdBy: v.string() },
|
|
30
|
+
returns: v.id("sessions"),
|
|
31
|
+
handler: async (ctx, args) => {
|
|
32
|
+
const orgId = requireOrgId(args.orgId);
|
|
33
|
+
if (args.createdBy.trim().length === 0) {
|
|
34
|
+
throw new Error("Validation: createdBy must not be empty");
|
|
35
|
+
}
|
|
36
|
+
const agent = await ctx.db.get(args.agentId);
|
|
37
|
+
assertSameOrg(orgId, agent, "Agent not found");
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
const sessionId = await ctx.db.insert("sessions", {
|
|
40
|
+
orgId,
|
|
41
|
+
agentId: args.agentId,
|
|
42
|
+
status: "running",
|
|
43
|
+
createdBy: args.createdBy,
|
|
44
|
+
createdAt: now,
|
|
45
|
+
updatedAt: now,
|
|
46
|
+
});
|
|
47
|
+
await insertEvent(ctx, { orgId, sessionId, name: "session.started" });
|
|
48
|
+
await ctx.runQuery(api.engine.resolvers.resolveManifest, {
|
|
49
|
+
orgId,
|
|
50
|
+
sessionId,
|
|
51
|
+
});
|
|
52
|
+
return sessionId;
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
// sendTurn — turn.started boundary. Rereads the manifest (eve parity: dynamic
|
|
56
|
+
// resolvers only allowed to reread at session.started/turn.started).
|
|
57
|
+
// `input` is the caller-owned turn payload (already-serialized JSON/text);
|
|
58
|
+
// this component does not interpret it -- it is stored verbatim as the
|
|
59
|
+
// event payload for replay.
|
|
60
|
+
export const sendTurn = mutation({
|
|
61
|
+
args: { orgId: v.string(), sessionId: v.id("sessions"), input: v.string() },
|
|
62
|
+
returns: v.null(),
|
|
63
|
+
handler: async (ctx, args) => {
|
|
64
|
+
const orgId = requireOrgId(args.orgId);
|
|
65
|
+
const session = await ctx.db.get(args.sessionId);
|
|
66
|
+
assertSameOrg(orgId, session, "Session not found");
|
|
67
|
+
if (session.status !== "running") {
|
|
68
|
+
throw new Error(`Session is not running (status=${session.status})`);
|
|
69
|
+
}
|
|
70
|
+
await insertEvent(ctx, {
|
|
71
|
+
orgId,
|
|
72
|
+
sessionId: args.sessionId,
|
|
73
|
+
name: "turn.started",
|
|
74
|
+
payload: args.input,
|
|
75
|
+
});
|
|
76
|
+
await ctx.runQuery(api.engine.resolvers.resolveManifest, {
|
|
77
|
+
orgId,
|
|
78
|
+
sessionId: args.sessionId,
|
|
79
|
+
});
|
|
80
|
+
await ctx.db.patch(args.sessionId, { updatedAt: Date.now() });
|
|
81
|
+
return null;
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
// appendReasoning — one streamed reasoning chunk. Convex query reactivity
|
|
85
|
+
// on `listEvents` is the streaming transport: each insert here is pushed to
|
|
86
|
+
// every subscriber live, no separate channel needed.
|
|
87
|
+
export const appendReasoning = mutation({
|
|
88
|
+
args: { orgId: v.string(), sessionId: v.id("sessions"), chunk: v.string() },
|
|
89
|
+
returns: v.null(),
|
|
90
|
+
handler: async (ctx, args) => {
|
|
91
|
+
const orgId = requireOrgId(args.orgId);
|
|
92
|
+
const session = await ctx.db.get(args.sessionId);
|
|
93
|
+
assertSameOrg(orgId, session, "Session not found");
|
|
94
|
+
await insertEvent(ctx, {
|
|
95
|
+
orgId,
|
|
96
|
+
sessionId: args.sessionId,
|
|
97
|
+
name: "turn.reasoning",
|
|
98
|
+
payload: args.chunk,
|
|
99
|
+
});
|
|
100
|
+
return null;
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
// requestToolCall — gates on the agent's toolApprovals row for `toolName`
|
|
104
|
+
// (default "never" = no gate). "always" gates every call; "once" gates
|
|
105
|
+
// unless already resolved earlier in THIS session. Returns whether the
|
|
106
|
+
// caller may proceed immediately or must wait for
|
|
107
|
+
// `toolApprovals.respond`.
|
|
108
|
+
export const requestToolCall = mutation({
|
|
109
|
+
args: {
|
|
110
|
+
orgId: v.string(),
|
|
111
|
+
sessionId: v.id("sessions"),
|
|
112
|
+
toolName: v.string(),
|
|
113
|
+
argsJson: v.string(),
|
|
114
|
+
},
|
|
115
|
+
returns: v.object({ authorized: v.boolean(), approvalPending: v.boolean() }),
|
|
116
|
+
handler: async (ctx, args) => {
|
|
117
|
+
const orgId = requireOrgId(args.orgId);
|
|
118
|
+
const session = await ctx.db.get(args.sessionId);
|
|
119
|
+
assertSameOrg(orgId, session, "Session not found");
|
|
120
|
+
await insertEvent(ctx, {
|
|
121
|
+
orgId,
|
|
122
|
+
sessionId: args.sessionId,
|
|
123
|
+
name: "turn.toolCall.requested",
|
|
124
|
+
payload: JSON.stringify({ toolName: args.toolName, args: args.argsJson }),
|
|
125
|
+
});
|
|
126
|
+
const approvalRow = await ctx.db
|
|
127
|
+
.query("toolApprovals")
|
|
128
|
+
.withIndex("by_org_agent_tool", (q) => q
|
|
129
|
+
.eq("orgId", orgId)
|
|
130
|
+
.eq("agentId", session.agentId)
|
|
131
|
+
.eq("toolName", args.toolName))
|
|
132
|
+
.unique();
|
|
133
|
+
const approval = approvalRow?.approval ?? "never";
|
|
134
|
+
if (approval === "never") {
|
|
135
|
+
return { authorized: true, approvalPending: false };
|
|
136
|
+
}
|
|
137
|
+
if (approval === "always") {
|
|
138
|
+
await insertEvent(ctx, {
|
|
139
|
+
orgId,
|
|
140
|
+
sessionId: args.sessionId,
|
|
141
|
+
name: "tool.approval.requested",
|
|
142
|
+
payload: JSON.stringify({ toolName: args.toolName }),
|
|
143
|
+
});
|
|
144
|
+
return { authorized: false, approvalPending: true };
|
|
145
|
+
}
|
|
146
|
+
// approval === "once": gate unless this tool was already resolved once
|
|
147
|
+
// in this session.
|
|
148
|
+
const events = await ctx.db
|
|
149
|
+
.query("sessionEvents")
|
|
150
|
+
.withIndex("by_org_session", (q) => q.eq("orgId", orgId).eq("sessionId", args.sessionId))
|
|
151
|
+
.collect();
|
|
152
|
+
const alreadyResolvedThisSession = events.some((e) => e.name === "tool.approval.resolved" &&
|
|
153
|
+
e.payload !== undefined &&
|
|
154
|
+
JSON.parse(e.payload).toolName ===
|
|
155
|
+
args.toolName);
|
|
156
|
+
if (alreadyResolvedThisSession) {
|
|
157
|
+
return { authorized: true, approvalPending: false };
|
|
158
|
+
}
|
|
159
|
+
await insertEvent(ctx, {
|
|
160
|
+
orgId,
|
|
161
|
+
sessionId: args.sessionId,
|
|
162
|
+
name: "tool.approval.requested",
|
|
163
|
+
payload: JSON.stringify({ toolName: args.toolName }),
|
|
164
|
+
});
|
|
165
|
+
return { authorized: false, approvalPending: true };
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
// completeToolCall — the caller invokes this after actually running the
|
|
169
|
+
// tool (authorized immediately, or after `toolApprovals.respond` cleared
|
|
170
|
+
// the gate).
|
|
171
|
+
export const completeToolCall = mutation({
|
|
172
|
+
args: {
|
|
173
|
+
orgId: v.string(),
|
|
174
|
+
sessionId: v.id("sessions"),
|
|
175
|
+
toolName: v.string(),
|
|
176
|
+
resultJson: v.string(),
|
|
177
|
+
},
|
|
178
|
+
returns: v.null(),
|
|
179
|
+
handler: async (ctx, args) => {
|
|
180
|
+
const orgId = requireOrgId(args.orgId);
|
|
181
|
+
const session = await ctx.db.get(args.sessionId);
|
|
182
|
+
assertSameOrg(orgId, session, "Session not found");
|
|
183
|
+
await insertEvent(ctx, {
|
|
184
|
+
orgId,
|
|
185
|
+
sessionId: args.sessionId,
|
|
186
|
+
name: "turn.toolCall.completed",
|
|
187
|
+
payload: JSON.stringify({ toolName: args.toolName, result: args.resultJson }),
|
|
188
|
+
});
|
|
189
|
+
return null;
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
// completeTurn — closes the current turn.
|
|
193
|
+
export const completeTurn = mutation({
|
|
194
|
+
args: { orgId: v.string(), sessionId: v.id("sessions") },
|
|
195
|
+
returns: v.null(),
|
|
196
|
+
handler: async (ctx, args) => {
|
|
197
|
+
const orgId = requireOrgId(args.orgId);
|
|
198
|
+
const session = await ctx.db.get(args.sessionId);
|
|
199
|
+
assertSameOrg(orgId, session, "Session not found");
|
|
200
|
+
await insertEvent(ctx, {
|
|
201
|
+
orgId,
|
|
202
|
+
sessionId: args.sessionId,
|
|
203
|
+
name: "turn.completed",
|
|
204
|
+
});
|
|
205
|
+
await ctx.db.patch(args.sessionId, { updatedAt: Date.now() });
|
|
206
|
+
return null;
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
// listEvents — REPLAY ON RECONNECT: the full ordered event log for a
|
|
210
|
+
// session. A client that drops and reconnects calls this once to rebuild
|
|
211
|
+
// the thread, then subscribes (Convex reactivity) for anything after.
|
|
212
|
+
export const listEvents = query({
|
|
213
|
+
args: { orgId: v.string(), sessionId: v.id("sessions") },
|
|
214
|
+
returns: v.array(v.object({
|
|
215
|
+
_id: v.id("sessionEvents"),
|
|
216
|
+
_creationTime: v.number(),
|
|
217
|
+
name: v.union(v.literal("session.started"), v.literal("turn.started"), v.literal("turn.reasoning"), v.literal("turn.toolCall.requested"), v.literal("turn.toolCall.completed"), v.literal("turn.completed"), v.literal("tool.approval.requested"), v.literal("tool.approval.resolved")),
|
|
218
|
+
payload: v.optional(v.string()),
|
|
219
|
+
createdAt: v.number(),
|
|
220
|
+
})),
|
|
221
|
+
handler: async (ctx, args) => {
|
|
222
|
+
const orgId = requireOrgId(args.orgId);
|
|
223
|
+
const session = await ctx.db.get(args.sessionId);
|
|
224
|
+
assertSameOrg(orgId, session, "Session not found");
|
|
225
|
+
const events = await ctx.db
|
|
226
|
+
.query("sessionEvents")
|
|
227
|
+
.withIndex("by_org_session", (q) => q.eq("orgId", orgId).eq("sessionId", args.sessionId))
|
|
228
|
+
.collect();
|
|
229
|
+
return events
|
|
230
|
+
.sort((a, b) => a.createdAt - b.createdAt)
|
|
231
|
+
.map((e) => ({
|
|
232
|
+
_id: e._id,
|
|
233
|
+
_creationTime: e._creationTime,
|
|
234
|
+
name: e.name,
|
|
235
|
+
payload: e.payload,
|
|
236
|
+
createdAt: e.createdAt,
|
|
237
|
+
}));
|
|
238
|
+
},
|
|
239
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { mutation } from "../_generated/server.js";
|
|
3
|
+
import { assertSameOrg, requireOrgId } from "../lib/auth.js";
|
|
4
|
+
// engine/toolApprovals — respond to a pending approval request
|
|
5
|
+
// (`tool.approval.requested`, raised by `sessions.requestToolCall`). The
|
|
6
|
+
// decision (always/once/never) is WRITTEN as the agent's standing policy
|
|
7
|
+
// for that tool going forward (upsert on `by_org_agent_tool`), and
|
|
8
|
+
// `tool.approval.resolved` is appended so `sessions.requestToolCall`'s
|
|
9
|
+
// "once" check (and any replaying client) can see it happened.
|
|
10
|
+
export const respond = mutation({
|
|
11
|
+
args: {
|
|
12
|
+
orgId: v.string(),
|
|
13
|
+
sessionId: v.id("sessions"),
|
|
14
|
+
toolName: v.string(),
|
|
15
|
+
approval: v.union(v.literal("always"), v.literal("once"), v.literal("never")),
|
|
16
|
+
},
|
|
17
|
+
returns: v.null(),
|
|
18
|
+
handler: async (ctx, args) => {
|
|
19
|
+
const orgId = requireOrgId(args.orgId);
|
|
20
|
+
const session = await ctx.db.get(args.sessionId);
|
|
21
|
+
assertSameOrg(orgId, session, "Session not found");
|
|
22
|
+
const existing = await ctx.db
|
|
23
|
+
.query("toolApprovals")
|
|
24
|
+
.withIndex("by_org_agent_tool", (q) => q
|
|
25
|
+
.eq("orgId", orgId)
|
|
26
|
+
.eq("agentId", session.agentId)
|
|
27
|
+
.eq("toolName", args.toolName))
|
|
28
|
+
.unique();
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
if (existing) {
|
|
31
|
+
await ctx.db.patch(existing._id, { approval: args.approval, updatedAt: now });
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
await ctx.db.insert("toolApprovals", {
|
|
35
|
+
orgId,
|
|
36
|
+
agentId: session.agentId,
|
|
37
|
+
toolName: args.toolName,
|
|
38
|
+
approval: args.approval,
|
|
39
|
+
createdAt: now,
|
|
40
|
+
updatedAt: now,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
await ctx.db.insert("sessionEvents", {
|
|
44
|
+
orgId,
|
|
45
|
+
sessionId: args.sessionId,
|
|
46
|
+
name: "tool.approval.resolved",
|
|
47
|
+
payload: JSON.stringify({ toolName: args.toolName, approval: args.approval }),
|
|
48
|
+
createdAt: now,
|
|
49
|
+
});
|
|
50
|
+
return null;
|
|
51
|
+
},
|
|
52
|
+
});
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
export declare function requireOrgId(ctx: QueryCtx | MutationCtx): Promise<string>;
|
|
1
|
+
export declare function requireOrgId(orgId: string | null | undefined): string;
|
|
3
2
|
export declare function assertSameOrg(callerOrgId: string, doc: {
|
|
4
3
|
orgId: string;
|
|
5
4
|
} | null, notFoundMessage: string): asserts doc is {
|
|
@@ -1,21 +1,34 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
// Trust model (CORRECTIF BLOQUANT, VP k1741evjzm72rj91tkcbdhpskx8ah35a):
|
|
2
|
+
// Convex does NOT propagate the calling app's `ctx.auth` identity across a
|
|
3
|
+
// component boundary -- this is a platform isolation guarantee, not a
|
|
4
|
+
// choice this component makes (convex-test: "Auth doesn't propagate across
|
|
5
|
+
// component boundaries"). A handler inside this component can therefore
|
|
6
|
+
// NEVER read `ctx.auth.getUserIdentity()` and expect it to reflect the
|
|
7
|
+
// consumer app's end user -- there is no such identity to read here.
|
|
8
|
+
//
|
|
9
|
+
// The consumer app has ALREADY authenticated its user and resolved their
|
|
10
|
+
// orgId on ITS OWN side (e.g. Clerk org claim, read in the consumer's own
|
|
11
|
+
// `ctx.auth`) before ever calling into this component. `orgId` is passed
|
|
12
|
+
// as an explicit argument on every public function in this component.
|
|
13
|
+
//
|
|
14
|
+
// What does NOT change: this component still derives NOTHING from the
|
|
15
|
+
// client beyond what the consumer's public API surface exposes, still
|
|
16
|
+
// denies by default, and `orgId` is still the first field of every index
|
|
17
|
+
// and is still enforced on every document read via `assertSameOrg` below
|
|
18
|
+
// -- a caller-supplied `orgId` can never see another org's rows. The
|
|
19
|
+
// isolation boundary moved from "component reads ctx.auth" (impossible
|
|
20
|
+
// across a component boundary) to "component enforces the orgId the
|
|
21
|
+
// consumer already vouches for"; the boundary itself -- deny by default,
|
|
22
|
+
// orgId first, no cross-org reads -- is unchanged.
|
|
23
|
+
export function requireOrgId(orgId) {
|
|
24
|
+
if (!orgId || typeof orgId !== "string" || orgId.trim().length === 0) {
|
|
25
|
+
throw new Error("Unauthenticated: caller did not supply a non-empty orgId");
|
|
13
26
|
}
|
|
14
27
|
return orgId;
|
|
15
28
|
}
|
|
16
|
-
// Throws unless the document's orgId matches the caller
|
|
17
|
-
//
|
|
18
|
-
//
|
|
29
|
+
// Throws unless the document's orgId matches the caller-supplied orgId.
|
|
30
|
+
// Use on every read/write of a document fetched by _id, since db.get()
|
|
31
|
+
// does not filter by index.
|
|
19
32
|
export function assertSameOrg(callerOrgId, doc, notFoundMessage) {
|
|
20
33
|
if (!doc || doc.orgId !== callerOrgId) {
|
|
21
34
|
// Same error for "not found" and "cross-org" -- do not leak existence.
|
|
@@ -45,5 +45,80 @@ declare const _default: import("convex/server").SchemaDefinition<{
|
|
|
45
45
|
by_org: ["orgId", "_creationTime"];
|
|
46
46
|
by_org_job: ["orgId", "jobId", "_creationTime"];
|
|
47
47
|
}, {}, {}>;
|
|
48
|
+
agents: import("convex/server").TableDefinition<import("convex/values").VObject<{
|
|
49
|
+
name: string;
|
|
50
|
+
orgId: string;
|
|
51
|
+
status: "draft" | "live" | "archived";
|
|
52
|
+
createdAt: number;
|
|
53
|
+
updatedAt: number;
|
|
54
|
+
instructions: string;
|
|
55
|
+
model: string;
|
|
56
|
+
createdBy: string;
|
|
57
|
+
}, {
|
|
58
|
+
orgId: import("convex/values").VString<string, "required">;
|
|
59
|
+
name: import("convex/values").VString<string, "required">;
|
|
60
|
+
instructions: import("convex/values").VString<string, "required">;
|
|
61
|
+
model: import("convex/values").VString<string, "required">;
|
|
62
|
+
status: import("convex/values").VUnion<"draft" | "live" | "archived", [import("convex/values").VLiteral<"draft", "required">, import("convex/values").VLiteral<"live", "required">, import("convex/values").VLiteral<"archived", "required">], "required", never>;
|
|
63
|
+
createdBy: import("convex/values").VString<string, "required">;
|
|
64
|
+
createdAt: import("convex/values").VFloat64<number, "required">;
|
|
65
|
+
updatedAt: import("convex/values").VFloat64<number, "required">;
|
|
66
|
+
}, "required", "name" | "orgId" | "status" | "createdAt" | "updatedAt" | "instructions" | "model" | "createdBy">, {
|
|
67
|
+
by_org: ["orgId", "_creationTime"];
|
|
68
|
+
by_org_status: ["orgId", "status", "_creationTime"];
|
|
69
|
+
}, {}, {}>;
|
|
70
|
+
toolApprovals: import("convex/server").TableDefinition<import("convex/values").VObject<{
|
|
71
|
+
orgId: string;
|
|
72
|
+
createdAt: number;
|
|
73
|
+
updatedAt: number;
|
|
74
|
+
agentId: import("convex/values").GenericId<"agents">;
|
|
75
|
+
toolName: string;
|
|
76
|
+
approval: "always" | "once" | "never";
|
|
77
|
+
}, {
|
|
78
|
+
orgId: import("convex/values").VString<string, "required">;
|
|
79
|
+
agentId: import("convex/values").VId<import("convex/values").GenericId<"agents">, "required">;
|
|
80
|
+
toolName: import("convex/values").VString<string, "required">;
|
|
81
|
+
approval: import("convex/values").VUnion<"always" | "once" | "never", [import("convex/values").VLiteral<"always", "required">, import("convex/values").VLiteral<"once", "required">, import("convex/values").VLiteral<"never", "required">], "required", never>;
|
|
82
|
+
createdAt: import("convex/values").VFloat64<number, "required">;
|
|
83
|
+
updatedAt: import("convex/values").VFloat64<number, "required">;
|
|
84
|
+
}, "required", "orgId" | "createdAt" | "updatedAt" | "agentId" | "toolName" | "approval">, {
|
|
85
|
+
by_org: ["orgId", "_creationTime"];
|
|
86
|
+
by_org_agent: ["orgId", "agentId", "_creationTime"];
|
|
87
|
+
by_org_agent_tool: ["orgId", "agentId", "toolName", "_creationTime"];
|
|
88
|
+
}, {}, {}>;
|
|
89
|
+
sessions: import("convex/server").TableDefinition<import("convex/values").VObject<{
|
|
90
|
+
orgId: string;
|
|
91
|
+
status: "running" | "completed" | "cancelled";
|
|
92
|
+
createdAt: number;
|
|
93
|
+
updatedAt: number;
|
|
94
|
+
createdBy: string;
|
|
95
|
+
agentId: import("convex/values").GenericId<"agents">;
|
|
96
|
+
}, {
|
|
97
|
+
orgId: import("convex/values").VString<string, "required">;
|
|
98
|
+
agentId: import("convex/values").VId<import("convex/values").GenericId<"agents">, "required">;
|
|
99
|
+
status: import("convex/values").VUnion<"running" | "completed" | "cancelled", [import("convex/values").VLiteral<"running", "required">, import("convex/values").VLiteral<"completed", "required">, import("convex/values").VLiteral<"cancelled", "required">], "required", never>;
|
|
100
|
+
createdBy: import("convex/values").VString<string, "required">;
|
|
101
|
+
createdAt: import("convex/values").VFloat64<number, "required">;
|
|
102
|
+
updatedAt: import("convex/values").VFloat64<number, "required">;
|
|
103
|
+
}, "required", "orgId" | "status" | "createdAt" | "updatedAt" | "createdBy" | "agentId">, {
|
|
104
|
+
by_org: ["orgId", "_creationTime"];
|
|
105
|
+
by_org_agent: ["orgId", "agentId", "_creationTime"];
|
|
106
|
+
}, {}, {}>;
|
|
107
|
+
sessionEvents: import("convex/server").TableDefinition<import("convex/values").VObject<{
|
|
108
|
+
payload?: string | undefined;
|
|
109
|
+
name: "session.started" | "turn.started" | "turn.reasoning" | "turn.toolCall.requested" | "turn.toolCall.completed" | "turn.completed" | "tool.approval.requested" | "tool.approval.resolved";
|
|
110
|
+
orgId: string;
|
|
111
|
+
createdAt: number;
|
|
112
|
+
sessionId: import("convex/values").GenericId<"sessions">;
|
|
113
|
+
}, {
|
|
114
|
+
orgId: import("convex/values").VString<string, "required">;
|
|
115
|
+
sessionId: import("convex/values").VId<import("convex/values").GenericId<"sessions">, "required">;
|
|
116
|
+
name: import("convex/values").VUnion<"session.started" | "turn.started" | "turn.reasoning" | "turn.toolCall.requested" | "turn.toolCall.completed" | "turn.completed" | "tool.approval.requested" | "tool.approval.resolved", [import("convex/values").VLiteral<"session.started", "required">, import("convex/values").VLiteral<"turn.started", "required">, import("convex/values").VLiteral<"turn.reasoning", "required">, import("convex/values").VLiteral<"turn.toolCall.requested", "required">, import("convex/values").VLiteral<"turn.toolCall.completed", "required">, import("convex/values").VLiteral<"turn.completed", "required">, import("convex/values").VLiteral<"tool.approval.requested", "required">, import("convex/values").VLiteral<"tool.approval.resolved", "required">], "required", never>;
|
|
117
|
+
payload: import("convex/values").VString<string | undefined, "optional">;
|
|
118
|
+
createdAt: import("convex/values").VFloat64<number, "required">;
|
|
119
|
+
}, "required", "name" | "orgId" | "createdAt" | "sessionId" | "payload">, {
|
|
120
|
+
by_org: ["orgId", "_creationTime"];
|
|
121
|
+
by_org_session: ["orgId", "sessionId", "_creationTime"];
|
|
122
|
+
}, {}, {}>;
|
|
48
123
|
}, true>;
|
|
49
124
|
export default _default;
|
package/dist/component/schema.js
CHANGED
|
@@ -7,17 +7,18 @@ import { v } from "convex/values";
|
|
|
7
7
|
// `app.use(agentEngine)` — as opposed to `convex/schema.ts` at the repo
|
|
8
8
|
// root, which only backs THIS repo's own dev/test deployment.
|
|
9
9
|
//
|
|
10
|
-
// SCOPE (T2-QUATER
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
10
|
+
// SCOPE (T2-QUATER: durableJob only. T2-QUINQUIES, this pass: WIDENS the
|
|
11
|
+
// boundary to the SESSIONS surface Hestia's EveVantage V4 conversation
|
|
12
|
+
// thread needs — session start/turn, streamed reasoning/tool-call events,
|
|
13
|
+
// toolApprovals response, replay on reconnect, dynamic resolver — see
|
|
14
|
+
// README "What's callable"). Still DELIBERATELY NARROW: `skills`,
|
|
15
|
+
// `connections`, `subagents`, `habits`, `sandboxConfig`, `modelKeys`,
|
|
16
|
+
// `mandates`, `awaitEventTimeouts`, `wakeableWork` are NOT re-hosted here —
|
|
17
|
+
// each of those needs its own component-level test harness first (Day 128:
|
|
18
|
+
// never ship unverified). `agents` is included, minimally, because a
|
|
19
|
+
// session cannot exist without an owning agent row the component itself
|
|
20
|
+
// can read (component isolation: this package cannot read a consumer app's
|
|
21
|
+
// OWN `agents` table across the component boundary).
|
|
21
22
|
export default defineSchema({
|
|
22
23
|
// durableJobs — GENERIC durable step-batch primitive (I1, SYNTHESE.md
|
|
23
24
|
// §5/§7). See convex/engine/durableJob.ts (repo root) for the full
|
|
@@ -53,4 +54,77 @@ export default defineSchema({
|
|
|
53
54
|
})
|
|
54
55
|
.index("by_org", ["orgId"])
|
|
55
56
|
.index("by_org_job", ["orgId", "jobId"]),
|
|
57
|
+
// ---------------------------------------------------------------------
|
|
58
|
+
// T2-QUINQUIES — SESSIONS surface (EveVantage V4 conversation thread).
|
|
59
|
+
// ---------------------------------------------------------------------
|
|
60
|
+
// agents — minimal root sheet, ONLY the fields the sessions surface
|
|
61
|
+
// needs to resolve a manifest (name/instructions/model) + gate tool
|
|
62
|
+
// calls (via toolApprovals below). The full agent aggregate (skills,
|
|
63
|
+
// connections, subagents, habits, sandboxConfig, modelKeys) stays out of
|
|
64
|
+
// this component boundary for this pass — see the SCOPE note above.
|
|
65
|
+
agents: defineTable({
|
|
66
|
+
orgId: v.string(),
|
|
67
|
+
name: v.string(),
|
|
68
|
+
instructions: v.string(),
|
|
69
|
+
model: v.string(),
|
|
70
|
+
status: v.union(v.literal("draft"), v.literal("live"), v.literal("archived")),
|
|
71
|
+
createdBy: v.string(),
|
|
72
|
+
createdAt: v.number(),
|
|
73
|
+
updatedAt: v.number(),
|
|
74
|
+
})
|
|
75
|
+
.index("by_org", ["orgId"])
|
|
76
|
+
.index("by_org_status", ["orgId", "status"]),
|
|
77
|
+
// toolApprovals — approval gate per (agent, tool), eve parity
|
|
78
|
+
// (public/definitions/tool.ts:117-125). "never" (default) means a tool
|
|
79
|
+
// call never pauses for human approval; "always"/"once" gate every /
|
|
80
|
+
// first call of a session via a `tool.approval.requested` sessionEvent
|
|
81
|
+
// that `toolApprovals.respond` resolves.
|
|
82
|
+
toolApprovals: defineTable({
|
|
83
|
+
orgId: v.string(),
|
|
84
|
+
agentId: v.id("agents"),
|
|
85
|
+
toolName: v.string(),
|
|
86
|
+
approval: v.union(v.literal("always"), v.literal("once"), v.literal("never")),
|
|
87
|
+
createdAt: v.number(),
|
|
88
|
+
updatedAt: v.number(),
|
|
89
|
+
})
|
|
90
|
+
.index("by_org", ["orgId"])
|
|
91
|
+
.index("by_org_agent", ["orgId", "agentId"])
|
|
92
|
+
.index("by_org_agent_tool", ["orgId", "agentId", "toolName"]),
|
|
93
|
+
// sessions — one conversation container per `sessions.start` call. This
|
|
94
|
+
// pass deliberately does NOT tie a session to a `@convex-dev/workflow`
|
|
95
|
+
// run (unlike the repo-root, app-level `convex/engine/sessions.ts`
|
|
96
|
+
// prototype this was ported from) — long-task durability is already
|
|
97
|
+
// covered by `engine/durableJob.ts`; a consumer that needs a durable
|
|
98
|
+
// workflow driving turns can wire one on top using `durableJob.start`
|
|
99
|
+
// with its own step function. This is a deliberate divergence ("notre
|
|
100
|
+
// ligne") from the app-level prototype, narrowing the shipped surface to
|
|
101
|
+
// what has a working component-level test harness behind it.
|
|
102
|
+
sessions: defineTable({
|
|
103
|
+
orgId: v.string(),
|
|
104
|
+
agentId: v.id("agents"),
|
|
105
|
+
status: v.union(v.literal("running"), v.literal("completed"), v.literal("cancelled")),
|
|
106
|
+
createdBy: v.string(),
|
|
107
|
+
createdAt: v.number(),
|
|
108
|
+
updatedAt: v.number(),
|
|
109
|
+
})
|
|
110
|
+
.index("by_org", ["orgId"])
|
|
111
|
+
.index("by_org_agent", ["orgId", "agentId"]),
|
|
112
|
+
// sessionEvents — the append-only event log a client replays on
|
|
113
|
+
// reconnect (`sessions.listEvents`) and that IS the streaming transport:
|
|
114
|
+
// Convex query subscriptions push new rows to every subscriber in real
|
|
115
|
+
// time, so "streaming the reasoning and tool calls" is simply inserting
|
|
116
|
+
// rows here as they happen and letting reactivity carry them to the
|
|
117
|
+
// client — no separate transport needed. Event names mirror eve's
|
|
118
|
+
// session/turn boundaries (`session.started`, `turn.started`,
|
|
119
|
+
// dynamic-tool-definition.ts:40-46) plus the turn-body events this pass
|
|
120
|
+
// adds (reasoning chunks, tool-call lifecycle, approval lifecycle).
|
|
121
|
+
sessionEvents: defineTable({
|
|
122
|
+
orgId: v.string(),
|
|
123
|
+
sessionId: v.id("sessions"),
|
|
124
|
+
name: v.union(v.literal("session.started"), v.literal("turn.started"), v.literal("turn.reasoning"), v.literal("turn.toolCall.requested"), v.literal("turn.toolCall.completed"), v.literal("turn.completed"), v.literal("tool.approval.requested"), v.literal("tool.approval.resolved")),
|
|
125
|
+
payload: v.optional(v.string()), // JSON-serialized, free contract per event name
|
|
126
|
+
createdAt: v.number(),
|
|
127
|
+
})
|
|
128
|
+
.index("by_org", ["orgId"])
|
|
129
|
+
.index("by_org_session", ["orgId", "sessionId"]),
|
|
56
130
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { v } from "convex/values";
|
|
2
|
-
import { internalMutation, internalQuery } from "../_generated/server";
|
|
2
|
+
import { internalMutation, internalQuery } from "../_generated/server.js";
|
|
3
3
|
// testFixtures/callerSteps — stand-ins for a CALLER'S OWN step function
|
|
4
4
|
// (the thing a real consumer, e.g. VantagePeers, would define in ITS OWN
|
|
5
5
|
// app and pass to `engine/durableJob.start` as `stepFunctionHandle` via
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vantageos/agent-engine",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.4",
|
|
4
4
|
"description": "Reusable Convex component: durable agent engine. Agent = data aggregate (root sheet + child tables), never compiled code. Built on @convex-dev/workflow + @convex-dev/workpool (+ @convex-dev/agent). Eve parity by design.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -38,6 +38,10 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@convex-dev/agent": "0.6.4",
|
|
41
|
+
"@convex-dev/aggregate": "^0.2.2",
|
|
42
|
+
"@convex-dev/crons": "^0.2.2",
|
|
43
|
+
"@convex-dev/rate-limiter": "^0.3.2",
|
|
44
|
+
"@convex-dev/sharded-counter": "^0.2.1",
|
|
41
45
|
"@convex-dev/workflow": "0.4.4",
|
|
42
46
|
"@convex-dev/workpool": "0.4.7",
|
|
43
47
|
"ai": "6.0.35",
|