@vantageos/agent-engine 0.1.0-alpha.2 → 0.1.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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 table (dynamic cron per agent/org, `@convex-dev/crons`) | **notre ligne (naming)** — Eve calls these schedules/hooks; we call them habits (product decision) and back them with runtime-registered crons instead of static config |
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 naming)
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
@@ -247,42 +326,88 @@ export default app;
247
326
  Since T2-QUATER, `component/` (sibling of `convex/`, NOT nested under it —
248
327
  see "Why `component/` and not `convex/component/`" below) hosts the
249
328
  **actual, compiled, published functions** for a real slice of the engine,
250
- not just the component's identity:
251
-
252
- - `component/schema.ts` — the `durableJobs` table (+ a test-only
253
- `callerStepInvocations` fixture table).
254
- - `component/engine/durableJob.ts` — `start` / `getStatus` / `cancel`
255
- (public contract) + `durableJobWorkflowV1` (the versioned pause/resume/
256
- replay workflow) + its internal step-runner mutations.
329
+ not just the component's identity. T2-QUINQUIES widens this to the
330
+ SESSIONS surface — EveVantage V4's conversation thread.
331
+
332
+ - `component/schema.ts` — `durableJobs`, `agents`, `toolApprovals`,
333
+ `sessions`, `sessionEvents` (+ a test-only `callerStepInvocations`
334
+ fixture table).
335
+ - `component/engine/durableJob.ts` — `start` / `getStatus` / `cancel`.
336
+ - `component/engine/agents.ts` — `create` / `get` (minimal root-sheet
337
+ bootstrap; no `update`/`list`/skills/connections in this pass).
338
+ - `component/engine/resolvers.ts` — `resolveManifest` (dynamic reread at
339
+ session/turn boundaries; returns `agentId`/`name`/`instructions`/`model`/
340
+ `toolApprovals` — `skills`/`connections` NOT included in this pass).
341
+ - `component/engine/sessions.ts` — the conversation thread contract.
342
+ - `component/engine/toolApprovals.ts` — `respond`.
257
343
  - `component/lib/auth.ts` — `requireOrgId` / `assertSameOrg`: orgId is
258
344
  derived server-side and is the first field of every index; deny by
259
345
  default travels WITH the component, not just with this repo's own app.
260
346
 
261
- **Callable through the published component today:**
262
- `components.agentEngine.engine.durableJob.start(...)`,
263
- `.getStatus(...)`, `.cancel(...)` — verified two ways: (1)
264
- `component/durableJob.componentPackaging.test.ts` runs `convex-test`
265
- directly against `component/_generated/api` (the exact source tree
347
+ **Callable through the published component today** (function signatures —
348
+ all args/returns are explicit Convex validators, see the source files
349
+ above for the exact `v.*` shapes):
350
+
351
+ ```
352
+ components.agentEngine.engine.durableJob.start({ kind, stepFunctionHandle, totalSteps, maxAttemptsPerStep }) -> Id<"durableJobs">
353
+ components.agentEngine.engine.durableJob.getStatus({ jobId }) -> { status, cursor, totalSteps, lastError?, abandonedAtStep? }
354
+ components.agentEngine.engine.durableJob.cancel({ jobId }) -> null
355
+
356
+ components.agentEngine.engine.agents.create({ name, instructions, model }) -> Id<"agents">
357
+ components.agentEngine.engine.agents.get({ agentId }) -> agent | null
358
+
359
+ components.agentEngine.engine.resolvers.resolveManifest({ sessionId }) -> { agentId, name, instructions, model, toolApprovals[] }
360
+
361
+ components.agentEngine.engine.sessions.start({ agentId }) -> Id<"sessions"> // session.started
362
+ components.agentEngine.engine.sessions.sendTurn({ sessionId, input }) -> null // turn.started, rereads resolveManifest
363
+ components.agentEngine.engine.sessions.appendReasoning({ sessionId, chunk }) -> null // streamed reasoning event
364
+ components.agentEngine.engine.sessions.requestToolCall({ sessionId, toolName, argsJson }) -> { authorized, approvalPending }
365
+ components.agentEngine.engine.sessions.completeToolCall({ sessionId, toolName, resultJson }) -> null
366
+ components.agentEngine.engine.sessions.completeTurn({ sessionId }) -> null // turn.completed
367
+ components.agentEngine.engine.sessions.listEvents({ sessionId }) -> sessionEvent[] // replay on reconnect (ordered by createdAt)
368
+
369
+ components.agentEngine.engine.toolApprovals.respond({ sessionId, toolName, approval: "always"|"once"|"never" }) -> null
370
+ ```
371
+
372
+ Streaming model: `sessionEvents` IS the transport. Convex query
373
+ subscriptions on `sessions.listEvents` push every new row (reasoning
374
+ chunk, tool-call lifecycle, approval lifecycle) to every subscriber live —
375
+ there is no separate streaming channel to wire. A client that reconnects
376
+ calls `listEvents` once to rebuild the whole thread, then keeps
377
+ subscribing.
378
+
379
+ Tool-approval gate (`toolApprovals.approval`, default `"never"`):
380
+ `"never"` never pauses; `"always"` pauses (raises
381
+ `tool.approval.requested`) on every `requestToolCall`; `"once"` pauses
382
+ unless `tool.approval.resolved` for that tool was already recorded EARLIER
383
+ IN THE SAME SESSION. `toolApprovals.respond` both writes the standing
384
+ policy (upsert on `orgId`+`agentId`+`toolName`) and appends
385
+ `tool.approval.resolved` for the calling session.
386
+
387
+ Verified two ways, same discipline as T2-QUATER: (1)
388
+ `component/durableJob.componentPackaging.test.ts` +
389
+ `component/sessions.componentPackaging.test.ts` run `convex-test` directly
390
+ against `component/_generated/api` (the exact source tree
266
391
  `tsc -p tsconfig.build.json` compiles into `dist/component/`, which is
267
- what `npm pack` ships), so the compiled artifact has a real green test
268
- 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
- dev` project, register the component, call `durableJob.start` +
271
- `getStatus` from the consumer's own function) — see the T2-QUATER
272
- completion report for the verbatim transcript.
273
-
274
- **NOT yet callable through the published component** (still only exists
275
- in this repo's OWN dev/test deployment under `convex/schema.ts` +
392
+ what `npm pack` ships); (2) a from-scratch consumer probe (install the
393
+ published tarball into a bare `npx convex dev` project, register the
394
+ component, call `sessions.start`/`sendTurn` from the consumer's own
395
+ function, confirm unauthenticated calls throw from the package's own
396
+ `lib/auth`) — see the T2-QUINQUIES completion report for the verbatim
397
+ transcript.
398
+
399
+ **NOT yet callable through the published component** (still only exists in
400
+ this repo's OWN dev/test deployment under `convex/schema.ts` +
276
401
  `convex/agents.ts` + `convex/skills.ts`, used by `npx convex dev` and the
277
- repo-root `convex-test` suite): the agent aggregate itself — `agents`,
278
- `skills`, `subagents`, `connections`, `habits`, `sandboxConfig`,
279
- `toolApprovals`, `modelKeys` — and the rest of the durable-session engine
280
- — `sessions`, `sessionEvents`, `awaitEventTimeouts`, `wakeableWork`,
281
- `engine/wakeup.ts`, `engine/resolvers.ts`, `engine/workflows.ts`
282
- (`sessionWorkflowV1`). Porting each of those under `component/` without a
283
- working component-level `convex-test` harness behind it would ship
284
- unverified code — Day 128 forbids that; it is deliberately left for the
285
- next, separately-scoped migration.
402
+ repo-root `convex-test` suite): `skills`, `connections`, `subagents`,
403
+ `habits`, `sandboxConfig`, `modelKeys`, `mandates`, `approvalRequests`,
404
+ `awaitEventTimeouts`, `wakeableWork`, `engine/wakeup.ts`,
405
+ `engine/workflows.ts` (`sessionWorkflowV1` — this pass's `sessions` is
406
+ deliberately NOT tied to a `@convex-dev/workflow` run; see
407
+ `component/schema.ts`'s `sessions` table comment for why). Porting each of
408
+ those under `component/` without a working component-level `convex-test`
409
+ harness behind it would ship unverified code — Day 128 forbids that; it is
410
+ deliberately left for the next, separately-scoped migration.
286
411
 
287
412
  ### Why `component/` and not `convex/component/`
288
413
 
@@ -304,9 +429,10 @@ this exact repo layout, not guessed.
304
429
 
305
430
  T1 (schema + durable session engine, PR #1) merged @
306
431
  `9f3401ba63e6e663af95e9e4f5da42bc30b3550b`. I1 (long-task survival
307
- contract, `engine/durableJob.ts`) — not merged, Eta reviews, merge on Pi
308
- token. T2-TER (npm packaging + component identity + publication) and
309
- T2-QUATER (this pass: migrated `durableJob` + its schema + `lib/auth.ts`
310
- under the actual component boundary, `component/`, with its own
311
- `_generated/` and its own green `convex-test` suite) — see "Packaging &
312
- publication" above; **not merged**.
432
+ contract, `engine/durableJob.ts`) — not merged, Eta reviews. T4 (mandates
433
+ + habits) — **not merged** — Eta reviews, merge on Pi token. T2-TER (npm
434
+ packaging + component identity + publication) and T2-QUATER (this pass:
435
+ migrated `durableJob` + its schema + `lib/auth.ts` under the actual
436
+ component boundary, `component/`, with its own `_generated/` and its own
437
+ green `convex-test` suite) — see "Packaging & publication" above; **not
438
+ 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,19 @@
1
+ export declare const create: import("convex/server").RegisteredMutation<"public", {
2
+ name: string;
3
+ instructions: string;
4
+ model: string;
5
+ }, Promise<import("convex/values").GenericId<"agents">>>;
6
+ export declare const get: import("convex/server").RegisteredQuery<"public", {
7
+ agentId: import("convex/values").GenericId<"agents">;
8
+ }, Promise<{
9
+ _id: import("convex/values").GenericId<"agents">;
10
+ _creationTime: number;
11
+ name: string;
12
+ orgId: string;
13
+ status: "draft" | "live" | "archived";
14
+ createdAt: number;
15
+ updatedAt: number;
16
+ instructions: string;
17
+ model: string;
18
+ createdBy: string;
19
+ } | null>>;
@@ -0,0 +1,67 @@
1
+ import { v } from "convex/values";
2
+ import { mutation, query } from "../_generated/server";
3
+ import { requireOrgId } from "../lib/auth";
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
+ name: v.string(),
26
+ instructions: v.string(),
27
+ model: v.string(),
28
+ },
29
+ returns: v.id("agents"),
30
+ handler: async (ctx, args) => {
31
+ const orgId = await requireOrgId(ctx);
32
+ const identity = await ctx.auth.getUserIdentity();
33
+ if (!identity)
34
+ throw new Error("Unauthenticated");
35
+ if (args.name.trim().length === 0) {
36
+ throw new Error("Validation: name must not be empty");
37
+ }
38
+ if (args.instructions.trim().length === 0) {
39
+ throw new Error("Validation: instructions must not be empty");
40
+ }
41
+ if (args.model.trim().length === 0) {
42
+ throw new Error("Validation: model must not be empty");
43
+ }
44
+ const now = Date.now();
45
+ return await ctx.db.insert("agents", {
46
+ orgId,
47
+ name: args.name,
48
+ instructions: args.instructions,
49
+ model: args.model,
50
+ status: "live",
51
+ createdBy: identity.subject,
52
+ createdAt: now,
53
+ updatedAt: now,
54
+ });
55
+ },
56
+ });
57
+ export const get = query({
58
+ args: { agentId: v.id("agents") },
59
+ returns: v.union(agentDoc, v.null()),
60
+ handler: async (ctx, args) => {
61
+ const orgId = await requireOrgId(ctx);
62
+ const doc = await ctx.db.get(args.agentId);
63
+ if (!doc || doc.orgId !== orgId)
64
+ return null;
65
+ return doc;
66
+ },
67
+ });
@@ -0,0 +1,12 @@
1
+ export declare const resolveManifest: import("convex/server").RegisteredQuery<"public", {
2
+ sessionId: import("convex/values").GenericId<"sessions">;
3
+ }, Promise<{
4
+ agentId: import("convex/values").GenericId<"agents">;
5
+ name: string;
6
+ instructions: string;
7
+ model: string;
8
+ toolApprovals: {
9
+ toolName: string;
10
+ approval: "always" | "once" | "never";
11
+ }[];
12
+ }>>;
@@ -0,0 +1,50 @@
1
+ import { v } from "convex/values";
2
+ import { query } from "../_generated/server";
3
+ import { assertSameOrg, requireOrgId } from "../lib/auth";
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: { 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 = await requireOrgId(ctx);
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,36 @@
1
+ export declare const start: import("convex/server").RegisteredMutation<"public", {
2
+ agentId: import("convex/values").GenericId<"agents">;
3
+ }, Promise<import("convex/values").GenericId<"sessions">>>;
4
+ export declare const sendTurn: import("convex/server").RegisteredMutation<"public", {
5
+ sessionId: import("convex/values").GenericId<"sessions">;
6
+ input: string;
7
+ }, Promise<null>>;
8
+ export declare const appendReasoning: import("convex/server").RegisteredMutation<"public", {
9
+ sessionId: import("convex/values").GenericId<"sessions">;
10
+ chunk: string;
11
+ }, Promise<null>>;
12
+ export declare const requestToolCall: import("convex/server").RegisteredMutation<"public", {
13
+ toolName: string;
14
+ sessionId: import("convex/values").GenericId<"sessions">;
15
+ argsJson: string;
16
+ }, Promise<{
17
+ authorized: boolean;
18
+ approvalPending: boolean;
19
+ }>>;
20
+ export declare const completeToolCall: import("convex/server").RegisteredMutation<"public", {
21
+ toolName: string;
22
+ sessionId: import("convex/values").GenericId<"sessions">;
23
+ resultJson: string;
24
+ }, Promise<null>>;
25
+ export declare const completeTurn: import("convex/server").RegisteredMutation<"public", {
26
+ sessionId: import("convex/values").GenericId<"sessions">;
27
+ }, Promise<null>>;
28
+ export declare const listEvents: import("convex/server").RegisteredQuery<"public", {
29
+ sessionId: import("convex/values").GenericId<"sessions">;
30
+ }, Promise<{
31
+ _id: import("convex/values").GenericId<"sessionEvents">;
32
+ _creationTime: number;
33
+ name: "session.started" | "turn.started" | "turn.reasoning" | "turn.toolCall.requested" | "turn.toolCall.completed" | "turn.completed" | "tool.approval.requested" | "tool.approval.resolved";
34
+ payload: string | undefined;
35
+ createdAt: number;
36
+ }[]>>;
@@ -0,0 +1,233 @@
1
+ import { v } from "convex/values";
2
+ import { api } from "../_generated/api";
3
+ import { mutation, query } from "../_generated/server";
4
+ import { assertSameOrg, requireOrgId } from "../lib/auth";
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: { agentId: v.id("agents") },
30
+ returns: v.id("sessions"),
31
+ handler: async (ctx, args) => {
32
+ const orgId = await requireOrgId(ctx);
33
+ const identity = await ctx.auth.getUserIdentity();
34
+ if (!identity)
35
+ throw new Error("Unauthenticated");
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: identity.subject,
44
+ createdAt: now,
45
+ updatedAt: now,
46
+ });
47
+ await insertEvent(ctx, { orgId, sessionId, name: "session.started" });
48
+ await ctx.runQuery(api.engine.resolvers.resolveManifest, { sessionId });
49
+ return sessionId;
50
+ },
51
+ });
52
+ // sendTurn — turn.started boundary. Rereads the manifest (eve parity: dynamic
53
+ // resolvers only allowed to reread at session.started/turn.started).
54
+ // `input` is the caller-owned turn payload (already-serialized JSON/text);
55
+ // this component does not interpret it -- it is stored verbatim as the
56
+ // event payload for replay.
57
+ export const sendTurn = mutation({
58
+ args: { sessionId: v.id("sessions"), input: v.string() },
59
+ returns: v.null(),
60
+ handler: async (ctx, args) => {
61
+ const orgId = await requireOrgId(ctx);
62
+ const session = await ctx.db.get(args.sessionId);
63
+ assertSameOrg(orgId, session, "Session not found");
64
+ if (session.status !== "running") {
65
+ throw new Error(`Session is not running (status=${session.status})`);
66
+ }
67
+ await insertEvent(ctx, {
68
+ orgId,
69
+ sessionId: args.sessionId,
70
+ name: "turn.started",
71
+ payload: args.input,
72
+ });
73
+ await ctx.runQuery(api.engine.resolvers.resolveManifest, {
74
+ sessionId: args.sessionId,
75
+ });
76
+ await ctx.db.patch(args.sessionId, { updatedAt: Date.now() });
77
+ return null;
78
+ },
79
+ });
80
+ // appendReasoning — one streamed reasoning chunk. Convex query reactivity
81
+ // on `listEvents` is the streaming transport: each insert here is pushed to
82
+ // every subscriber live, no separate channel needed.
83
+ export const appendReasoning = mutation({
84
+ args: { sessionId: v.id("sessions"), chunk: v.string() },
85
+ returns: v.null(),
86
+ handler: async (ctx, args) => {
87
+ const orgId = await requireOrgId(ctx);
88
+ const session = await ctx.db.get(args.sessionId);
89
+ assertSameOrg(orgId, session, "Session not found");
90
+ await insertEvent(ctx, {
91
+ orgId,
92
+ sessionId: args.sessionId,
93
+ name: "turn.reasoning",
94
+ payload: args.chunk,
95
+ });
96
+ return null;
97
+ },
98
+ });
99
+ // requestToolCall — gates on the agent's toolApprovals row for `toolName`
100
+ // (default "never" = no gate). "always" gates every call; "once" gates
101
+ // unless already resolved earlier in THIS session. Returns whether the
102
+ // caller may proceed immediately or must wait for
103
+ // `toolApprovals.respond`.
104
+ export const requestToolCall = mutation({
105
+ args: {
106
+ sessionId: v.id("sessions"),
107
+ toolName: v.string(),
108
+ argsJson: v.string(),
109
+ },
110
+ returns: v.object({ authorized: v.boolean(), approvalPending: v.boolean() }),
111
+ handler: async (ctx, args) => {
112
+ const orgId = await requireOrgId(ctx);
113
+ const session = await ctx.db.get(args.sessionId);
114
+ assertSameOrg(orgId, session, "Session not found");
115
+ await insertEvent(ctx, {
116
+ orgId,
117
+ sessionId: args.sessionId,
118
+ name: "turn.toolCall.requested",
119
+ payload: JSON.stringify({ toolName: args.toolName, args: args.argsJson }),
120
+ });
121
+ const approvalRow = await ctx.db
122
+ .query("toolApprovals")
123
+ .withIndex("by_org_agent_tool", (q) => q
124
+ .eq("orgId", orgId)
125
+ .eq("agentId", session.agentId)
126
+ .eq("toolName", args.toolName))
127
+ .unique();
128
+ const approval = approvalRow?.approval ?? "never";
129
+ if (approval === "never") {
130
+ return { authorized: true, approvalPending: false };
131
+ }
132
+ if (approval === "always") {
133
+ await insertEvent(ctx, {
134
+ orgId,
135
+ sessionId: args.sessionId,
136
+ name: "tool.approval.requested",
137
+ payload: JSON.stringify({ toolName: args.toolName }),
138
+ });
139
+ return { authorized: false, approvalPending: true };
140
+ }
141
+ // approval === "once": gate unless this tool was already resolved once
142
+ // in this session.
143
+ const events = await ctx.db
144
+ .query("sessionEvents")
145
+ .withIndex("by_org_session", (q) => q.eq("orgId", orgId).eq("sessionId", args.sessionId))
146
+ .collect();
147
+ const alreadyResolvedThisSession = events.some((e) => e.name === "tool.approval.resolved" &&
148
+ e.payload !== undefined &&
149
+ JSON.parse(e.payload).toolName ===
150
+ args.toolName);
151
+ if (alreadyResolvedThisSession) {
152
+ return { authorized: true, approvalPending: false };
153
+ }
154
+ await insertEvent(ctx, {
155
+ orgId,
156
+ sessionId: args.sessionId,
157
+ name: "tool.approval.requested",
158
+ payload: JSON.stringify({ toolName: args.toolName }),
159
+ });
160
+ return { authorized: false, approvalPending: true };
161
+ },
162
+ });
163
+ // completeToolCall — the caller invokes this after actually running the
164
+ // tool (authorized immediately, or after `toolApprovals.respond` cleared
165
+ // the gate).
166
+ export const completeToolCall = mutation({
167
+ args: {
168
+ sessionId: v.id("sessions"),
169
+ toolName: v.string(),
170
+ resultJson: v.string(),
171
+ },
172
+ returns: v.null(),
173
+ handler: async (ctx, args) => {
174
+ const orgId = await requireOrgId(ctx);
175
+ const session = await ctx.db.get(args.sessionId);
176
+ assertSameOrg(orgId, session, "Session not found");
177
+ await insertEvent(ctx, {
178
+ orgId,
179
+ sessionId: args.sessionId,
180
+ name: "turn.toolCall.completed",
181
+ payload: JSON.stringify({ toolName: args.toolName, result: args.resultJson }),
182
+ });
183
+ return null;
184
+ },
185
+ });
186
+ // completeTurn — closes the current turn.
187
+ export const completeTurn = mutation({
188
+ args: { sessionId: v.id("sessions") },
189
+ returns: v.null(),
190
+ handler: async (ctx, args) => {
191
+ const orgId = await requireOrgId(ctx);
192
+ const session = await ctx.db.get(args.sessionId);
193
+ assertSameOrg(orgId, session, "Session not found");
194
+ await insertEvent(ctx, {
195
+ orgId,
196
+ sessionId: args.sessionId,
197
+ name: "turn.completed",
198
+ });
199
+ await ctx.db.patch(args.sessionId, { updatedAt: Date.now() });
200
+ return null;
201
+ },
202
+ });
203
+ // listEvents — REPLAY ON RECONNECT: the full ordered event log for a
204
+ // session. A client that drops and reconnects calls this once to rebuild
205
+ // the thread, then subscribes (Convex reactivity) for anything after.
206
+ export const listEvents = query({
207
+ args: { sessionId: v.id("sessions") },
208
+ returns: v.array(v.object({
209
+ _id: v.id("sessionEvents"),
210
+ _creationTime: v.number(),
211
+ 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")),
212
+ payload: v.optional(v.string()),
213
+ createdAt: v.number(),
214
+ })),
215
+ handler: async (ctx, args) => {
216
+ const orgId = await requireOrgId(ctx);
217
+ const session = await ctx.db.get(args.sessionId);
218
+ assertSameOrg(orgId, session, "Session not found");
219
+ const events = await ctx.db
220
+ .query("sessionEvents")
221
+ .withIndex("by_org_session", (q) => q.eq("orgId", orgId).eq("sessionId", args.sessionId))
222
+ .collect();
223
+ return events
224
+ .sort((a, b) => a.createdAt - b.createdAt)
225
+ .map((e) => ({
226
+ _id: e._id,
227
+ _creationTime: e._creationTime,
228
+ name: e.name,
229
+ payload: e.payload,
230
+ createdAt: e.createdAt,
231
+ }));
232
+ },
233
+ });
@@ -0,0 +1,5 @@
1
+ export declare const respond: import("convex/server").RegisteredMutation<"public", {
2
+ toolName: string;
3
+ approval: "always" | "once" | "never";
4
+ sessionId: import("convex/values").GenericId<"sessions">;
5
+ }, Promise<null>>;
@@ -0,0 +1,51 @@
1
+ import { v } from "convex/values";
2
+ import { mutation } from "../_generated/server";
3
+ import { assertSameOrg, requireOrgId } from "../lib/auth";
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
+ sessionId: v.id("sessions"),
13
+ toolName: v.string(),
14
+ approval: v.union(v.literal("always"), v.literal("once"), v.literal("never")),
15
+ },
16
+ returns: v.null(),
17
+ handler: async (ctx, args) => {
18
+ const orgId = await requireOrgId(ctx);
19
+ const session = await ctx.db.get(args.sessionId);
20
+ assertSameOrg(orgId, session, "Session not found");
21
+ const existing = await ctx.db
22
+ .query("toolApprovals")
23
+ .withIndex("by_org_agent_tool", (q) => q
24
+ .eq("orgId", orgId)
25
+ .eq("agentId", session.agentId)
26
+ .eq("toolName", args.toolName))
27
+ .unique();
28
+ const now = Date.now();
29
+ if (existing) {
30
+ await ctx.db.patch(existing._id, { approval: args.approval, updatedAt: now });
31
+ }
32
+ else {
33
+ await ctx.db.insert("toolApprovals", {
34
+ orgId,
35
+ agentId: session.agentId,
36
+ toolName: args.toolName,
37
+ approval: args.approval,
38
+ createdAt: now,
39
+ updatedAt: now,
40
+ });
41
+ }
42
+ await ctx.db.insert("sessionEvents", {
43
+ orgId,
44
+ sessionId: args.sessionId,
45
+ name: "tool.approval.resolved",
46
+ payload: JSON.stringify({ toolName: args.toolName, approval: args.approval }),
47
+ createdAt: now,
48
+ });
49
+ return null;
50
+ },
51
+ });
@@ -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;
@@ -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, deliberately narrow — see README "What's callable"):
11
- // only the tables `engine/durableJob.ts` requires. The full agent
12
- // aggregate (agents, skills, subagents, connections, habits,
13
- // sandboxConfig, toolApprovals, modelKeys, sessions, sessionEvents,
14
- // awaitEventTimeouts, wakeableWork, wakeupDemoResults) is NOT yet
15
- // re-hosted under this component boundary — that is the next, separately
16
- // scoped migration. Porting the durableJob engine first unblocks
17
- // VantagePeers/Sigma's I1 need (durable long-task survival) without
18
- // widening the surface being shipped-and-verified beyond what has an
19
- // actual working component-level test harness behind it (Day 128: never
20
- // ship unverified).
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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vantageos/agent-engine",
3
- "version": "0.1.0-alpha.2",
3
+ "version": "0.1.0-alpha.3",
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",