@wopr-network/defcon 1.6.1 → 1.8.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/dist/src/engine/engine.d.ts +1 -0
- package/dist/src/engine/engine.js +36 -9
- package/dist/src/engine/event-types.d.ts +8 -0
- package/dist/src/engine/flow-spawner.js +1 -1
- package/dist/src/execution/admin-schemas.d.ts +5 -0
- package/dist/src/execution/admin-schemas.js +5 -0
- package/dist/src/execution/cli.js +1 -0
- package/dist/src/execution/mcp-server.d.ts +1 -0
- package/dist/src/execution/mcp-server.js +102 -25
- package/dist/src/repositories/drizzle/entity.repo.d.ts +3 -1
- package/dist/src/repositories/drizzle/entity.repo.js +14 -2
- package/dist/src/repositories/drizzle/flow.repo.d.ts +1 -0
- package/dist/src/repositories/drizzle/flow.repo.js +65 -0
- package/dist/src/repositories/drizzle/schema.d.ts +19 -0
- package/dist/src/repositories/drizzle/schema.js +2 -0
- package/dist/src/repositories/interfaces.d.ts +8 -1
- package/drizzle/0015_add_parent_entity_id.sql +2 -0
- package/drizzle/meta/_journal.json +8 -1
- package/package.json +1 -1
|
@@ -28,6 +28,7 @@ export interface EngineStatus {
|
|
|
28
28
|
flows: Record<string, Record<string, number>>;
|
|
29
29
|
activeInvocations: number;
|
|
30
30
|
pendingClaims: number;
|
|
31
|
+
versionDistribution: Record<string, Record<string, number>>;
|
|
31
32
|
}
|
|
32
33
|
export interface EngineDeps {
|
|
33
34
|
entityRepo: IEntityRepository;
|
|
@@ -72,10 +72,13 @@ export class Engine {
|
|
|
72
72
|
const entity = await this.entityRepo.get(entityId);
|
|
73
73
|
if (!entity)
|
|
74
74
|
throw new NotFoundError(`Entity "${entityId}" not found`);
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
if (entity.state === "cancelled") {
|
|
76
|
+
throw new ValidationError(`Entity "${entityId}" is cancelled and cannot be transitioned`);
|
|
77
|
+
}
|
|
78
|
+
// 2. Load flow at entity's pinned version
|
|
79
|
+
const flow = await this.flowRepo.getAtVersion(entity.flowId, entity.flowVersion);
|
|
77
80
|
if (!flow)
|
|
78
|
-
throw new NotFoundError(`Flow "${entity.flowId}" not found`);
|
|
81
|
+
throw new NotFoundError(`Flow "${entity.flowId}" version ${entity.flowVersion} not found`);
|
|
79
82
|
// 3. Find transition
|
|
80
83
|
const transition = findTransition(flow, entity.state, signal, { entity }, true, this.logger);
|
|
81
84
|
if (!transition)
|
|
@@ -325,7 +328,7 @@ export class Engine {
|
|
|
325
328
|
const flow = await this.flowRepo.getByName(flowName);
|
|
326
329
|
if (!flow)
|
|
327
330
|
throw new NotFoundError(`Flow "${flowName}" not found`);
|
|
328
|
-
let entity = await this.entityRepo.create(flow.id, flow.initialState, refs);
|
|
331
|
+
let entity = await this.entityRepo.create(flow.id, flow.initialState, refs, flow.version);
|
|
329
332
|
// Store any caller-supplied payload as initial artifacts so prompt templates
|
|
330
333
|
// can access refs like {{entity.artifacts.refs.linear.id}}.
|
|
331
334
|
if (payload && Object.keys(payload).length > 0) {
|
|
@@ -466,6 +469,8 @@ export class Engine {
|
|
|
466
469
|
const entity = entityMap.get(pending.entityId);
|
|
467
470
|
if (!entity)
|
|
468
471
|
continue;
|
|
472
|
+
if (entity.state === "cancelled")
|
|
473
|
+
continue;
|
|
469
474
|
// Guard: entity state must still match the invocation's stage — if another worker
|
|
470
475
|
// transitioned the entity between candidate fetch and now, skip this candidate.
|
|
471
476
|
if (entity.state !== pending.stage)
|
|
@@ -516,9 +521,11 @@ export class Engine {
|
|
|
516
521
|
this.logger.warn(`[engine] setAffinity failed for entity ${claimed.id} worker ${worker_id} — continuing:`, err);
|
|
517
522
|
}
|
|
518
523
|
}
|
|
519
|
-
const
|
|
524
|
+
const versionedFlow = await this.flowRepo.getAtVersion(claimed.flowId, claimed.flowVersion);
|
|
525
|
+
const effectiveFlow = versionedFlow ?? flow;
|
|
526
|
+
const state = effectiveFlow.states.find((s) => s.name === pending.stage);
|
|
520
527
|
const build = state
|
|
521
|
-
? await this.buildPromptForEntity(state, claimed,
|
|
528
|
+
? await this.buildPromptForEntity(state, claimed, effectiveFlow)
|
|
522
529
|
: { prompt: pending.prompt, context: null };
|
|
523
530
|
await this.eventEmitter.emit({
|
|
524
531
|
type: "entity.claimed",
|
|
@@ -543,12 +550,18 @@ export class Engine {
|
|
|
543
550
|
const claimed = await this.entityRepo.claim(flow.id, state.name, worker_id ?? `agent:${role}`);
|
|
544
551
|
if (!claimed)
|
|
545
552
|
continue;
|
|
546
|
-
|
|
553
|
+
if (claimed.state === "cancelled") {
|
|
554
|
+
await this.entityRepo.release(claimed.id, worker_id ?? `agent:${role}`);
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
const claimedVersionedFlow = await this.flowRepo.getAtVersion(claimed.flowId, claimed.flowVersion);
|
|
558
|
+
const claimedEffectiveFlow = claimedVersionedFlow ?? flow;
|
|
559
|
+
const canCreate = await this.checkConcurrency(claimedEffectiveFlow, claimed);
|
|
547
560
|
if (!canCreate) {
|
|
548
561
|
await this.entityRepo.release(claimed.id, worker_id ?? `agent:${role}`);
|
|
549
562
|
continue;
|
|
550
563
|
}
|
|
551
|
-
const build = await this.buildPromptForEntity(state, claimed,
|
|
564
|
+
const build = await this.buildPromptForEntity(state, claimed, claimedEffectiveFlow);
|
|
552
565
|
const invocation = await this.invocationRepo.create(claimed.id, state.name, build.prompt, build.mode, undefined, build.systemPrompt || build.userContent
|
|
553
566
|
? { systemPrompt: build.systemPrompt, userContent: build.userContent }
|
|
554
567
|
: undefined, state.agentRole ?? null);
|
|
@@ -647,7 +660,21 @@ export class Engine {
|
|
|
647
660
|
activeInvocations += active;
|
|
648
661
|
pendingClaims += pending;
|
|
649
662
|
}
|
|
650
|
-
|
|
663
|
+
const versionDistribution = {};
|
|
664
|
+
for (const flow of allFlows) {
|
|
665
|
+
const versionCounts = {};
|
|
666
|
+
for (const state of flow.states) {
|
|
667
|
+
const stateEntities = await this.entityRepo.findByFlowAndState(flow.id, state.name);
|
|
668
|
+
for (const e of stateEntities) {
|
|
669
|
+
const vKey = String(e.flowVersion);
|
|
670
|
+
versionCounts[vKey] = (versionCounts[vKey] ?? 0) + 1;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
if (Object.keys(versionCounts).length > 0) {
|
|
674
|
+
versionDistribution[flow.id] = versionCounts;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return { flows: statusData, activeInvocations, pendingClaims, versionDistribution };
|
|
651
678
|
}
|
|
652
679
|
startReaper(intervalMs, entityTtlMs = 60_000) {
|
|
653
680
|
let tickInFlight = false;
|
|
@@ -24,6 +24,14 @@ export type EngineEvent = {
|
|
|
24
24
|
entityId: string;
|
|
25
25
|
flowId: string;
|
|
26
26
|
emittedAt: Date;
|
|
27
|
+
} | {
|
|
28
|
+
type: "entity.cancelled";
|
|
29
|
+
entityId: string;
|
|
30
|
+
flowId: string;
|
|
31
|
+
cancelledBy: string;
|
|
32
|
+
reason: string | null;
|
|
33
|
+
cascade: boolean;
|
|
34
|
+
emittedAt: Date;
|
|
27
35
|
} | {
|
|
28
36
|
type: "invocation.created";
|
|
29
37
|
entityId: string;
|
|
@@ -11,7 +11,7 @@ export async function executeSpawn(transition, parentEntity, flowRepo, entityRep
|
|
|
11
11
|
const flow = await flowRepo.getByName(transition.spawnFlow);
|
|
12
12
|
if (!flow)
|
|
13
13
|
throw new NotFoundError(`Spawn flow "${transition.spawnFlow}" not found`);
|
|
14
|
-
const childEntity = await entityRepo.create(flow.id, flow.initialState, parentEntity.refs ?? undefined);
|
|
14
|
+
const childEntity = await entityRepo.create(flow.id, flow.initialState, parentEntity.refs ?? undefined, undefined, parentEntity.id);
|
|
15
15
|
try {
|
|
16
16
|
await entityRepo.appendSpawnedChild(parentEntity.id, {
|
|
17
17
|
childId: childEntity.id,
|
|
@@ -119,11 +119,16 @@ export declare const AdminFlowPauseSchema: z.ZodObject<{
|
|
|
119
119
|
}, z.core.$strip>;
|
|
120
120
|
export declare const AdminEntityCancelSchema: z.ZodObject<{
|
|
121
121
|
entity_id: z.ZodString;
|
|
122
|
+
cascade: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
123
|
+
reason: z.ZodOptional<z.ZodString>;
|
|
122
124
|
}, z.core.$strip>;
|
|
123
125
|
export declare const AdminEntityResetSchema: z.ZodObject<{
|
|
124
126
|
entity_id: z.ZodString;
|
|
125
127
|
target_state: z.ZodString;
|
|
126
128
|
}, z.core.$strip>;
|
|
129
|
+
export declare const AdminEntityMigrateSchema: z.ZodObject<{
|
|
130
|
+
entity_id: z.ZodString;
|
|
131
|
+
}, z.core.$strip>;
|
|
127
132
|
export declare const AdminWorkerDrainSchema: z.ZodObject<{
|
|
128
133
|
worker_id: z.ZodString;
|
|
129
134
|
}, z.core.$strip>;
|
|
@@ -128,11 +128,16 @@ export const AdminFlowPauseSchema = z.object({
|
|
|
128
128
|
});
|
|
129
129
|
export const AdminEntityCancelSchema = z.object({
|
|
130
130
|
entity_id: z.string().min(1),
|
|
131
|
+
cascade: z.boolean().optional().default(false),
|
|
132
|
+
reason: z.string().optional(),
|
|
131
133
|
});
|
|
132
134
|
export const AdminEntityResetSchema = z.object({
|
|
133
135
|
entity_id: z.string().min(1),
|
|
134
136
|
target_state: z.string().min(1),
|
|
135
137
|
});
|
|
138
|
+
export const AdminEntityMigrateSchema = z.object({
|
|
139
|
+
entity_id: z.string().min(1),
|
|
140
|
+
});
|
|
136
141
|
export const AdminWorkerDrainSchema = z.object({
|
|
137
142
|
worker_id: z.string().min(1),
|
|
138
143
|
});
|
|
@@ -184,6 +184,7 @@ program
|
|
|
184
184
|
transitions: transitionLogRepo,
|
|
185
185
|
eventRepo: new DrizzleEventRepository(db),
|
|
186
186
|
engine,
|
|
187
|
+
withTransaction: (fn) => withTransaction(sqlite, fn),
|
|
187
188
|
};
|
|
188
189
|
const reaperInterval = parseInt(opts.reaperInterval, 10);
|
|
189
190
|
if (Number.isNaN(reaperInterval) || reaperInterval < 1000) {
|
|
@@ -11,6 +11,7 @@ export interface McpServerDeps {
|
|
|
11
11
|
eventRepo: IEventRepository;
|
|
12
12
|
engine?: Engine;
|
|
13
13
|
logger?: Logger;
|
|
14
|
+
withTransaction?: <T>(fn: () => T | Promise<T>) => Promise<T>;
|
|
14
15
|
}
|
|
15
16
|
export interface McpServerOpts {
|
|
16
17
|
/** DEFCON_ADMIN_TOKEN — if set, admin.* tools require this token */
|
|
@@ -6,7 +6,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprot
|
|
|
6
6
|
import { DEFAULT_TIMEOUT_PROMPT } from "../engine/constants.js";
|
|
7
7
|
import { isTerminal } from "../engine/state-machine.js";
|
|
8
8
|
import { consoleLogger } from "../logger.js";
|
|
9
|
-
import { AdminEntityCancelSchema, AdminEntityResetSchema, AdminFlowCreateSchema, AdminFlowPauseSchema, AdminFlowRestoreSchema, AdminFlowSnapshotSchema, AdminFlowUpdateSchema, AdminGateAttachSchema, AdminGateCreateSchema, AdminGateRerunSchema, AdminStateCreateSchema, AdminStateUpdateSchema, AdminTransitionCreateSchema, AdminTransitionUpdateSchema, AdminWorkerDrainSchema, } from "./admin-schemas.js";
|
|
9
|
+
import { AdminEntityCancelSchema, AdminEntityMigrateSchema, AdminEntityResetSchema, AdminFlowCreateSchema, AdminFlowPauseSchema, AdminFlowRestoreSchema, AdminFlowSnapshotSchema, AdminFlowUpdateSchema, AdminGateAttachSchema, AdminGateCreateSchema, AdminGateRerunSchema, AdminStateCreateSchema, AdminStateUpdateSchema, AdminTransitionCreateSchema, AdminTransitionUpdateSchema, AdminWorkerDrainSchema, } from "./admin-schemas.js";
|
|
10
10
|
import { handleFlowClaim } from "./handlers/flow.js";
|
|
11
11
|
import { FlowFailSchema, FlowGetPromptSchema, FlowReportSchema, FlowSeedSchema, QueryEntitiesSchema, QueryEntitySchema, QueryFlowSchema, QueryInvocationsSchema, } from "./tool-schemas.js";
|
|
12
12
|
function getSystemDefaultGateTimeoutMs() {
|
|
@@ -350,8 +350,16 @@ const TOOL_DEFINITIONS = [
|
|
|
350
350
|
},
|
|
351
351
|
{
|
|
352
352
|
name: "admin.entity.cancel",
|
|
353
|
-
description: "Cancel an entity — fails active invocation, moves to 'cancelled' terminal state.",
|
|
354
|
-
inputSchema: {
|
|
353
|
+
description: "Cancel an entity — fails active invocation, moves to 'cancelled' terminal state. Use cascade to cancel all descendant entities.",
|
|
354
|
+
inputSchema: {
|
|
355
|
+
type: "object",
|
|
356
|
+
properties: {
|
|
357
|
+
entity_id: { type: "string" },
|
|
358
|
+
cascade: { type: "boolean", description: "Recursively cancel all child entities" },
|
|
359
|
+
reason: { type: "string", description: "Cancellation reason" },
|
|
360
|
+
},
|
|
361
|
+
required: ["entity_id"],
|
|
362
|
+
},
|
|
355
363
|
},
|
|
356
364
|
{
|
|
357
365
|
name: "admin.entity.reset",
|
|
@@ -362,6 +370,11 @@ const TOOL_DEFINITIONS = [
|
|
|
362
370
|
required: ["entity_id", "target_state"],
|
|
363
371
|
},
|
|
364
372
|
},
|
|
373
|
+
{
|
|
374
|
+
name: "admin.entity.migrate",
|
|
375
|
+
description: "Migrate an entity to the latest flow version. Validates that the entity's current state exists in the new version.",
|
|
376
|
+
inputSchema: { type: "object", properties: { entity_id: { type: "string" } }, required: ["entity_id"] },
|
|
377
|
+
},
|
|
365
378
|
{
|
|
366
379
|
name: "admin.worker.drain",
|
|
367
380
|
description: "Mark a worker as draining — claimWork() will skip it.",
|
|
@@ -465,6 +478,8 @@ export async function callToolHandler(deps, name, safeArgs, opts) {
|
|
|
465
478
|
return await handleAdminEntityCancel(deps, safeArgs);
|
|
466
479
|
case "admin.entity.reset":
|
|
467
480
|
return await handleAdminEntityReset(deps, safeArgs);
|
|
481
|
+
case "admin.entity.migrate":
|
|
482
|
+
return await handleAdminEntityMigrate(deps, safeArgs);
|
|
468
483
|
case "admin.worker.drain":
|
|
469
484
|
return await handleAdminWorkerDrain(deps, safeArgs);
|
|
470
485
|
case "admin.worker.undrain":
|
|
@@ -748,31 +763,67 @@ async function handleAdminEntityCancel(deps, args) {
|
|
|
748
763
|
const v = validateInput(AdminEntityCancelSchema, args);
|
|
749
764
|
if (!v.ok)
|
|
750
765
|
return v.result;
|
|
751
|
-
const
|
|
766
|
+
const { entity_id, cascade, reason } = v.data;
|
|
767
|
+
const entity = await deps.entities.get(entity_id);
|
|
752
768
|
if (!entity)
|
|
753
|
-
return errorResult(`Entity not found: ${
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
const
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
if (
|
|
763
|
-
|
|
769
|
+
return errorResult(`Entity not found: ${entity_id}`);
|
|
770
|
+
if (entity.state === "cancelled" && !cascade)
|
|
771
|
+
return errorResult(`Entity ${entity_id} is already cancelled`);
|
|
772
|
+
const cancelled = [];
|
|
773
|
+
const MAX_CANCEL_DEPTH = 100;
|
|
774
|
+
async function cancelOne(eid, depth, visited) {
|
|
775
|
+
if (visited.has(eid))
|
|
776
|
+
return;
|
|
777
|
+
visited.add(eid);
|
|
778
|
+
if (depth > MAX_CANCEL_DEPTH) {
|
|
779
|
+
throw new Error(`cancelOne exceeded maximum recursion depth of ${MAX_CANCEL_DEPTH} — possible cycle or pathologically deep entity tree`);
|
|
780
|
+
}
|
|
781
|
+
const e = await deps.entities.get(eid);
|
|
782
|
+
const alreadyCancelled = !e || e.state === "cancelled";
|
|
783
|
+
if (!alreadyCancelled) {
|
|
784
|
+
const invocations = await deps.invocations.findByEntity(eid);
|
|
785
|
+
for (const inv of invocations) {
|
|
786
|
+
if (inv.completedAt === null && inv.failedAt === null) {
|
|
787
|
+
await deps.invocations.fail(inv.id, reason ?? "Cancelled by admin");
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
await deps.entities.cancelEntity(eid);
|
|
791
|
+
await deps.transitions.record({
|
|
792
|
+
entityId: eid,
|
|
793
|
+
fromState: e.state,
|
|
794
|
+
toState: "cancelled",
|
|
795
|
+
trigger: "admin.cancel",
|
|
796
|
+
invocationId: null,
|
|
797
|
+
timestamp: new Date(),
|
|
798
|
+
});
|
|
799
|
+
if (deps.engine) {
|
|
800
|
+
await deps.engine.emit({
|
|
801
|
+
type: "entity.cancelled",
|
|
802
|
+
entityId: eid,
|
|
803
|
+
flowId: e.flowId,
|
|
804
|
+
cancelledBy: "admin",
|
|
805
|
+
reason: reason ?? null,
|
|
806
|
+
cascade: cascade ?? false,
|
|
807
|
+
emittedAt: new Date(),
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
cancelled.push(eid);
|
|
811
|
+
}
|
|
812
|
+
if (cascade) {
|
|
813
|
+
const children = await deps.entities.findByParentId(eid);
|
|
814
|
+
for (const child of children) {
|
|
815
|
+
await cancelOne(child.id, depth + 1, visited);
|
|
816
|
+
}
|
|
764
817
|
}
|
|
765
818
|
}
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
});
|
|
775
|
-
return jsonResult({ cancelled: true, entity_id: v.data.entity_id });
|
|
819
|
+
const run = () => cancelOne(entity_id, 0, new Set());
|
|
820
|
+
if (deps.withTransaction) {
|
|
821
|
+
await deps.withTransaction(run);
|
|
822
|
+
}
|
|
823
|
+
else {
|
|
824
|
+
await run();
|
|
825
|
+
}
|
|
826
|
+
return jsonResult({ cancelled: true, entity_id, cancelled_count: cancelled.length, cancelled_ids: cancelled });
|
|
776
827
|
}
|
|
777
828
|
async function handleAdminEntityReset(deps, args) {
|
|
778
829
|
const v = validateInput(AdminEntityResetSchema, args);
|
|
@@ -804,6 +855,32 @@ async function handleAdminEntityReset(deps, args) {
|
|
|
804
855
|
});
|
|
805
856
|
return jsonResult({ reset: true, entity_id: v.data.entity_id, state: updated.state });
|
|
806
857
|
}
|
|
858
|
+
async function handleAdminEntityMigrate(deps, args) {
|
|
859
|
+
const v = validateInput(AdminEntityMigrateSchema, args);
|
|
860
|
+
if (!v.ok)
|
|
861
|
+
return v.result;
|
|
862
|
+
const { entity_id: entityId } = v.data;
|
|
863
|
+
const entity = await deps.entities.get(entityId);
|
|
864
|
+
if (!entity)
|
|
865
|
+
return errorResult(`Entity not found: ${entityId}`);
|
|
866
|
+
const flow = await deps.flows.get(entity.flowId);
|
|
867
|
+
if (!flow)
|
|
868
|
+
return errorResult(`Flow not found: ${entity.flowId}`);
|
|
869
|
+
if (entity.flowVersion === flow.version) {
|
|
870
|
+
return jsonResult({
|
|
871
|
+
migrated: false,
|
|
872
|
+
message: "Entity is already on the latest version",
|
|
873
|
+
flowVersion: flow.version,
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
const stateExists = flow.states.some((s) => s.name === entity.state);
|
|
877
|
+
if (!stateExists) {
|
|
878
|
+
return errorResult(`Cannot migrate entity ${entityId}: current state '${entity.state}' does not exist in flow '${flow.name}' version ${flow.version}`);
|
|
879
|
+
}
|
|
880
|
+
await deps.entities.updateFlowVersion(entityId, flow.version);
|
|
881
|
+
const updated = await deps.entities.get(entityId);
|
|
882
|
+
return jsonResult(updated);
|
|
883
|
+
}
|
|
807
884
|
async function handleAdminWorkerDrain(deps, args) {
|
|
808
885
|
const v = validateInput(AdminWorkerDrainSchema, args);
|
|
809
886
|
if (!v.ok)
|
|
@@ -6,7 +6,7 @@ export declare class DrizzleEntityRepository implements IEntityRepository {
|
|
|
6
6
|
private db;
|
|
7
7
|
constructor(db: Db);
|
|
8
8
|
private toEntity;
|
|
9
|
-
create(flowId: string, initialState: string, refs?: Refs): Promise<Entity>;
|
|
9
|
+
create(flowId: string, initialState: string, refs?: Refs, flowVersion?: number, parentEntityId?: string): Promise<Entity>;
|
|
10
10
|
get(id: string): Promise<Entity | null>;
|
|
11
11
|
findByFlowAndState(flowId: string, state: string, limit?: number): Promise<Entity[]>;
|
|
12
12
|
hasAnyInFlowAndState(flowId: string, stateNames: string[]): Promise<boolean>;
|
|
@@ -23,7 +23,9 @@ export declare class DrizzleEntityRepository implements IEntityRepository {
|
|
|
23
23
|
reapExpired(ttlMs: number): Promise<string[]>;
|
|
24
24
|
setAffinity(entityId: string, workerId: string, role: string, expiresAt: Date): Promise<void>;
|
|
25
25
|
clearExpiredAffinity(): Promise<string[]>;
|
|
26
|
+
findByParentId(parentEntityId: string): Promise<Entity[]>;
|
|
26
27
|
cancelEntity(entityId: string): Promise<void>;
|
|
27
28
|
resetEntity(entityId: string, targetState: string): Promise<Entity>;
|
|
29
|
+
updateFlowVersion(entityId: string, version: number): Promise<void>;
|
|
28
30
|
}
|
|
29
31
|
export {};
|
|
@@ -22,9 +22,10 @@ export class DrizzleEntityRepository {
|
|
|
22
22
|
affinityWorkerId: row.affinityWorkerId ?? null,
|
|
23
23
|
affinityRole: row.affinityRole ?? null,
|
|
24
24
|
affinityExpiresAt: row.affinityExpiresAt ? new Date(row.affinityExpiresAt) : null,
|
|
25
|
+
parentEntityId: row.parentEntityId ?? null,
|
|
25
26
|
};
|
|
26
27
|
}
|
|
27
|
-
async create(flowId, initialState, refs) {
|
|
28
|
+
async create(flowId, initialState, refs, flowVersion, parentEntityId) {
|
|
28
29
|
const now = Date.now();
|
|
29
30
|
const id = crypto.randomUUID();
|
|
30
31
|
const row = {
|
|
@@ -35,12 +36,13 @@ export class DrizzleEntityRepository {
|
|
|
35
36
|
artifacts: null,
|
|
36
37
|
claimedBy: null,
|
|
37
38
|
claimedAt: null,
|
|
38
|
-
flowVersion: 1,
|
|
39
|
+
flowVersion: flowVersion ?? 1,
|
|
39
40
|
createdAt: now,
|
|
40
41
|
updatedAt: now,
|
|
41
42
|
affinityWorkerId: null,
|
|
42
43
|
affinityRole: null,
|
|
43
44
|
affinityExpiresAt: null,
|
|
45
|
+
parentEntityId: parentEntityId ?? null,
|
|
44
46
|
};
|
|
45
47
|
await this.db.insert(entities).values(row);
|
|
46
48
|
return this.toEntity(row);
|
|
@@ -188,6 +190,10 @@ export class DrizzleEntityRepository {
|
|
|
188
190
|
.all();
|
|
189
191
|
return rows.map((r) => r.id);
|
|
190
192
|
}
|
|
193
|
+
async findByParentId(parentEntityId) {
|
|
194
|
+
const rows = await this.db.select().from(entities).where(eq(entities.parentEntityId, parentEntityId));
|
|
195
|
+
return rows.map((r) => this.toEntity(r));
|
|
196
|
+
}
|
|
191
197
|
async cancelEntity(entityId) {
|
|
192
198
|
await this.db
|
|
193
199
|
.update(entities)
|
|
@@ -205,4 +211,10 @@ export class DrizzleEntityRepository {
|
|
|
205
211
|
throw new NotFoundError(`Entity not found: ${entityId}`);
|
|
206
212
|
return this.toEntity(rows[0]);
|
|
207
213
|
}
|
|
214
|
+
async updateFlowVersion(entityId, version) {
|
|
215
|
+
await this.db
|
|
216
|
+
.update(entities)
|
|
217
|
+
.set({ flowVersion: version, updatedAt: Date.now() })
|
|
218
|
+
.where(eq(entities.id, entityId));
|
|
219
|
+
}
|
|
208
220
|
}
|
|
@@ -9,6 +9,7 @@ export declare class DrizzleFlowRepository implements IFlowRepository {
|
|
|
9
9
|
create(input: CreateFlowInput): Promise<Flow>;
|
|
10
10
|
get(id: string): Promise<Flow | null>;
|
|
11
11
|
getByName(name: string): Promise<Flow | null>;
|
|
12
|
+
getAtVersion(flowId: string, version: number): Promise<Flow | null>;
|
|
12
13
|
list(): Promise<Flow[]>;
|
|
13
14
|
listAll(): Promise<Flow[]>;
|
|
14
15
|
update(id: string, changes: UpdateFlowInput): Promise<Flow>;
|
|
@@ -116,6 +116,67 @@ export class DrizzleFlowRepository {
|
|
|
116
116
|
return null;
|
|
117
117
|
return this.hydrateFlow(rows[0]);
|
|
118
118
|
}
|
|
119
|
+
async getAtVersion(flowId, version) {
|
|
120
|
+
const current = await this.get(flowId);
|
|
121
|
+
if (!current)
|
|
122
|
+
return null;
|
|
123
|
+
if (current.version === version)
|
|
124
|
+
return current;
|
|
125
|
+
const rows = this.db
|
|
126
|
+
.select()
|
|
127
|
+
.from(flowVersions)
|
|
128
|
+
.where(and(eq(flowVersions.flowId, flowId), eq(flowVersions.version, version)))
|
|
129
|
+
.all();
|
|
130
|
+
if (rows.length === 0)
|
|
131
|
+
return null;
|
|
132
|
+
const snap = rows[0].snapshot;
|
|
133
|
+
return {
|
|
134
|
+
id: flowId,
|
|
135
|
+
name: snap.name ?? current.name,
|
|
136
|
+
description: snap.description ?? null,
|
|
137
|
+
entitySchema: snap.entitySchema ?? null,
|
|
138
|
+
initialState: snap.initialState ?? current.initialState,
|
|
139
|
+
maxConcurrent: snap.maxConcurrent ?? 0,
|
|
140
|
+
maxConcurrentPerRepo: snap.maxConcurrentPerRepo ?? 0,
|
|
141
|
+
affinityWindowMs: snap.affinityWindowMs ?? 300000,
|
|
142
|
+
claimRetryAfterMs: snap.claimRetryAfterMs ?? null,
|
|
143
|
+
gateTimeoutMs: snap.gateTimeoutMs ?? null,
|
|
144
|
+
version,
|
|
145
|
+
createdBy: snap.createdBy ?? null,
|
|
146
|
+
discipline: snap.discipline ?? null,
|
|
147
|
+
defaultModelTier: snap.defaultModelTier ?? null,
|
|
148
|
+
timeoutPrompt: snap.timeoutPrompt ?? null,
|
|
149
|
+
paused: current.paused,
|
|
150
|
+
createdAt: snap.createdAt ? new Date(snap.createdAt) : null,
|
|
151
|
+
updatedAt: snap.updatedAt ? new Date(snap.updatedAt) : null,
|
|
152
|
+
states: (snap.states ?? []).map((s) => ({
|
|
153
|
+
id: s.id,
|
|
154
|
+
flowId,
|
|
155
|
+
name: s.name,
|
|
156
|
+
agentRole: s.agentRole ?? null,
|
|
157
|
+
modelTier: s.modelTier ?? null,
|
|
158
|
+
mode: s.mode ?? "passive",
|
|
159
|
+
promptTemplate: s.promptTemplate ?? null,
|
|
160
|
+
constraints: s.constraints ?? null,
|
|
161
|
+
onEnter: s.onEnter ?? null,
|
|
162
|
+
onExit: s.onExit ?? null,
|
|
163
|
+
retryAfterMs: s.retryAfterMs ?? null,
|
|
164
|
+
})),
|
|
165
|
+
transitions: (snap.transitions ?? []).map((t) => ({
|
|
166
|
+
id: t.id,
|
|
167
|
+
flowId,
|
|
168
|
+
fromState: t.fromState,
|
|
169
|
+
toState: t.toState,
|
|
170
|
+
trigger: t.trigger,
|
|
171
|
+
gateId: t.gateId ?? null,
|
|
172
|
+
condition: t.condition ?? null,
|
|
173
|
+
priority: t.priority ?? 0,
|
|
174
|
+
spawnFlow: t.spawnFlow ?? null,
|
|
175
|
+
spawnTemplate: t.spawnTemplate ?? null,
|
|
176
|
+
createdAt: t.createdAt ? new Date(t.createdAt) : null,
|
|
177
|
+
})),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
119
180
|
async list() {
|
|
120
181
|
const rows = this.db.select().from(flowDefinitions).all();
|
|
121
182
|
return rows.map((row) => this.hydrateFlow(row));
|
|
@@ -306,6 +367,10 @@ export class DrizzleFlowRepository {
|
|
|
306
367
|
createdAt: now,
|
|
307
368
|
})
|
|
308
369
|
.run();
|
|
370
|
+
tx.update(flowDefinitions)
|
|
371
|
+
.set({ version: version + 1, updatedAt: now })
|
|
372
|
+
.where(eq(flowDefinitions.id, flowId))
|
|
373
|
+
.run();
|
|
309
374
|
return version;
|
|
310
375
|
});
|
|
311
376
|
return {
|
|
@@ -1326,6 +1326,25 @@ export declare const entities: import("drizzle-orm/sqlite-core").SQLiteTableWith
|
|
|
1326
1326
|
identity: undefined;
|
|
1327
1327
|
generated: undefined;
|
|
1328
1328
|
}, {}, {}>;
|
|
1329
|
+
parentEntityId: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
1330
|
+
name: "parent_entity_id";
|
|
1331
|
+
tableName: "entities";
|
|
1332
|
+
dataType: "string";
|
|
1333
|
+
columnType: "SQLiteText";
|
|
1334
|
+
data: string;
|
|
1335
|
+
driverParam: string;
|
|
1336
|
+
notNull: false;
|
|
1337
|
+
hasDefault: false;
|
|
1338
|
+
isPrimaryKey: false;
|
|
1339
|
+
isAutoincrement: false;
|
|
1340
|
+
hasRuntimeDefault: false;
|
|
1341
|
+
enumValues: [string, ...string[]];
|
|
1342
|
+
baseColumn: never;
|
|
1343
|
+
identity: undefined;
|
|
1344
|
+
generated: undefined;
|
|
1345
|
+
}, {}, {
|
|
1346
|
+
length: number | undefined;
|
|
1347
|
+
}>;
|
|
1329
1348
|
};
|
|
1330
1349
|
dialect: "sqlite";
|
|
1331
1350
|
}>;
|
|
@@ -95,10 +95,12 @@ export const entities = sqliteTable("entities", {
|
|
|
95
95
|
affinityWorkerId: text("affinity_worker_id"),
|
|
96
96
|
affinityRole: text("affinity_role"),
|
|
97
97
|
affinityExpiresAt: integer("affinity_expires_at"),
|
|
98
|
+
parentEntityId: text("parent_entity_id"),
|
|
98
99
|
}, (table) => ({
|
|
99
100
|
flowStateIdx: index("entities_flow_state_idx").on(table.flowId, table.state),
|
|
100
101
|
claimIdx: index("entities_claim_idx").on(table.flowId, table.state, table.claimedBy),
|
|
101
102
|
affinityIdx: index("entities_affinity_idx").on(table.affinityWorkerId, table.affinityRole, table.affinityExpiresAt),
|
|
103
|
+
parentIdx: index("entities_parent_idx").on(table.parentEntityId),
|
|
102
104
|
}));
|
|
103
105
|
export const invocations = sqliteTable("invocations", {
|
|
104
106
|
id: text("id").primaryKey(),
|
|
@@ -35,6 +35,7 @@ export interface Entity {
|
|
|
35
35
|
affinityWorkerId: string | null;
|
|
36
36
|
affinityRole: string | null;
|
|
37
37
|
affinityExpiresAt: Date | null;
|
|
38
|
+
parentEntityId: string | null;
|
|
38
39
|
}
|
|
39
40
|
/** A single agent invocation tied to an entity */
|
|
40
41
|
export interface Invocation {
|
|
@@ -224,7 +225,7 @@ export interface CreateGateInput {
|
|
|
224
225
|
/** Data-access contract for entity lifecycle operations. */
|
|
225
226
|
export interface IEntityRepository {
|
|
226
227
|
/** Create a new entity in the given flow's initial state. */
|
|
227
|
-
create(flowId: string, initialState: string, refs?: Refs): Promise<Entity>;
|
|
228
|
+
create(flowId: string, initialState: string, refs?: Refs, flowVersion?: number, parentEntityId?: string): Promise<Entity>;
|
|
228
229
|
/** Get an entity by ID, or null if not found. */
|
|
229
230
|
get(id: string): Promise<Entity | null>;
|
|
230
231
|
/** Find entities in a given flow and state, up to an optional limit. */
|
|
@@ -255,10 +256,14 @@ export interface IEntityRepository {
|
|
|
255
256
|
childFlow: string;
|
|
256
257
|
spawnedAt: string;
|
|
257
258
|
}): Promise<void>;
|
|
259
|
+
/** Find all direct children of a parent entity. */
|
|
260
|
+
findByParentId(parentEntityId: string): Promise<Entity[]>;
|
|
258
261
|
/** Move entity to 'cancelled' terminal state and clear claimedBy/claimedAt. */
|
|
259
262
|
cancelEntity(entityId: string): Promise<void>;
|
|
260
263
|
/** Move entity to targetState and clear claimedBy/claimedAt. Returns the updated entity. */
|
|
261
264
|
resetEntity(entityId: string, targetState: string): Promise<Entity>;
|
|
265
|
+
/** Update an entity's pinned flow version. */
|
|
266
|
+
updateFlowVersion(entityId: string, version: number): Promise<void>;
|
|
262
267
|
}
|
|
263
268
|
/** Fields that can be updated on a flow's top-level definition */
|
|
264
269
|
export type UpdateFlowInput = Partial<Omit<Flow, "id" | "states" | "transitions" | "createdAt" | "updatedAt">>;
|
|
@@ -276,6 +281,8 @@ export interface IFlowRepository {
|
|
|
276
281
|
get(id: string): Promise<Flow | null>;
|
|
277
282
|
/** Get a flow by unique name. Returns null if not found. */
|
|
278
283
|
getByName(name: string): Promise<Flow | null>;
|
|
284
|
+
/** Load a flow definition at a specific version. Returns the snapshot if version < current, or live flow if version === current. */
|
|
285
|
+
getAtVersion(flowId: string, version: number): Promise<Flow | null>;
|
|
279
286
|
/** Update a flow's top-level fields. */
|
|
280
287
|
update(id: string, changes: UpdateFlowInput): Promise<Flow>;
|
|
281
288
|
/** Add a state definition to a flow. */
|
|
@@ -106,6 +106,13 @@
|
|
|
106
106
|
"when": 1773031139526,
|
|
107
107
|
"tag": "0014_smiling_crusher_hogan",
|
|
108
108
|
"breakpoints": true
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"idx": 15,
|
|
112
|
+
"version": "6",
|
|
113
|
+
"when": 1773200000001,
|
|
114
|
+
"tag": "0015_add_parent_entity_id",
|
|
115
|
+
"breakpoints": true
|
|
109
116
|
}
|
|
110
117
|
]
|
|
111
|
-
}
|
|
118
|
+
}
|