@wopr-network/defcon 1.5.0 → 1.6.1
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.d.ts +4 -0
- package/dist/src/engine/engine.js +54 -6
- 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/execution/cli.js +2 -0
- package/dist/src/main.d.ts +6 -0
- package/dist/src/main.js +20 -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`
|
|
@@ -38,6 +38,8 @@ export interface EngineDeps {
|
|
|
38
38
|
adapters: Map<string, unknown>;
|
|
39
39
|
eventEmitter: IEventBusAdapter;
|
|
40
40
|
logger?: Logger;
|
|
41
|
+
/** Optional transaction wrapper from the database layer. When provided, processSignal runs inside a single transaction and events are flushed only after successful commit. */
|
|
42
|
+
withTransaction?: <T>(fn: () => T | Promise<T>) => Promise<T>;
|
|
41
43
|
}
|
|
42
44
|
export declare class Engine {
|
|
43
45
|
private entityRepo;
|
|
@@ -48,6 +50,7 @@ export declare class Engine {
|
|
|
48
50
|
readonly adapters: Map<string, unknown>;
|
|
49
51
|
private eventEmitter;
|
|
50
52
|
private readonly logger;
|
|
53
|
+
private readonly withTransactionFn;
|
|
51
54
|
private drainingWorkers;
|
|
52
55
|
constructor(deps: EngineDeps);
|
|
53
56
|
drainWorker(workerId: string): void;
|
|
@@ -56,6 +59,7 @@ export declare class Engine {
|
|
|
56
59
|
listDrainingWorkers(): string[];
|
|
57
60
|
emit(event: import("./event-types.js").EngineEvent): Promise<void>;
|
|
58
61
|
processSignal(entityId: string, signal: string, artifacts?: Artifacts, triggeringInvocationId?: string): Promise<ProcessSignalResult>;
|
|
62
|
+
private _processSignalInner;
|
|
59
63
|
/**
|
|
60
64
|
* Evaluate a gate and return a routing decision:
|
|
61
65
|
* - `proceed` — gate passed, continue to transition.toState
|
|
@@ -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;
|
|
@@ -16,6 +17,7 @@ export class Engine {
|
|
|
16
17
|
adapters;
|
|
17
18
|
eventEmitter;
|
|
18
19
|
logger;
|
|
20
|
+
withTransactionFn;
|
|
19
21
|
drainingWorkers = new Set();
|
|
20
22
|
constructor(deps) {
|
|
21
23
|
this.entityRepo = deps.entityRepo;
|
|
@@ -26,6 +28,7 @@ export class Engine {
|
|
|
26
28
|
this.adapters = deps.adapters;
|
|
27
29
|
this.eventEmitter = deps.eventEmitter;
|
|
28
30
|
this.logger = deps.logger ?? consoleLogger;
|
|
31
|
+
this.withTransactionFn = deps.withTransaction ?? null;
|
|
29
32
|
}
|
|
30
33
|
drainWorker(workerId) {
|
|
31
34
|
this.drainingWorkers.add(workerId);
|
|
@@ -43,6 +46,28 @@ export class Engine {
|
|
|
43
46
|
await this.eventEmitter.emit(event);
|
|
44
47
|
}
|
|
45
48
|
async processSignal(entityId, signal, artifacts, triggeringInvocationId) {
|
|
49
|
+
const pendingEvents = [];
|
|
50
|
+
const bufferingEmitter = {
|
|
51
|
+
emit: async (event) => {
|
|
52
|
+
pendingEvents.push(event);
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
if (this.withTransactionFn) {
|
|
56
|
+
// Run DB writes inside a transaction; events are buffered and only
|
|
57
|
+
// flushed to real subscribers after successful COMMIT.
|
|
58
|
+
const result = await this.withTransactionFn(() => this._processSignalInner(entityId, signal, artifacts, triggeringInvocationId, bufferingEmitter));
|
|
59
|
+
for (const event of pendingEvents) {
|
|
60
|
+
await this.eventEmitter.emit(event);
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
const result = await this._processSignalInner(entityId, signal, artifacts, triggeringInvocationId, bufferingEmitter);
|
|
65
|
+
for (const event of pendingEvents) {
|
|
66
|
+
await this.eventEmitter.emit(event);
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
async _processSignalInner(entityId, signal, artifacts, triggeringInvocationId, emitter = this.eventEmitter) {
|
|
46
71
|
// 1. Load entity
|
|
47
72
|
const entity = await this.entityRepo.get(entityId);
|
|
48
73
|
if (!entity)
|
|
@@ -67,6 +92,29 @@ export class Engine {
|
|
|
67
92
|
const trigger = routing.kind === "redirect" ? routing.trigger : signal;
|
|
68
93
|
const spawnFlow = routing.kind === "redirect" ? null : transition.spawnFlow;
|
|
69
94
|
const { gatesPassed } = routing;
|
|
95
|
+
// 4b. Execute onExit hook on the DEPARTING state (before transition)
|
|
96
|
+
const departingStateDef = flow.states.find((s) => s.name === entity.state);
|
|
97
|
+
if (departingStateDef?.onExit) {
|
|
98
|
+
const onExitResult = await executeOnExit(departingStateDef.onExit, entity);
|
|
99
|
+
if (onExitResult.error) {
|
|
100
|
+
this.logger.warn(`[engine] onExit failed for entity ${entityId} state ${entity.state}: ${onExitResult.error}`);
|
|
101
|
+
await this.eventEmitter.emit({
|
|
102
|
+
type: "onExit.failed",
|
|
103
|
+
entityId,
|
|
104
|
+
state: entity.state,
|
|
105
|
+
error: onExitResult.error,
|
|
106
|
+
emittedAt: new Date(),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
await this.eventEmitter.emit({
|
|
111
|
+
type: "onExit.completed",
|
|
112
|
+
entityId,
|
|
113
|
+
state: entity.state,
|
|
114
|
+
emittedAt: new Date(),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
70
118
|
// 5. Transition entity
|
|
71
119
|
let updated = await this.entityRepo.transition(entityId, toState, trigger, artifacts);
|
|
72
120
|
// Clear gate_failures on successful transition so stale failures don't bleed into future agent prompts
|
|
@@ -74,7 +122,7 @@ export class Engine {
|
|
|
74
122
|
// Keep the in-memory entity in sync so buildInvocation sees the cleared failures
|
|
75
123
|
updated = { ...updated, artifacts: { ...updated.artifacts, gate_failures: [] } };
|
|
76
124
|
// 6. Emit transition event
|
|
77
|
-
await
|
|
125
|
+
await emitter.emit({
|
|
78
126
|
type: "entity.transitioned",
|
|
79
127
|
entityId,
|
|
80
128
|
flowId: flow.id,
|
|
@@ -94,7 +142,7 @@ export class Engine {
|
|
|
94
142
|
if (newStateDef?.onEnter) {
|
|
95
143
|
const onEnterResult = await executeOnEnter(newStateDef.onEnter, updated, this.entityRepo);
|
|
96
144
|
if (onEnterResult.skipped) {
|
|
97
|
-
await
|
|
145
|
+
await emitter.emit({
|
|
98
146
|
type: "onEnter.skipped",
|
|
99
147
|
entityId,
|
|
100
148
|
state: toState,
|
|
@@ -102,7 +150,7 @@ export class Engine {
|
|
|
102
150
|
});
|
|
103
151
|
}
|
|
104
152
|
else if (onEnterResult.error) {
|
|
105
|
-
await
|
|
153
|
+
await emitter.emit({
|
|
106
154
|
type: "onEnter.failed",
|
|
107
155
|
entityId,
|
|
108
156
|
state: toState,
|
|
@@ -127,7 +175,7 @@ export class Engine {
|
|
|
127
175
|
};
|
|
128
176
|
}
|
|
129
177
|
else {
|
|
130
|
-
await
|
|
178
|
+
await emitter.emit({
|
|
131
179
|
type: "onEnter.completed",
|
|
132
180
|
entityId,
|
|
133
181
|
state: toState,
|
|
@@ -155,7 +203,7 @@ export class Engine {
|
|
|
155
203
|
? { systemPrompt: build.systemPrompt, userContent: build.userContent }
|
|
156
204
|
: undefined, newStateDef.agentRole ?? null);
|
|
157
205
|
result.invocationId = invocation.id;
|
|
158
|
-
await
|
|
206
|
+
await emitter.emit({
|
|
159
207
|
type: "invocation.created",
|
|
160
208
|
entityId,
|
|
161
209
|
invocationId: invocation.id,
|
|
@@ -178,7 +226,7 @@ export class Engine {
|
|
|
178
226
|
const spawned = await executeSpawn({ spawnFlow }, updated, this.flowRepo, this.entityRepo, this.logger);
|
|
179
227
|
if (spawned) {
|
|
180
228
|
result.spawned = [spawned.id];
|
|
181
|
-
await
|
|
229
|
+
await emitter.emit({
|
|
182
230
|
type: "flow.spawned",
|
|
183
231
|
entityId,
|
|
184
232
|
flowId: flow.id,
|
|
@@ -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
|
+
}
|
|
@@ -14,6 +14,7 @@ import { loadSeed } from "../config/seed-loader.js";
|
|
|
14
14
|
import { resolveCorsOrigin } from "../cors.js";
|
|
15
15
|
import { Engine } from "../engine/engine.js";
|
|
16
16
|
import { EventEmitter } from "../engine/event-emitter.js";
|
|
17
|
+
import { withTransaction } from "../main.js";
|
|
17
18
|
import { DrizzleEntityRepository } from "../repositories/drizzle/entity.repo.js";
|
|
18
19
|
import { DrizzleEventRepository } from "../repositories/drizzle/event.repo.js";
|
|
19
20
|
import { DrizzleFlowRepository } from "../repositories/drizzle/flow.repo.js";
|
|
@@ -173,6 +174,7 @@ program
|
|
|
173
174
|
transitionLogRepo,
|
|
174
175
|
adapters: new Map(),
|
|
175
176
|
eventEmitter,
|
|
177
|
+
withTransaction: (fn) => withTransaction(sqlite, fn),
|
|
176
178
|
});
|
|
177
179
|
const deps = {
|
|
178
180
|
entities: entityRepo,
|
package/dist/src/main.d.ts
CHANGED
|
@@ -5,6 +5,12 @@ export declare function createDatabase(dbPath?: string): {
|
|
|
5
5
|
db: ReturnType<typeof drizzle<typeof schema>>;
|
|
6
6
|
sqlite: Database.Database;
|
|
7
7
|
};
|
|
8
|
+
/**
|
|
9
|
+
* Wraps `fn` in a BEGIN/COMMIT/ROLLBACK transaction on `sqlite`.
|
|
10
|
+
* If already inside a transaction, runs `fn` directly (allows nested calls).
|
|
11
|
+
* Supports both synchronous and Promise-returning `fn`.
|
|
12
|
+
*/
|
|
13
|
+
export declare function withTransaction<T>(sqlite: Database.Database, fn: () => T | Promise<T>): Promise<T>;
|
|
8
14
|
export declare function runMigrations(db: ReturnType<typeof drizzle>, migrationsFolder?: string): void;
|
|
9
15
|
export declare function bootstrap(dbPath?: string): {
|
|
10
16
|
db: ReturnType<typeof drizzle>;
|
package/dist/src/main.js
CHANGED
|
@@ -10,6 +10,26 @@ export function createDatabase(dbPath = DB_PATH) {
|
|
|
10
10
|
const db = drizzle(sqlite, { schema });
|
|
11
11
|
return { db, sqlite };
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* Wraps `fn` in a BEGIN/COMMIT/ROLLBACK transaction on `sqlite`.
|
|
15
|
+
* If already inside a transaction, runs `fn` directly (allows nested calls).
|
|
16
|
+
* Supports both synchronous and Promise-returning `fn`.
|
|
17
|
+
*/
|
|
18
|
+
export async function withTransaction(sqlite, fn) {
|
|
19
|
+
if (sqlite.inTransaction) {
|
|
20
|
+
return fn();
|
|
21
|
+
}
|
|
22
|
+
sqlite.exec("BEGIN");
|
|
23
|
+
try {
|
|
24
|
+
const result = await fn();
|
|
25
|
+
sqlite.exec("COMMIT");
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
sqlite.exec("ROLLBACK");
|
|
30
|
+
throw err;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
13
33
|
export function runMigrations(db, migrationsFolder = "./drizzle") {
|
|
14
34
|
migrate(db, { migrationsFolder });
|
|
15
35
|
}
|
|
@@ -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;
|