@vantageos/agent-engine 0.1.0-alpha.7 → 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.
Files changed (62) hide show
  1. package/README.md +153 -50
  2. package/dist/component/_generated/api.d.ts +14 -27
  3. package/dist/component/_generated/api.js +0 -25
  4. package/dist/component/_generated/component.d.ts +369 -92
  5. package/dist/component/_generated/component.js +0 -9
  6. package/dist/component/_generated/dataModel.d.ts +0 -38
  7. package/dist/component/_generated/dataModel.js +0 -9
  8. package/dist/component/_generated/server.d.ts +0 -106
  9. package/dist/component/_generated/server.js +0 -69
  10. package/dist/component/convex.config.js +0 -44
  11. package/dist/component/engine/agents.js +0 -7
  12. package/dist/component/engine/awaitEventTimeout.d.ts +1 -1
  13. package/dist/component/engine/awaitEventTimeout.js +18 -26
  14. package/dist/component/engine/connections.d.ts +74 -0
  15. package/dist/component/engine/connections.js +254 -0
  16. package/dist/component/engine/delegation.d.ts +1 -1
  17. package/dist/component/engine/delegation.js +0 -74
  18. package/dist/component/engine/directory.js +0 -18
  19. package/dist/component/engine/durableJob.d.ts +34 -4
  20. package/dist/component/engine/durableJob.js +333 -86
  21. package/dist/component/engine/mandates.js +0 -59
  22. package/dist/component/engine/queue.d.ts +97 -0
  23. package/dist/component/engine/queue.js +201 -0
  24. package/dist/component/engine/resolvers.d.ts +1 -1
  25. package/dist/component/engine/resolvers.js +6 -18
  26. package/dist/component/engine/sandboxCallback.d.ts +1 -1
  27. package/dist/component/engine/sandboxCallback.js +2 -79
  28. package/dist/component/engine/sandboxConfigProvider.d.ts +9 -0
  29. package/dist/component/engine/sandboxConfigProvider.js +32 -0
  30. package/dist/component/engine/sessions.d.ts +28 -10
  31. package/dist/component/engine/sessions.js +153 -165
  32. package/dist/component/engine/streams.d.ts +47 -0
  33. package/dist/component/engine/streams.js +162 -0
  34. package/dist/component/engine/toolApprovals.d.ts +1 -1
  35. package/dist/component/engine/toolApprovals.js +12 -37
  36. package/dist/component/engine/vpConnection.js +0 -40
  37. package/dist/component/engine/wakeup.d.ts +5 -7
  38. package/dist/component/engine/wakeup.js +54 -127
  39. package/dist/component/engine/workflows.d.ts +42 -0
  40. package/dist/component/engine/workflows.js +100 -41
  41. package/dist/component/engine/world.d.ts +249 -0
  42. package/dist/component/engine/world.js +665 -0
  43. package/dist/component/lib/auth.js +0 -26
  44. package/dist/component/lib/connectionSecret.d.ts +5 -0
  45. package/dist/component/lib/connectionSecret.js +32 -0
  46. package/dist/component/schema.d.ts +227 -77
  47. package/dist/component/schema.js +137 -250
  48. package/package.json +17 -3
  49. package/dist/component/engine/habits.d.ts +0 -70
  50. package/dist/component/engine/habits.js +0 -218
  51. package/dist/component/testFixtures/callerSteps.d.ts +0 -20
  52. package/dist/component/testFixtures/callerSteps.js +0 -90
  53. package/dist/component/testFixtures/delegationDemoWorkflow.d.ts +0 -18
  54. package/dist/component/testFixtures/delegationDemoWorkflow.js +0 -75
  55. package/dist/component/testFixtures/sandboxDemoWorkflow.d.ts +0 -19
  56. package/dist/component/testFixtures/sandboxDemoWorkflow.js +0 -69
  57. package/dist/component/testFixtures/vpTool.d.ts +0 -4
  58. package/dist/component/testFixtures/vpTool.js +0 -25
  59. package/dist/component/testFixtures/wakeupDemoWorkflow.d.ts +0 -19
  60. package/dist/component/testFixtures/wakeupDemoWorkflow.js +0 -77
  61. package/dist/component/testFixtures/wakeupWork.d.ts +0 -8
  62. 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
- // engine/sessions — THE FIL DE CONVERSATION contract (T2-QUINQUIES). This
7
- // is what unblocks EveVantage V4: open a session, send a turn, stream
8
- // reasoning/tool calls, gate on tool approval, replay the thread on
9
- // reconnect. See component/schema.ts for the event log this is built on
10
- // (`sessionEvents` IS the streaming transport via Convex reactivity — no
11
- // separate channel).
12
- //
13
- // Named events mirror eve's session/turn boundaries
14
- // (dynamic-tool-definition.ts:40-46): `session.started`, `turn.started`.
15
- // `resolvers.resolveManifest` (engine/resolvers.ts) is reread at BOTH of
16
- // those boundaries — never mid-turn — so an agent edit is visible at the
17
- // very next turn with zero redeploy.
18
- async function insertEvent(ctx, args) {
19
- await ctx.db.insert("sessionEvents", {
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
- sessionId: args.sessionId,
22
- name: args.name,
23
- payload: args.payload,
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 = "v1";
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.id("sessions"), workflowId: v.string() }),
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 sessionId = await ctx.db.insert("sessions", {
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 insertEvent(ctx, { orgId, sessionId, name: "session.started" });
76
- await ctx.runQuery(api.engine.resolvers.resolveManifest, {
65
+ const runId = await openLegacyRun(ctx, {
77
66
  orgId,
78
- sessionId,
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.sessionWorkflowV1, { sessionId });
81
- await ctx.db.patch(sessionId, {
82
- workflowId,
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
- return { sessionId, workflowId };
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.id("sessions"), input: v.string() },
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 session = await ctx.db.get(args.sessionId);
99
- assertSameOrg(orgId, session, "Session not found");
100
- if (session.status !== "running") {
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.db.patch(args.sessionId, { updatedAt: Date.now() });
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.id("sessions"), chunk: v.string() },
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 session = await ctx.db.get(args.sessionId);
126
- assertSameOrg(orgId, session, "Session not found");
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.id("sessions"),
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 session = await ctx.db.get(args.sessionId);
160
- assertSameOrg(orgId, session, "Session not found");
134
+ const run = await loadSessionRun(ctx, orgId, args.sessionId);
135
+ const agentId = agentIdOfRun(ctx, run);
161
136
  await insertEvent(ctx, {
162
137
  orgId,
163
- sessionId: args.sessionId,
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
- sessionId: args.sessionId,
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
- // approval === "once": gate unless this tool's approval was already
188
- // resolved with `decision: "approved"` in this session. A prior
189
- // `decision: "denied"` resolution must NOT be treated as
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
- const resolutionsForTool = events
199
- .filter((e) => e.name === "tool.approval.resolved" &&
200
- e.payload !== undefined &&
201
- JSON.parse(e.payload).toolName ===
202
- args.toolName)
203
- .sort((a, b) => a.createdAt - b.createdAt);
204
- const latestResolution = resolutionsForTool[resolutionsForTool.length - 1];
205
- const latestDecision = latestResolution?.payload
206
- ? JSON.parse(latestResolution.payload)
207
- .decision
208
- : undefined;
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
- sessionId: args.sessionId,
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.id("sessions"),
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 session = await ctx.db.get(args.sessionId);
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
- sessionId: args.sessionId,
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.id("sessions") },
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 session = await ctx.db.get(args.sessionId);
255
- assertSameOrg(orgId, session, "Session not found");
256
- await insertEvent(ctx, {
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
- sessionId: args.sessionId,
259
- name: "turn.completed",
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.db.patch(args.sessionId, { updatedAt: Date.now() });
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.id("sessions") },
257
+ args: { orgId: v.string(), sessionId: v.string() },
270
258
  returns: v.array(v.object({
271
- _id: v.id("sessionEvents"),
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 session = await ctx.db.get(args.sessionId);
283
- assertSameOrg(orgId, session, "Session not found");
284
- const events = await ctx.db
285
- .query("sessionEvents")
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
- return events
289
- .sort((a, b) => a.createdAt - b.createdAt)
290
- .map((e) => ({
291
- _id: e._id,
292
- _creationTime: e._creationTime,
293
- name: e.name,
294
- payload: e.payload,
295
- createdAt: e.createdAt,
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: import("convex/values").GenericId<"sessions">;
5
+ sessionId: string;
6
6
  decision: "approved" | "denied";
7
7
  }, Promise<null>>;