Files

157 lines
5.0 KiB
TypeScript

import { v } from "convex/values";
import type { Id } from "./_generated/dataModel";
import type { MutationCtx } from "./_generated/server";
import { internalMutation } from "./_generated/server";
/**
* The system actors used by the coordinator and agent tooling. The existing
* `events.actor` union already permits `{ kind: "system", service }` and
* `{ kind: "tool", tool }`; these constants name the canonical services.
*/
export const SYSTEM_SERVICE_PROJECT_SETUP = "project-setup";
export const SYSTEM_SERVICE_AGENT_GATEWAY = "agent-gateway";
interface AppendInput {
readonly actor:
| { readonly kind: "system"; readonly service: string }
| { readonly kind: "tool"; readonly tool: string };
readonly artifactIds?: readonly Id<"artifacts">[];
readonly causationId?: Id<"events">;
readonly correlationId: string;
readonly idempotencyKey: string;
readonly organizationId: Id<"organizations">;
readonly payload: unknown;
readonly projectId?: Id<"projects">;
readonly replyToEventId?: Id<"events">;
readonly scopeKind: "global" | "work";
readonly threadId?: Id<"threads">;
readonly type: string;
readonly visibility: "timeline" | "compact" | "internal";
readonly workId?: Id<"works">;
}
/**
* Append an immutable system/tool event with exact-once semantics: an event
* with the same `(organizationId, idempotencyKey)` is returned unchanged on
* retry. This is the shared primitive every coordinator phase and agent tool
* writes through.
*/
const appendEventOnce = async (
ctx: MutationCtx,
input: AppendInput
): Promise<Id<"events">> => {
const existing = await ctx.db
.query("events")
.withIndex("by_organizationId_and_idempotencyKey", (q) =>
q
.eq("organizationId", input.organizationId)
.eq("idempotencyKey", input.idempotencyKey)
)
.unique();
if (existing) {
return existing._id;
}
const now = Date.now();
return await ctx.db.insert("events", {
actor: input.actor,
artifactIds: input.artifactIds ? [...input.artifactIds] : undefined,
causationId: input.causationId,
correlationId: input.correlationId,
idempotencyKey: input.idempotencyKey,
occurredAt: now,
organizationId: input.organizationId,
payload: input.payload,
projectId: input.projectId,
recordedAt: now,
replyToEventId: input.replyToEventId,
scopeKind: input.scopeKind,
threadId: input.threadId,
type: input.type,
visibility: input.visibility,
workId: input.workId,
});
};
export const appendSystemEvent = internalMutation({
args: {
actorService: v.string(),
artifactIds: v.optional(v.array(v.id("artifacts"))),
causationId: v.optional(v.id("events")),
correlationId: v.string(),
idempotencyKey: v.string(),
organizationId: v.id("organizations"),
payload: v.any(),
projectId: v.optional(v.id("projects")),
replyToEventId: v.optional(v.id("events")),
scopeKind: v.union(v.literal("global"), v.literal("work")),
threadId: v.optional(v.id("threads")),
type: v.string(),
visibility: v.union(
v.literal("timeline"),
v.literal("compact"),
v.literal("internal")
),
workId: v.optional(v.id("works")),
},
handler: (ctx, args): Promise<Id<"events">> =>
appendEventOnce(ctx, {
actor: { kind: "system", service: args.actorService },
artifactIds: args.artifactIds,
causationId: args.causationId,
correlationId: args.correlationId,
idempotencyKey: args.idempotencyKey,
organizationId: args.organizationId,
payload: args.payload,
projectId: args.projectId,
replyToEventId: args.replyToEventId,
scopeKind: args.scopeKind,
threadId: args.threadId,
type: args.type,
visibility: args.visibility,
workId: args.workId,
}),
});
export const appendToolEvent = internalMutation({
args: {
actorTool: v.string(),
artifactIds: v.optional(v.array(v.id("artifacts"))),
causationId: v.optional(v.id("events")),
correlationId: v.string(),
idempotencyKey: v.string(),
organizationId: v.id("organizations"),
payload: v.any(),
projectId: v.optional(v.id("projects")),
replyToEventId: v.optional(v.id("events")),
scopeKind: v.union(v.literal("global"), v.literal("work")),
threadId: v.optional(v.id("threads")),
type: v.string(),
visibility: v.union(
v.literal("timeline"),
v.literal("compact"),
v.literal("internal")
),
workId: v.optional(v.id("works")),
},
handler: (ctx, args): Promise<Id<"events">> =>
appendEventOnce(ctx, {
actor: { kind: "tool", tool: args.actorTool },
artifactIds: args.artifactIds,
causationId: args.causationId,
correlationId: args.correlationId,
idempotencyKey: args.idempotencyKey,
organizationId: args.organizationId,
payload: args.payload,
projectId: args.projectId,
replyToEventId: args.replyToEventId,
scopeKind: args.scopeKind,
threadId: args.threadId,
type: args.type,
visibility: args.visibility,
workId: args.workId,
}),
});
export { appendEventOnce };