@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.
- package/README.md +33 -8
- package/dist/component/_generated/api.d.ts +14 -3
- package/dist/component/_generated/api.js +0 -25
- package/dist/component/_generated/component.d.ts +369 -73
- package/dist/component/_generated/component.js +0 -9
- package/dist/component/_generated/dataModel.js +0 -9
- package/dist/component/_generated/server.js +0 -69
- package/dist/component/convex.config.js +0 -44
- package/dist/component/engine/agents.d.ts +3 -3
- package/dist/component/engine/agents.js +0 -7
- package/dist/component/engine/awaitEventTimeout.d.ts +1 -1
- package/dist/component/engine/awaitEventTimeout.js +16 -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 +5 -5
- package/dist/component/engine/delegation.js +0 -74
- package/dist/component/engine/directory.js +0 -18
- package/dist/component/engine/durableJob.d.ts +24 -17
- package/dist/component/engine/durableJob.js +182 -138
- package/dist/component/engine/mandates.d.ts +2 -2
- 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 +2 -2
- 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 +130 -170
- 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 +7 -9
- package/dist/component/engine/wakeup.js +54 -127
- package/dist/component/engine/workflows.d.ts +26 -8
- package/dist/component/engine/workflows.js +59 -64
- package/dist/component/engine/world.d.ts +250 -0
- package/dist/component/engine/world.js +689 -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 +240 -175
- package/dist/component/schema.js +134 -294
- package/package.json +21 -6
- 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
|
@@ -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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
59
|
+
sessionId: string;
|
|
43
60
|
}, Promise<{
|
|
44
|
-
_id:
|
|
61
|
+
_id: string;
|
|
45
62
|
_creationTime: number;
|
|
46
|
-
name:
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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 = "
|
|
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.
|
|
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
|
|
67
|
-
const sessionId = await ctx.db.insert("sessions", {
|
|
45
|
+
const runId = await openRun(ctx, {
|
|
68
46
|
orgId,
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
47
|
+
kind: "session",
|
|
48
|
+
data: {
|
|
49
|
+
agentId: args.agentId,
|
|
50
|
+
createdBy: args.createdBy,
|
|
51
|
+
workflowVersion: CURRENT_SESSION_WORKFLOW_VERSION,
|
|
52
|
+
},
|
|
74
53
|
});
|
|
75
|
-
await
|
|
76
|
-
|
|
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
|
-
|
|
81
|
-
|
|
58
|
+
await bindWorkflowToRun(ctx, {
|
|
59
|
+
orgId,
|
|
60
|
+
runId,
|
|
82
61
|
workflowId,
|
|
83
62
|
workflowVersion: CURRENT_SESSION_WORKFLOW_VERSION,
|
|
84
63
|
});
|
|
85
|
-
|
|
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.
|
|
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
|
|
99
|
-
|
|
100
|
-
|
|
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.
|
|
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
|
|
126
|
-
|
|
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.
|
|
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
|
|
160
|
-
|
|
114
|
+
const run = await loadSessionRun(ctx, orgId, args.sessionId);
|
|
115
|
+
const agentId = agentIdOfRun(ctx, run);
|
|
161
116
|
await insertEvent(ctx, {
|
|
162
117
|
orgId,
|
|
163
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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))
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
e.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
259
|
-
|
|
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.
|
|
229
|
+
args: { orgId: v.string(), sessionId: v.string() },
|
|
270
230
|
returns: v.array(v.object({
|
|
271
|
-
_id: v.
|
|
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
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
.
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
+
}[]>>;
|