@vantageos/agent-engine 0.1.0-alpha.1 → 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 +198 -17
- package/dist/component/_generated/api.d.ts +50 -0
- package/dist/component/_generated/api.js +30 -0
- package/dist/component/_generated/component.d.ts +124 -0
- package/dist/component/_generated/component.js +10 -0
- package/dist/component/_generated/dataModel.d.ts +45 -0
- package/dist/component/_generated/dataModel.js +10 -0
- package/dist/component/_generated/server.d.ts +120 -0
- package/dist/component/_generated/server.js +77 -0
- package/dist/component/convex.config.js +19 -21
- package/dist/component/engine/agents.d.ts +19 -0
- package/dist/component/engine/agents.js +67 -0
- package/dist/component/engine/durableJob.d.ts +43 -0
- package/dist/component/engine/durableJob.js +262 -0
- package/dist/component/engine/resolvers.d.ts +12 -0
- package/dist/component/engine/resolvers.js +50 -0
- package/dist/component/engine/sessions.d.ts +36 -0
- package/dist/component/engine/sessions.js +233 -0
- package/dist/component/engine/toolApprovals.d.ts +5 -0
- package/dist/component/engine/toolApprovals.js +51 -0
- package/dist/component/lib/auth.d.ts +7 -0
- package/dist/component/lib/auth.js +24 -0
- package/dist/component/schema.d.ts +124 -0
- package/dist/component/schema.js +130 -0
- package/dist/component/testFixtures/callerSteps.d.ts +20 -0
- package/dist/component/testFixtures/callerSteps.js +90 -0
- package/package.json +8 -1
|
@@ -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,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
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { QueryCtx, MutationCtx } from "../_generated/server";
|
|
2
|
+
export declare function requireOrgId(ctx: QueryCtx | MutationCtx): Promise<string>;
|
|
3
|
+
export declare function assertSameOrg(callerOrgId: string, doc: {
|
|
4
|
+
orgId: string;
|
|
5
|
+
} | null, notFoundMessage: string): asserts doc is {
|
|
6
|
+
orgId: string;
|
|
7
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Deny by default. orgId is NEVER read from client args — it is derived
|
|
2
|
+
// server-side from the authenticated identity (Clerk org claim). Every
|
|
3
|
+
// mutation/query in this component must call this before touching data.
|
|
4
|
+
export async function requireOrgId(ctx) {
|
|
5
|
+
const identity = await ctx.auth.getUserIdentity();
|
|
6
|
+
if (!identity) {
|
|
7
|
+
throw new Error("Unauthenticated: no identity on ctx.auth");
|
|
8
|
+
}
|
|
9
|
+
// Clerk org-scoped JWT template exposes `org_id` as a top-level claim.
|
|
10
|
+
const orgId = identity.org_id;
|
|
11
|
+
if (!orgId || typeof orgId !== "string" || orgId.length === 0) {
|
|
12
|
+
throw new Error("Unauthenticated: identity carries no org_id claim");
|
|
13
|
+
}
|
|
14
|
+
return orgId;
|
|
15
|
+
}
|
|
16
|
+
// Throws unless the document's orgId matches the caller's server-derived
|
|
17
|
+
// orgId. Use on every read/write of a document fetched by _id, since
|
|
18
|
+
// db.get() does not filter by index.
|
|
19
|
+
export function assertSameOrg(callerOrgId, doc, notFoundMessage) {
|
|
20
|
+
if (!doc || doc.orgId !== callerOrgId) {
|
|
21
|
+
// Same error for "not found" and "cross-org" -- do not leak existence.
|
|
22
|
+
throw new Error(notFoundMessage);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
declare const _default: import("convex/server").SchemaDefinition<{
|
|
2
|
+
durableJobs: import("convex/server").TableDefinition<import("convex/values").VObject<{
|
|
3
|
+
workflowId?: string | undefined;
|
|
4
|
+
lastError?: string | undefined;
|
|
5
|
+
abandonedAtStep?: number | undefined;
|
|
6
|
+
orgId: string;
|
|
7
|
+
stepFunctionHandle: string;
|
|
8
|
+
totalSteps: number;
|
|
9
|
+
maxAttemptsPerStep: number;
|
|
10
|
+
cursor: number;
|
|
11
|
+
status: "running" | "completed" | "abandoned" | "cancelled";
|
|
12
|
+
workflowVersion: string;
|
|
13
|
+
createdAt: number;
|
|
14
|
+
updatedAt: number;
|
|
15
|
+
kind: string;
|
|
16
|
+
}, {
|
|
17
|
+
orgId: import("convex/values").VString<string, "required">;
|
|
18
|
+
kind: import("convex/values").VString<string, "required">;
|
|
19
|
+
stepFunctionHandle: import("convex/values").VString<string, "required">;
|
|
20
|
+
totalSteps: import("convex/values").VFloat64<number, "required">;
|
|
21
|
+
maxAttemptsPerStep: import("convex/values").VFloat64<number, "required">;
|
|
22
|
+
cursor: import("convex/values").VFloat64<number, "required">;
|
|
23
|
+
status: import("convex/values").VUnion<"running" | "completed" | "abandoned" | "cancelled", [import("convex/values").VLiteral<"running", "required">, import("convex/values").VLiteral<"completed", "required">, import("convex/values").VLiteral<"abandoned", "required">, import("convex/values").VLiteral<"cancelled", "required">], "required", never>;
|
|
24
|
+
workflowId: import("convex/values").VString<string | undefined, "optional">;
|
|
25
|
+
workflowVersion: import("convex/values").VString<string, "required">;
|
|
26
|
+
lastError: import("convex/values").VString<string | undefined, "optional">;
|
|
27
|
+
abandonedAtStep: import("convex/values").VFloat64<number | undefined, "optional">;
|
|
28
|
+
createdAt: import("convex/values").VFloat64<number, "required">;
|
|
29
|
+
updatedAt: import("convex/values").VFloat64<number, "required">;
|
|
30
|
+
}, "required", "orgId" | "stepFunctionHandle" | "totalSteps" | "maxAttemptsPerStep" | "cursor" | "status" | "workflowId" | "workflowVersion" | "lastError" | "abandonedAtStep" | "createdAt" | "updatedAt" | "kind">, {
|
|
31
|
+
by_org: ["orgId", "_creationTime"];
|
|
32
|
+
by_org_status: ["orgId", "status", "_creationTime"];
|
|
33
|
+
}, {}, {}>;
|
|
34
|
+
callerStepInvocations: import("convex/server").TableDefinition<import("convex/values").VObject<{
|
|
35
|
+
orgId: string;
|
|
36
|
+
createdAt: number;
|
|
37
|
+
jobId: import("convex/values").GenericId<"durableJobs">;
|
|
38
|
+
stepIndex: number;
|
|
39
|
+
}, {
|
|
40
|
+
orgId: import("convex/values").VString<string, "required">;
|
|
41
|
+
jobId: import("convex/values").VId<import("convex/values").GenericId<"durableJobs">, "required">;
|
|
42
|
+
stepIndex: import("convex/values").VFloat64<number, "required">;
|
|
43
|
+
createdAt: import("convex/values").VFloat64<number, "required">;
|
|
44
|
+
}, "required", "orgId" | "createdAt" | "jobId" | "stepIndex">, {
|
|
45
|
+
by_org: ["orgId", "_creationTime"];
|
|
46
|
+
by_org_job: ["orgId", "jobId", "_creationTime"];
|
|
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
|
+
}, {}, {}>;
|
|
123
|
+
}, true>;
|
|
124
|
+
export default _default;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { defineSchema, defineTable } from "convex/server";
|
|
2
|
+
import { v } from "convex/values";
|
|
3
|
+
// @vantageos/agent-engine — COMPONENT schema (T2-QUATER).
|
|
4
|
+
//
|
|
5
|
+
// This is the schema that actually ships INSIDE the published package and
|
|
6
|
+
// is deployed under the component boundary when a consumer registers
|
|
7
|
+
// `app.use(agentEngine)` — as opposed to `convex/schema.ts` at the repo
|
|
8
|
+
// root, which only backs THIS repo's own dev/test deployment.
|
|
9
|
+
//
|
|
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).
|
|
22
|
+
export default defineSchema({
|
|
23
|
+
// durableJobs — GENERIC durable step-batch primitive (I1, SYNTHESE.md
|
|
24
|
+
// §5/§7). See convex/engine/durableJob.ts (repo root) for the full
|
|
25
|
+
// contract description; this table definition is byte-identical to the
|
|
26
|
+
// one there.
|
|
27
|
+
durableJobs: defineTable({
|
|
28
|
+
orgId: v.string(),
|
|
29
|
+
kind: v.string(),
|
|
30
|
+
stepFunctionHandle: v.string(),
|
|
31
|
+
totalSteps: v.number(),
|
|
32
|
+
maxAttemptsPerStep: v.number(),
|
|
33
|
+
cursor: v.number(),
|
|
34
|
+
status: v.union(v.literal("running"), v.literal("completed"), v.literal("abandoned"), v.literal("cancelled")),
|
|
35
|
+
workflowId: v.optional(v.string()),
|
|
36
|
+
workflowVersion: v.string(),
|
|
37
|
+
lastError: v.optional(v.string()),
|
|
38
|
+
abandonedAtStep: v.optional(v.number()),
|
|
39
|
+
createdAt: v.number(),
|
|
40
|
+
updatedAt: v.number(),
|
|
41
|
+
})
|
|
42
|
+
.index("by_org", ["orgId"])
|
|
43
|
+
.index("by_org_status", ["orgId", "status"]),
|
|
44
|
+
// callerStepInvocations — TEST-ONLY fixture table, see
|
|
45
|
+
// convex/component/testFixtures/callerSteps.ts. Not part of the public
|
|
46
|
+
// contract; kept so the component-level test harness can prove the
|
|
47
|
+
// deterministic-replay / abandon-after-N properties the same way the
|
|
48
|
+
// repo-root test suite does.
|
|
49
|
+
callerStepInvocations: defineTable({
|
|
50
|
+
orgId: v.string(),
|
|
51
|
+
jobId: v.id("durableJobs"),
|
|
52
|
+
stepIndex: v.number(),
|
|
53
|
+
createdAt: v.number(),
|
|
54
|
+
})
|
|
55
|
+
.index("by_org", ["orgId"])
|
|
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"]),
|
|
130
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare const getAttemptCount: import("convex/server").RegisteredQuery<"internal", {
|
|
2
|
+
jobId: import("convex/values").GenericId<"durableJobs">;
|
|
3
|
+
stepIndex: number;
|
|
4
|
+
}, Promise<number>>;
|
|
5
|
+
export declare const resetAttemptCounts: import("convex/server").RegisteredMutation<"internal", {}, Promise<null>>;
|
|
6
|
+
export declare const alwaysSucceeds: import("convex/server").RegisteredMutation<"internal", {
|
|
7
|
+
orgId: string;
|
|
8
|
+
jobId: import("convex/values").GenericId<"durableJobs">;
|
|
9
|
+
stepIndex: number;
|
|
10
|
+
}, Promise<null>>;
|
|
11
|
+
export declare const alwaysFails: import("convex/server").RegisteredMutation<"internal", {
|
|
12
|
+
orgId: string;
|
|
13
|
+
jobId: import("convex/values").GenericId<"durableJobs">;
|
|
14
|
+
stepIndex: number;
|
|
15
|
+
}, Promise<never>>;
|
|
16
|
+
export declare const failsFirstAttemptThenSucceeds: import("convex/server").RegisteredMutation<"internal", {
|
|
17
|
+
orgId: string;
|
|
18
|
+
jobId: import("convex/values").GenericId<"durableJobs">;
|
|
19
|
+
stepIndex: number;
|
|
20
|
+
}, Promise<null>>;
|