@wopr-network/defcon 1.5.0 → 1.6.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/config/seed-loader.js +1 -0
- package/dist/src/config/zod-schemas.d.ts +12 -0
- package/dist/src/config/zod-schemas.js +10 -0
- package/dist/src/engine/engine.js +24 -0
- package/dist/src/engine/event-types.d.ts +11 -0
- package/dist/src/engine/on-exit.d.ts +6 -0
- package/dist/src/engine/on-exit.js +45 -0
- package/dist/src/repositories/drizzle/flow.repo.js +5 -0
- package/dist/src/repositories/drizzle/schema.d.ts +17 -0
- package/dist/src/repositories/drizzle/schema.js +1 -0
- package/dist/src/repositories/interfaces.d.ts +7 -0
- package/drizzle/0014_smiling_crusher_hogan.sql +1 -0
- package/drizzle/meta/0014_snapshot.json +1100 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +1 -1
|
@@ -20,6 +20,10 @@ export declare const OnEnterSchema: z.ZodObject<{
|
|
|
20
20
|
artifacts: z.ZodArray<z.ZodString>;
|
|
21
21
|
timeout_ms: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
22
22
|
}, z.core.$strip>;
|
|
23
|
+
export declare const OnExitSchema: z.ZodObject<{
|
|
24
|
+
command: z.ZodString;
|
|
25
|
+
timeout_ms: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
26
|
+
}, z.core.$strip>;
|
|
23
27
|
export declare const StateDefinitionSchema: z.ZodObject<{
|
|
24
28
|
name: z.ZodString;
|
|
25
29
|
flowName: z.ZodString;
|
|
@@ -36,6 +40,10 @@ export declare const StateDefinitionSchema: z.ZodObject<{
|
|
|
36
40
|
artifacts: z.ZodArray<z.ZodString>;
|
|
37
41
|
timeout_ms: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
38
42
|
}, z.core.$strip>>;
|
|
43
|
+
onExit: z.ZodOptional<z.ZodObject<{
|
|
44
|
+
command: z.ZodString;
|
|
45
|
+
timeout_ms: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
46
|
+
}, z.core.$strip>>;
|
|
39
47
|
retryAfterMs: z.ZodOptional<z.ZodNumber>;
|
|
40
48
|
}, z.core.$strip>;
|
|
41
49
|
export declare const CommandGateSchema: z.ZodObject<{
|
|
@@ -152,6 +160,10 @@ export declare const SeedFileSchema: z.ZodObject<{
|
|
|
152
160
|
artifacts: z.ZodArray<z.ZodString>;
|
|
153
161
|
timeout_ms: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
154
162
|
}, z.core.$strip>>;
|
|
163
|
+
onExit: z.ZodOptional<z.ZodObject<{
|
|
164
|
+
command: z.ZodString;
|
|
165
|
+
timeout_ms: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
166
|
+
}, z.core.$strip>>;
|
|
155
167
|
retryAfterMs: z.ZodOptional<z.ZodNumber>;
|
|
156
168
|
}, z.core.$strip>>;
|
|
157
169
|
gates: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
@@ -34,6 +34,15 @@ export const OnEnterSchema = z.object({
|
|
|
34
34
|
artifacts: z.array(z.string().min(1)).min(1),
|
|
35
35
|
timeout_ms: z.number().int().min(0).optional().default(30000),
|
|
36
36
|
});
|
|
37
|
+
export const OnExitSchema = z.object({
|
|
38
|
+
command: z
|
|
39
|
+
.string()
|
|
40
|
+
.min(1)
|
|
41
|
+
.refine((val) => validateTemplate(val), {
|
|
42
|
+
message: "onExit command contains disallowed Handlebars expressions",
|
|
43
|
+
}),
|
|
44
|
+
timeout_ms: z.number().int().min(1).optional().default(30000),
|
|
45
|
+
});
|
|
37
46
|
export const StateDefinitionSchema = z.object({
|
|
38
47
|
name: z.string().min(1),
|
|
39
48
|
flowName: z.string().min(1),
|
|
@@ -49,6 +58,7 @@ export const StateDefinitionSchema = z.object({
|
|
|
49
58
|
.optional(),
|
|
50
59
|
constraints: z.record(z.string(), z.unknown()).optional(),
|
|
51
60
|
onEnter: OnEnterSchema.optional(),
|
|
61
|
+
onExit: OnExitSchema.optional(),
|
|
52
62
|
retryAfterMs: z.number().int().min(0).optional(),
|
|
53
63
|
});
|
|
54
64
|
// Gate: discriminated union on `type`
|
|
@@ -6,6 +6,7 @@ import { evaluateGate } from "./gate-evaluator.js";
|
|
|
6
6
|
import { getHandlebars } from "./handlebars.js";
|
|
7
7
|
import { buildInvocation } from "./invocation-builder.js";
|
|
8
8
|
import { executeOnEnter } from "./on-enter.js";
|
|
9
|
+
import { executeOnExit } from "./on-exit.js";
|
|
9
10
|
import { findTransition, isTerminal } from "./state-machine.js";
|
|
10
11
|
export class Engine {
|
|
11
12
|
entityRepo;
|
|
@@ -67,6 +68,29 @@ export class Engine {
|
|
|
67
68
|
const trigger = routing.kind === "redirect" ? routing.trigger : signal;
|
|
68
69
|
const spawnFlow = routing.kind === "redirect" ? null : transition.spawnFlow;
|
|
69
70
|
const { gatesPassed } = routing;
|
|
71
|
+
// 4b. Execute onExit hook on the DEPARTING state (before transition)
|
|
72
|
+
const departingStateDef = flow.states.find((s) => s.name === entity.state);
|
|
73
|
+
if (departingStateDef?.onExit) {
|
|
74
|
+
const onExitResult = await executeOnExit(departingStateDef.onExit, entity);
|
|
75
|
+
if (onExitResult.error) {
|
|
76
|
+
this.logger.warn(`[engine] onExit failed for entity ${entityId} state ${entity.state}: ${onExitResult.error}`);
|
|
77
|
+
await this.eventEmitter.emit({
|
|
78
|
+
type: "onExit.failed",
|
|
79
|
+
entityId,
|
|
80
|
+
state: entity.state,
|
|
81
|
+
error: onExitResult.error,
|
|
82
|
+
emittedAt: new Date(),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
await this.eventEmitter.emit({
|
|
87
|
+
type: "onExit.completed",
|
|
88
|
+
entityId,
|
|
89
|
+
state: entity.state,
|
|
90
|
+
emittedAt: new Date(),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
70
94
|
// 5. Transition entity
|
|
71
95
|
let updated = await this.entityRepo.transition(entityId, toState, trigger, artifacts);
|
|
72
96
|
// Clear gate_failures on successful transition so stale failures don't bleed into future agent prompts
|
|
@@ -104,6 +104,17 @@ export type EngineEvent = {
|
|
|
104
104
|
entityId: string;
|
|
105
105
|
state: string;
|
|
106
106
|
emittedAt: Date;
|
|
107
|
+
} | {
|
|
108
|
+
type: "onExit.completed";
|
|
109
|
+
entityId: string;
|
|
110
|
+
state: string;
|
|
111
|
+
emittedAt: Date;
|
|
112
|
+
} | {
|
|
113
|
+
type: "onExit.failed";
|
|
114
|
+
entityId: string;
|
|
115
|
+
state: string;
|
|
116
|
+
error: string;
|
|
117
|
+
emittedAt: Date;
|
|
107
118
|
};
|
|
108
119
|
/** Adapter for broadcasting engine events to external systems. */
|
|
109
120
|
export interface IEventBusAdapter {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { getHandlebars } from "./handlebars.js";
|
|
3
|
+
export async function executeOnExit(onExit, entity) {
|
|
4
|
+
const hbs = getHandlebars();
|
|
5
|
+
let renderedCommand;
|
|
6
|
+
// Merge artifact refs into entity.refs (same pattern as on-enter.ts)
|
|
7
|
+
const artifactRefs = entity.artifacts !== null &&
|
|
8
|
+
typeof entity.artifacts === "object" &&
|
|
9
|
+
"refs" in entity.artifacts &&
|
|
10
|
+
entity.artifacts.refs !== null &&
|
|
11
|
+
typeof entity.artifacts.refs === "object"
|
|
12
|
+
? entity.artifacts.refs
|
|
13
|
+
: {};
|
|
14
|
+
const entityForContext = { ...entity, refs: { ...artifactRefs, ...(entity.refs ?? {}) } };
|
|
15
|
+
try {
|
|
16
|
+
renderedCommand = hbs.compile(onExit.command)({ entity: entityForContext });
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
const error = `onExit template error: ${err instanceof Error ? err.message : String(err)}`;
|
|
20
|
+
return { error, timedOut: false };
|
|
21
|
+
}
|
|
22
|
+
const timeoutMs = onExit.timeout_ms ?? 30000;
|
|
23
|
+
const { exitCode, stdout, stderr, timedOut } = await runCommand(renderedCommand, timeoutMs);
|
|
24
|
+
if (timedOut) {
|
|
25
|
+
return { error: `onExit command timed out after ${timeoutMs}ms`, timedOut: true };
|
|
26
|
+
}
|
|
27
|
+
if (exitCode !== 0) {
|
|
28
|
+
return { error: `onExit command exited with code ${exitCode}: ${stderr || stdout}`, timedOut: false };
|
|
29
|
+
}
|
|
30
|
+
return { error: null, timedOut: false };
|
|
31
|
+
}
|
|
32
|
+
function runCommand(command, timeoutMs) {
|
|
33
|
+
return new Promise((resolve) => {
|
|
34
|
+
execFile("/bin/sh", ["-c", command], { timeout: timeoutMs }, (error, stdout, stderr) => {
|
|
35
|
+
const execErr = error;
|
|
36
|
+
const timedOut = execErr !== null && execErr.killed === true;
|
|
37
|
+
resolve({
|
|
38
|
+
exitCode: execErr ? (typeof execErr.code === "number" ? execErr.code : 1) : 0,
|
|
39
|
+
stdout: stdout.trim(),
|
|
40
|
+
stderr: stderr.trim(),
|
|
41
|
+
timedOut,
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -15,6 +15,7 @@ function rowToState(r) {
|
|
|
15
15
|
promptTemplate: r.promptTemplate ?? null,
|
|
16
16
|
constraints: r.constraints,
|
|
17
17
|
onEnter: r.onEnter ?? null,
|
|
18
|
+
onExit: r.onExit ?? null,
|
|
18
19
|
retryAfterMs: r.retryAfterMs ?? null,
|
|
19
20
|
};
|
|
20
21
|
}
|
|
@@ -174,6 +175,7 @@ export class DrizzleFlowRepository {
|
|
|
174
175
|
promptTemplate: state.promptTemplate ?? null,
|
|
175
176
|
constraints: (state.constraints ?? null),
|
|
176
177
|
onEnter: (state.onEnter ?? null),
|
|
178
|
+
onExit: (state.onExit ?? null),
|
|
177
179
|
retryAfterMs: state.retryAfterMs ?? null,
|
|
178
180
|
};
|
|
179
181
|
this.db.transaction((tx) => {
|
|
@@ -201,6 +203,8 @@ export class DrizzleFlowRepository {
|
|
|
201
203
|
updateValues.constraints = changes.constraints;
|
|
202
204
|
if (changes.onEnter !== undefined)
|
|
203
205
|
updateValues.onEnter = changes.onEnter;
|
|
206
|
+
if (changes.onExit !== undefined)
|
|
207
|
+
updateValues.onExit = changes.onExit;
|
|
204
208
|
if (changes.retryAfterMs !== undefined)
|
|
205
209
|
updateValues.retryAfterMs = changes.retryAfterMs;
|
|
206
210
|
if (Object.keys(updateValues).length > 0) {
|
|
@@ -338,6 +342,7 @@ export class DrizzleFlowRepository {
|
|
|
338
342
|
promptTemplate: s.promptTemplate,
|
|
339
343
|
constraints: s.constraints,
|
|
340
344
|
onEnter: (s.onEnter ?? null),
|
|
345
|
+
onExit: (s.onExit ?? null),
|
|
341
346
|
retryAfterMs: s.retryAfterMs ?? null,
|
|
342
347
|
})
|
|
343
348
|
.run();
|
|
@@ -498,6 +498,23 @@ export declare const stateDefinitions: import("drizzle-orm/sqlite-core").SQLiteT
|
|
|
498
498
|
identity: undefined;
|
|
499
499
|
generated: undefined;
|
|
500
500
|
}, {}, {}>;
|
|
501
|
+
onExit: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
502
|
+
name: "on_exit";
|
|
503
|
+
tableName: "state_definitions";
|
|
504
|
+
dataType: "json";
|
|
505
|
+
columnType: "SQLiteTextJson";
|
|
506
|
+
data: unknown;
|
|
507
|
+
driverParam: string;
|
|
508
|
+
notNull: false;
|
|
509
|
+
hasDefault: false;
|
|
510
|
+
isPrimaryKey: false;
|
|
511
|
+
isAutoincrement: false;
|
|
512
|
+
hasRuntimeDefault: false;
|
|
513
|
+
enumValues: undefined;
|
|
514
|
+
baseColumn: never;
|
|
515
|
+
identity: undefined;
|
|
516
|
+
generated: undefined;
|
|
517
|
+
}, {}, {}>;
|
|
501
518
|
retryAfterMs: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
502
519
|
name: "retry_after_ms";
|
|
503
520
|
tableName: "state_definitions";
|
|
@@ -32,6 +32,7 @@ export const stateDefinitions = sqliteTable("state_definitions", {
|
|
|
32
32
|
promptTemplate: text("prompt_template"),
|
|
33
33
|
constraints: text("constraints", { mode: "json" }),
|
|
34
34
|
onEnter: text("on_enter", { mode: "json" }),
|
|
35
|
+
onExit: text("on_exit", { mode: "json" }),
|
|
35
36
|
retryAfterMs: integer("retry_after_ms"),
|
|
36
37
|
}, (table) => ({
|
|
37
38
|
flowNameUnique: uniqueIndex("state_definitions_flow_name_unique").on(table.flowId, table.name),
|
|
@@ -12,6 +12,11 @@ export interface OnEnterConfig {
|
|
|
12
12
|
artifacts: string[];
|
|
13
13
|
timeout_ms?: number;
|
|
14
14
|
}
|
|
15
|
+
/** Configuration for running a cleanup command when an entity exits a state */
|
|
16
|
+
export interface OnExitConfig {
|
|
17
|
+
command: string;
|
|
18
|
+
timeout_ms?: number;
|
|
19
|
+
}
|
|
15
20
|
/** Invocation execution mode */
|
|
16
21
|
export type Mode = "active" | "passive";
|
|
17
22
|
/** Runtime entity tracked through a flow */
|
|
@@ -90,6 +95,7 @@ export interface State {
|
|
|
90
95
|
promptTemplate: string | null;
|
|
91
96
|
constraints: Record<string, unknown> | null;
|
|
92
97
|
onEnter: OnEnterConfig | null;
|
|
98
|
+
onExit: OnExitConfig | null;
|
|
93
99
|
/** Override check_back delay for workers claiming this state. Falls back to Flow.claimRetryAfterMs. */
|
|
94
100
|
retryAfterMs: number | null;
|
|
95
101
|
}
|
|
@@ -186,6 +192,7 @@ export interface CreateStateInput {
|
|
|
186
192
|
promptTemplate?: string;
|
|
187
193
|
constraints?: Record<string, unknown>;
|
|
188
194
|
onEnter?: OnEnterConfig;
|
|
195
|
+
onExit?: OnExitConfig;
|
|
189
196
|
retryAfterMs?: number;
|
|
190
197
|
}
|
|
191
198
|
/** Input for adding a transition rule */
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ALTER TABLE `state_definitions` ADD `on_exit` text;
|