@vantageos/agent-engine 0.1.0-alpha.8 → 0.3.0-alpha.1

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 +33 -8
  2. package/dist/component/_generated/api.d.ts +14 -3
  3. package/dist/component/_generated/api.js +0 -25
  4. package/dist/component/_generated/component.d.ts +369 -73
  5. package/dist/component/_generated/component.js +0 -9
  6. package/dist/component/_generated/dataModel.js +0 -9
  7. package/dist/component/_generated/server.js +0 -69
  8. package/dist/component/convex.config.js +0 -44
  9. package/dist/component/engine/agents.d.ts +3 -3
  10. package/dist/component/engine/agents.js +0 -7
  11. package/dist/component/engine/awaitEventTimeout.d.ts +1 -1
  12. package/dist/component/engine/awaitEventTimeout.js +16 -26
  13. package/dist/component/engine/connections.d.ts +74 -0
  14. package/dist/component/engine/connections.js +254 -0
  15. package/dist/component/engine/delegation.d.ts +5 -5
  16. package/dist/component/engine/delegation.js +0 -74
  17. package/dist/component/engine/directory.js +0 -18
  18. package/dist/component/engine/durableJob.d.ts +24 -17
  19. package/dist/component/engine/durableJob.js +182 -138
  20. package/dist/component/engine/mandates.d.ts +2 -2
  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 +2 -2
  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 +130 -170
  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 +7 -9
  38. package/dist/component/engine/wakeup.js +54 -127
  39. package/dist/component/engine/workflows.d.ts +26 -8
  40. package/dist/component/engine/workflows.js +59 -64
  41. package/dist/component/engine/world.d.ts +250 -0
  42. package/dist/component/engine/world.js +689 -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 +240 -175
  47. package/dist/component/schema.js +134 -294
  48. package/package.json +21 -6
  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
@@ -0,0 +1,32 @@
1
+ import { v } from "convex/values";
2
+ import { mutation, query } from "../_generated/server.js";
3
+ import { assertSameOrg, requireOrgId } from "../lib/auth.js";
4
+ export const getBoxId = query({
5
+ args: { orgId: v.string(), agentId: v.id("agents") },
6
+ returns: v.union(v.string(), v.null()),
7
+ handler: async (ctx, args) => {
8
+ const orgId = requireOrgId(args.orgId);
9
+ const row = await ctx.db
10
+ .query("sandboxConfig")
11
+ .withIndex("by_org_agent", (q) => q.eq("orgId", orgId).eq("agentId", args.agentId))
12
+ .unique();
13
+ return row?.boxId ?? null;
14
+ },
15
+ });
16
+ export const setBoxId = mutation({
17
+ args: { orgId: v.string(), agentId: v.id("agents"), boxId: v.string() },
18
+ returns: v.null(),
19
+ handler: async (ctx, args) => {
20
+ const orgId = requireOrgId(args.orgId);
21
+ if (args.boxId.length === 0) {
22
+ throw new Error("Validation: boxId must not be empty");
23
+ }
24
+ const row = await ctx.db
25
+ .query("sandboxConfig")
26
+ .withIndex("by_org_agent", (q) => q.eq("orgId", orgId).eq("agentId", args.agentId))
27
+ .unique();
28
+ assertSameOrg(orgId, row, "sandboxConfig row not found for this agent");
29
+ await ctx.db.patch(row._id, { boxId: args.boxId, updatedAt: Date.now() });
30
+ return null;
31
+ },
32
+ });
@@ -1,26 +1,38 @@
1
- import type { Id } from "../_generated/dataModel.js";
1
+ import { type DatabaseReader, type MutationCtx } from "../_generated/server.js";
2
+ import { type LoadedRun } from "./world.js";
3
+ declare const SESSION_EVENT_NAMES: readonly ["session.started", "turn.started", "turn.reasoning", "turn.toolCall.requested", "turn.toolCall.completed", "turn.toolCall.denied", "turn.completed", "tool.approval.requested", "tool.approval.resolved", "sandbox.timeout"];
4
+ type SessionEventName = (typeof SESSION_EVENT_NAMES)[number];
5
+ export declare function loadSessionRun(ctx: {
6
+ db: DatabaseReader;
7
+ }, orgId: string, sessionId: string): Promise<LoadedRun>;
8
+ export declare function insertEvent(ctx: MutationCtx, args: {
9
+ orgId: string;
10
+ run: LoadedRun;
11
+ name: SessionEventName;
12
+ payload?: string;
13
+ }): Promise<void>;
2
14
  export declare const start: import("convex/server").RegisteredMutation<"public", {
3
15
  orgId: string;
4
16
  createdBy: string;
5
17
  agentId: import("convex/values").GenericId<"agents">;
6
18
  }, Promise<{
7
- sessionId: Id<"sessions">;
19
+ sessionId: string;
8
20
  workflowId: string;
9
21
  }>>;
10
22
  export declare const sendTurn: import("convex/server").RegisteredMutation<"public", {
11
23
  orgId: string;
12
- sessionId: import("convex/values").GenericId<"sessions">;
24
+ sessionId: string;
13
25
  input: string;
14
26
  }, Promise<null>>;
15
27
  export declare const appendReasoning: import("convex/server").RegisteredMutation<"public", {
16
28
  orgId: string;
17
- sessionId: import("convex/values").GenericId<"sessions">;
29
+ sessionId: string;
18
30
  chunk: string;
19
31
  }, Promise<null>>;
20
32
  export declare const requestToolCall: import("convex/server").RegisteredMutation<"public", {
21
33
  orgId: string;
22
34
  toolName: string;
23
- sessionId: import("convex/values").GenericId<"sessions">;
35
+ sessionId: string;
24
36
  argsJson: string;
25
37
  }, Promise<{
26
38
  authorized: boolean;
@@ -30,20 +42,26 @@ export declare const requestToolCall: import("convex/server").RegisteredMutation
30
42
  export declare const completeToolCall: import("convex/server").RegisteredMutation<"public", {
31
43
  orgId: string;
32
44
  toolName: string;
33
- sessionId: import("convex/values").GenericId<"sessions">;
45
+ sessionId: string;
34
46
  resultJson: string;
35
47
  }, Promise<null>>;
36
48
  export declare const completeTurn: import("convex/server").RegisteredMutation<"public", {
37
49
  orgId: string;
38
- sessionId: import("convex/values").GenericId<"sessions">;
50
+ sessionId: string;
51
+ }, Promise<null>>;
52
+ export declare const end: import("convex/server").RegisteredMutation<"public", {
53
+ orgId: string;
54
+ sessionId: string;
55
+ outcome: "completed" | "cancelled";
39
56
  }, Promise<null>>;
40
57
  export declare const listEvents: import("convex/server").RegisteredQuery<"public", {
41
58
  orgId: string;
42
- sessionId: import("convex/values").GenericId<"sessions">;
59
+ sessionId: string;
43
60
  }, Promise<{
44
- _id: import("convex/values").GenericId<"sessionEvents">;
61
+ _id: string;
45
62
  _creationTime: number;
46
- name: "session.started" | "turn.started" | "turn.reasoning" | "turn.toolCall.requested" | "turn.toolCall.completed" | "turn.toolCall.denied" | "turn.completed" | "tool.approval.requested" | "tool.approval.resolved" | "sandbox.timeout";
63
+ name: SessionEventName;
47
64
  payload: string | undefined;
48
65
  createdAt: number;
49
66
  }[]>>;
67
+ export {};
@@ -1,61 +1,40 @@
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, bindWorkflowToRun, loadRun, openRun, 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, "session", 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
  });
26
33
  }
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.
34
+ const CURRENT_SESSION_WORKFLOW_VERSION = "v2";
44
35
  export const start = mutation({
45
36
  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.
37
+ returns: v.object({ sessionId: v.string(), workflowId: v.string() }),
59
38
  handler: async (ctx, args) => {
60
39
  const orgId = requireOrgId(args.orgId);
61
40
  if (args.createdBy.trim().length === 0) {
@@ -63,113 +42,86 @@ export const start = mutation({
63
42
  }
64
43
  const agent = await ctx.db.get(args.agentId);
65
44
  assertSameOrg(orgId, agent, "Agent not found");
66
- const now = Date.now();
67
- const sessionId = await ctx.db.insert("sessions", {
45
+ const runId = await openRun(ctx, {
68
46
  orgId,
69
- agentId: args.agentId,
70
- status: "running",
71
- createdBy: args.createdBy,
72
- createdAt: now,
73
- updatedAt: now,
47
+ kind: "session",
48
+ data: {
49
+ agentId: args.agentId,
50
+ createdBy: args.createdBy,
51
+ workflowVersion: CURRENT_SESSION_WORKFLOW_VERSION,
52
+ },
74
53
  });
75
- await insertEvent(ctx, { orgId, sessionId, name: "session.started" });
76
- await ctx.runQuery(api.engine.resolvers.resolveManifest, {
77
- orgId,
78
- sessionId,
54
+ const workflowId = await workflow.start(ctx, internal.engine.workflows.sessionWorkflowV2, { orgId, runId }, {
55
+ onComplete: internal.engine.workflows.onSessionWorkflowCompleteV2,
56
+ context: { orgId, runId },
79
57
  });
80
- const workflowId = await workflow.start(ctx, internal.engine.workflows.sessionWorkflowV1, { sessionId });
81
- await ctx.db.patch(sessionId, {
58
+ await bindWorkflowToRun(ctx, {
59
+ orgId,
60
+ runId,
82
61
  workflowId,
83
62
  workflowVersion: CURRENT_SESSION_WORKFLOW_VERSION,
84
63
  });
85
- return { sessionId, workflowId };
64
+ const run = await loadSessionRun(ctx, orgId, runId);
65
+ await insertEvent(ctx, { orgId, run, name: "session.started" });
66
+ await ctx.runQuery(api.engine.resolvers.resolveManifest, {
67
+ orgId,
68
+ sessionId: runId,
69
+ });
70
+ return { sessionId: runId, workflowId };
86
71
  },
87
72
  });
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
73
  export const sendTurn = mutation({
94
- args: { orgId: v.string(), sessionId: v.id("sessions"), input: v.string() },
74
+ args: { orgId: v.string(), sessionId: v.string(), input: v.string() },
95
75
  returns: v.null(),
96
76
  handler: async (ctx, args) => {
97
77
  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})`);
78
+ const run = await loadSessionRun(ctx, orgId, args.sessionId);
79
+ if (run.status !== "running") {
80
+ throw new Error(`Session is not running (status=${run.status})`);
102
81
  }
103
- await insertEvent(ctx, {
104
- orgId,
105
- sessionId: args.sessionId,
106
- name: "turn.started",
107
- payload: args.input,
108
- });
82
+ await insertEvent(ctx, { orgId, run, name: "turn.started", payload: args.input });
109
83
  await ctx.runQuery(api.engine.resolvers.resolveManifest, {
110
84
  orgId,
111
85
  sessionId: args.sessionId,
112
86
  });
113
- await ctx.db.patch(args.sessionId, { updatedAt: Date.now() });
114
87
  return null;
115
88
  },
116
89
  });
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
90
  export const appendReasoning = mutation({
121
- args: { orgId: v.string(), sessionId: v.id("sessions"), chunk: v.string() },
91
+ args: { orgId: v.string(), sessionId: v.string(), chunk: v.string() },
122
92
  returns: v.null(),
123
93
  handler: async (ctx, args) => {
124
94
  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
- });
95
+ const run = await loadSessionRun(ctx, orgId, args.sessionId);
96
+ await insertEvent(ctx, { orgId, run, name: "turn.reasoning", payload: args.chunk });
133
97
  return null;
134
98
  },
135
99
  });
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
100
  export const requestToolCall = mutation({
142
101
  args: {
143
102
  orgId: v.string(),
144
- sessionId: v.id("sessions"),
103
+ sessionId: v.string(),
145
104
  toolName: v.string(),
146
105
  argsJson: v.string(),
147
106
  },
148
107
  returns: v.object({
149
108
  authorized: v.boolean(),
150
109
  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
110
  denied: v.boolean(),
156
111
  }),
157
112
  handler: async (ctx, args) => {
158
113
  const orgId = requireOrgId(args.orgId);
159
- const session = await ctx.db.get(args.sessionId);
160
- assertSameOrg(orgId, session, "Session not found");
114
+ const run = await loadSessionRun(ctx, orgId, args.sessionId);
115
+ const agentId = agentIdOfRun(ctx, run);
161
116
  await insertEvent(ctx, {
162
117
  orgId,
163
- sessionId: args.sessionId,
118
+ run,
164
119
  name: "turn.toolCall.requested",
165
120
  payload: JSON.stringify({ toolName: args.toolName, args: args.argsJson }),
166
121
  });
167
122
  const approvalRow = await ctx.db
168
123
  .query("toolApprovals")
169
- .withIndex("by_org_agent_tool", (q) => q
170
- .eq("orgId", orgId)
171
- .eq("agentId", session.agentId)
172
- .eq("toolName", args.toolName))
124
+ .withIndex("by_org_agent_tool", (q) => q.eq("orgId", orgId).eq("agentId", agentId).eq("toolName", args.toolName))
173
125
  .unique();
174
126
  const approval = approvalRow?.approval ?? "never";
175
127
  if (approval === "never") {
@@ -178,34 +130,32 @@ export const requestToolCall = mutation({
178
130
  if (approval === "always") {
179
131
  await insertEvent(ctx, {
180
132
  orgId,
181
- sessionId: args.sessionId,
133
+ run,
182
134
  name: "tool.approval.requested",
183
135
  payload: JSON.stringify({ toolName: args.toolName }),
184
136
  });
185
137
  return { authorized: false, approvalPending: true, denied: false };
186
138
  }
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))
139
+ const journal = await ctx.db
140
+ .query("worldEvents")
141
+ .withIndex("by_org_run_seq", (q) => q.eq("orgId", orgId).eq("runId", run.runId))
197
142
  .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;
143
+ let latestDecision;
144
+ for (const e of journal) {
145
+ if (e.eventType !== "tool.approval.resolved")
146
+ continue;
147
+ let resolved;
148
+ try {
149
+ resolved = JSON.parse(e.dataJson);
150
+ }
151
+ catch {
152
+ continue;
153
+ }
154
+ if (resolved.toolName === args.toolName) {
155
+ latestDecision =
156
+ typeof resolved.decision === "string" ? resolved.decision : undefined;
157
+ }
158
+ }
209
159
  if (latestDecision === "approved") {
210
160
  return { authorized: true, approvalPending: false, denied: false };
211
161
  }
@@ -214,85 +164,95 @@ export const requestToolCall = mutation({
214
164
  }
215
165
  await insertEvent(ctx, {
216
166
  orgId,
217
- sessionId: args.sessionId,
167
+ run,
218
168
  name: "tool.approval.requested",
219
169
  payload: JSON.stringify({ toolName: args.toolName }),
220
170
  });
221
171
  return { authorized: false, approvalPending: true, denied: false };
222
172
  },
223
173
  });
224
- // completeToolCall — the caller invokes this after actually running the
225
- // tool (authorized immediately, or after `toolApprovals.respond` cleared
226
- // the gate).
227
174
  export const completeToolCall = mutation({
228
175
  args: {
229
176
  orgId: v.string(),
230
- sessionId: v.id("sessions"),
177
+ sessionId: v.string(),
231
178
  toolName: v.string(),
232
179
  resultJson: v.string(),
233
180
  },
234
181
  returns: v.null(),
235
182
  handler: async (ctx, args) => {
236
183
  const orgId = requireOrgId(args.orgId);
237
- const session = await ctx.db.get(args.sessionId);
238
- assertSameOrg(orgId, session, "Session not found");
184
+ const run = await loadSessionRun(ctx, orgId, args.sessionId);
239
185
  await insertEvent(ctx, {
240
186
  orgId,
241
- sessionId: args.sessionId,
187
+ run,
242
188
  name: "turn.toolCall.completed",
243
189
  payload: JSON.stringify({ toolName: args.toolName, result: args.resultJson }),
244
190
  });
245
191
  return null;
246
192
  },
247
193
  });
248
- // completeTurn — closes the current turn.
249
194
  export const completeTurn = mutation({
250
- args: { orgId: v.string(), sessionId: v.id("sessions") },
195
+ args: { orgId: v.string(), sessionId: v.string() },
251
196
  returns: v.null(),
252
197
  handler: async (ctx, args) => {
253
198
  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, {
199
+ const run = await loadSessionRun(ctx, orgId, args.sessionId);
200
+ await insertEvent(ctx, { orgId, run, name: "turn.completed" });
201
+ return null;
202
+ },
203
+ });
204
+ export const end = mutation({
205
+ args: {
206
+ orgId: v.string(),
207
+ sessionId: v.string(),
208
+ outcome: v.union(v.literal("completed"), v.literal("cancelled")),
209
+ },
210
+ returns: v.null(),
211
+ handler: async (ctx, args) => {
212
+ const orgId = requireOrgId(args.orgId);
213
+ const run = await loadSessionRun(ctx, orgId, args.sessionId);
214
+ if (run.status !== "running") {
215
+ if (run.status === args.outcome)
216
+ return null;
217
+ throw new Error(`Session already ended (status=${run.status}); cannot end as ${args.outcome}`);
218
+ }
219
+ await appendEventCore(ctx, {
257
220
  orgId,
258
- sessionId: args.sessionId,
259
- name: "turn.completed",
221
+ runId: run.runId,
222
+ eventType: args.outcome === "completed" ? "run_completed" : "run_cancelled",
223
+ dataJson: JSON.stringify({ outcome: args.outcome }),
260
224
  });
261
- await ctx.db.patch(args.sessionId, { updatedAt: Date.now() });
262
225
  return null;
263
226
  },
264
227
  });
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
228
  export const listEvents = query({
269
- args: { orgId: v.string(), sessionId: v.id("sessions") },
229
+ args: { orgId: v.string(), sessionId: v.string() },
270
230
  returns: v.array(v.object({
271
- _id: v.id("sessionEvents"),
231
+ _id: v.string(),
272
232
  _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")),
233
+ 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
234
  payload: v.optional(v.string()),
278
235
  createdAt: v.number(),
279
236
  })),
280
237
  handler: async (ctx, args) => {
281
238
  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))
239
+ const run = await loadSessionRun(ctx, orgId, args.sessionId);
240
+ const journal = await ctx.db
241
+ .query("worldEvents")
242
+ .withIndex("by_org_run_seq", (q) => q.eq("orgId", orgId).eq("runId", run.runId))
287
243
  .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
- }));
244
+ const out = [];
245
+ for (const e of journal) {
246
+ if (!isSessionEventName(e.eventType))
247
+ continue;
248
+ out.push({
249
+ _id: slotToEventId(e.seq),
250
+ _creationTime: e.createdAt,
251
+ name: e.eventType,
252
+ payload: payloadOrUndefined(e.dataJson),
253
+ createdAt: e.createdAt,
254
+ });
255
+ }
256
+ return out;
297
257
  },
298
258
  });
@@ -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
+ runId: string;
28
+ limit: number;
29
+ cursor: string | null;
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
+ }[]>>;