@wopr-network/defcon 1.4.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/api/wire-types.d.ts +1 -0
- package/dist/src/config/seed-loader.js +3 -0
- package/dist/src/config/zod-schemas.d.ts +50 -0
- package/dist/src/config/zod-schemas.js +16 -0
- package/dist/src/engine/engine.d.ts +7 -0
- package/dist/src/engine/engine.js +134 -94
- package/dist/src/engine/event-types.d.ts +18 -0
- package/dist/src/engine/flow-spawner.d.ts +4 -2
- package/dist/src/engine/gate-evaluator.d.ts +4 -0
- package/dist/src/engine/gate-evaluator.js +27 -0
- package/dist/src/engine/on-exit.d.ts +6 -0
- package/dist/src/engine/on-exit.js +45 -0
- package/dist/src/execution/active-runner.js +2 -2
- package/dist/src/execution/handlers/flow.js +3 -2
- package/dist/src/execution/mcp-server.js +2 -2
- package/dist/src/repositories/drizzle/flow.repo.js +10 -1
- package/dist/src/repositories/drizzle/gate.repo.js +2 -0
- package/dist/src/repositories/drizzle/invocation.repo.d.ts +1 -1
- package/dist/src/repositories/drizzle/invocation.repo.js +3 -1
- package/dist/src/repositories/drizzle/schema.d.ts +34 -0
- package/dist/src/repositories/drizzle/schema.js +2 -0
- package/dist/src/repositories/interfaces.d.ts +24 -2
- package/drizzle/0013_gate_outcomes.sql +1 -0
- package/drizzle/0014_smiling_crusher_hogan.sql +1 -0
- package/drizzle/meta/0014_snapshot.json +1100 -0
- package/drizzle/meta/_journal.json +14 -0
- package/package.json +1 -1
|
@@ -63,6 +63,7 @@ async function parseSeedAndLoad(json, flowRepo, gateRepo, db) {
|
|
|
63
63
|
timeoutMs: g.timeoutMs,
|
|
64
64
|
failurePrompt: g.failurePrompt,
|
|
65
65
|
timeoutPrompt: g.timeoutPrompt,
|
|
66
|
+
outcomes: g.outcomes,
|
|
66
67
|
});
|
|
67
68
|
gateNameToId.set(g.name, gate.id);
|
|
68
69
|
}
|
|
@@ -86,11 +87,13 @@ async function parseSeedAndLoad(json, flowRepo, gateRepo, db) {
|
|
|
86
87
|
for (const s of flowStates) {
|
|
87
88
|
await flowRepo.addState(flow.id, {
|
|
88
89
|
name: s.name,
|
|
90
|
+
agentRole: s.agentRole,
|
|
89
91
|
modelTier: s.modelTier,
|
|
90
92
|
mode: s.mode,
|
|
91
93
|
promptTemplate: s.promptTemplate,
|
|
92
94
|
constraints: s.constraints,
|
|
93
95
|
onEnter: s.onEnter,
|
|
96
|
+
onExit: s.onExit,
|
|
94
97
|
retryAfterMs: s.retryAfterMs,
|
|
95
98
|
});
|
|
96
99
|
}
|
|
@@ -20,9 +20,14 @@ 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;
|
|
30
|
+
agentRole: z.ZodOptional<z.ZodString>;
|
|
26
31
|
modelTier: z.ZodOptional<z.ZodString>;
|
|
27
32
|
mode: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
|
|
28
33
|
passive: "passive";
|
|
@@ -35,6 +40,10 @@ export declare const StateDefinitionSchema: z.ZodObject<{
|
|
|
35
40
|
artifacts: z.ZodArray<z.ZodString>;
|
|
36
41
|
timeout_ms: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
37
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>>;
|
|
38
47
|
retryAfterMs: z.ZodOptional<z.ZodNumber>;
|
|
39
48
|
}, z.core.$strip>;
|
|
40
49
|
export declare const CommandGateSchema: z.ZodObject<{
|
|
@@ -42,6 +51,10 @@ export declare const CommandGateSchema: z.ZodObject<{
|
|
|
42
51
|
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
43
52
|
failurePrompt: z.ZodOptional<z.ZodString>;
|
|
44
53
|
timeoutPrompt: z.ZodOptional<z.ZodString>;
|
|
54
|
+
outcomes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
55
|
+
proceed: z.ZodOptional<z.ZodBoolean>;
|
|
56
|
+
toState: z.ZodOptional<z.ZodString>;
|
|
57
|
+
}, z.core.$strip>>>;
|
|
45
58
|
type: z.ZodLiteral<"command">;
|
|
46
59
|
command: z.ZodString;
|
|
47
60
|
}, z.core.$strip>;
|
|
@@ -50,6 +63,10 @@ export declare const FunctionGateSchema: z.ZodObject<{
|
|
|
50
63
|
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
51
64
|
failurePrompt: z.ZodOptional<z.ZodString>;
|
|
52
65
|
timeoutPrompt: z.ZodOptional<z.ZodString>;
|
|
66
|
+
outcomes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
67
|
+
proceed: z.ZodOptional<z.ZodBoolean>;
|
|
68
|
+
toState: z.ZodOptional<z.ZodString>;
|
|
69
|
+
}, z.core.$strip>>>;
|
|
53
70
|
type: z.ZodLiteral<"function">;
|
|
54
71
|
functionRef: z.ZodString;
|
|
55
72
|
}, z.core.$strip>;
|
|
@@ -58,6 +75,10 @@ export declare const ApiGateSchema: z.ZodObject<{
|
|
|
58
75
|
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
59
76
|
failurePrompt: z.ZodOptional<z.ZodString>;
|
|
60
77
|
timeoutPrompt: z.ZodOptional<z.ZodString>;
|
|
78
|
+
outcomes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
79
|
+
proceed: z.ZodOptional<z.ZodBoolean>;
|
|
80
|
+
toState: z.ZodOptional<z.ZodString>;
|
|
81
|
+
}, z.core.$strip>>>;
|
|
61
82
|
type: z.ZodLiteral<"api">;
|
|
62
83
|
apiConfig: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
63
84
|
}, z.core.$strip>;
|
|
@@ -66,6 +87,10 @@ export declare const GateDefinitionSchema: z.ZodDiscriminatedUnion<[z.ZodObject<
|
|
|
66
87
|
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
67
88
|
failurePrompt: z.ZodOptional<z.ZodString>;
|
|
68
89
|
timeoutPrompt: z.ZodOptional<z.ZodString>;
|
|
90
|
+
outcomes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
91
|
+
proceed: z.ZodOptional<z.ZodBoolean>;
|
|
92
|
+
toState: z.ZodOptional<z.ZodString>;
|
|
93
|
+
}, z.core.$strip>>>;
|
|
69
94
|
type: z.ZodLiteral<"command">;
|
|
70
95
|
command: z.ZodString;
|
|
71
96
|
}, z.core.$strip>, z.ZodObject<{
|
|
@@ -73,6 +98,10 @@ export declare const GateDefinitionSchema: z.ZodDiscriminatedUnion<[z.ZodObject<
|
|
|
73
98
|
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
74
99
|
failurePrompt: z.ZodOptional<z.ZodString>;
|
|
75
100
|
timeoutPrompt: z.ZodOptional<z.ZodString>;
|
|
101
|
+
outcomes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
102
|
+
proceed: z.ZodOptional<z.ZodBoolean>;
|
|
103
|
+
toState: z.ZodOptional<z.ZodString>;
|
|
104
|
+
}, z.core.$strip>>>;
|
|
76
105
|
type: z.ZodLiteral<"function">;
|
|
77
106
|
functionRef: z.ZodString;
|
|
78
107
|
}, z.core.$strip>, z.ZodObject<{
|
|
@@ -80,6 +109,10 @@ export declare const GateDefinitionSchema: z.ZodDiscriminatedUnion<[z.ZodObject<
|
|
|
80
109
|
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
81
110
|
failurePrompt: z.ZodOptional<z.ZodString>;
|
|
82
111
|
timeoutPrompt: z.ZodOptional<z.ZodString>;
|
|
112
|
+
outcomes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
113
|
+
proceed: z.ZodOptional<z.ZodBoolean>;
|
|
114
|
+
toState: z.ZodOptional<z.ZodString>;
|
|
115
|
+
}, z.core.$strip>>>;
|
|
83
116
|
type: z.ZodLiteral<"api">;
|
|
84
117
|
apiConfig: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
85
118
|
}, z.core.$strip>], "type">;
|
|
@@ -114,6 +147,7 @@ export declare const SeedFileSchema: z.ZodObject<{
|
|
|
114
147
|
states: z.ZodArray<z.ZodObject<{
|
|
115
148
|
name: z.ZodString;
|
|
116
149
|
flowName: z.ZodString;
|
|
150
|
+
agentRole: z.ZodOptional<z.ZodString>;
|
|
117
151
|
modelTier: z.ZodOptional<z.ZodString>;
|
|
118
152
|
mode: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
|
|
119
153
|
passive: "passive";
|
|
@@ -126,6 +160,10 @@ export declare const SeedFileSchema: z.ZodObject<{
|
|
|
126
160
|
artifacts: z.ZodArray<z.ZodString>;
|
|
127
161
|
timeout_ms: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
128
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>>;
|
|
129
167
|
retryAfterMs: z.ZodOptional<z.ZodNumber>;
|
|
130
168
|
}, z.core.$strip>>;
|
|
131
169
|
gates: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
@@ -133,6 +171,10 @@ export declare const SeedFileSchema: z.ZodObject<{
|
|
|
133
171
|
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
134
172
|
failurePrompt: z.ZodOptional<z.ZodString>;
|
|
135
173
|
timeoutPrompt: z.ZodOptional<z.ZodString>;
|
|
174
|
+
outcomes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
175
|
+
proceed: z.ZodOptional<z.ZodBoolean>;
|
|
176
|
+
toState: z.ZodOptional<z.ZodString>;
|
|
177
|
+
}, z.core.$strip>>>;
|
|
136
178
|
type: z.ZodLiteral<"command">;
|
|
137
179
|
command: z.ZodString;
|
|
138
180
|
}, z.core.$strip>, z.ZodObject<{
|
|
@@ -140,6 +182,10 @@ export declare const SeedFileSchema: z.ZodObject<{
|
|
|
140
182
|
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
141
183
|
failurePrompt: z.ZodOptional<z.ZodString>;
|
|
142
184
|
timeoutPrompt: z.ZodOptional<z.ZodString>;
|
|
185
|
+
outcomes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
186
|
+
proceed: z.ZodOptional<z.ZodBoolean>;
|
|
187
|
+
toState: z.ZodOptional<z.ZodString>;
|
|
188
|
+
}, z.core.$strip>>>;
|
|
143
189
|
type: z.ZodLiteral<"function">;
|
|
144
190
|
functionRef: z.ZodString;
|
|
145
191
|
}, z.core.$strip>, z.ZodObject<{
|
|
@@ -147,6 +193,10 @@ export declare const SeedFileSchema: z.ZodObject<{
|
|
|
147
193
|
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
148
194
|
failurePrompt: z.ZodOptional<z.ZodString>;
|
|
149
195
|
timeoutPrompt: z.ZodOptional<z.ZodString>;
|
|
196
|
+
outcomes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
197
|
+
proceed: z.ZodOptional<z.ZodBoolean>;
|
|
198
|
+
toState: z.ZodOptional<z.ZodString>;
|
|
199
|
+
}, z.core.$strip>>>;
|
|
150
200
|
type: z.ZodLiteral<"api">;
|
|
151
201
|
apiConfig: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
152
202
|
}, z.core.$strip>], "type">>>>;
|
|
@@ -34,9 +34,19 @@ 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
|
+
agentRole: z.string().optional(),
|
|
40
50
|
modelTier: z.string().optional(),
|
|
41
51
|
mode: z.enum(["passive", "active"]).optional().default("passive"),
|
|
42
52
|
promptTemplate: z
|
|
@@ -48,14 +58,20 @@ export const StateDefinitionSchema = z.object({
|
|
|
48
58
|
.optional(),
|
|
49
59
|
constraints: z.record(z.string(), z.unknown()).optional(),
|
|
50
60
|
onEnter: OnEnterSchema.optional(),
|
|
61
|
+
onExit: OnExitSchema.optional(),
|
|
51
62
|
retryAfterMs: z.number().int().min(0).optional(),
|
|
52
63
|
});
|
|
53
64
|
// Gate: discriminated union on `type`
|
|
65
|
+
const GateOutcomeSchema = z.object({
|
|
66
|
+
proceed: z.boolean().optional(),
|
|
67
|
+
toState: z.string().min(1).optional(),
|
|
68
|
+
});
|
|
54
69
|
const BaseGateSchema = z.object({
|
|
55
70
|
name: z.string().min(1),
|
|
56
71
|
timeoutMs: z.number().int().min(1).optional(),
|
|
57
72
|
failurePrompt: z.string().optional(),
|
|
58
73
|
timeoutPrompt: z.string().min(1).optional(),
|
|
74
|
+
outcomes: z.record(z.string(), GateOutcomeSchema).optional(),
|
|
59
75
|
});
|
|
60
76
|
export const CommandGateSchema = BaseGateSchema.extend({
|
|
61
77
|
type: z.literal("command"),
|
|
@@ -56,6 +56,13 @@ export declare class Engine {
|
|
|
56
56
|
listDrainingWorkers(): string[];
|
|
57
57
|
emit(event: import("./event-types.js").EngineEvent): Promise<void>;
|
|
58
58
|
processSignal(entityId: string, signal: string, artifacts?: Artifacts, triggeringInvocationId?: string): Promise<ProcessSignalResult>;
|
|
59
|
+
/**
|
|
60
|
+
* Evaluate a gate and return a routing decision:
|
|
61
|
+
* - `proceed` — gate passed, continue to transition.toState
|
|
62
|
+
* - `redirect` — gate outcome maps to a different toState
|
|
63
|
+
* - `block` — gate failed or timed out; caller should return this as the signal result
|
|
64
|
+
*/
|
|
65
|
+
private resolveGate;
|
|
59
66
|
createEntity(flowName: string, refs?: Record<string, {
|
|
60
67
|
adapter: string;
|
|
61
68
|
id: string;
|
|
@@ -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;
|
|
@@ -55,83 +56,43 @@ export class Engine {
|
|
|
55
56
|
const transition = findTransition(flow, entity.state, signal, { entity }, true, this.logger);
|
|
56
57
|
if (!transition)
|
|
57
58
|
throw new ValidationError(`No transition from "${entity.state}" on signal "${signal}" in flow "${flow.name}"`);
|
|
58
|
-
// 4. Evaluate gate
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
59
|
+
// 4. Evaluate gate — returns a routing decision or a block result to return immediately.
|
|
60
|
+
const routing = transition.gateId
|
|
61
|
+
? await this.resolveGate(transition.gateId, entity, flow)
|
|
62
|
+
: { kind: "proceed", gatesPassed: [] };
|
|
63
|
+
if (routing.kind === "block") {
|
|
64
|
+
return { gated: true, ...routing, terminal: false };
|
|
65
|
+
}
|
|
66
|
+
// Gate passed or redirected — determine the actual destination.
|
|
67
|
+
const toState = routing.kind === "redirect" ? routing.toState : transition.toState;
|
|
68
|
+
const trigger = routing.kind === "redirect" ? routing.trigger : signal;
|
|
69
|
+
const spawnFlow = routing.kind === "redirect" ? null : transition.spawnFlow;
|
|
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(),
|
|
80
91
|
});
|
|
81
|
-
if (gateResult.timedOut) {
|
|
82
|
-
await this.eventEmitter.emit({
|
|
83
|
-
type: "gate.timedOut",
|
|
84
|
-
entityId,
|
|
85
|
-
gateId: gate.id,
|
|
86
|
-
emittedAt: new Date(),
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
else {
|
|
90
|
-
await this.eventEmitter.emit({
|
|
91
|
-
type: "gate.failed",
|
|
92
|
-
entityId,
|
|
93
|
-
gateId: gate.id,
|
|
94
|
-
emittedAt: new Date(),
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
let resolvedTimeoutPrompt;
|
|
98
|
-
if (gateResult.timedOut) {
|
|
99
|
-
const rawTemplate = gate.timeoutPrompt ?? flow.timeoutPrompt ?? DEFAULT_TIMEOUT_PROMPT;
|
|
100
|
-
try {
|
|
101
|
-
const hbs = getHandlebars();
|
|
102
|
-
const template = hbs.compile(rawTemplate);
|
|
103
|
-
resolvedTimeoutPrompt = template({
|
|
104
|
-
entity,
|
|
105
|
-
flow,
|
|
106
|
-
gate: { name: gate.name, output: gateResult.output },
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
catch (err) {
|
|
110
|
-
this.logger.error("[engine] Failed to render timeoutPrompt template:", err);
|
|
111
|
-
resolvedTimeoutPrompt = DEFAULT_TIMEOUT_PROMPT;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
return {
|
|
115
|
-
gated: true,
|
|
116
|
-
gateTimedOut: gateResult.timedOut,
|
|
117
|
-
gateOutput: gateResult.output,
|
|
118
|
-
gateName: gate.name,
|
|
119
|
-
failurePrompt: gate.failurePrompt ?? undefined,
|
|
120
|
-
timeoutPrompt: resolvedTimeoutPrompt,
|
|
121
|
-
gatesPassed,
|
|
122
|
-
terminal: false,
|
|
123
|
-
};
|
|
124
92
|
}
|
|
125
|
-
gatesPassed.push(gate.name);
|
|
126
|
-
await this.eventEmitter.emit({
|
|
127
|
-
type: "gate.passed",
|
|
128
|
-
entityId,
|
|
129
|
-
gateId: gate.id,
|
|
130
|
-
emittedAt: new Date(),
|
|
131
|
-
});
|
|
132
93
|
}
|
|
133
94
|
// 5. Transition entity
|
|
134
|
-
let updated = await this.entityRepo.transition(entityId,
|
|
95
|
+
let updated = await this.entityRepo.transition(entityId, toState, trigger, artifacts);
|
|
135
96
|
// Clear gate_failures on successful transition so stale failures don't bleed into future agent prompts
|
|
136
97
|
await this.entityRepo.updateArtifacts(entityId, { gate_failures: [] });
|
|
137
98
|
// Keep the in-memory entity in sync so buildInvocation sees the cleared failures
|
|
@@ -142,25 +103,25 @@ export class Engine {
|
|
|
142
103
|
entityId,
|
|
143
104
|
flowId: flow.id,
|
|
144
105
|
fromState: entity.state,
|
|
145
|
-
toState
|
|
146
|
-
trigger
|
|
106
|
+
toState,
|
|
107
|
+
trigger,
|
|
147
108
|
emittedAt: new Date(),
|
|
148
109
|
});
|
|
149
110
|
const result = {
|
|
150
|
-
newState:
|
|
111
|
+
newState: toState,
|
|
151
112
|
gatesPassed,
|
|
152
113
|
gated: false,
|
|
153
114
|
terminal: false,
|
|
154
115
|
};
|
|
155
116
|
// 6b. Execute onEnter hook if defined on the new state
|
|
156
|
-
const newStateDef = flow.states.find((s) => s.name ===
|
|
117
|
+
const newStateDef = flow.states.find((s) => s.name === toState);
|
|
157
118
|
if (newStateDef?.onEnter) {
|
|
158
119
|
const onEnterResult = await executeOnEnter(newStateDef.onEnter, updated, this.entityRepo);
|
|
159
120
|
if (onEnterResult.skipped) {
|
|
160
121
|
await this.eventEmitter.emit({
|
|
161
122
|
type: "onEnter.skipped",
|
|
162
123
|
entityId,
|
|
163
|
-
state:
|
|
124
|
+
state: toState,
|
|
164
125
|
emittedAt: new Date(),
|
|
165
126
|
});
|
|
166
127
|
}
|
|
@@ -168,20 +129,20 @@ export class Engine {
|
|
|
168
129
|
await this.eventEmitter.emit({
|
|
169
130
|
type: "onEnter.failed",
|
|
170
131
|
entityId,
|
|
171
|
-
state:
|
|
132
|
+
state: toState,
|
|
172
133
|
error: onEnterResult.error,
|
|
173
134
|
emittedAt: new Date(),
|
|
174
135
|
});
|
|
175
136
|
await this.transitionLogRepo.record({
|
|
176
137
|
entityId,
|
|
177
138
|
fromState: entity.state,
|
|
178
|
-
toState
|
|
179
|
-
trigger
|
|
139
|
+
toState,
|
|
140
|
+
trigger,
|
|
180
141
|
invocationId: triggeringInvocationId ?? null,
|
|
181
142
|
timestamp: new Date(),
|
|
182
143
|
});
|
|
183
144
|
return {
|
|
184
|
-
newState:
|
|
145
|
+
newState: toState,
|
|
185
146
|
gatesPassed,
|
|
186
147
|
gated: false,
|
|
187
148
|
onEnterFailed: true,
|
|
@@ -193,7 +154,7 @@ export class Engine {
|
|
|
193
154
|
await this.eventEmitter.emit({
|
|
194
155
|
type: "onEnter.completed",
|
|
195
156
|
entityId,
|
|
196
|
-
state:
|
|
157
|
+
state: toState,
|
|
197
158
|
artifacts: onEnterResult.artifacts ?? {},
|
|
198
159
|
emittedAt: new Date(),
|
|
199
160
|
});
|
|
@@ -214,15 +175,15 @@ export class Engine {
|
|
|
214
175
|
]);
|
|
215
176
|
const enriched = { ...updated, invocations, gateResults };
|
|
216
177
|
const build = await buildInvocation(newStateDef, enriched, this.adapters, flow, this.logger);
|
|
217
|
-
const invocation = await this.invocationRepo.create(entityId,
|
|
178
|
+
const invocation = await this.invocationRepo.create(entityId, toState, build.prompt, build.mode, undefined, build.systemPrompt || build.userContent
|
|
218
179
|
? { systemPrompt: build.systemPrompt, userContent: build.userContent }
|
|
219
|
-
: undefined);
|
|
180
|
+
: undefined, newStateDef.agentRole ?? null);
|
|
220
181
|
result.invocationId = invocation.id;
|
|
221
182
|
await this.eventEmitter.emit({
|
|
222
183
|
type: "invocation.created",
|
|
223
184
|
entityId,
|
|
224
185
|
invocationId: invocation.id,
|
|
225
|
-
stage:
|
|
186
|
+
stage: toState,
|
|
226
187
|
emittedAt: new Date(),
|
|
227
188
|
});
|
|
228
189
|
}
|
|
@@ -232,13 +193,13 @@ export class Engine {
|
|
|
232
193
|
await this.transitionLogRepo.record({
|
|
233
194
|
entityId,
|
|
234
195
|
fromState: entity.state,
|
|
235
|
-
toState
|
|
236
|
-
trigger
|
|
196
|
+
toState,
|
|
197
|
+
trigger,
|
|
237
198
|
invocationId: triggeringInvocationId ?? null,
|
|
238
199
|
timestamp: new Date(),
|
|
239
200
|
});
|
|
240
201
|
// 9. Spawn child flows
|
|
241
|
-
const spawned = await executeSpawn(
|
|
202
|
+
const spawned = await executeSpawn({ spawnFlow }, updated, this.flowRepo, this.entityRepo, this.logger);
|
|
242
203
|
if (spawned) {
|
|
243
204
|
result.spawned = [spawned.id];
|
|
244
205
|
await this.eventEmitter.emit({
|
|
@@ -249,14 +210,93 @@ export class Engine {
|
|
|
249
210
|
emittedAt: new Date(),
|
|
250
211
|
});
|
|
251
212
|
}
|
|
252
|
-
// 10. Mark terminal
|
|
253
|
-
|
|
254
|
-
if (isTerminal(flow, transition.toState)) {
|
|
213
|
+
// 10. Mark terminal
|
|
214
|
+
if (isTerminal(flow, toState)) {
|
|
255
215
|
result.terminal = true;
|
|
256
216
|
result.spawned = result.spawned ?? [];
|
|
257
217
|
}
|
|
258
218
|
return result;
|
|
259
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* Evaluate a gate and return a routing decision:
|
|
222
|
+
* - `proceed` — gate passed, continue to transition.toState
|
|
223
|
+
* - `redirect` — gate outcome maps to a different toState
|
|
224
|
+
* - `block` — gate failed or timed out; caller should return this as the signal result
|
|
225
|
+
*/
|
|
226
|
+
async resolveGate(gateId, entity, flow) {
|
|
227
|
+
const gate = await this.gateRepo.get(gateId);
|
|
228
|
+
if (!gate)
|
|
229
|
+
throw new NotFoundError(`Gate "${gateId}" not found`);
|
|
230
|
+
const gateResult = await evaluateGate(gate, entity, this.gateRepo, flow.gateTimeoutMs);
|
|
231
|
+
const namedOutcome = gateResult.outcome && gate.outcomes ? gate.outcomes[gateResult.outcome] : undefined;
|
|
232
|
+
if (namedOutcome?.toState) {
|
|
233
|
+
const outcomeLabel = gateResult.outcome ?? gate.name;
|
|
234
|
+
await this.eventEmitter.emit({
|
|
235
|
+
type: "gate.redirected",
|
|
236
|
+
entityId: entity.id,
|
|
237
|
+
gateId: gate.id,
|
|
238
|
+
outcome: outcomeLabel,
|
|
239
|
+
toState: namedOutcome.toState,
|
|
240
|
+
emittedAt: new Date(),
|
|
241
|
+
});
|
|
242
|
+
return {
|
|
243
|
+
kind: "redirect",
|
|
244
|
+
toState: namedOutcome.toState,
|
|
245
|
+
trigger: `gate:${gate.name}:${outcomeLabel}`,
|
|
246
|
+
gatesPassed: [gate.name],
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
if (namedOutcome?.proceed || (!namedOutcome && gateResult.passed)) {
|
|
250
|
+
await this.eventEmitter.emit({
|
|
251
|
+
type: "gate.passed",
|
|
252
|
+
entityId: entity.id,
|
|
253
|
+
gateId: gate.id,
|
|
254
|
+
emittedAt: new Date(),
|
|
255
|
+
});
|
|
256
|
+
return { kind: "proceed", gatesPassed: [gate.name] };
|
|
257
|
+
}
|
|
258
|
+
// Gate failed — persist failure context and emit event
|
|
259
|
+
const priorFailures = Array.isArray(entity.artifacts?.gate_failures)
|
|
260
|
+
? entity.artifacts.gate_failures
|
|
261
|
+
: [];
|
|
262
|
+
await this.entityRepo.updateArtifacts(entity.id, {
|
|
263
|
+
gate_failures: [
|
|
264
|
+
...priorFailures,
|
|
265
|
+
{ gateId: gate.id, gateName: gate.name, output: gateResult.output, failedAt: new Date().toISOString() },
|
|
266
|
+
],
|
|
267
|
+
});
|
|
268
|
+
await this.eventEmitter.emit({
|
|
269
|
+
type: gateResult.timedOut ? "gate.timedOut" : "gate.failed",
|
|
270
|
+
entityId: entity.id,
|
|
271
|
+
gateId: gate.id,
|
|
272
|
+
emittedAt: new Date(),
|
|
273
|
+
});
|
|
274
|
+
let timeoutPrompt;
|
|
275
|
+
if (gateResult.timedOut) {
|
|
276
|
+
const rawTemplate = gate.timeoutPrompt ?? flow.timeoutPrompt ?? DEFAULT_TIMEOUT_PROMPT;
|
|
277
|
+
try {
|
|
278
|
+
const hbs = getHandlebars();
|
|
279
|
+
timeoutPrompt = hbs.compile(rawTemplate)({
|
|
280
|
+
entity,
|
|
281
|
+
flow,
|
|
282
|
+
gate: { name: gate.name, output: gateResult.output },
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
catch (err) {
|
|
286
|
+
this.logger.error("[engine] Failed to render timeoutPrompt template:", err);
|
|
287
|
+
timeoutPrompt = DEFAULT_TIMEOUT_PROMPT;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
kind: "block",
|
|
292
|
+
gateTimedOut: gateResult.timedOut,
|
|
293
|
+
gateOutput: gateResult.output,
|
|
294
|
+
gateName: gate.name,
|
|
295
|
+
failurePrompt: gate.failurePrompt ?? undefined,
|
|
296
|
+
timeoutPrompt,
|
|
297
|
+
gatesPassed: [],
|
|
298
|
+
};
|
|
299
|
+
}
|
|
260
300
|
async createEntity(flowName, refs, payload) {
|
|
261
301
|
const flow = await this.flowRepo.getByName(flowName);
|
|
262
302
|
if (!flow)
|
|
@@ -315,7 +355,7 @@ export class Engine {
|
|
|
315
355
|
const build = await buildInvocation(initialState, enriched, this.adapters, flow, this.logger);
|
|
316
356
|
await this.invocationRepo.create(entity.id, flow.initialState, build.prompt, build.mode, undefined, build.systemPrompt || build.userContent
|
|
317
357
|
? { systemPrompt: build.systemPrompt, userContent: build.userContent }
|
|
318
|
-
: undefined);
|
|
358
|
+
: undefined, initialState.agentRole ?? null);
|
|
319
359
|
}
|
|
320
360
|
return entity;
|
|
321
361
|
}
|
|
@@ -487,7 +527,7 @@ export class Engine {
|
|
|
487
527
|
const build = await this.buildPromptForEntity(state, claimed, flow);
|
|
488
528
|
const invocation = await this.invocationRepo.create(claimed.id, state.name, build.prompt, build.mode, undefined, build.systemPrompt || build.userContent
|
|
489
529
|
? { systemPrompt: build.systemPrompt, userContent: build.userContent }
|
|
490
|
-
: undefined);
|
|
530
|
+
: undefined, state.agentRole ?? null);
|
|
491
531
|
const entityClaimToken = worker_id ?? `agent:${role}`;
|
|
492
532
|
let claimedInvocation;
|
|
493
533
|
try {
|
|
@@ -68,6 +68,13 @@ export type EngineEvent = {
|
|
|
68
68
|
entityId: string;
|
|
69
69
|
gateId: string;
|
|
70
70
|
emittedAt: Date;
|
|
71
|
+
} | {
|
|
72
|
+
type: "gate.redirected";
|
|
73
|
+
entityId: string;
|
|
74
|
+
gateId: string;
|
|
75
|
+
outcome: string;
|
|
76
|
+
toState: string;
|
|
77
|
+
emittedAt: Date;
|
|
71
78
|
} | {
|
|
72
79
|
type: "flow.spawned";
|
|
73
80
|
entityId: string;
|
|
@@ -97,6 +104,17 @@ export type EngineEvent = {
|
|
|
97
104
|
entityId: string;
|
|
98
105
|
state: string;
|
|
99
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;
|
|
100
118
|
};
|
|
101
119
|
/** Adapter for broadcasting engine events to external systems. */
|
|
102
120
|
export interface IEventBusAdapter {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { Logger } from "../logger.js";
|
|
2
|
-
import type { Entity, IEntityRepository, IFlowRepository
|
|
2
|
+
import type { Entity, IEntityRepository, IFlowRepository } from "../repositories/interfaces.js";
|
|
3
3
|
/**
|
|
4
4
|
* If the transition has a spawnFlow, look up that flow and create a new entity in it.
|
|
5
5
|
* The spawned entity inherits the parent entity's refs.
|
|
6
6
|
* Returns the spawned entity, or null if no spawn is configured.
|
|
7
7
|
*/
|
|
8
|
-
export declare function executeSpawn(transition:
|
|
8
|
+
export declare function executeSpawn(transition: {
|
|
9
|
+
spawnFlow: string | null | undefined;
|
|
10
|
+
}, parentEntity: Entity, flowRepo: IFlowRepository, entityRepo: IEntityRepository, logger?: Logger): Promise<Entity | null>;
|
|
@@ -3,6 +3,10 @@ export interface GateEvalResult {
|
|
|
3
3
|
passed: boolean;
|
|
4
4
|
timedOut: boolean;
|
|
5
5
|
output: string;
|
|
6
|
+
/** Named outcome from structured JSON output, if the gate emitted one. */
|
|
7
|
+
outcome?: string;
|
|
8
|
+
/** Human-readable message from structured JSON output. */
|
|
9
|
+
message?: string;
|
|
6
10
|
}
|
|
7
11
|
export declare function resolveGateTimeout(gateTimeoutMs: number | null | undefined, flowGateTimeoutMs: number | null | undefined): number;
|
|
8
12
|
/**
|
|
@@ -59,6 +59,33 @@ export async function evaluateGate(gate, entity, gateRepo, flowGateTimeoutMs) {
|
|
|
59
59
|
passed = result.exitCode === 0;
|
|
60
60
|
output = result.output;
|
|
61
61
|
timedOut = result.timedOut;
|
|
62
|
+
// Parse structured JSON outcome from the last non-empty output line.
|
|
63
|
+
// Gate scripts can emit { outcome: string, message?: string } as their final line
|
|
64
|
+
// to enable named outcome routing (e.g. "blocked" → toState: "fixing").
|
|
65
|
+
const lastLine = result.output
|
|
66
|
+
.split("\n")
|
|
67
|
+
.map((l) => l.trim())
|
|
68
|
+
.filter(Boolean)
|
|
69
|
+
.at(-1);
|
|
70
|
+
if (lastLine?.startsWith("{")) {
|
|
71
|
+
try {
|
|
72
|
+
const parsed = JSON.parse(lastLine);
|
|
73
|
+
if (typeof parsed.outcome === "string") {
|
|
74
|
+
const outcomeResult = {
|
|
75
|
+
passed,
|
|
76
|
+
timedOut,
|
|
77
|
+
output: result.output,
|
|
78
|
+
outcome: parsed.outcome,
|
|
79
|
+
message: typeof parsed.message === "string" ? parsed.message : undefined,
|
|
80
|
+
};
|
|
81
|
+
await gateRepo.record(entity.id, gate.id, passed, result.output);
|
|
82
|
+
return outcomeResult;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Not JSON — treat output as plain text, fall through to normal path
|
|
87
|
+
}
|
|
88
|
+
}
|
|
62
89
|
}
|
|
63
90
|
else if (gate.type === "function") {
|
|
64
91
|
try {
|