@wopr-network/defcon 1.6.1 → 1.7.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 +27 -9
- package/dist/src/execution/admin-schemas.d.ts +3 -0
- package/dist/src/execution/admin-schemas.js +3 -0
- package/dist/src/execution/mcp-server.js +34 -1
- package/dist/src/repositories/drizzle/entity.repo.d.ts +2 -1
- package/dist/src/repositories/drizzle/entity.repo.js +8 -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/interfaces.d.ts +5 -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,10 @@ 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
|
-
// 2. Load flow
|
|
76
|
-
const flow = await this.flowRepo.
|
|
75
|
+
// 2. Load flow at entity's pinned version
|
|
76
|
+
const flow = await this.flowRepo.getAtVersion(entity.flowId, entity.flowVersion);
|
|
77
77
|
if (!flow)
|
|
78
|
-
throw new NotFoundError(`Flow "${entity.flowId}" not found`);
|
|
78
|
+
throw new NotFoundError(`Flow "${entity.flowId}" version ${entity.flowVersion} not found`);
|
|
79
79
|
// 3. Find transition
|
|
80
80
|
const transition = findTransition(flow, entity.state, signal, { entity }, true, this.logger);
|
|
81
81
|
if (!transition)
|
|
@@ -325,7 +325,7 @@ export class Engine {
|
|
|
325
325
|
const flow = await this.flowRepo.getByName(flowName);
|
|
326
326
|
if (!flow)
|
|
327
327
|
throw new NotFoundError(`Flow "${flowName}" not found`);
|
|
328
|
-
let entity = await this.entityRepo.create(flow.id, flow.initialState, refs);
|
|
328
|
+
let entity = await this.entityRepo.create(flow.id, flow.initialState, refs, flow.version);
|
|
329
329
|
// Store any caller-supplied payload as initial artifacts so prompt templates
|
|
330
330
|
// can access refs like {{entity.artifacts.refs.linear.id}}.
|
|
331
331
|
if (payload && Object.keys(payload).length > 0) {
|
|
@@ -516,9 +516,11 @@ export class Engine {
|
|
|
516
516
|
this.logger.warn(`[engine] setAffinity failed for entity ${claimed.id} worker ${worker_id} — continuing:`, err);
|
|
517
517
|
}
|
|
518
518
|
}
|
|
519
|
-
const
|
|
519
|
+
const versionedFlow = await this.flowRepo.getAtVersion(claimed.flowId, claimed.flowVersion);
|
|
520
|
+
const effectiveFlow = versionedFlow ?? flow;
|
|
521
|
+
const state = effectiveFlow.states.find((s) => s.name === pending.stage);
|
|
520
522
|
const build = state
|
|
521
|
-
? await this.buildPromptForEntity(state, claimed,
|
|
523
|
+
? await this.buildPromptForEntity(state, claimed, effectiveFlow)
|
|
522
524
|
: { prompt: pending.prompt, context: null };
|
|
523
525
|
await this.eventEmitter.emit({
|
|
524
526
|
type: "entity.claimed",
|
|
@@ -543,12 +545,14 @@ export class Engine {
|
|
|
543
545
|
const claimed = await this.entityRepo.claim(flow.id, state.name, worker_id ?? `agent:${role}`);
|
|
544
546
|
if (!claimed)
|
|
545
547
|
continue;
|
|
546
|
-
const
|
|
548
|
+
const claimedVersionedFlow = await this.flowRepo.getAtVersion(claimed.flowId, claimed.flowVersion);
|
|
549
|
+
const claimedEffectiveFlow = claimedVersionedFlow ?? flow;
|
|
550
|
+
const canCreate = await this.checkConcurrency(claimedEffectiveFlow, claimed);
|
|
547
551
|
if (!canCreate) {
|
|
548
552
|
await this.entityRepo.release(claimed.id, worker_id ?? `agent:${role}`);
|
|
549
553
|
continue;
|
|
550
554
|
}
|
|
551
|
-
const build = await this.buildPromptForEntity(state, claimed,
|
|
555
|
+
const build = await this.buildPromptForEntity(state, claimed, claimedEffectiveFlow);
|
|
552
556
|
const invocation = await this.invocationRepo.create(claimed.id, state.name, build.prompt, build.mode, undefined, build.systemPrompt || build.userContent
|
|
553
557
|
? { systemPrompt: build.systemPrompt, userContent: build.userContent }
|
|
554
558
|
: undefined, state.agentRole ?? null);
|
|
@@ -647,7 +651,21 @@ export class Engine {
|
|
|
647
651
|
activeInvocations += active;
|
|
648
652
|
pendingClaims += pending;
|
|
649
653
|
}
|
|
650
|
-
|
|
654
|
+
const versionDistribution = {};
|
|
655
|
+
for (const flow of allFlows) {
|
|
656
|
+
const versionCounts = {};
|
|
657
|
+
for (const state of flow.states) {
|
|
658
|
+
const stateEntities = await this.entityRepo.findByFlowAndState(flow.id, state.name);
|
|
659
|
+
for (const e of stateEntities) {
|
|
660
|
+
const vKey = String(e.flowVersion);
|
|
661
|
+
versionCounts[vKey] = (versionCounts[vKey] ?? 0) + 1;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
if (Object.keys(versionCounts).length > 0) {
|
|
665
|
+
versionDistribution[flow.id] = versionCounts;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return { flows: statusData, activeInvocations, pendingClaims, versionDistribution };
|
|
651
669
|
}
|
|
652
670
|
startReaper(intervalMs, entityTtlMs = 60_000) {
|
|
653
671
|
let tickInFlight = false;
|
|
@@ -124,6 +124,9 @@ export declare const AdminEntityResetSchema: z.ZodObject<{
|
|
|
124
124
|
entity_id: z.ZodString;
|
|
125
125
|
target_state: z.ZodString;
|
|
126
126
|
}, z.core.$strip>;
|
|
127
|
+
export declare const AdminEntityMigrateSchema: z.ZodObject<{
|
|
128
|
+
entity_id: z.ZodString;
|
|
129
|
+
}, z.core.$strip>;
|
|
127
130
|
export declare const AdminWorkerDrainSchema: z.ZodObject<{
|
|
128
131
|
worker_id: z.ZodString;
|
|
129
132
|
}, z.core.$strip>;
|
|
@@ -133,6 +133,9 @@ export const AdminEntityResetSchema = z.object({
|
|
|
133
133
|
entity_id: z.string().min(1),
|
|
134
134
|
target_state: z.string().min(1),
|
|
135
135
|
});
|
|
136
|
+
export const AdminEntityMigrateSchema = z.object({
|
|
137
|
+
entity_id: z.string().min(1),
|
|
138
|
+
});
|
|
136
139
|
export const AdminWorkerDrainSchema = z.object({
|
|
137
140
|
worker_id: z.string().min(1),
|
|
138
141
|
});
|
|
@@ -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() {
|
|
@@ -362,6 +362,11 @@ const TOOL_DEFINITIONS = [
|
|
|
362
362
|
required: ["entity_id", "target_state"],
|
|
363
363
|
},
|
|
364
364
|
},
|
|
365
|
+
{
|
|
366
|
+
name: "admin.entity.migrate",
|
|
367
|
+
description: "Migrate an entity to the latest flow version. Validates that the entity's current state exists in the new version.",
|
|
368
|
+
inputSchema: { type: "object", properties: { entity_id: { type: "string" } }, required: ["entity_id"] },
|
|
369
|
+
},
|
|
365
370
|
{
|
|
366
371
|
name: "admin.worker.drain",
|
|
367
372
|
description: "Mark a worker as draining — claimWork() will skip it.",
|
|
@@ -465,6 +470,8 @@ export async function callToolHandler(deps, name, safeArgs, opts) {
|
|
|
465
470
|
return await handleAdminEntityCancel(deps, safeArgs);
|
|
466
471
|
case "admin.entity.reset":
|
|
467
472
|
return await handleAdminEntityReset(deps, safeArgs);
|
|
473
|
+
case "admin.entity.migrate":
|
|
474
|
+
return await handleAdminEntityMigrate(deps, safeArgs);
|
|
468
475
|
case "admin.worker.drain":
|
|
469
476
|
return await handleAdminWorkerDrain(deps, safeArgs);
|
|
470
477
|
case "admin.worker.undrain":
|
|
@@ -804,6 +811,32 @@ async function handleAdminEntityReset(deps, args) {
|
|
|
804
811
|
});
|
|
805
812
|
return jsonResult({ reset: true, entity_id: v.data.entity_id, state: updated.state });
|
|
806
813
|
}
|
|
814
|
+
async function handleAdminEntityMigrate(deps, args) {
|
|
815
|
+
const v = validateInput(AdminEntityMigrateSchema, args);
|
|
816
|
+
if (!v.ok)
|
|
817
|
+
return v.result;
|
|
818
|
+
const { entity_id: entityId } = v.data;
|
|
819
|
+
const entity = await deps.entities.get(entityId);
|
|
820
|
+
if (!entity)
|
|
821
|
+
return errorResult(`Entity not found: ${entityId}`);
|
|
822
|
+
const flow = await deps.flows.get(entity.flowId);
|
|
823
|
+
if (!flow)
|
|
824
|
+
return errorResult(`Flow not found: ${entity.flowId}`);
|
|
825
|
+
if (entity.flowVersion === flow.version) {
|
|
826
|
+
return jsonResult({
|
|
827
|
+
migrated: false,
|
|
828
|
+
message: "Entity is already on the latest version",
|
|
829
|
+
flowVersion: flow.version,
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
const stateExists = flow.states.some((s) => s.name === entity.state);
|
|
833
|
+
if (!stateExists) {
|
|
834
|
+
return errorResult(`Cannot migrate entity ${entityId}: current state '${entity.state}' does not exist in flow '${flow.name}' version ${flow.version}`);
|
|
835
|
+
}
|
|
836
|
+
await deps.entities.updateFlowVersion(entityId, flow.version);
|
|
837
|
+
const updated = await deps.entities.get(entityId);
|
|
838
|
+
return jsonResult(updated);
|
|
839
|
+
}
|
|
807
840
|
async function handleAdminWorkerDrain(deps, args) {
|
|
808
841
|
const v = validateInput(AdminWorkerDrainSchema, args);
|
|
809
842
|
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): 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>;
|
|
@@ -25,5 +25,6 @@ export declare class DrizzleEntityRepository implements IEntityRepository {
|
|
|
25
25
|
clearExpiredAffinity(): Promise<string[]>;
|
|
26
26
|
cancelEntity(entityId: string): Promise<void>;
|
|
27
27
|
resetEntity(entityId: string, targetState: string): Promise<Entity>;
|
|
28
|
+
updateFlowVersion(entityId: string, version: number): Promise<void>;
|
|
28
29
|
}
|
|
29
30
|
export {};
|
|
@@ -24,7 +24,7 @@ export class DrizzleEntityRepository {
|
|
|
24
24
|
affinityExpiresAt: row.affinityExpiresAt ? new Date(row.affinityExpiresAt) : null,
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
|
-
async create(flowId, initialState, refs) {
|
|
27
|
+
async create(flowId, initialState, refs, flowVersion) {
|
|
28
28
|
const now = Date.now();
|
|
29
29
|
const id = crypto.randomUUID();
|
|
30
30
|
const row = {
|
|
@@ -35,7 +35,7 @@ export class DrizzleEntityRepository {
|
|
|
35
35
|
artifacts: null,
|
|
36
36
|
claimedBy: null,
|
|
37
37
|
claimedAt: null,
|
|
38
|
-
flowVersion: 1,
|
|
38
|
+
flowVersion: flowVersion ?? 1,
|
|
39
39
|
createdAt: now,
|
|
40
40
|
updatedAt: now,
|
|
41
41
|
affinityWorkerId: null,
|
|
@@ -205,4 +205,10 @@ export class DrizzleEntityRepository {
|
|
|
205
205
|
throw new NotFoundError(`Entity not found: ${entityId}`);
|
|
206
206
|
return this.toEntity(rows[0]);
|
|
207
207
|
}
|
|
208
|
+
async updateFlowVersion(entityId, version) {
|
|
209
|
+
await this.db
|
|
210
|
+
.update(entities)
|
|
211
|
+
.set({ flowVersion: version, updatedAt: Date.now() })
|
|
212
|
+
.where(eq(entities.id, entityId));
|
|
213
|
+
}
|
|
208
214
|
}
|
|
@@ -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 {
|
|
@@ -224,7 +224,7 @@ export interface CreateGateInput {
|
|
|
224
224
|
/** Data-access contract for entity lifecycle operations. */
|
|
225
225
|
export interface IEntityRepository {
|
|
226
226
|
/** Create a new entity in the given flow's initial state. */
|
|
227
|
-
create(flowId: string, initialState: string, refs?: Refs): Promise<Entity>;
|
|
227
|
+
create(flowId: string, initialState: string, refs?: Refs, flowVersion?: number): Promise<Entity>;
|
|
228
228
|
/** Get an entity by ID, or null if not found. */
|
|
229
229
|
get(id: string): Promise<Entity | null>;
|
|
230
230
|
/** Find entities in a given flow and state, up to an optional limit. */
|
|
@@ -259,6 +259,8 @@ export interface IEntityRepository {
|
|
|
259
259
|
cancelEntity(entityId: string): Promise<void>;
|
|
260
260
|
/** Move entity to targetState and clear claimedBy/claimedAt. Returns the updated entity. */
|
|
261
261
|
resetEntity(entityId: string, targetState: string): Promise<Entity>;
|
|
262
|
+
/** Update an entity's pinned flow version. */
|
|
263
|
+
updateFlowVersion(entityId: string, version: number): Promise<void>;
|
|
262
264
|
}
|
|
263
265
|
/** Fields that can be updated on a flow's top-level definition */
|
|
264
266
|
export type UpdateFlowInput = Partial<Omit<Flow, "id" | "states" | "transitions" | "createdAt" | "updatedAt">>;
|
|
@@ -276,6 +278,8 @@ export interface IFlowRepository {
|
|
|
276
278
|
get(id: string): Promise<Flow | null>;
|
|
277
279
|
/** Get a flow by unique name. Returns null if not found. */
|
|
278
280
|
getByName(name: string): Promise<Flow | null>;
|
|
281
|
+
/** Load a flow definition at a specific version. Returns the snapshot if version < current, or live flow if version === current. */
|
|
282
|
+
getAtVersion(flowId: string, version: number): Promise<Flow | null>;
|
|
279
283
|
/** Update a flow's top-level fields. */
|
|
280
284
|
update(id: string, changes: UpdateFlowInput): Promise<Flow>;
|
|
281
285
|
/** Add a state definition to a flow. */
|