@webgrow/skillhub 0.1.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/convex/runs.ts ADDED
@@ -0,0 +1,183 @@
1
+ import { v } from "convex/values";
2
+ import { mutation, query } from "./_generated/server";
3
+
4
+ const activeStatuses = ["running", "waiting", "paused"] as const;
5
+
6
+ async function nextRunEventSequence(ctx: any, runId: any) {
7
+ const latest = await ctx.db
8
+ .query("runEvents")
9
+ .withIndex("by_run_sequence", (q: any) => q.eq("runId", runId))
10
+ .order("desc")
11
+ .first();
12
+
13
+ return (latest?.sequence ?? 0) + 1;
14
+ }
15
+
16
+ export const createRun = mutation({
17
+ args: {
18
+ loopSlug: v.string(),
19
+ loopVersionId: v.optional(v.id("loopVersions")),
20
+ prompt: v.optional(v.string()),
21
+ graph: v.optional(v.any()),
22
+ controls: v.optional(v.any()),
23
+ context: v.optional(v.any()),
24
+ },
25
+ handler: async (ctx, args) => {
26
+ const now = Date.now();
27
+ const runId = await ctx.db.insert("loopRuns", {
28
+ loopSlug: args.loopSlug,
29
+ loopVersionId: args.loopVersionId,
30
+ status: "running",
31
+ activeNodeId: args.graph?.entryNodeId,
32
+ prompt: args.prompt,
33
+ graph: args.graph,
34
+ controls: args.controls,
35
+ context: args.context,
36
+ transitionCount: 0,
37
+ startedAt: now,
38
+ updatedAt: now,
39
+ });
40
+
41
+ await ctx.db.insert("runEvents", {
42
+ runId,
43
+ sequence: 1,
44
+ eventType: "run.created",
45
+ level: "info",
46
+ message: `Run created for ${args.loopSlug}`,
47
+ payload: {
48
+ loopSlug: args.loopSlug,
49
+ prompt: args.prompt,
50
+ },
51
+ createdAt: now,
52
+ });
53
+
54
+ return runId;
55
+ },
56
+ });
57
+
58
+ export const getRun = query({
59
+ args: {
60
+ runId: v.id("loopRuns"),
61
+ eventLimit: v.optional(v.number()),
62
+ logLimit: v.optional(v.number()),
63
+ },
64
+ handler: async (ctx, args) => {
65
+ const run = await ctx.db.get(args.runId);
66
+ if (!run) {
67
+ return null;
68
+ }
69
+
70
+ const eventLimit = Math.min(args.eventLimit ?? 100, 500);
71
+ const logLimit = Math.min(args.logLimit ?? 100, 500);
72
+
73
+ const [events, hostActions, logs, artifacts] = await Promise.all([
74
+ ctx.db
75
+ .query("runEvents")
76
+ .withIndex("by_run_sequence", (q) => q.eq("runId", args.runId))
77
+ .order("desc")
78
+ .take(eventLimit),
79
+ ctx.db
80
+ .query("hostActions")
81
+ .withIndex("by_run_createdAt", (q) => q.eq("runId", args.runId))
82
+ .order("desc")
83
+ .take(100),
84
+ ctx.db
85
+ .query("bridgeLogs")
86
+ .withIndex("by_run_createdAt", (q) => q.eq("runId", args.runId))
87
+ .order("desc")
88
+ .take(logLimit),
89
+ ctx.db
90
+ .query("artifacts")
91
+ .withIndex("by_run_createdAt", (q) => q.eq("runId", args.runId))
92
+ .order("desc")
93
+ .take(100),
94
+ ]);
95
+
96
+ return {
97
+ run,
98
+ events: events.reverse(),
99
+ hostActions: hostActions.reverse(),
100
+ logs: logs.reverse(),
101
+ artifacts: artifacts.reverse(),
102
+ };
103
+ },
104
+ });
105
+
106
+ export const listActive = query({
107
+ args: {
108
+ limit: v.optional(v.number()),
109
+ },
110
+ handler: async (ctx, args) => {
111
+ const limit = Math.min(args.limit ?? 50, 200);
112
+ const batches = await Promise.all(
113
+ activeStatuses.map((status) =>
114
+ ctx.db
115
+ .query("loopRuns")
116
+ .withIndex("by_status_updatedAt", (q) => q.eq("status", status))
117
+ .order("desc")
118
+ .take(limit),
119
+ ),
120
+ );
121
+
122
+ return batches
123
+ .flat()
124
+ .sort((a, b) => b.updatedAt - a.updatedAt)
125
+ .slice(0, limit);
126
+ },
127
+ });
128
+
129
+ export const listRecent = query({
130
+ args: {
131
+ limit: v.optional(v.number()),
132
+ },
133
+ handler: async (ctx, args) => {
134
+ const limit = Math.min(args.limit ?? 20, 100);
135
+ return await ctx.db
136
+ .query("loopRuns")
137
+ .withIndex("by_updatedAt")
138
+ .order("desc")
139
+ .take(limit);
140
+ },
141
+ });
142
+
143
+ export const appendEvent = mutation({
144
+ args: {
145
+ runId: v.id("loopRuns"),
146
+ eventType: v.string(),
147
+ level: v.union(
148
+ v.literal("debug"),
149
+ v.literal("info"),
150
+ v.literal("warn"),
151
+ v.literal("error"),
152
+ ),
153
+ message: v.optional(v.string()),
154
+ nodeId: v.optional(v.string()),
155
+ edgeId: v.optional(v.string()),
156
+ payload: v.optional(v.any()),
157
+ },
158
+ handler: async (ctx, args) => {
159
+ const run = await ctx.db.get(args.runId);
160
+ if (!run) {
161
+ throw new Error(`Run not found: ${args.runId}`);
162
+ }
163
+
164
+ const now = Date.now();
165
+ const sequence = await nextRunEventSequence(ctx, args.runId);
166
+
167
+ await ctx.db.insert("runEvents", {
168
+ runId: args.runId,
169
+ sequence,
170
+ eventType: args.eventType,
171
+ level: args.level,
172
+ message: args.message,
173
+ nodeId: args.nodeId,
174
+ edgeId: args.edgeId,
175
+ payload: args.payload,
176
+ createdAt: now,
177
+ });
178
+
179
+ await ctx.db.patch(args.runId, { updatedAt: now });
180
+
181
+ return sequence;
182
+ },
183
+ });
@@ -0,0 +1,220 @@
1
+ import { defineSchema, defineTable } from "convex/server";
2
+ import { v } from "convex/values";
3
+
4
+ const runStatus = v.union(
5
+ v.literal("running"),
6
+ v.literal("waiting"),
7
+ v.literal("paused"),
8
+ v.literal("completed"),
9
+ v.literal("failed"),
10
+ v.literal("canceled"),
11
+ );
12
+
13
+ const actionStatus = v.union(
14
+ v.literal("pending"),
15
+ v.literal("claimed"),
16
+ v.literal("applying"),
17
+ v.literal("completed"),
18
+ v.literal("failed"),
19
+ v.literal("skipped"),
20
+ v.literal("expired"),
21
+ );
22
+
23
+ const leaseStatus = v.union(
24
+ v.literal("active"),
25
+ v.literal("released"),
26
+ v.literal("completed"),
27
+ v.literal("expired"),
28
+ v.literal("failed"),
29
+ );
30
+
31
+ const logLevel = v.union(
32
+ v.literal("debug"),
33
+ v.literal("info"),
34
+ v.literal("warn"),
35
+ v.literal("error"),
36
+ );
37
+
38
+ const skillVersionStatus = v.union(
39
+ v.literal("draft"),
40
+ v.literal("published"),
41
+ v.literal("archived"),
42
+ );
43
+
44
+ const loopVersionStatus = v.union(
45
+ v.literal("draft"),
46
+ v.literal("published"),
47
+ v.literal("archived"),
48
+ );
49
+
50
+ export default defineSchema({
51
+ skills: defineTable({
52
+ slug: v.string(),
53
+ name: v.string(),
54
+ summary: v.optional(v.string()),
55
+ tags: v.array(v.string()),
56
+ latestVersionId: v.optional(v.id("skillVersions")),
57
+ draftVersionId: v.optional(v.id("skillVersions")),
58
+ createdAt: v.number(),
59
+ updatedAt: v.number(),
60
+ })
61
+ .index("by_slug", ["slug"])
62
+ .index("by_updatedAt", ["updatedAt"]),
63
+
64
+ skillVersions: defineTable({
65
+ skillId: v.id("skills"),
66
+ version: v.string(),
67
+ status: skillVersionStatus,
68
+ instructions: v.string(),
69
+ inputSpec: v.optional(v.string()),
70
+ outputSpec: v.optional(v.string()),
71
+ testPrompt: v.optional(v.string()),
72
+ createdAt: v.number(),
73
+ updatedAt: v.number(),
74
+ publishedAt: v.optional(v.number()),
75
+ })
76
+ .index("by_skill_status", ["skillId", "status"])
77
+ .index("by_skill_version", ["skillId", "version"])
78
+ .index("by_status_updatedAt", ["status", "updatedAt"]),
79
+
80
+ loops: defineTable({
81
+ slug: v.string(),
82
+ name: v.string(),
83
+ description: v.optional(v.string()),
84
+ primarySkill: v.optional(v.string()),
85
+ tags: v.array(v.string()),
86
+ latestVersionId: v.optional(v.id("loopVersions")),
87
+ draftVersionId: v.optional(v.id("loopVersions")),
88
+ createdAt: v.number(),
89
+ updatedAt: v.number(),
90
+ })
91
+ .index("by_slug", ["slug"])
92
+ .index("by_updatedAt", ["updatedAt"]),
93
+
94
+ loopVersions: defineTable({
95
+ loopId: v.id("loops"),
96
+ version: v.string(),
97
+ status: loopVersionStatus,
98
+ manifest: v.any(),
99
+ contentHash: v.string(),
100
+ createdAt: v.number(),
101
+ updatedAt: v.number(),
102
+ publishedAt: v.optional(v.number()),
103
+ })
104
+ .index("by_loop_status", ["loopId", "status"])
105
+ .index("by_loop_version", ["loopId", "version"])
106
+ .index("by_content_hash", ["contentHash"]),
107
+
108
+ loopRuns: defineTable({
109
+ loopSlug: v.string(),
110
+ loopVersionId: v.optional(v.id("loopVersions")),
111
+ status: runStatus,
112
+ activeNodeId: v.optional(v.string()),
113
+ prompt: v.optional(v.string()),
114
+ summary: v.optional(v.string()),
115
+ graph: v.optional(v.any()),
116
+ controls: v.optional(v.any()),
117
+ context: v.optional(v.any()),
118
+ transitionCount: v.number(),
119
+ startedAt: v.number(),
120
+ completedAt: v.optional(v.number()),
121
+ updatedAt: v.number(),
122
+ })
123
+ .index("by_status_updatedAt", ["status", "updatedAt"])
124
+ .index("by_loop_status", ["loopSlug", "status"])
125
+ .index("by_updatedAt", ["updatedAt"]),
126
+
127
+ runEvents: defineTable({
128
+ runId: v.id("loopRuns"),
129
+ sequence: v.number(),
130
+ eventType: v.string(),
131
+ nodeId: v.optional(v.string()),
132
+ edgeId: v.optional(v.string()),
133
+ level: logLevel,
134
+ message: v.optional(v.string()),
135
+ payload: v.optional(v.any()),
136
+ createdAt: v.number(),
137
+ })
138
+ .index("by_run_sequence", ["runId", "sequence"])
139
+ .index("by_run_createdAt", ["runId", "createdAt"])
140
+ .index("by_type_createdAt", ["eventType", "createdAt"]),
141
+
142
+ hostActions: defineTable({
143
+ runId: v.id("loopRuns"),
144
+ loopSlug: v.string(),
145
+ nodeId: v.optional(v.string()),
146
+ actionType: v.string(),
147
+ title: v.string(),
148
+ status: actionStatus,
149
+ idempotencyKey: v.string(),
150
+ payload: v.any(),
151
+ codexTool: v.optional(v.any()),
152
+ claimId: v.optional(v.string()),
153
+ claimedBy: v.optional(v.string()),
154
+ claimedAt: v.optional(v.number()),
155
+ leaseExpiresAt: v.optional(v.number()),
156
+ result: v.optional(v.any()),
157
+ error: v.optional(v.string()),
158
+ completedAt: v.optional(v.number()),
159
+ createdAt: v.number(),
160
+ updatedAt: v.number(),
161
+ })
162
+ .index("by_status_createdAt", ["status", "createdAt"])
163
+ .index("by_run_status", ["runId", "status"])
164
+ .index("by_run_createdAt", ["runId", "createdAt"])
165
+ .index("by_claim", ["claimId"])
166
+ .index("by_idempotency", ["idempotencyKey"]),
167
+
168
+ bridgeSessions: defineTable({
169
+ name: v.string(),
170
+ kind: v.string(),
171
+ status: v.union(v.literal("online"), v.literal("offline"), v.literal("draining")),
172
+ capabilities: v.array(v.string()),
173
+ lastSeenAt: v.number(),
174
+ createdAt: v.number(),
175
+ updatedAt: v.number(),
176
+ })
177
+ .index("by_status", ["status"])
178
+ .index("by_lastSeenAt", ["lastSeenAt"]),
179
+
180
+ bridgeLeases: defineTable({
181
+ actionId: v.id("hostActions"),
182
+ sessionId: v.optional(v.id("bridgeSessions")),
183
+ claimId: v.string(),
184
+ status: leaseStatus,
185
+ leaseExpiresAt: v.number(),
186
+ heartbeatAt: v.number(),
187
+ createdAt: v.number(),
188
+ updatedAt: v.number(),
189
+ })
190
+ .index("by_claim", ["claimId"])
191
+ .index("by_action_status", ["actionId", "status"])
192
+ .index("by_session_status", ["sessionId", "status"])
193
+ .index("by_expiry", ["leaseExpiresAt"]),
194
+
195
+ bridgeLogs: defineTable({
196
+ runId: v.optional(v.id("loopRuns")),
197
+ actionId: v.optional(v.id("hostActions")),
198
+ claimId: v.optional(v.string()),
199
+ source: v.string(),
200
+ level: logLevel,
201
+ message: v.string(),
202
+ data: v.optional(v.any()),
203
+ createdAt: v.number(),
204
+ })
205
+ .index("by_run_createdAt", ["runId", "createdAt"])
206
+ .index("by_action_createdAt", ["actionId", "createdAt"])
207
+ .index("by_claim_createdAt", ["claimId", "createdAt"]),
208
+
209
+ artifacts: defineTable({
210
+ runId: v.id("loopRuns"),
211
+ actionId: v.optional(v.id("hostActions")),
212
+ kind: v.string(),
213
+ label: v.string(),
214
+ uri: v.optional(v.string()),
215
+ data: v.optional(v.any()),
216
+ createdAt: v.number(),
217
+ })
218
+ .index("by_run_createdAt", ["runId", "createdAt"])
219
+ .index("by_action_createdAt", ["actionId", "createdAt"]),
220
+ });