@vantageos/agent-engine 0.1.0-alpha.3 → 0.1.0-alpha.5
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 +120 -54
- package/dist/component/_generated/component.d.ts +20 -2
- package/dist/component/engine/agents.d.ts +3 -0
- package/dist/component/engine/agents.js +11 -9
- package/dist/component/engine/durableJob.d.ts +3 -0
- package/dist/component/engine/durableJob.js +9 -8
- package/dist/component/engine/resolvers.d.ts +1 -0
- package/dist/component/engine/resolvers.js +4 -4
- package/dist/component/engine/sessions.d.ts +10 -1
- package/dist/component/engine/sessions.js +60 -31
- package/dist/component/engine/toolApprovals.d.ts +3 -1
- package/dist/component/engine/toolApprovals.js +68 -29
- package/dist/component/lib/auth.d.ts +1 -2
- package/dist/component/lib/auth.js +28 -15
- package/dist/component/schema.d.ts +2 -2
- package/dist/component/schema.js +8 -1
- package/dist/component/testFixtures/callerSteps.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -214,10 +214,31 @@ Two gates now exist, and they check different things:
|
|
|
214
214
|
hand-edit of existing generated content.**
|
|
215
215
|
2. **Content, requires a live deployment** --
|
|
216
216
|
`scripts/check-generated-content.mjs` (`npm run
|
|
217
|
-
check:generated:content`).
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
217
|
+
check:generated:content`). Runs `npx convex codegen` against the dev
|
|
218
|
+
deployment for BOTH `convex/_generated/` (the host repo's own dev
|
|
219
|
+
deployment bindings) AND, since 0.1.0-alpha.5, `component/_generated/`
|
|
220
|
+
via `npx convex codegen --component-dir component` -- the checked-in
|
|
221
|
+
file `tsc -p tsconfig.build.json` copies verbatim into the PUBLISHED
|
|
222
|
+
`dist/component/_generated/component.d.ts`, i.e. the exact type a
|
|
223
|
+
consuming app checks `components.agentEngine.*` calls against. Diffs
|
|
224
|
+
each byte-for-byte (`git diff --exit-code`). Catches everything,
|
|
225
|
+
including a single hand-edited character.
|
|
226
|
+
|
|
227
|
+
**`component/_generated/` was NOT covered before 0.1.0-alpha.5** — that
|
|
228
|
+
gap is why `0.1.0-alpha.4` shipped a `component.d.ts` stale against the
|
|
229
|
+
real component schema (missing `orgId`/`createdBy` on
|
|
230
|
+
sessions/agents/durableJob/toolApprovals args), which broke
|
|
231
|
+
`tsc --noEmit` for any consumer calling `sessions.start`/`durableJob.start`
|
|
232
|
+
with `orgId` (Hestia, VP k17ea6x3tht8xa3rxm139t23gx8ajkf3, defaut 1).
|
|
233
|
+
The visibility (`"internal"`) on every entry is, by measured comparison
|
|
234
|
+
against `@convex-dev/rate-limiter`'s own published `component.d.ts`,
|
|
235
|
+
CORRECT and unrelated to that defect — every official Convex component
|
|
236
|
+
types its cross-component `ComponentApi` as `"internal"` regardless of
|
|
237
|
+
whether the underlying function is declared `mutation`/`query` or
|
|
238
|
+
`internalMutation`/`internalQuery`; `ctx.runMutation`'s own signature
|
|
239
|
+
(`convex/server`) accepts `FunctionReference<"mutation", "public" |
|
|
240
|
+
"internal">` either way. The root cause was staleness of the args
|
|
241
|
+
shape, not visibility.
|
|
221
242
|
|
|
222
243
|
**Three-state exit contract** (never two -- a two-state gate cannot
|
|
223
244
|
distinguish "I measured, it's clean" from "I could not measure at
|
|
@@ -321,80 +342,125 @@ app.use(agentEngine);
|
|
|
321
342
|
export default app;
|
|
322
343
|
```
|
|
323
344
|
|
|
345
|
+
### Trust model — READ THIS FIRST (CORRECTIF, VP k1741evjzm72rj91tkcbdhpskx8ah35a)
|
|
346
|
+
|
|
347
|
+
**Convex does not propagate the calling app's `ctx.auth` identity across a
|
|
348
|
+
component boundary.** This is a platform isolation guarantee, not a choice
|
|
349
|
+
this package makes (`convex-test` states it verbatim: "Auth doesn't
|
|
350
|
+
propagate across component boundaries"). A function inside this component
|
|
351
|
+
therefore can NEVER read `ctx.auth.getUserIdentity()` and expect it to
|
|
352
|
+
reflect the consumer app's end user.
|
|
353
|
+
|
|
354
|
+
**`orgId` is an explicit ARGUMENT on every public function in this
|
|
355
|
+
component.** The calling app is responsible for authenticating its user and
|
|
356
|
+
resolving that user's `orgId` on its OWN side (e.g. reading a Clerk
|
|
357
|
+
org-scoped claim from its own `ctx.auth`) BEFORE calling into this
|
|
358
|
+
component — this component cannot do that step for you, it is a platform
|
|
359
|
+
constraint, not a design preference.
|
|
360
|
+
|
|
361
|
+
What did NOT change: the component still denies by default
|
|
362
|
+
(`lib/auth.ts:requireOrgId` throws on a missing/empty `orgId`), `orgId` is
|
|
363
|
+
still the first field of every index, and every document fetched by `_id`
|
|
364
|
+
is still checked with `lib/auth.ts:assertSameOrg` before being returned or
|
|
365
|
+
mutated — a caller-supplied `orgId` can never read or write another org's
|
|
366
|
+
rows. The isolation boundary moved from "component reads `ctx.auth`"
|
|
367
|
+
(impossible across a component boundary) to "component enforces the
|
|
368
|
+
`orgId` argument the consumer already vouches for"; deny-by-default,
|
|
369
|
+
orgId-first-index, no-cross-org-reads are unchanged.
|
|
370
|
+
|
|
324
371
|
### What's callable, and what isn't (read this before wiring a consumer)
|
|
325
372
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
SESSIONS surface — EveVantage V4's conversation thread.
|
|
373
|
+
`component/` (sibling of `convex/`, NOT nested under it — see "Why
|
|
374
|
+
`component/` and not `convex/component/`" below) hosts the **actual,
|
|
375
|
+
compiled, published functions** for a real slice of the engine, not just
|
|
376
|
+
the component's identity:
|
|
331
377
|
|
|
332
378
|
- `component/schema.ts` — `durableJobs`, `agents`, `toolApprovals`,
|
|
333
379
|
`sessions`, `sessionEvents` (+ a test-only `callerStepInvocations`
|
|
334
380
|
fixture table).
|
|
335
|
-
- `component/engine/durableJob.ts` — `start` / `getStatus` / `cancel
|
|
381
|
+
- `component/engine/durableJob.ts` — `start` / `getStatus` / `cancel`
|
|
382
|
+
(public contract) + `durableJobWorkflowV1` (the versioned pause/resume/
|
|
383
|
+
replay workflow) + its internal step-runner mutations.
|
|
336
384
|
- `component/engine/agents.ts` — `create` / `get` (minimal root-sheet
|
|
337
385
|
bootstrap; no `update`/`list`/skills/connections in this pass).
|
|
338
386
|
- `component/engine/resolvers.ts` — `resolveManifest` (dynamic reread at
|
|
339
387
|
session/turn boundaries; returns `agentId`/`name`/`instructions`/`model`/
|
|
340
388
|
`toolApprovals` — `skills`/`connections` NOT included in this pass).
|
|
341
|
-
- `component/engine/sessions.ts` — the conversation thread contract
|
|
389
|
+
- `component/engine/sessions.ts` — the conversation thread contract:
|
|
390
|
+
`start` / `sendTurn` / `appendReasoning` / `requestToolCall` /
|
|
391
|
+
`completeToolCall` / `completeTurn` / `listEvents`.
|
|
342
392
|
- `component/engine/toolApprovals.ts` — `respond`.
|
|
343
|
-
- `component/lib/auth.ts` — `requireOrgId` / `assertSameOrg`:
|
|
344
|
-
|
|
345
|
-
default travels WITH the component, not just with this repo's own app.
|
|
393
|
+
- `component/lib/auth.ts` — `requireOrgId` / `assertSameOrg`: see "Trust
|
|
394
|
+
model" above.
|
|
346
395
|
|
|
347
|
-
**Callable through the published component today**
|
|
348
|
-
|
|
349
|
-
above for the exact `v.*` shapes):
|
|
396
|
+
**Callable through the published component today** — every function below
|
|
397
|
+
takes `orgId: v.string()` as an explicit argument (see the source files
|
|
398
|
+
above for the exact `v.*` shapes of the remaining args/returns):
|
|
350
399
|
|
|
351
400
|
```
|
|
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
|
|
401
|
+
components.agentEngine.engine.durableJob.start({ orgId, kind, stepFunctionHandle, totalSteps, maxAttemptsPerStep }) -> Id<"durableJobs">
|
|
402
|
+
components.agentEngine.engine.durableJob.getStatus({ orgId, jobId }) -> { status, cursor, totalSteps, lastError?, abandonedAtStep? }
|
|
403
|
+
components.agentEngine.engine.durableJob.cancel({ orgId, jobId }) -> null
|
|
355
404
|
|
|
356
|
-
components.agentEngine.engine.agents.create({ name, instructions, model }) -> Id<"agents">
|
|
357
|
-
components.agentEngine.engine.agents.get({ agentId }) -> agent | null
|
|
405
|
+
components.agentEngine.engine.agents.create({ orgId, createdBy, name, instructions, model }) -> Id<"agents">
|
|
406
|
+
components.agentEngine.engine.agents.get({ orgId, agentId }) -> agent | null
|
|
358
407
|
|
|
359
|
-
components.agentEngine.engine.resolvers.resolveManifest({ sessionId }) -> { agentId, name, instructions, model, toolApprovals[] }
|
|
408
|
+
components.agentEngine.engine.resolvers.resolveManifest({ orgId, sessionId }) -> { agentId, name, instructions, model, toolApprovals[] }
|
|
360
409
|
|
|
361
|
-
components.agentEngine.engine.sessions.start({ agentId }) -> Id<"sessions">
|
|
362
|
-
components.agentEngine.engine.sessions.sendTurn({ sessionId, input }) -> null
|
|
363
|
-
components.agentEngine.engine.sessions.appendReasoning({ sessionId, chunk }) -> null
|
|
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
|
|
367
|
-
components.agentEngine.engine.sessions.listEvents({ sessionId }) -> sessionEvent[]
|
|
410
|
+
components.agentEngine.engine.sessions.start({ orgId, agentId, createdBy }) -> Id<"sessions">
|
|
411
|
+
components.agentEngine.engine.sessions.sendTurn({ orgId, sessionId, input }) -> null
|
|
412
|
+
components.agentEngine.engine.sessions.appendReasoning({ orgId, sessionId, chunk }) -> null
|
|
413
|
+
components.agentEngine.engine.sessions.requestToolCall({ orgId, sessionId, toolName, argsJson }) -> { authorized, approvalPending, denied }
|
|
414
|
+
components.agentEngine.engine.sessions.completeToolCall({ orgId, sessionId, toolName, resultJson }) -> null
|
|
415
|
+
components.agentEngine.engine.sessions.completeTurn({ orgId, sessionId }) -> null
|
|
416
|
+
components.agentEngine.engine.sessions.listEvents({ orgId, sessionId }) -> sessionEvent[]
|
|
368
417
|
|
|
369
|
-
components.agentEngine.engine.toolApprovals.respond({ sessionId, toolName,
|
|
418
|
+
components.agentEngine.engine.toolApprovals.respond({ orgId, sessionId, toolName, decision: "approved"|"denied", updatePolicy?: "always"|"once"|"never" }) -> null
|
|
370
419
|
```
|
|
371
420
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
`tool.approval.requested`
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
`
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
`
|
|
421
|
+
### `toolApprovals.respond` — `decision` vs `updatePolicy` (0.1.0-alpha.5)
|
|
422
|
+
|
|
423
|
+
Two DISTINCT axes were conflated pre-alpha.5 (VP
|
|
424
|
+
k17ea6x3tht8xa3rxm139t23gx8ajkf3, defaut 2) — a single `approval:
|
|
425
|
+
"always"|"once"|"never"` argument was overloaded to mean both "the standing
|
|
426
|
+
policy for this tool" AND "the answer to this one pending request", and
|
|
427
|
+
there was no literal for "refuse this turn only". Fixed by splitting them:
|
|
428
|
+
|
|
429
|
+
- **`decision`** (required) — the response to THIS ONE pending
|
|
430
|
+
`tool.approval.requested` event, for THIS turn only. `"approved"` |
|
|
431
|
+
`"denied"`. **Eve parity, direct** — vocabulary matches Eve's
|
|
432
|
+
`ApprovalStatus` (`packages/eve/src/public/definitions/approval.ts`:
|
|
433
|
+
`"approved" | "denied" | "not-applicable" | "user-approval" | ...`); we
|
|
434
|
+
did not invent a `deny` literal, we aligned with the one Eve already has.
|
|
435
|
+
`"denied"` is a terminal refusal for the turn: `sessions.requestToolCall`
|
|
436
|
+
reports back `{ authorized: false, approvalPending: false, denied: true
|
|
437
|
+
}` (never re-authorized on retry within the session), and a dedicated
|
|
438
|
+
`turn.toolCall.denied` sessionEvent is appended — there is NO
|
|
439
|
+
`turn.toolCall.completed` for a denied call, i.e. the tool was never run
|
|
440
|
+
and the turn is NOT silently treated as done.
|
|
441
|
+
- **`updatePolicy`** (optional) — persists a NEW standing gate
|
|
442
|
+
(`"always"|"once"|"never"`, eve parity: `eve/tools/approval` helpers) for
|
|
443
|
+
this `(agentId, toolName)` pair going forward, independent of `decision`.
|
|
444
|
+
A human can deny this turn's call and leave the policy alone (ask again
|
|
445
|
+
next time), or approve this turn and simultaneously flip the policy to
|
|
446
|
+
`"always"`/`"never"` so future calls stop pausing. `"never"` still means
|
|
447
|
+
Eve's "never require approval" (auto-authorize) — that policy literal is
|
|
448
|
+
unchanged and is NOT the same thing as a `"denied"` decision.
|
|
449
|
+
|
|
450
|
+
`createdBy` (on `agents.create` / `sessions.start`) is, for the same
|
|
451
|
+
platform reason as `orgId`, no longer read from `ctx.auth.getUserIdentity()`
|
|
452
|
+
— the consumer passes the acting user's own identifier.
|
|
453
|
+
|
|
454
|
+
Verified two ways: (1) `component/durableJob.componentPackaging.test.ts` +
|
|
389
455
|
`component/sessions.componentPackaging.test.ts` run `convex-test` directly
|
|
390
456
|
against `component/_generated/api` (the exact source tree
|
|
391
457
|
`tsc -p tsconfig.build.json` compiles into `dist/component/`, which is
|
|
392
|
-
what `npm pack` ships)
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
`
|
|
397
|
-
transcript.
|
|
458
|
+
what `npm pack` ships), so the compiled artifact has a real green test
|
|
459
|
+
suite behind it, not just the repo-root dev app's; (2) a from-scratch
|
|
460
|
+
consumer probe (install the published tarball into a bare `npx convex dev`
|
|
461
|
+
project, register the component, call `durableJob.start` + `sessions.start`
|
|
462
|
+
from the consumer's own function, passing `orgId` explicitly) — see the
|
|
463
|
+
completion report for the verbatim transcript.
|
|
398
464
|
|
|
399
465
|
**NOT yet callable through the published component** (still only exists in
|
|
400
466
|
this repo's OWN dev/test deployment under `convex/schema.ts` +
|
|
@@ -22,12 +22,15 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
22
22
|
engine: {
|
|
23
23
|
agents: {
|
|
24
24
|
create: FunctionReference<"mutation", "internal", {
|
|
25
|
+
createdBy: string;
|
|
25
26
|
instructions: string;
|
|
26
27
|
model: string;
|
|
27
28
|
name: string;
|
|
29
|
+
orgId: string;
|
|
28
30
|
}, string, Name>;
|
|
29
31
|
get: FunctionReference<"query", "internal", {
|
|
30
32
|
agentId: string;
|
|
33
|
+
orgId: string;
|
|
31
34
|
}, {
|
|
32
35
|
_creationTime: number;
|
|
33
36
|
_id: string;
|
|
@@ -44,9 +47,11 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
44
47
|
durableJob: {
|
|
45
48
|
cancel: FunctionReference<"mutation", "internal", {
|
|
46
49
|
jobId: string;
|
|
50
|
+
orgId: string;
|
|
47
51
|
}, null, Name>;
|
|
48
52
|
getStatus: FunctionReference<"query", "internal", {
|
|
49
53
|
jobId: string;
|
|
54
|
+
orgId: string;
|
|
50
55
|
}, {
|
|
51
56
|
abandonedAtStep?: number;
|
|
52
57
|
cursor: number;
|
|
@@ -57,12 +62,14 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
57
62
|
start: FunctionReference<"mutation", "internal", {
|
|
58
63
|
kind: string;
|
|
59
64
|
maxAttemptsPerStep: number;
|
|
65
|
+
orgId: string;
|
|
60
66
|
stepFunctionHandle: string;
|
|
61
67
|
totalSteps: number;
|
|
62
68
|
}, string, Name>;
|
|
63
69
|
};
|
|
64
70
|
resolvers: {
|
|
65
71
|
resolveManifest: FunctionReference<"query", "internal", {
|
|
72
|
+
orgId: string;
|
|
66
73
|
sessionId: string;
|
|
67
74
|
}, {
|
|
68
75
|
agentId: string;
|
|
@@ -78,46 +85,57 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
78
85
|
sessions: {
|
|
79
86
|
appendReasoning: FunctionReference<"mutation", "internal", {
|
|
80
87
|
chunk: string;
|
|
88
|
+
orgId: string;
|
|
81
89
|
sessionId: string;
|
|
82
90
|
}, null, Name>;
|
|
83
91
|
completeToolCall: FunctionReference<"mutation", "internal", {
|
|
92
|
+
orgId: string;
|
|
84
93
|
resultJson: string;
|
|
85
94
|
sessionId: string;
|
|
86
95
|
toolName: string;
|
|
87
96
|
}, null, Name>;
|
|
88
97
|
completeTurn: FunctionReference<"mutation", "internal", {
|
|
98
|
+
orgId: string;
|
|
89
99
|
sessionId: string;
|
|
90
100
|
}, null, Name>;
|
|
91
101
|
listEvents: FunctionReference<"query", "internal", {
|
|
102
|
+
orgId: string;
|
|
92
103
|
sessionId: string;
|
|
93
104
|
}, Array<{
|
|
94
105
|
_creationTime: number;
|
|
95
106
|
_id: string;
|
|
96
107
|
createdAt: number;
|
|
97
|
-
name: "session.started" | "turn.started" | "turn.reasoning" | "turn.toolCall.requested" | "turn.toolCall.completed" | "turn.completed" | "tool.approval.requested" | "tool.approval.resolved";
|
|
108
|
+
name: "session.started" | "turn.started" | "turn.reasoning" | "turn.toolCall.requested" | "turn.toolCall.completed" | "turn.toolCall.denied" | "turn.completed" | "tool.approval.requested" | "tool.approval.resolved";
|
|
98
109
|
payload?: string;
|
|
99
110
|
}>, Name>;
|
|
100
111
|
requestToolCall: FunctionReference<"mutation", "internal", {
|
|
101
112
|
argsJson: string;
|
|
113
|
+
orgId: string;
|
|
102
114
|
sessionId: string;
|
|
103
115
|
toolName: string;
|
|
104
116
|
}, {
|
|
105
117
|
approvalPending: boolean;
|
|
106
118
|
authorized: boolean;
|
|
119
|
+
denied: boolean;
|
|
107
120
|
}, Name>;
|
|
108
121
|
sendTurn: FunctionReference<"mutation", "internal", {
|
|
109
122
|
input: string;
|
|
123
|
+
orgId: string;
|
|
110
124
|
sessionId: string;
|
|
111
125
|
}, null, Name>;
|
|
112
126
|
start: FunctionReference<"mutation", "internal", {
|
|
113
127
|
agentId: string;
|
|
128
|
+
createdBy: string;
|
|
129
|
+
orgId: string;
|
|
114
130
|
}, string, Name>;
|
|
115
131
|
};
|
|
116
132
|
toolApprovals: {
|
|
117
133
|
respond: FunctionReference<"mutation", "internal", {
|
|
118
|
-
|
|
134
|
+
decision: "approved" | "denied";
|
|
135
|
+
orgId: string;
|
|
119
136
|
sessionId: string;
|
|
120
137
|
toolName: string;
|
|
138
|
+
updatePolicy?: "always" | "once" | "never";
|
|
121
139
|
}, null, Name>;
|
|
122
140
|
};
|
|
123
141
|
};
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
export declare const create: import("convex/server").RegisteredMutation<"public", {
|
|
2
2
|
name: string;
|
|
3
|
+
orgId: string;
|
|
3
4
|
instructions: string;
|
|
4
5
|
model: string;
|
|
6
|
+
createdBy: string;
|
|
5
7
|
}, Promise<import("convex/values").GenericId<"agents">>>;
|
|
6
8
|
export declare const get: import("convex/server").RegisteredQuery<"public", {
|
|
9
|
+
orgId: string;
|
|
7
10
|
agentId: import("convex/values").GenericId<"agents">;
|
|
8
11
|
}, Promise<{
|
|
9
12
|
_id: import("convex/values").GenericId<"agents">;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { v } from "convex/values";
|
|
2
|
-
import { mutation, query } from "../_generated/server";
|
|
3
|
-
import { requireOrgId } from "../lib/auth";
|
|
2
|
+
import { mutation, query } from "../_generated/server.js";
|
|
3
|
+
import { requireOrgId } from "../lib/auth.js";
|
|
4
4
|
// engine/agents — MINIMAL root-sheet bootstrap for the sessions surface
|
|
5
5
|
// (T2-QUINQUIES). `sessions.start` needs an `agentId` it can read INSIDE
|
|
6
6
|
// the component boundary (component isolation: it cannot reach into a
|
|
@@ -22,16 +22,18 @@ const agentDoc = v.object({
|
|
|
22
22
|
});
|
|
23
23
|
export const create = mutation({
|
|
24
24
|
args: {
|
|
25
|
+
orgId: v.string(),
|
|
26
|
+
createdBy: v.string(),
|
|
25
27
|
name: v.string(),
|
|
26
28
|
instructions: v.string(),
|
|
27
29
|
model: v.string(),
|
|
28
30
|
},
|
|
29
31
|
returns: v.id("agents"),
|
|
30
32
|
handler: async (ctx, args) => {
|
|
31
|
-
const orgId =
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
const orgId = requireOrgId(args.orgId);
|
|
34
|
+
if (args.createdBy.trim().length === 0) {
|
|
35
|
+
throw new Error("Validation: createdBy must not be empty");
|
|
36
|
+
}
|
|
35
37
|
if (args.name.trim().length === 0) {
|
|
36
38
|
throw new Error("Validation: name must not be empty");
|
|
37
39
|
}
|
|
@@ -48,17 +50,17 @@ export const create = mutation({
|
|
|
48
50
|
instructions: args.instructions,
|
|
49
51
|
model: args.model,
|
|
50
52
|
status: "live",
|
|
51
|
-
createdBy:
|
|
53
|
+
createdBy: args.createdBy,
|
|
52
54
|
createdAt: now,
|
|
53
55
|
updatedAt: now,
|
|
54
56
|
});
|
|
55
57
|
},
|
|
56
58
|
});
|
|
57
59
|
export const get = query({
|
|
58
|
-
args: { agentId: v.id("agents") },
|
|
60
|
+
args: { orgId: v.string(), agentId: v.id("agents") },
|
|
59
61
|
returns: v.union(agentDoc, v.null()),
|
|
60
62
|
handler: async (ctx, args) => {
|
|
61
|
-
const orgId =
|
|
63
|
+
const orgId = requireOrgId(args.orgId);
|
|
62
64
|
const doc = await ctx.db.get(args.agentId);
|
|
63
65
|
if (!doc || doc.orgId !== orgId)
|
|
64
66
|
return null;
|
|
@@ -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")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { v } from "convex/values";
|
|
2
|
-
import { query } from "../_generated/server";
|
|
3
|
-
import { assertSameOrg, requireOrgId } from "../lib/auth";
|
|
2
|
+
import { query } from "../_generated/server.js";
|
|
3
|
+
import { assertSameOrg, requireOrgId } from "../lib/auth.js";
|
|
4
4
|
// resolveManifest — the dynamic resolver (eve parity:
|
|
5
5
|
// `dynamicInstructions`/`dynamicSkills`/`dynamicTools`,
|
|
6
6
|
// packages/eve/src/compiler/manifest.ts:722-725). Produced FRESH from the
|
|
@@ -15,7 +15,7 @@ import { assertSameOrg, requireOrgId } from "../lib/auth";
|
|
|
15
15
|
// component/schema.ts SCOPE note); this resolver returns only what the
|
|
16
16
|
// component boundary currently owns.
|
|
17
17
|
export const resolveManifest = query({
|
|
18
|
-
args: { sessionId: v.id("sessions") },
|
|
18
|
+
args: { orgId: v.string(), sessionId: v.id("sessions") },
|
|
19
19
|
returns: v.object({
|
|
20
20
|
agentId: v.id("agents"),
|
|
21
21
|
name: v.string(),
|
|
@@ -27,7 +27,7 @@ export const resolveManifest = query({
|
|
|
27
27
|
})),
|
|
28
28
|
}),
|
|
29
29
|
handler: async (ctx, args) => {
|
|
30
|
-
const orgId =
|
|
30
|
+
const orgId = requireOrgId(args.orgId);
|
|
31
31
|
const session = await ctx.db.get(args.sessionId);
|
|
32
32
|
assertSameOrg(orgId, session, "Session not found");
|
|
33
33
|
const agent = await ctx.db.get(session.agentId);
|
|
@@ -1,36 +1,45 @@
|
|
|
1
1
|
export declare const start: import("convex/server").RegisteredMutation<"public", {
|
|
2
|
+
orgId: string;
|
|
3
|
+
createdBy: string;
|
|
2
4
|
agentId: import("convex/values").GenericId<"agents">;
|
|
3
5
|
}, Promise<import("convex/values").GenericId<"sessions">>>;
|
|
4
6
|
export declare const sendTurn: import("convex/server").RegisteredMutation<"public", {
|
|
7
|
+
orgId: string;
|
|
5
8
|
sessionId: import("convex/values").GenericId<"sessions">;
|
|
6
9
|
input: string;
|
|
7
10
|
}, Promise<null>>;
|
|
8
11
|
export declare const appendReasoning: import("convex/server").RegisteredMutation<"public", {
|
|
12
|
+
orgId: string;
|
|
9
13
|
sessionId: import("convex/values").GenericId<"sessions">;
|
|
10
14
|
chunk: string;
|
|
11
15
|
}, Promise<null>>;
|
|
12
16
|
export declare const requestToolCall: import("convex/server").RegisteredMutation<"public", {
|
|
17
|
+
orgId: string;
|
|
13
18
|
toolName: string;
|
|
14
19
|
sessionId: import("convex/values").GenericId<"sessions">;
|
|
15
20
|
argsJson: string;
|
|
16
21
|
}, Promise<{
|
|
17
22
|
authorized: boolean;
|
|
18
23
|
approvalPending: boolean;
|
|
24
|
+
denied: boolean;
|
|
19
25
|
}>>;
|
|
20
26
|
export declare const completeToolCall: import("convex/server").RegisteredMutation<"public", {
|
|
27
|
+
orgId: string;
|
|
21
28
|
toolName: string;
|
|
22
29
|
sessionId: import("convex/values").GenericId<"sessions">;
|
|
23
30
|
resultJson: string;
|
|
24
31
|
}, Promise<null>>;
|
|
25
32
|
export declare const completeTurn: import("convex/server").RegisteredMutation<"public", {
|
|
33
|
+
orgId: string;
|
|
26
34
|
sessionId: import("convex/values").GenericId<"sessions">;
|
|
27
35
|
}, Promise<null>>;
|
|
28
36
|
export declare const listEvents: import("convex/server").RegisteredQuery<"public", {
|
|
37
|
+
orgId: string;
|
|
29
38
|
sessionId: import("convex/values").GenericId<"sessions">;
|
|
30
39
|
}, Promise<{
|
|
31
40
|
_id: import("convex/values").GenericId<"sessionEvents">;
|
|
32
41
|
_creationTime: number;
|
|
33
|
-
name: "session.started" | "turn.started" | "turn.reasoning" | "turn.toolCall.requested" | "turn.toolCall.completed" | "turn.completed" | "tool.approval.requested" | "tool.approval.resolved";
|
|
42
|
+
name: "session.started" | "turn.started" | "turn.reasoning" | "turn.toolCall.requested" | "turn.toolCall.completed" | "turn.toolCall.denied" | "turn.completed" | "tool.approval.requested" | "tool.approval.resolved";
|
|
34
43
|
payload: string | undefined;
|
|
35
44
|
createdAt: number;
|
|
36
45
|
}[]>>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
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";
|
|
2
|
+
import { api } from "../_generated/api.js";
|
|
3
|
+
import { mutation, query } from "../_generated/server.js";
|
|
4
|
+
import { assertSameOrg, requireOrgId } from "../lib/auth.js";
|
|
5
5
|
// engine/sessions — THE FIL DE CONVERSATION contract (T2-QUINQUIES). This
|
|
6
6
|
// is what unblocks EveVantage V4: open a session, send a turn, stream
|
|
7
7
|
// reasoning/tool calls, gate on tool approval, replay the thread on
|
|
@@ -26,13 +26,13 @@ async function insertEvent(ctx, args) {
|
|
|
26
26
|
// start — opens a session (session.started boundary), rereading the
|
|
27
27
|
// manifest immediately so a caller can act on it before the first turn.
|
|
28
28
|
export const start = mutation({
|
|
29
|
-
args: { agentId: v.id("agents") },
|
|
29
|
+
args: { orgId: v.string(), agentId: v.id("agents"), createdBy: v.string() },
|
|
30
30
|
returns: v.id("sessions"),
|
|
31
31
|
handler: async (ctx, args) => {
|
|
32
|
-
const orgId =
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
36
|
const agent = await ctx.db.get(args.agentId);
|
|
37
37
|
assertSameOrg(orgId, agent, "Agent not found");
|
|
38
38
|
const now = Date.now();
|
|
@@ -40,12 +40,15 @@ export const start = mutation({
|
|
|
40
40
|
orgId,
|
|
41
41
|
agentId: args.agentId,
|
|
42
42
|
status: "running",
|
|
43
|
-
createdBy:
|
|
43
|
+
createdBy: args.createdBy,
|
|
44
44
|
createdAt: now,
|
|
45
45
|
updatedAt: now,
|
|
46
46
|
});
|
|
47
47
|
await insertEvent(ctx, { orgId, sessionId, name: "session.started" });
|
|
48
|
-
await ctx.runQuery(api.engine.resolvers.resolveManifest, {
|
|
48
|
+
await ctx.runQuery(api.engine.resolvers.resolveManifest, {
|
|
49
|
+
orgId,
|
|
50
|
+
sessionId,
|
|
51
|
+
});
|
|
49
52
|
return sessionId;
|
|
50
53
|
},
|
|
51
54
|
});
|
|
@@ -55,10 +58,10 @@ export const start = mutation({
|
|
|
55
58
|
// this component does not interpret it -- it is stored verbatim as the
|
|
56
59
|
// event payload for replay.
|
|
57
60
|
export const sendTurn = mutation({
|
|
58
|
-
args: { sessionId: v.id("sessions"), input: v.string() },
|
|
61
|
+
args: { orgId: v.string(), sessionId: v.id("sessions"), input: v.string() },
|
|
59
62
|
returns: v.null(),
|
|
60
63
|
handler: async (ctx, args) => {
|
|
61
|
-
const orgId =
|
|
64
|
+
const orgId = requireOrgId(args.orgId);
|
|
62
65
|
const session = await ctx.db.get(args.sessionId);
|
|
63
66
|
assertSameOrg(orgId, session, "Session not found");
|
|
64
67
|
if (session.status !== "running") {
|
|
@@ -71,6 +74,7 @@ export const sendTurn = mutation({
|
|
|
71
74
|
payload: args.input,
|
|
72
75
|
});
|
|
73
76
|
await ctx.runQuery(api.engine.resolvers.resolveManifest, {
|
|
77
|
+
orgId,
|
|
74
78
|
sessionId: args.sessionId,
|
|
75
79
|
});
|
|
76
80
|
await ctx.db.patch(args.sessionId, { updatedAt: Date.now() });
|
|
@@ -81,10 +85,10 @@ export const sendTurn = mutation({
|
|
|
81
85
|
// on `listEvents` is the streaming transport: each insert here is pushed to
|
|
82
86
|
// every subscriber live, no separate channel needed.
|
|
83
87
|
export const appendReasoning = mutation({
|
|
84
|
-
args: { sessionId: v.id("sessions"), chunk: v.string() },
|
|
88
|
+
args: { orgId: v.string(), sessionId: v.id("sessions"), chunk: v.string() },
|
|
85
89
|
returns: v.null(),
|
|
86
90
|
handler: async (ctx, args) => {
|
|
87
|
-
const orgId =
|
|
91
|
+
const orgId = requireOrgId(args.orgId);
|
|
88
92
|
const session = await ctx.db.get(args.sessionId);
|
|
89
93
|
assertSameOrg(orgId, session, "Session not found");
|
|
90
94
|
await insertEvent(ctx, {
|
|
@@ -103,13 +107,22 @@ export const appendReasoning = mutation({
|
|
|
103
107
|
// `toolApprovals.respond`.
|
|
104
108
|
export const requestToolCall = mutation({
|
|
105
109
|
args: {
|
|
110
|
+
orgId: v.string(),
|
|
106
111
|
sessionId: v.id("sessions"),
|
|
107
112
|
toolName: v.string(),
|
|
108
113
|
argsJson: v.string(),
|
|
109
114
|
},
|
|
110
|
-
returns: v.object({
|
|
115
|
+
returns: v.object({
|
|
116
|
+
authorized: v.boolean(),
|
|
117
|
+
approvalPending: v.boolean(),
|
|
118
|
+
// Explicit terminal refusal for THIS turn (VP
|
|
119
|
+
// k17ea6x3tht8xa3rxm139t23gx8ajkf3, defaut 2). Distinct from
|
|
120
|
+
// `approvalPending: false, authorized: true` (approved/never-gated)
|
|
121
|
+
// -- a caller must check this before treating the call as skippable.
|
|
122
|
+
denied: v.boolean(),
|
|
123
|
+
}),
|
|
111
124
|
handler: async (ctx, args) => {
|
|
112
|
-
const orgId =
|
|
125
|
+
const orgId = requireOrgId(args.orgId);
|
|
113
126
|
const session = await ctx.db.get(args.sessionId);
|
|
114
127
|
assertSameOrg(orgId, session, "Session not found");
|
|
115
128
|
await insertEvent(ctx, {
|
|
@@ -127,7 +140,7 @@ export const requestToolCall = mutation({
|
|
|
127
140
|
.unique();
|
|
128
141
|
const approval = approvalRow?.approval ?? "never";
|
|
129
142
|
if (approval === "never") {
|
|
130
|
-
return { authorized: true, approvalPending: false };
|
|
143
|
+
return { authorized: true, approvalPending: false, denied: false };
|
|
131
144
|
}
|
|
132
145
|
if (approval === "always") {
|
|
133
146
|
await insertEvent(ctx, {
|
|
@@ -136,20 +149,35 @@ export const requestToolCall = mutation({
|
|
|
136
149
|
name: "tool.approval.requested",
|
|
137
150
|
payload: JSON.stringify({ toolName: args.toolName }),
|
|
138
151
|
});
|
|
139
|
-
return { authorized: false, approvalPending: true };
|
|
152
|
+
return { authorized: false, approvalPending: true, denied: false };
|
|
140
153
|
}
|
|
141
|
-
// approval === "once": gate unless this tool was already
|
|
142
|
-
// in this session.
|
|
154
|
+
// approval === "once": gate unless this tool's approval was already
|
|
155
|
+
// resolved with `decision: "approved"` in this session. A prior
|
|
156
|
+
// `decision: "denied"` resolution must NOT be treated as
|
|
157
|
+
// authorization (that was the pre-alpha.5 bug -- "outil sauté, tour
|
|
158
|
+
// complété quand même") -- it is reported back as a terminal refusal
|
|
159
|
+
// instead, and the caller re-requests (a fresh human decision) rather
|
|
160
|
+
// than silently proceeding.
|
|
143
161
|
const events = await ctx.db
|
|
144
162
|
.query("sessionEvents")
|
|
145
163
|
.withIndex("by_org_session", (q) => q.eq("orgId", orgId).eq("sessionId", args.sessionId))
|
|
146
164
|
.collect();
|
|
147
|
-
const
|
|
165
|
+
const resolutionsForTool = events
|
|
166
|
+
.filter((e) => e.name === "tool.approval.resolved" &&
|
|
148
167
|
e.payload !== undefined &&
|
|
149
168
|
JSON.parse(e.payload).toolName ===
|
|
150
|
-
args.toolName)
|
|
151
|
-
|
|
152
|
-
|
|
169
|
+
args.toolName)
|
|
170
|
+
.sort((a, b) => a.createdAt - b.createdAt);
|
|
171
|
+
const latestResolution = resolutionsForTool[resolutionsForTool.length - 1];
|
|
172
|
+
const latestDecision = latestResolution?.payload
|
|
173
|
+
? JSON.parse(latestResolution.payload)
|
|
174
|
+
.decision
|
|
175
|
+
: undefined;
|
|
176
|
+
if (latestDecision === "approved") {
|
|
177
|
+
return { authorized: true, approvalPending: false, denied: false };
|
|
178
|
+
}
|
|
179
|
+
if (latestDecision === "denied") {
|
|
180
|
+
return { authorized: false, approvalPending: false, denied: true };
|
|
153
181
|
}
|
|
154
182
|
await insertEvent(ctx, {
|
|
155
183
|
orgId,
|
|
@@ -157,7 +185,7 @@ export const requestToolCall = mutation({
|
|
|
157
185
|
name: "tool.approval.requested",
|
|
158
186
|
payload: JSON.stringify({ toolName: args.toolName }),
|
|
159
187
|
});
|
|
160
|
-
return { authorized: false, approvalPending: true };
|
|
188
|
+
return { authorized: false, approvalPending: true, denied: false };
|
|
161
189
|
},
|
|
162
190
|
});
|
|
163
191
|
// completeToolCall — the caller invokes this after actually running the
|
|
@@ -165,13 +193,14 @@ export const requestToolCall = mutation({
|
|
|
165
193
|
// the gate).
|
|
166
194
|
export const completeToolCall = mutation({
|
|
167
195
|
args: {
|
|
196
|
+
orgId: v.string(),
|
|
168
197
|
sessionId: v.id("sessions"),
|
|
169
198
|
toolName: v.string(),
|
|
170
199
|
resultJson: v.string(),
|
|
171
200
|
},
|
|
172
201
|
returns: v.null(),
|
|
173
202
|
handler: async (ctx, args) => {
|
|
174
|
-
const orgId =
|
|
203
|
+
const orgId = requireOrgId(args.orgId);
|
|
175
204
|
const session = await ctx.db.get(args.sessionId);
|
|
176
205
|
assertSameOrg(orgId, session, "Session not found");
|
|
177
206
|
await insertEvent(ctx, {
|
|
@@ -185,10 +214,10 @@ export const completeToolCall = mutation({
|
|
|
185
214
|
});
|
|
186
215
|
// completeTurn — closes the current turn.
|
|
187
216
|
export const completeTurn = mutation({
|
|
188
|
-
args: { sessionId: v.id("sessions") },
|
|
217
|
+
args: { orgId: v.string(), sessionId: v.id("sessions") },
|
|
189
218
|
returns: v.null(),
|
|
190
219
|
handler: async (ctx, args) => {
|
|
191
|
-
const orgId =
|
|
220
|
+
const orgId = requireOrgId(args.orgId);
|
|
192
221
|
const session = await ctx.db.get(args.sessionId);
|
|
193
222
|
assertSameOrg(orgId, session, "Session not found");
|
|
194
223
|
await insertEvent(ctx, {
|
|
@@ -204,16 +233,16 @@ export const completeTurn = mutation({
|
|
|
204
233
|
// session. A client that drops and reconnects calls this once to rebuild
|
|
205
234
|
// the thread, then subscribes (Convex reactivity) for anything after.
|
|
206
235
|
export const listEvents = query({
|
|
207
|
-
args: { sessionId: v.id("sessions") },
|
|
236
|
+
args: { orgId: v.string(), sessionId: v.id("sessions") },
|
|
208
237
|
returns: v.array(v.object({
|
|
209
238
|
_id: v.id("sessionEvents"),
|
|
210
239
|
_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")),
|
|
240
|
+
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.toolCall.denied"), v.literal("turn.completed"), v.literal("tool.approval.requested"), v.literal("tool.approval.resolved")),
|
|
212
241
|
payload: v.optional(v.string()),
|
|
213
242
|
createdAt: v.number(),
|
|
214
243
|
})),
|
|
215
244
|
handler: async (ctx, args) => {
|
|
216
|
-
const orgId =
|
|
245
|
+
const orgId = requireOrgId(args.orgId);
|
|
217
246
|
const session = await ctx.db.get(args.sessionId);
|
|
218
247
|
assertSameOrg(orgId, session, "Session not found");
|
|
219
248
|
const events = await ctx.db
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export declare const respond: import("convex/server").RegisteredMutation<"public", {
|
|
2
|
+
updatePolicy?: "always" | "once" | "never" | undefined;
|
|
3
|
+
orgId: string;
|
|
2
4
|
toolName: string;
|
|
3
|
-
approval: "always" | "once" | "never";
|
|
4
5
|
sessionId: import("convex/values").GenericId<"sessions">;
|
|
6
|
+
decision: "approved" | "denied";
|
|
5
7
|
}, Promise<null>>;
|
|
@@ -1,51 +1,90 @@
|
|
|
1
1
|
import { v } from "convex/values";
|
|
2
|
-
import { mutation } from "../_generated/server";
|
|
3
|
-
import { assertSameOrg, requireOrgId } from "../lib/auth";
|
|
2
|
+
import { mutation } from "../_generated/server.js";
|
|
3
|
+
import { assertSameOrg, requireOrgId } from "../lib/auth.js";
|
|
4
4
|
// engine/toolApprovals — respond to a pending approval request
|
|
5
|
-
// (`tool.approval.requested`, raised by `sessions.requestToolCall`).
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
5
|
+
// (`tool.approval.requested`, raised by `sessions.requestToolCall`).
|
|
6
|
+
//
|
|
7
|
+
// TWO DISTINCT AXES (VP k17ea6x3tht8xa3rxm139t23gx8ajkf3, defaut 2 --
|
|
8
|
+
// these were conflated pre-alpha.5, which is the defect this fixes):
|
|
9
|
+
//
|
|
10
|
+
// 1. `decision` -- the RESPONSE to THIS ONE pending approval request,
|
|
11
|
+
// for THIS turn only. Vocabulary aligned with Eve's `ApprovalStatus`
|
|
12
|
+
// (packages/eve/src/public/definitions/approval.ts: "approved" |
|
|
13
|
+
// "denied" | ...) -- eve parity, cited verbatim, not "notre ligne".
|
|
14
|
+
// `"denied"` means: this turn's tool call is refused, full stop. It
|
|
15
|
+
// does NOT change the agent's standing policy for the tool, and it
|
|
16
|
+
// must NOT be interpreted as "tool skipped, turn completed anyway" --
|
|
17
|
+
// it appends a `turn.toolCall.denied` sessionEvent instead of letting
|
|
18
|
+
// the caller silently proceed to `completeToolCall`.
|
|
19
|
+
//
|
|
20
|
+
// 2. `updatePolicy` -- optional, persists a NEW standing policy
|
|
21
|
+
// (always | once | never, eve parity: eve/tools/approval helpers) for
|
|
22
|
+
// this (agent, tool) pair going forward, upserted on
|
|
23
|
+
// `by_org_agent_tool`. Independent of `decision`: a human can deny
|
|
24
|
+
// this turn's call AND leave the policy untouched (ask again next
|
|
25
|
+
// time), or approve this turn AND simultaneously flip the policy to
|
|
26
|
+
// "always"/"never" so future calls don't pause.
|
|
10
27
|
export const respond = mutation({
|
|
11
28
|
args: {
|
|
29
|
+
orgId: v.string(),
|
|
12
30
|
sessionId: v.id("sessions"),
|
|
13
31
|
toolName: v.string(),
|
|
14
|
-
|
|
32
|
+
decision: v.union(v.literal("approved"), v.literal("denied")),
|
|
33
|
+
updatePolicy: v.optional(v.union(v.literal("always"), v.literal("once"), v.literal("never"))),
|
|
15
34
|
},
|
|
16
35
|
returns: v.null(),
|
|
17
36
|
handler: async (ctx, args) => {
|
|
18
|
-
const orgId =
|
|
37
|
+
const orgId = requireOrgId(args.orgId);
|
|
19
38
|
const session = await ctx.db.get(args.sessionId);
|
|
20
39
|
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
40
|
const now = Date.now();
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
if (args.updatePolicy !== undefined) {
|
|
42
|
+
const existing = await ctx.db
|
|
43
|
+
.query("toolApprovals")
|
|
44
|
+
.withIndex("by_org_agent_tool", (q) => q
|
|
45
|
+
.eq("orgId", orgId)
|
|
46
|
+
.eq("agentId", session.agentId)
|
|
47
|
+
.eq("toolName", args.toolName))
|
|
48
|
+
.unique();
|
|
49
|
+
if (existing) {
|
|
50
|
+
await ctx.db.patch(existing._id, {
|
|
51
|
+
approval: args.updatePolicy,
|
|
52
|
+
updatedAt: now,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
await ctx.db.insert("toolApprovals", {
|
|
57
|
+
orgId,
|
|
58
|
+
agentId: session.agentId,
|
|
59
|
+
toolName: args.toolName,
|
|
60
|
+
approval: args.updatePolicy,
|
|
61
|
+
createdAt: now,
|
|
62
|
+
updatedAt: now,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
41
65
|
}
|
|
42
66
|
await ctx.db.insert("sessionEvents", {
|
|
43
67
|
orgId,
|
|
44
68
|
sessionId: args.sessionId,
|
|
45
69
|
name: "tool.approval.resolved",
|
|
46
|
-
payload: JSON.stringify({
|
|
70
|
+
payload: JSON.stringify({
|
|
71
|
+
toolName: args.toolName,
|
|
72
|
+
decision: args.decision,
|
|
73
|
+
updatedPolicy: args.updatePolicy ?? null,
|
|
74
|
+
}),
|
|
47
75
|
createdAt: now,
|
|
48
76
|
});
|
|
77
|
+
if (args.decision === "denied") {
|
|
78
|
+
// Explicit refusal state for THIS turn's tool call -- never a
|
|
79
|
+
// silent "turn.toolCall.completed". See module doc above.
|
|
80
|
+
await ctx.db.insert("sessionEvents", {
|
|
81
|
+
orgId,
|
|
82
|
+
sessionId: args.sessionId,
|
|
83
|
+
name: "turn.toolCall.denied",
|
|
84
|
+
payload: JSON.stringify({ toolName: args.toolName }),
|
|
85
|
+
createdAt: now,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
49
88
|
return null;
|
|
50
89
|
},
|
|
51
90
|
});
|
|
@@ -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.
|
|
@@ -106,14 +106,14 @@ declare const _default: import("convex/server").SchemaDefinition<{
|
|
|
106
106
|
}, {}, {}>;
|
|
107
107
|
sessionEvents: import("convex/server").TableDefinition<import("convex/values").VObject<{
|
|
108
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";
|
|
109
|
+
name: "session.started" | "turn.started" | "turn.reasoning" | "turn.toolCall.requested" | "turn.toolCall.completed" | "turn.toolCall.denied" | "turn.completed" | "tool.approval.requested" | "tool.approval.resolved";
|
|
110
110
|
orgId: string;
|
|
111
111
|
createdAt: number;
|
|
112
112
|
sessionId: import("convex/values").GenericId<"sessions">;
|
|
113
113
|
}, {
|
|
114
114
|
orgId: import("convex/values").VString<string, "required">;
|
|
115
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>;
|
|
116
|
+
name: import("convex/values").VUnion<"session.started" | "turn.started" | "turn.reasoning" | "turn.toolCall.requested" | "turn.toolCall.completed" | "turn.toolCall.denied" | "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.toolCall.denied", "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
117
|
payload: import("convex/values").VString<string | undefined, "optional">;
|
|
118
118
|
createdAt: import("convex/values").VFloat64<number, "required">;
|
|
119
119
|
}, "required", "name" | "orgId" | "createdAt" | "sessionId" | "payload">, {
|
package/dist/component/schema.js
CHANGED
|
@@ -121,7 +121,14 @@ export default defineSchema({
|
|
|
121
121
|
sessionEvents: defineTable({
|
|
122
122
|
orgId: v.string(),
|
|
123
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"),
|
|
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"),
|
|
125
|
+
// "notre ligne" (VP k17ea6x3tht8xa3rxm139t23gx8ajkf3, defaut 2):
|
|
126
|
+
// explicit terminal state for a one-turn approval REFUSAL, distinct
|
|
127
|
+
// from "turn.toolCall.completed" (tool actually ran). Without this,
|
|
128
|
+
// a denied tool call had no event of its own -- a consumer had no
|
|
129
|
+
// way to tell "refused this turn" from "skipped, turn completed
|
|
130
|
+
// anyway", which is the wrong default Hestia had to code around.
|
|
131
|
+
v.literal("turn.toolCall.denied"), v.literal("turn.completed"), v.literal("tool.approval.requested"), v.literal("tool.approval.resolved")),
|
|
125
132
|
payload: v.optional(v.string()), // JSON-serialized, free contract per event name
|
|
126
133
|
createdAt: v.number(),
|
|
127
134
|
})
|
|
@@ -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.5",
|
|
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",
|