@vantageos/agent-engine 0.1.0-alpha.8 → 0.3.0-alpha.0
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 +33 -8
- package/dist/component/_generated/api.d.ts +14 -27
- package/dist/component/_generated/api.js +0 -25
- package/dist/component/_generated/component.d.ts +369 -92
- package/dist/component/_generated/component.js +0 -9
- package/dist/component/_generated/dataModel.d.ts +0 -38
- package/dist/component/_generated/dataModel.js +0 -9
- package/dist/component/_generated/server.d.ts +0 -106
- package/dist/component/_generated/server.js +0 -69
- package/dist/component/convex.config.js +0 -44
- package/dist/component/engine/agents.js +0 -7
- package/dist/component/engine/awaitEventTimeout.d.ts +1 -1
- package/dist/component/engine/awaitEventTimeout.js +18 -26
- package/dist/component/engine/connections.d.ts +74 -0
- package/dist/component/engine/connections.js +254 -0
- package/dist/component/engine/delegation.d.ts +1 -1
- package/dist/component/engine/delegation.js +0 -74
- package/dist/component/engine/directory.js +0 -18
- package/dist/component/engine/durableJob.d.ts +34 -4
- package/dist/component/engine/durableJob.js +333 -86
- package/dist/component/engine/mandates.js +0 -59
- package/dist/component/engine/queue.d.ts +97 -0
- package/dist/component/engine/queue.js +201 -0
- package/dist/component/engine/resolvers.d.ts +1 -1
- package/dist/component/engine/resolvers.js +6 -18
- package/dist/component/engine/sandboxCallback.d.ts +1 -1
- package/dist/component/engine/sandboxCallback.js +2 -79
- package/dist/component/engine/sandboxConfigProvider.d.ts +9 -0
- package/dist/component/engine/sandboxConfigProvider.js +32 -0
- package/dist/component/engine/sessions.d.ts +28 -10
- package/dist/component/engine/sessions.js +153 -165
- package/dist/component/engine/streams.d.ts +47 -0
- package/dist/component/engine/streams.js +162 -0
- package/dist/component/engine/toolApprovals.d.ts +1 -1
- package/dist/component/engine/toolApprovals.js +12 -37
- package/dist/component/engine/vpConnection.js +0 -40
- package/dist/component/engine/wakeup.d.ts +5 -7
- package/dist/component/engine/wakeup.js +54 -127
- package/dist/component/engine/workflows.d.ts +42 -0
- package/dist/component/engine/workflows.js +100 -41
- package/dist/component/engine/world.d.ts +249 -0
- package/dist/component/engine/world.js +665 -0
- package/dist/component/lib/auth.js +0 -26
- package/dist/component/lib/connectionSecret.d.ts +5 -0
- package/dist/component/lib/connectionSecret.js +32 -0
- package/dist/component/schema.d.ts +227 -77
- package/dist/component/schema.js +137 -250
- package/package.json +17 -3
- package/dist/component/engine/habits.d.ts +0 -70
- package/dist/component/engine/habits.js +0 -218
- package/dist/component/testFixtures/callerSteps.d.ts +0 -20
- package/dist/component/testFixtures/callerSteps.js +0 -90
- package/dist/component/testFixtures/delegationDemoWorkflow.d.ts +0 -18
- package/dist/component/testFixtures/delegationDemoWorkflow.js +0 -75
- package/dist/component/testFixtures/sandboxDemoWorkflow.d.ts +0 -19
- package/dist/component/testFixtures/sandboxDemoWorkflow.js +0 -69
- package/dist/component/testFixtures/vpTool.d.ts +0 -4
- package/dist/component/testFixtures/vpTool.js +0 -25
- package/dist/component/testFixtures/wakeupDemoWorkflow.d.ts +0 -19
- package/dist/component/testFixtures/wakeupDemoWorkflow.js +0 -77
- package/dist/component/testFixtures/wakeupWork.d.ts +0 -8
- package/dist/component/testFixtures/wakeupWork.js +0 -21
|
@@ -1,61 +1,50 @@
|
|
|
1
1
|
import { v } from "convex/values";
|
|
2
2
|
import { api, internal } from "../_generated/api.js";
|
|
3
|
-
import { mutation, query } from "../_generated/server.js";
|
|
3
|
+
import { mutation, query, } from "../_generated/server.js";
|
|
4
4
|
import { assertSameOrg, requireOrgId } from "../lib/auth.js";
|
|
5
5
|
import { workflow } from "./workflows.js";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
6
|
+
import { agentIdOfRun, appendEventCore, legacySessionOf, loadRun, openLegacyRun, payloadOrUndefined, slotToEventId, } from "./world.js";
|
|
7
|
+
const SESSION_EVENT_NAMES = [
|
|
8
|
+
"session.started",
|
|
9
|
+
"turn.started",
|
|
10
|
+
"turn.reasoning",
|
|
11
|
+
"turn.toolCall.requested",
|
|
12
|
+
"turn.toolCall.completed",
|
|
13
|
+
"turn.toolCall.denied",
|
|
14
|
+
"turn.completed",
|
|
15
|
+
"tool.approval.requested",
|
|
16
|
+
"tool.approval.resolved",
|
|
17
|
+
"sandbox.timeout",
|
|
18
|
+
];
|
|
19
|
+
function isSessionEventName(name) {
|
|
20
|
+
return SESSION_EVENT_NAMES.includes(name);
|
|
21
|
+
}
|
|
22
|
+
const NOT_FOUND = "Session not found";
|
|
23
|
+
export async function loadSessionRun(ctx, orgId, sessionId) {
|
|
24
|
+
return await loadRun(ctx, orgId, sessionId, "sessions", NOT_FOUND);
|
|
25
|
+
}
|
|
26
|
+
export async function insertEvent(ctx, args) {
|
|
27
|
+
await appendEventCore(ctx, {
|
|
20
28
|
orgId: args.orgId,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
createdAt: Date.now(),
|
|
29
|
+
runId: args.run.runId,
|
|
30
|
+
eventType: args.name,
|
|
31
|
+
dataJson: args.payload ?? "{}",
|
|
25
32
|
});
|
|
33
|
+
const legacy = await legacySessionOf(ctx, args.run, args.orgId);
|
|
34
|
+
if (legacy !== null) {
|
|
35
|
+
await ctx.db.insert("sessionEvents", {
|
|
36
|
+
orgId: args.orgId,
|
|
37
|
+
sessionId: legacy._id,
|
|
38
|
+
name: args.name,
|
|
39
|
+
payload: args.payload,
|
|
40
|
+
createdAt: Date.now(),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
26
43
|
}
|
|
27
|
-
const CURRENT_SESSION_WORKFLOW_VERSION = "
|
|
28
|
-
// start — opens a session (session.started boundary), rereading the
|
|
29
|
-
// manifest immediately so a caller can act on it before the first turn,
|
|
30
|
-
// then STARTS the durable, versioned session workflow
|
|
31
|
-
// (engine/workflows.ts:sessionWorkflowV1) that pilots this session from
|
|
32
|
-
// here on.
|
|
33
|
-
//
|
|
34
|
-
// RETURN CONTRACT ("intentional divergence from Eve"): `start` returns
|
|
35
|
-
// `{ sessionId, workflowId }` instead of a bare `sessionId`. A bare ID
|
|
36
|
-
// would be the shape for a session with no workflow to reference;
|
|
37
|
-
// once a session IS piloted by a real, durable workflow run, silently
|
|
38
|
-
// dropping that run's id would make it unobservable/uncancellable to
|
|
39
|
-
// every caller except one that re-reads the session row. Returning both
|
|
40
|
-
// fields explicitly, rather than only patching `workflowId` onto the row
|
|
41
|
-
// and making callers re-fetch, keeps `start`'s result self-sufficient for
|
|
42
|
-
// the common "immediately act on the new session" case while the session
|
|
43
|
-
// row remains the durable source of truth for reconnects/replays.
|
|
44
|
+
const CURRENT_SESSION_WORKFLOW_VERSION = "v2";
|
|
44
45
|
export const start = mutation({
|
|
45
46
|
args: { orgId: v.string(), agentId: v.id("agents"), createdBy: v.string() },
|
|
46
|
-
returns: v.object({ sessionId: v.
|
|
47
|
-
// EXPLICIT return-type annotation ("intentional divergence from Eve" -- Convex pattern for
|
|
48
|
-
// any handler that references `components`/a WorkflowManager built on
|
|
49
|
-
// `components.workflow`): `workflow.start` closes over `components`,
|
|
50
|
-
// which closes back over this component's own generated `api` --
|
|
51
|
-
// `sessions.start` -> `workflow.start` -> `components` -> `sessions` is
|
|
52
|
-
// a type-level cycle. Without an explicit handler return type, TS
|
|
53
|
-
// widens the inferred return to `any` while resolving that cycle, and
|
|
54
|
-
// `.d.ts` EMISSION (declaration:true, tsconfig.build.json) fails --
|
|
55
|
-
// TS2502/TS2615/TS7022 -- even though `tsc --noEmit` at the repo root
|
|
56
|
-
// stays green (noEmit skips the exact codepath that breaks). Measured
|
|
57
|
-
// on PR #13 (build gate review): `npm run build` is the build that SHIPS; a
|
|
58
|
-
// green `tsc --noEmit` is not sufficient proof by itself.
|
|
47
|
+
returns: v.object({ sessionId: v.string(), workflowId: v.string() }),
|
|
59
48
|
handler: async (ctx, args) => {
|
|
60
49
|
const orgId = requireOrgId(args.orgId);
|
|
61
50
|
if (args.createdBy.trim().length === 0) {
|
|
@@ -64,112 +53,95 @@ export const start = mutation({
|
|
|
64
53
|
const agent = await ctx.db.get(args.agentId);
|
|
65
54
|
assertSameOrg(orgId, agent, "Agent not found");
|
|
66
55
|
const now = Date.now();
|
|
67
|
-
const
|
|
56
|
+
const legacySessionId = await ctx.db.insert("sessions", {
|
|
68
57
|
orgId,
|
|
69
58
|
agentId: args.agentId,
|
|
70
59
|
status: "running",
|
|
71
60
|
createdBy: args.createdBy,
|
|
72
61
|
createdAt: now,
|
|
73
62
|
updatedAt: now,
|
|
63
|
+
workflowVersion: CURRENT_SESSION_WORKFLOW_VERSION,
|
|
74
64
|
});
|
|
75
|
-
await
|
|
76
|
-
await ctx.runQuery(api.engine.resolvers.resolveManifest, {
|
|
65
|
+
const runId = await openLegacyRun(ctx, {
|
|
77
66
|
orgId,
|
|
78
|
-
|
|
67
|
+
table: "sessions",
|
|
68
|
+
legacyId: legacySessionId,
|
|
69
|
+
data: {
|
|
70
|
+
agentId: args.agentId,
|
|
71
|
+
createdBy: args.createdBy,
|
|
72
|
+
workflowVersion: CURRENT_SESSION_WORKFLOW_VERSION,
|
|
73
|
+
},
|
|
79
74
|
});
|
|
80
|
-
const workflowId = await workflow.start(ctx, internal.engine.workflows.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
workflowVersion: CURRENT_SESSION_WORKFLOW_VERSION,
|
|
75
|
+
const workflowId = await workflow.start(ctx, internal.engine.workflows.sessionWorkflowV2, { orgId, runId }, {
|
|
76
|
+
onComplete: internal.engine.workflows.onSessionWorkflowCompleteV2,
|
|
77
|
+
context: { orgId, runId },
|
|
84
78
|
});
|
|
85
|
-
|
|
79
|
+
await ctx.db.patch(legacySessionId, { workflowId });
|
|
80
|
+
const run = await loadSessionRun(ctx, orgId, runId);
|
|
81
|
+
await insertEvent(ctx, { orgId, run, name: "session.started" });
|
|
82
|
+
await ctx.runQuery(api.engine.resolvers.resolveManifest, {
|
|
83
|
+
orgId,
|
|
84
|
+
sessionId: runId,
|
|
85
|
+
});
|
|
86
|
+
return { sessionId: runId, workflowId };
|
|
86
87
|
},
|
|
87
88
|
});
|
|
88
|
-
// sendTurn — turn.started boundary. Rereads the manifest (eve parity: dynamic
|
|
89
|
-
// resolvers only allowed to reread at session.started/turn.started).
|
|
90
|
-
// `input` is the caller-owned turn payload (already-serialized JSON/text);
|
|
91
|
-
// this component does not interpret it -- it is stored verbatim as the
|
|
92
|
-
// event payload for replay.
|
|
93
89
|
export const sendTurn = mutation({
|
|
94
|
-
args: { orgId: v.string(), sessionId: v.
|
|
90
|
+
args: { orgId: v.string(), sessionId: v.string(), input: v.string() },
|
|
95
91
|
returns: v.null(),
|
|
96
92
|
handler: async (ctx, args) => {
|
|
97
93
|
const orgId = requireOrgId(args.orgId);
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
throw new Error(`Session is not running (status=${session.status})`);
|
|
94
|
+
const run = await loadSessionRun(ctx, orgId, args.sessionId);
|
|
95
|
+
if (run.status !== "running") {
|
|
96
|
+
throw new Error(`Session is not running (status=${run.status})`);
|
|
102
97
|
}
|
|
103
|
-
await insertEvent(ctx, {
|
|
104
|
-
orgId,
|
|
105
|
-
sessionId: args.sessionId,
|
|
106
|
-
name: "turn.started",
|
|
107
|
-
payload: args.input,
|
|
108
|
-
});
|
|
98
|
+
await insertEvent(ctx, { orgId, run, name: "turn.started", payload: args.input });
|
|
109
99
|
await ctx.runQuery(api.engine.resolvers.resolveManifest, {
|
|
110
100
|
orgId,
|
|
111
101
|
sessionId: args.sessionId,
|
|
112
102
|
});
|
|
113
|
-
await ctx
|
|
103
|
+
const legacy = await legacySessionOf(ctx, run, orgId);
|
|
104
|
+
if (legacy !== null) {
|
|
105
|
+
await ctx.db.patch(legacy._id, { updatedAt: Date.now() });
|
|
106
|
+
}
|
|
114
107
|
return null;
|
|
115
108
|
},
|
|
116
109
|
});
|
|
117
|
-
// appendReasoning — one streamed reasoning chunk. Convex query reactivity
|
|
118
|
-
// on `listEvents` is the streaming transport: each insert here is pushed to
|
|
119
|
-
// every subscriber live, no separate channel needed.
|
|
120
110
|
export const appendReasoning = mutation({
|
|
121
|
-
args: { orgId: v.string(), sessionId: v.
|
|
111
|
+
args: { orgId: v.string(), sessionId: v.string(), chunk: v.string() },
|
|
122
112
|
returns: v.null(),
|
|
123
113
|
handler: async (ctx, args) => {
|
|
124
114
|
const orgId = requireOrgId(args.orgId);
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
await insertEvent(ctx, {
|
|
128
|
-
orgId,
|
|
129
|
-
sessionId: args.sessionId,
|
|
130
|
-
name: "turn.reasoning",
|
|
131
|
-
payload: args.chunk,
|
|
132
|
-
});
|
|
115
|
+
const run = await loadSessionRun(ctx, orgId, args.sessionId);
|
|
116
|
+
await insertEvent(ctx, { orgId, run, name: "turn.reasoning", payload: args.chunk });
|
|
133
117
|
return null;
|
|
134
118
|
},
|
|
135
119
|
});
|
|
136
|
-
// requestToolCall — gates on the agent's toolApprovals row for `toolName`
|
|
137
|
-
// (default "never" = no gate). "always" gates every call; "once" gates
|
|
138
|
-
// unless already resolved earlier in THIS session. Returns whether the
|
|
139
|
-
// caller may proceed immediately or must wait for
|
|
140
|
-
// `toolApprovals.respond`.
|
|
141
120
|
export const requestToolCall = mutation({
|
|
142
121
|
args: {
|
|
143
122
|
orgId: v.string(),
|
|
144
|
-
sessionId: v.
|
|
123
|
+
sessionId: v.string(),
|
|
145
124
|
toolName: v.string(),
|
|
146
125
|
argsJson: v.string(),
|
|
147
126
|
},
|
|
148
127
|
returns: v.object({
|
|
149
128
|
authorized: v.boolean(),
|
|
150
129
|
approvalPending: v.boolean(),
|
|
151
|
-
// Explicit terminal refusal for THIS turn (VP
|
|
152
|
-
// k17ea6x3tht8xa3rxm139t23gx8ajkf3, defaut 2). Distinct from
|
|
153
|
-
// `approvalPending: false, authorized: true` (approved/never-gated)
|
|
154
|
-
// -- a caller must check this before treating the call as skippable.
|
|
155
130
|
denied: v.boolean(),
|
|
156
131
|
}),
|
|
157
132
|
handler: async (ctx, args) => {
|
|
158
133
|
const orgId = requireOrgId(args.orgId);
|
|
159
|
-
const
|
|
160
|
-
|
|
134
|
+
const run = await loadSessionRun(ctx, orgId, args.sessionId);
|
|
135
|
+
const agentId = agentIdOfRun(ctx, run);
|
|
161
136
|
await insertEvent(ctx, {
|
|
162
137
|
orgId,
|
|
163
|
-
|
|
138
|
+
run,
|
|
164
139
|
name: "turn.toolCall.requested",
|
|
165
140
|
payload: JSON.stringify({ toolName: args.toolName, args: args.argsJson }),
|
|
166
141
|
});
|
|
167
142
|
const approvalRow = await ctx.db
|
|
168
143
|
.query("toolApprovals")
|
|
169
|
-
.withIndex("by_org_agent_tool", (q) => q
|
|
170
|
-
.eq("orgId", orgId)
|
|
171
|
-
.eq("agentId", session.agentId)
|
|
172
|
-
.eq("toolName", args.toolName))
|
|
144
|
+
.withIndex("by_org_agent_tool", (q) => q.eq("orgId", orgId).eq("agentId", agentId).eq("toolName", args.toolName))
|
|
173
145
|
.unique();
|
|
174
146
|
const approval = approvalRow?.approval ?? "never";
|
|
175
147
|
if (approval === "never") {
|
|
@@ -178,34 +150,32 @@ export const requestToolCall = mutation({
|
|
|
178
150
|
if (approval === "always") {
|
|
179
151
|
await insertEvent(ctx, {
|
|
180
152
|
orgId,
|
|
181
|
-
|
|
153
|
+
run,
|
|
182
154
|
name: "tool.approval.requested",
|
|
183
155
|
payload: JSON.stringify({ toolName: args.toolName }),
|
|
184
156
|
});
|
|
185
157
|
return { authorized: false, approvalPending: true, denied: false };
|
|
186
158
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
// authorization (the tool would be skipped while the turn still
|
|
191
|
-
// completed) -- it is reported back as a terminal refusal
|
|
192
|
-
// instead, and the caller re-requests (a fresh human decision) rather
|
|
193
|
-
// than silently proceeding.
|
|
194
|
-
const events = await ctx.db
|
|
195
|
-
.query("sessionEvents")
|
|
196
|
-
.withIndex("by_org_session", (q) => q.eq("orgId", orgId).eq("sessionId", args.sessionId))
|
|
159
|
+
const journal = await ctx.db
|
|
160
|
+
.query("worldEvents")
|
|
161
|
+
.withIndex("by_org_run_seq", (q) => q.eq("orgId", orgId).eq("runId", run.runId))
|
|
197
162
|
.collect();
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
e.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
163
|
+
let latestDecision;
|
|
164
|
+
for (const e of journal) {
|
|
165
|
+
if (e.eventType !== "tool.approval.resolved")
|
|
166
|
+
continue;
|
|
167
|
+
let resolved;
|
|
168
|
+
try {
|
|
169
|
+
resolved = JSON.parse(e.dataJson);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (resolved.toolName === args.toolName) {
|
|
175
|
+
latestDecision =
|
|
176
|
+
typeof resolved.decision === "string" ? resolved.decision : undefined;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
209
179
|
if (latestDecision === "approved") {
|
|
210
180
|
return { authorized: true, approvalPending: false, denied: false };
|
|
211
181
|
}
|
|
@@ -214,85 +184,103 @@ export const requestToolCall = mutation({
|
|
|
214
184
|
}
|
|
215
185
|
await insertEvent(ctx, {
|
|
216
186
|
orgId,
|
|
217
|
-
|
|
187
|
+
run,
|
|
218
188
|
name: "tool.approval.requested",
|
|
219
189
|
payload: JSON.stringify({ toolName: args.toolName }),
|
|
220
190
|
});
|
|
221
191
|
return { authorized: false, approvalPending: true, denied: false };
|
|
222
192
|
},
|
|
223
193
|
});
|
|
224
|
-
// completeToolCall — the caller invokes this after actually running the
|
|
225
|
-
// tool (authorized immediately, or after `toolApprovals.respond` cleared
|
|
226
|
-
// the gate).
|
|
227
194
|
export const completeToolCall = mutation({
|
|
228
195
|
args: {
|
|
229
196
|
orgId: v.string(),
|
|
230
|
-
sessionId: v.
|
|
197
|
+
sessionId: v.string(),
|
|
231
198
|
toolName: v.string(),
|
|
232
199
|
resultJson: v.string(),
|
|
233
200
|
},
|
|
234
201
|
returns: v.null(),
|
|
235
202
|
handler: async (ctx, args) => {
|
|
236
203
|
const orgId = requireOrgId(args.orgId);
|
|
237
|
-
const
|
|
238
|
-
assertSameOrg(orgId, session, "Session not found");
|
|
204
|
+
const run = await loadSessionRun(ctx, orgId, args.sessionId);
|
|
239
205
|
await insertEvent(ctx, {
|
|
240
206
|
orgId,
|
|
241
|
-
|
|
207
|
+
run,
|
|
242
208
|
name: "turn.toolCall.completed",
|
|
243
209
|
payload: JSON.stringify({ toolName: args.toolName, result: args.resultJson }),
|
|
244
210
|
});
|
|
245
211
|
return null;
|
|
246
212
|
},
|
|
247
213
|
});
|
|
248
|
-
// completeTurn — closes the current turn.
|
|
249
214
|
export const completeTurn = mutation({
|
|
250
|
-
args: { orgId: v.string(), sessionId: v.
|
|
215
|
+
args: { orgId: v.string(), sessionId: v.string() },
|
|
251
216
|
returns: v.null(),
|
|
252
217
|
handler: async (ctx, args) => {
|
|
253
218
|
const orgId = requireOrgId(args.orgId);
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
await
|
|
219
|
+
const run = await loadSessionRun(ctx, orgId, args.sessionId);
|
|
220
|
+
await insertEvent(ctx, { orgId, run, name: "turn.completed" });
|
|
221
|
+
const legacy = await legacySessionOf(ctx, run, orgId);
|
|
222
|
+
if (legacy !== null) {
|
|
223
|
+
await ctx.db.patch(legacy._id, { updatedAt: Date.now() });
|
|
224
|
+
}
|
|
225
|
+
return null;
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
export const end = mutation({
|
|
229
|
+
args: {
|
|
230
|
+
orgId: v.string(),
|
|
231
|
+
sessionId: v.string(),
|
|
232
|
+
outcome: v.union(v.literal("completed"), v.literal("cancelled")),
|
|
233
|
+
},
|
|
234
|
+
returns: v.null(),
|
|
235
|
+
handler: async (ctx, args) => {
|
|
236
|
+
const orgId = requireOrgId(args.orgId);
|
|
237
|
+
const run = await loadSessionRun(ctx, orgId, args.sessionId);
|
|
238
|
+
if (run.status !== "running") {
|
|
239
|
+
if (run.status === args.outcome)
|
|
240
|
+
return null;
|
|
241
|
+
throw new Error(`Session already ended (status=${run.status}); cannot end as ${args.outcome}`);
|
|
242
|
+
}
|
|
243
|
+
await appendEventCore(ctx, {
|
|
257
244
|
orgId,
|
|
258
|
-
|
|
259
|
-
|
|
245
|
+
runId: run.runId,
|
|
246
|
+
eventType: args.outcome === "completed" ? "run_completed" : "run_cancelled",
|
|
247
|
+
dataJson: JSON.stringify({ outcome: args.outcome }),
|
|
260
248
|
});
|
|
261
|
-
await ctx
|
|
249
|
+
const legacy = await legacySessionOf(ctx, run, orgId);
|
|
250
|
+
if (legacy !== null) {
|
|
251
|
+
await ctx.db.patch(legacy._id, { status: args.outcome, updatedAt: Date.now() });
|
|
252
|
+
}
|
|
262
253
|
return null;
|
|
263
254
|
},
|
|
264
255
|
});
|
|
265
|
-
// listEvents — REPLAY ON RECONNECT: the full ordered event log for a
|
|
266
|
-
// session. A client that drops and reconnects calls this once to rebuild
|
|
267
|
-
// the thread, then subscribes (Convex reactivity) for anything after.
|
|
268
256
|
export const listEvents = query({
|
|
269
|
-
args: { orgId: v.string(), sessionId: v.
|
|
257
|
+
args: { orgId: v.string(), sessionId: v.string() },
|
|
270
258
|
returns: v.array(v.object({
|
|
271
|
-
_id: v.
|
|
259
|
+
_id: v.string(),
|
|
272
260
|
_creationTime: v.number(),
|
|
273
|
-
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"),
|
|
274
|
-
// Blank event auto-inserted by
|
|
275
|
-
// `engine/awaitEventTimeout.fire` (workflow#177).
|
|
276
|
-
v.literal("sandbox.timeout")),
|
|
261
|
+
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"), v.literal("sandbox.timeout")),
|
|
277
262
|
payload: v.optional(v.string()),
|
|
278
263
|
createdAt: v.number(),
|
|
279
264
|
})),
|
|
280
265
|
handler: async (ctx, args) => {
|
|
281
266
|
const orgId = requireOrgId(args.orgId);
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
.
|
|
286
|
-
.withIndex("by_org_session", (q) => q.eq("orgId", orgId).eq("sessionId", args.sessionId))
|
|
267
|
+
const run = await loadSessionRun(ctx, orgId, args.sessionId);
|
|
268
|
+
const journal = await ctx.db
|
|
269
|
+
.query("worldEvents")
|
|
270
|
+
.withIndex("by_org_run_seq", (q) => q.eq("orgId", orgId).eq("runId", run.runId))
|
|
287
271
|
.collect();
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
272
|
+
const out = [];
|
|
273
|
+
for (const e of journal) {
|
|
274
|
+
if (!isSessionEventName(e.eventType))
|
|
275
|
+
continue;
|
|
276
|
+
out.push({
|
|
277
|
+
_id: slotToEventId(e.seq),
|
|
278
|
+
_creationTime: e.createdAt,
|
|
279
|
+
name: e.eventType,
|
|
280
|
+
payload: payloadOrUndefined(e.dataJson),
|
|
281
|
+
createdAt: e.createdAt,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
return out;
|
|
297
285
|
},
|
|
298
286
|
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export declare const writeChunk: import("convex/server").RegisteredMutation<"public", {
|
|
2
|
+
name: string;
|
|
3
|
+
orgId: string;
|
|
4
|
+
runId: string;
|
|
5
|
+
dataBase64: string;
|
|
6
|
+
}, Promise<null>>;
|
|
7
|
+
export declare const closeStream: import("convex/server").RegisteredMutation<"public", {
|
|
8
|
+
name: string;
|
|
9
|
+
orgId: string;
|
|
10
|
+
runId: string;
|
|
11
|
+
}, Promise<null>>;
|
|
12
|
+
export declare const getStreamInfo: import("convex/server").RegisteredQuery<"public", {
|
|
13
|
+
name: string;
|
|
14
|
+
orgId: string;
|
|
15
|
+
runId: string;
|
|
16
|
+
}, Promise<{
|
|
17
|
+
tailIndex: number;
|
|
18
|
+
done: boolean;
|
|
19
|
+
}>>;
|
|
20
|
+
export declare const listStreamNames: import("convex/server").RegisteredQuery<"public", {
|
|
21
|
+
orgId: string;
|
|
22
|
+
runId: string;
|
|
23
|
+
}, Promise<string[]>>;
|
|
24
|
+
export declare const listChunksPage: import("convex/server").RegisteredQuery<"public", {
|
|
25
|
+
name: string;
|
|
26
|
+
orgId: string;
|
|
27
|
+
cursor: string | null;
|
|
28
|
+
runId: string;
|
|
29
|
+
limit: number;
|
|
30
|
+
}, Promise<{
|
|
31
|
+
page: {
|
|
32
|
+
index: number;
|
|
33
|
+
dataBase64: string;
|
|
34
|
+
}[];
|
|
35
|
+
isDone: boolean;
|
|
36
|
+
continueCursor: string;
|
|
37
|
+
}>>;
|
|
38
|
+
export declare const listChunksFromIndex: import("convex/server").RegisteredQuery<"public", {
|
|
39
|
+
name: string;
|
|
40
|
+
orgId: string;
|
|
41
|
+
runId: string;
|
|
42
|
+
limit: number;
|
|
43
|
+
startIndex: number;
|
|
44
|
+
}, Promise<{
|
|
45
|
+
index: number;
|
|
46
|
+
dataBase64: string;
|
|
47
|
+
}[]>>;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { mutation, query } from "../_generated/server.js";
|
|
3
|
+
import { requireOrgId } from "../lib/auth.js";
|
|
4
|
+
const streamInfoResult = v.object({ tailIndex: v.number(), done: v.boolean() });
|
|
5
|
+
const streamChunkResult = v.object({ index: v.number(), dataBase64: v.string() });
|
|
6
|
+
async function getStreamDoc(ctx, orgId, runId, name) {
|
|
7
|
+
return await ctx.db
|
|
8
|
+
.query("worldStreams")
|
|
9
|
+
.withIndex("by_org_run_name", (q) => q.eq("orgId", orgId).eq("runId", runId).eq("name", name))
|
|
10
|
+
.unique();
|
|
11
|
+
}
|
|
12
|
+
export const writeChunk = mutation({
|
|
13
|
+
args: { orgId: v.string(), runId: v.string(), name: v.string(), dataBase64: v.string() },
|
|
14
|
+
returns: v.null(),
|
|
15
|
+
handler: async (ctx, args) => {
|
|
16
|
+
const orgId = requireOrgId(args.orgId);
|
|
17
|
+
const now = Date.now();
|
|
18
|
+
let stream = await getStreamDoc(ctx, orgId, args.runId, args.name);
|
|
19
|
+
if (stream && stream.orgId !== orgId) {
|
|
20
|
+
stream = null;
|
|
21
|
+
}
|
|
22
|
+
if (!stream) {
|
|
23
|
+
const id = await ctx.db.insert("worldStreams", {
|
|
24
|
+
orgId,
|
|
25
|
+
runId: args.runId,
|
|
26
|
+
name: args.name,
|
|
27
|
+
chunkCount: 0,
|
|
28
|
+
done: false,
|
|
29
|
+
createdAt: now,
|
|
30
|
+
updatedAt: now,
|
|
31
|
+
});
|
|
32
|
+
stream = await ctx.db.get(id);
|
|
33
|
+
}
|
|
34
|
+
if (!stream) {
|
|
35
|
+
throw new Error("Stream row failed to materialise");
|
|
36
|
+
}
|
|
37
|
+
if (stream.done) {
|
|
38
|
+
throw new Error(`Cannot write to a closed stream: ${args.runId}/${args.name}`);
|
|
39
|
+
}
|
|
40
|
+
const index = stream.chunkCount;
|
|
41
|
+
await ctx.db.insert("worldStreamChunks", {
|
|
42
|
+
orgId,
|
|
43
|
+
runId: args.runId,
|
|
44
|
+
name: args.name,
|
|
45
|
+
index,
|
|
46
|
+
dataBase64: args.dataBase64,
|
|
47
|
+
createdAt: now,
|
|
48
|
+
});
|
|
49
|
+
await ctx.db.patch(stream._id, { chunkCount: index + 1, updatedAt: now });
|
|
50
|
+
return null;
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
export const closeStream = mutation({
|
|
54
|
+
args: { orgId: v.string(), runId: v.string(), name: v.string() },
|
|
55
|
+
returns: v.null(),
|
|
56
|
+
handler: async (ctx, args) => {
|
|
57
|
+
const orgId = requireOrgId(args.orgId);
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
let stream = await getStreamDoc(ctx, orgId, args.runId, args.name);
|
|
60
|
+
if (stream && stream.orgId !== orgId) {
|
|
61
|
+
stream = null;
|
|
62
|
+
}
|
|
63
|
+
if (!stream) {
|
|
64
|
+
const id = await ctx.db.insert("worldStreams", {
|
|
65
|
+
orgId,
|
|
66
|
+
runId: args.runId,
|
|
67
|
+
name: args.name,
|
|
68
|
+
chunkCount: 0,
|
|
69
|
+
done: true,
|
|
70
|
+
createdAt: now,
|
|
71
|
+
updatedAt: now,
|
|
72
|
+
});
|
|
73
|
+
stream = await ctx.db.get(id);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
await ctx.db.patch(stream._id, { done: true, updatedAt: now });
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
export const getStreamInfo = query({
|
|
82
|
+
args: { orgId: v.string(), runId: v.string(), name: v.string() },
|
|
83
|
+
returns: streamInfoResult,
|
|
84
|
+
handler: async (ctx, args) => {
|
|
85
|
+
const orgId = requireOrgId(args.orgId);
|
|
86
|
+
const doc = await getStreamDoc(ctx, orgId, args.runId, args.name);
|
|
87
|
+
if (!doc || doc.orgId !== orgId) {
|
|
88
|
+
return { tailIndex: -1, done: false };
|
|
89
|
+
}
|
|
90
|
+
return { tailIndex: doc.chunkCount - 1, done: doc.done };
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
export const listStreamNames = query({
|
|
94
|
+
args: { orgId: v.string(), runId: v.string() },
|
|
95
|
+
returns: v.array(v.string()),
|
|
96
|
+
handler: async (ctx, args) => {
|
|
97
|
+
const orgId = requireOrgId(args.orgId);
|
|
98
|
+
const docs = await ctx.db
|
|
99
|
+
.query("worldStreams")
|
|
100
|
+
.withIndex("by_org_run", (q) => q.eq("orgId", orgId).eq("runId", args.runId))
|
|
101
|
+
.collect();
|
|
102
|
+
return docs.filter((d) => d.orgId === orgId).map((d) => d.name);
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
export const listChunksPage = query({
|
|
106
|
+
args: {
|
|
107
|
+
orgId: v.string(),
|
|
108
|
+
runId: v.string(),
|
|
109
|
+
name: v.string(),
|
|
110
|
+
limit: v.number(),
|
|
111
|
+
cursor: v.union(v.string(), v.null()),
|
|
112
|
+
},
|
|
113
|
+
returns: v.object({
|
|
114
|
+
page: v.array(streamChunkResult),
|
|
115
|
+
isDone: v.boolean(),
|
|
116
|
+
continueCursor: v.string(),
|
|
117
|
+
}),
|
|
118
|
+
handler: async (ctx, args) => {
|
|
119
|
+
const orgId = requireOrgId(args.orgId);
|
|
120
|
+
const startIndex = args.cursor === null ? 0 : Number(args.cursor);
|
|
121
|
+
const docs = await ctx.db
|
|
122
|
+
.query("worldStreamChunks")
|
|
123
|
+
.withIndex("by_org_run_name_index", (q) => q
|
|
124
|
+
.eq("orgId", orgId)
|
|
125
|
+
.eq("runId", args.runId)
|
|
126
|
+
.eq("name", args.name)
|
|
127
|
+
.gte("index", startIndex))
|
|
128
|
+
.order("asc")
|
|
129
|
+
.take(args.limit + 1);
|
|
130
|
+
const owned = docs.filter((d) => d.orgId === orgId);
|
|
131
|
+
const isDone = owned.length <= args.limit;
|
|
132
|
+
const page = isDone ? owned : owned.slice(0, args.limit);
|
|
133
|
+
return {
|
|
134
|
+
page: page.map((c) => ({ index: c.index, dataBase64: c.dataBase64 })),
|
|
135
|
+
isDone,
|
|
136
|
+
continueCursor: isDone ? "" : String(page[page.length - 1].index + 1),
|
|
137
|
+
};
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
export const listChunksFromIndex = query({
|
|
141
|
+
args: {
|
|
142
|
+
orgId: v.string(),
|
|
143
|
+
runId: v.string(),
|
|
144
|
+
name: v.string(),
|
|
145
|
+
startIndex: v.number(),
|
|
146
|
+
limit: v.number(),
|
|
147
|
+
},
|
|
148
|
+
returns: v.array(streamChunkResult),
|
|
149
|
+
handler: async (ctx, args) => {
|
|
150
|
+
const orgId = requireOrgId(args.orgId);
|
|
151
|
+
const docs = await ctx.db
|
|
152
|
+
.query("worldStreamChunks")
|
|
153
|
+
.withIndex("by_org_run_name_index", (q) => q
|
|
154
|
+
.eq("orgId", orgId)
|
|
155
|
+
.eq("runId", args.runId)
|
|
156
|
+
.eq("name", args.name)
|
|
157
|
+
.gte("index", args.startIndex))
|
|
158
|
+
.order("asc")
|
|
159
|
+
.take(args.limit);
|
|
160
|
+
return docs.filter((d) => d.orgId === orgId).map((c) => ({ index: c.index, dataBase64: c.dataBase64 }));
|
|
161
|
+
},
|
|
162
|
+
});
|
|
@@ -2,6 +2,6 @@ export declare const respond: import("convex/server").RegisteredMutation<"public
|
|
|
2
2
|
updatePolicy?: "always" | "once" | "never" | undefined;
|
|
3
3
|
orgId: string;
|
|
4
4
|
toolName: string;
|
|
5
|
-
sessionId:
|
|
5
|
+
sessionId: string;
|
|
6
6
|
decision: "approved" | "denied";
|
|
7
7
|
}, Promise<null>>;
|