@vantageos/agent-engine 0.1.0-alpha.4 → 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 +56 -6
- package/dist/component/_generated/component.d.ts +20 -2
- package/dist/component/engine/sessions.d.ts +2 -1
- package/dist/component/engine/sessions.js +34 -11
- package/dist/component/engine/toolApprovals.d.ts +2 -1
- package/dist/component/engine/toolApprovals.js +64 -26
- package/dist/component/schema.d.ts +2 -2
- package/dist/component/schema.js +8 -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
|
|
@@ -389,14 +410,43 @@ components.agentEngine.engine.resolvers.resolveManifest({ orgId, sessionId }) ->
|
|
|
389
410
|
components.agentEngine.engine.sessions.start({ orgId, agentId, createdBy }) -> Id<"sessions">
|
|
390
411
|
components.agentEngine.engine.sessions.sendTurn({ orgId, sessionId, input }) -> null
|
|
391
412
|
components.agentEngine.engine.sessions.appendReasoning({ orgId, sessionId, chunk }) -> null
|
|
392
|
-
components.agentEngine.engine.sessions.requestToolCall({ orgId, sessionId, toolName, argsJson }) -> { authorized, approvalPending }
|
|
413
|
+
components.agentEngine.engine.sessions.requestToolCall({ orgId, sessionId, toolName, argsJson }) -> { authorized, approvalPending, denied }
|
|
393
414
|
components.agentEngine.engine.sessions.completeToolCall({ orgId, sessionId, toolName, resultJson }) -> null
|
|
394
415
|
components.agentEngine.engine.sessions.completeTurn({ orgId, sessionId }) -> null
|
|
395
416
|
components.agentEngine.engine.sessions.listEvents({ orgId, sessionId }) -> sessionEvent[]
|
|
396
417
|
|
|
397
|
-
components.agentEngine.engine.toolApprovals.respond({ orgId, sessionId, toolName,
|
|
418
|
+
components.agentEngine.engine.toolApprovals.respond({ orgId, sessionId, toolName, decision: "approved"|"denied", updatePolicy?: "always"|"once"|"never" }) -> null
|
|
398
419
|
```
|
|
399
420
|
|
|
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
|
+
|
|
400
450
|
`createdBy` (on `agents.create` / `sessions.start`) is, for the same
|
|
401
451
|
platform reason as `orgId`, no longer read from `ctx.auth.getUserIdentity()`
|
|
402
452
|
— the consumer passes the acting user's own identifier.
|
|
@@ -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
|
};
|
|
@@ -21,6 +21,7 @@ export declare const requestToolCall: import("convex/server").RegisteredMutation
|
|
|
21
21
|
}, Promise<{
|
|
22
22
|
authorized: boolean;
|
|
23
23
|
approvalPending: boolean;
|
|
24
|
+
denied: boolean;
|
|
24
25
|
}>>;
|
|
25
26
|
export declare const completeToolCall: import("convex/server").RegisteredMutation<"public", {
|
|
26
27
|
orgId: string;
|
|
@@ -38,7 +39,7 @@ export declare const listEvents: import("convex/server").RegisteredQuery<"public
|
|
|
38
39
|
}, Promise<{
|
|
39
40
|
_id: import("convex/values").GenericId<"sessionEvents">;
|
|
40
41
|
_creationTime: number;
|
|
41
|
-
name: "session.started" | "turn.started" | "turn.reasoning" | "turn.toolCall.requested" | "turn.toolCall.completed" | "turn.completed" | "tool.approval.requested" | "tool.approval.resolved";
|
|
42
|
+
name: "session.started" | "turn.started" | "turn.reasoning" | "turn.toolCall.requested" | "turn.toolCall.completed" | "turn.toolCall.denied" | "turn.completed" | "tool.approval.requested" | "tool.approval.resolved";
|
|
42
43
|
payload: string | undefined;
|
|
43
44
|
createdAt: number;
|
|
44
45
|
}[]>>;
|
|
@@ -112,7 +112,15 @@ export const requestToolCall = mutation({
|
|
|
112
112
|
toolName: v.string(),
|
|
113
113
|
argsJson: v.string(),
|
|
114
114
|
},
|
|
115
|
-
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
|
+
}),
|
|
116
124
|
handler: async (ctx, args) => {
|
|
117
125
|
const orgId = requireOrgId(args.orgId);
|
|
118
126
|
const session = await ctx.db.get(args.sessionId);
|
|
@@ -132,7 +140,7 @@ export const requestToolCall = mutation({
|
|
|
132
140
|
.unique();
|
|
133
141
|
const approval = approvalRow?.approval ?? "never";
|
|
134
142
|
if (approval === "never") {
|
|
135
|
-
return { authorized: true, approvalPending: false };
|
|
143
|
+
return { authorized: true, approvalPending: false, denied: false };
|
|
136
144
|
}
|
|
137
145
|
if (approval === "always") {
|
|
138
146
|
await insertEvent(ctx, {
|
|
@@ -141,20 +149,35 @@ export const requestToolCall = mutation({
|
|
|
141
149
|
name: "tool.approval.requested",
|
|
142
150
|
payload: JSON.stringify({ toolName: args.toolName }),
|
|
143
151
|
});
|
|
144
|
-
return { authorized: false, approvalPending: true };
|
|
152
|
+
return { authorized: false, approvalPending: true, denied: false };
|
|
145
153
|
}
|
|
146
|
-
// approval === "once": gate unless this tool was already
|
|
147
|
-
// 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.
|
|
148
161
|
const events = await ctx.db
|
|
149
162
|
.query("sessionEvents")
|
|
150
163
|
.withIndex("by_org_session", (q) => q.eq("orgId", orgId).eq("sessionId", args.sessionId))
|
|
151
164
|
.collect();
|
|
152
|
-
const
|
|
165
|
+
const resolutionsForTool = events
|
|
166
|
+
.filter((e) => e.name === "tool.approval.resolved" &&
|
|
153
167
|
e.payload !== undefined &&
|
|
154
168
|
JSON.parse(e.payload).toolName ===
|
|
155
|
-
args.toolName)
|
|
156
|
-
|
|
157
|
-
|
|
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 };
|
|
158
181
|
}
|
|
159
182
|
await insertEvent(ctx, {
|
|
160
183
|
orgId,
|
|
@@ -162,7 +185,7 @@ export const requestToolCall = mutation({
|
|
|
162
185
|
name: "tool.approval.requested",
|
|
163
186
|
payload: JSON.stringify({ toolName: args.toolName }),
|
|
164
187
|
});
|
|
165
|
-
return { authorized: false, approvalPending: true };
|
|
188
|
+
return { authorized: false, approvalPending: true, denied: false };
|
|
166
189
|
},
|
|
167
190
|
});
|
|
168
191
|
// completeToolCall — the caller invokes this after actually running the
|
|
@@ -214,7 +237,7 @@ export const listEvents = query({
|
|
|
214
237
|
returns: v.array(v.object({
|
|
215
238
|
_id: v.id("sessionEvents"),
|
|
216
239
|
_creationTime: v.number(),
|
|
217
|
-
name: v.union(v.literal("session.started"), v.literal("turn.started"), v.literal("turn.reasoning"), v.literal("turn.toolCall.requested"), v.literal("turn.toolCall.completed"), v.literal("turn.completed"), v.literal("tool.approval.requested"), v.literal("tool.approval.resolved")),
|
|
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")),
|
|
218
241
|
payload: v.optional(v.string()),
|
|
219
242
|
createdAt: v.number(),
|
|
220
243
|
})),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export declare const respond: import("convex/server").RegisteredMutation<"public", {
|
|
2
|
+
updatePolicy?: "always" | "once" | "never" | undefined;
|
|
2
3
|
orgId: string;
|
|
3
4
|
toolName: string;
|
|
4
|
-
approval: "always" | "once" | "never";
|
|
5
5
|
sessionId: import("convex/values").GenericId<"sessions">;
|
|
6
|
+
decision: "approved" | "denied";
|
|
6
7
|
}, Promise<null>>;
|
|
@@ -2,51 +2,89 @@ import { v } from "convex/values";
|
|
|
2
2
|
import { mutation } from "../_generated/server.js";
|
|
3
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: {
|
|
12
29
|
orgId: v.string(),
|
|
13
30
|
sessionId: v.id("sessions"),
|
|
14
31
|
toolName: v.string(),
|
|
15
|
-
|
|
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"))),
|
|
16
34
|
},
|
|
17
35
|
returns: v.null(),
|
|
18
36
|
handler: async (ctx, args) => {
|
|
19
37
|
const orgId = requireOrgId(args.orgId);
|
|
20
38
|
const session = await ctx.db.get(args.sessionId);
|
|
21
39
|
assertSameOrg(orgId, session, "Session not found");
|
|
22
|
-
const existing = await ctx.db
|
|
23
|
-
.query("toolApprovals")
|
|
24
|
-
.withIndex("by_org_agent_tool", (q) => q
|
|
25
|
-
.eq("orgId", orgId)
|
|
26
|
-
.eq("agentId", session.agentId)
|
|
27
|
-
.eq("toolName", args.toolName))
|
|
28
|
-
.unique();
|
|
29
40
|
const now = Date.now();
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
+
}
|
|
42
65
|
}
|
|
43
66
|
await ctx.db.insert("sessionEvents", {
|
|
44
67
|
orgId,
|
|
45
68
|
sessionId: args.sessionId,
|
|
46
69
|
name: "tool.approval.resolved",
|
|
47
|
-
payload: JSON.stringify({
|
|
70
|
+
payload: JSON.stringify({
|
|
71
|
+
toolName: args.toolName,
|
|
72
|
+
decision: args.decision,
|
|
73
|
+
updatedPolicy: args.updatePolicy ?? null,
|
|
74
|
+
}),
|
|
48
75
|
createdAt: now,
|
|
49
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
|
+
}
|
|
50
88
|
return null;
|
|
51
89
|
},
|
|
52
90
|
});
|
|
@@ -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
|
})
|
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",
|