@wopr-network/defcon 1.3.0 → 1.5.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 +4 -0
- package/dist/src/config/zod-schemas.d.ts +42 -0
- package/dist/src/config/zod-schemas.js +8 -0
- package/dist/src/engine/engine.d.ts +8 -0
- package/dist/src/engine/engine.js +115 -96
- package/dist/src/engine/event-types.d.ts +7 -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/execution/active-runner.js +2 -2
- package/dist/src/execution/handlers/flow.js +117 -59
- package/dist/src/execution/mcp-server.js +4 -48
- package/dist/src/repositories/drizzle/flow.repo.js +16 -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 +51 -0
- package/dist/src/repositories/drizzle/schema.js +3 -0
- package/dist/src/repositories/interfaces.d.ts +23 -2
- package/drizzle/0012_certain_human_fly.sql +2 -0
- package/drizzle/0013_gate_outcomes.sql +1 -0
- package/drizzle/meta/0010_snapshot.json +2 -2
- package/drizzle/meta/0012_snapshot.json +1086 -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
|
}
|
|
@@ -75,6 +76,7 @@ async function parseSeedAndLoad(json, flowRepo, gateRepo, db) {
|
|
|
75
76
|
initialState: f.initialState,
|
|
76
77
|
maxConcurrent: f.maxConcurrent,
|
|
77
78
|
maxConcurrentPerRepo: f.maxConcurrentPerRepo,
|
|
79
|
+
claimRetryAfterMs: f.claimRetryAfterMs,
|
|
78
80
|
gateTimeoutMs: f.gateTimeoutMs,
|
|
79
81
|
createdBy: f.createdBy,
|
|
80
82
|
discipline: f.discipline,
|
|
@@ -85,11 +87,13 @@ async function parseSeedAndLoad(json, flowRepo, gateRepo, db) {
|
|
|
85
87
|
for (const s of flowStates) {
|
|
86
88
|
await flowRepo.addState(flow.id, {
|
|
87
89
|
name: s.name,
|
|
90
|
+
agentRole: s.agentRole,
|
|
88
91
|
modelTier: s.modelTier,
|
|
89
92
|
mode: s.mode,
|
|
90
93
|
promptTemplate: s.promptTemplate,
|
|
91
94
|
constraints: s.constraints,
|
|
92
95
|
onEnter: s.onEnter,
|
|
96
|
+
retryAfterMs: s.retryAfterMs,
|
|
93
97
|
});
|
|
94
98
|
}
|
|
95
99
|
const flowTransitions = parsed.transitions.filter((t) => t.flowName === f.name);
|
|
@@ -7,6 +7,7 @@ export declare const FlowDefinitionSchema: z.ZodObject<{
|
|
|
7
7
|
maxConcurrent: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
8
8
|
maxConcurrentPerRepo: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
9
9
|
affinityWindowMs: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
10
|
+
claimRetryAfterMs: z.ZodOptional<z.ZodNumber>;
|
|
10
11
|
gateTimeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
11
12
|
version: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
12
13
|
createdBy: z.ZodOptional<z.ZodString>;
|
|
@@ -22,6 +23,7 @@ export declare const OnEnterSchema: z.ZodObject<{
|
|
|
22
23
|
export declare const StateDefinitionSchema: z.ZodObject<{
|
|
23
24
|
name: z.ZodString;
|
|
24
25
|
flowName: z.ZodString;
|
|
26
|
+
agentRole: z.ZodOptional<z.ZodString>;
|
|
25
27
|
modelTier: z.ZodOptional<z.ZodString>;
|
|
26
28
|
mode: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
|
|
27
29
|
passive: "passive";
|
|
@@ -34,12 +36,17 @@ export declare const StateDefinitionSchema: z.ZodObject<{
|
|
|
34
36
|
artifacts: z.ZodArray<z.ZodString>;
|
|
35
37
|
timeout_ms: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
36
38
|
}, z.core.$strip>>;
|
|
39
|
+
retryAfterMs: z.ZodOptional<z.ZodNumber>;
|
|
37
40
|
}, z.core.$strip>;
|
|
38
41
|
export declare const CommandGateSchema: z.ZodObject<{
|
|
39
42
|
name: z.ZodString;
|
|
40
43
|
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
41
44
|
failurePrompt: z.ZodOptional<z.ZodString>;
|
|
42
45
|
timeoutPrompt: z.ZodOptional<z.ZodString>;
|
|
46
|
+
outcomes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
47
|
+
proceed: z.ZodOptional<z.ZodBoolean>;
|
|
48
|
+
toState: z.ZodOptional<z.ZodString>;
|
|
49
|
+
}, z.core.$strip>>>;
|
|
43
50
|
type: z.ZodLiteral<"command">;
|
|
44
51
|
command: z.ZodString;
|
|
45
52
|
}, z.core.$strip>;
|
|
@@ -48,6 +55,10 @@ export declare const FunctionGateSchema: z.ZodObject<{
|
|
|
48
55
|
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
49
56
|
failurePrompt: z.ZodOptional<z.ZodString>;
|
|
50
57
|
timeoutPrompt: z.ZodOptional<z.ZodString>;
|
|
58
|
+
outcomes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
59
|
+
proceed: z.ZodOptional<z.ZodBoolean>;
|
|
60
|
+
toState: z.ZodOptional<z.ZodString>;
|
|
61
|
+
}, z.core.$strip>>>;
|
|
51
62
|
type: z.ZodLiteral<"function">;
|
|
52
63
|
functionRef: z.ZodString;
|
|
53
64
|
}, z.core.$strip>;
|
|
@@ -56,6 +67,10 @@ export declare const ApiGateSchema: z.ZodObject<{
|
|
|
56
67
|
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
57
68
|
failurePrompt: z.ZodOptional<z.ZodString>;
|
|
58
69
|
timeoutPrompt: z.ZodOptional<z.ZodString>;
|
|
70
|
+
outcomes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
71
|
+
proceed: z.ZodOptional<z.ZodBoolean>;
|
|
72
|
+
toState: z.ZodOptional<z.ZodString>;
|
|
73
|
+
}, z.core.$strip>>>;
|
|
59
74
|
type: z.ZodLiteral<"api">;
|
|
60
75
|
apiConfig: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
61
76
|
}, z.core.$strip>;
|
|
@@ -64,6 +79,10 @@ export declare const GateDefinitionSchema: z.ZodDiscriminatedUnion<[z.ZodObject<
|
|
|
64
79
|
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
65
80
|
failurePrompt: z.ZodOptional<z.ZodString>;
|
|
66
81
|
timeoutPrompt: z.ZodOptional<z.ZodString>;
|
|
82
|
+
outcomes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
83
|
+
proceed: z.ZodOptional<z.ZodBoolean>;
|
|
84
|
+
toState: z.ZodOptional<z.ZodString>;
|
|
85
|
+
}, z.core.$strip>>>;
|
|
67
86
|
type: z.ZodLiteral<"command">;
|
|
68
87
|
command: z.ZodString;
|
|
69
88
|
}, z.core.$strip>, z.ZodObject<{
|
|
@@ -71,6 +90,10 @@ export declare const GateDefinitionSchema: z.ZodDiscriminatedUnion<[z.ZodObject<
|
|
|
71
90
|
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
72
91
|
failurePrompt: z.ZodOptional<z.ZodString>;
|
|
73
92
|
timeoutPrompt: z.ZodOptional<z.ZodString>;
|
|
93
|
+
outcomes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
94
|
+
proceed: z.ZodOptional<z.ZodBoolean>;
|
|
95
|
+
toState: z.ZodOptional<z.ZodString>;
|
|
96
|
+
}, z.core.$strip>>>;
|
|
74
97
|
type: z.ZodLiteral<"function">;
|
|
75
98
|
functionRef: z.ZodString;
|
|
76
99
|
}, z.core.$strip>, z.ZodObject<{
|
|
@@ -78,6 +101,10 @@ export declare const GateDefinitionSchema: z.ZodDiscriminatedUnion<[z.ZodObject<
|
|
|
78
101
|
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
79
102
|
failurePrompt: z.ZodOptional<z.ZodString>;
|
|
80
103
|
timeoutPrompt: z.ZodOptional<z.ZodString>;
|
|
104
|
+
outcomes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
105
|
+
proceed: z.ZodOptional<z.ZodBoolean>;
|
|
106
|
+
toState: z.ZodOptional<z.ZodString>;
|
|
107
|
+
}, z.core.$strip>>>;
|
|
81
108
|
type: z.ZodLiteral<"api">;
|
|
82
109
|
apiConfig: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
83
110
|
}, z.core.$strip>], "type">;
|
|
@@ -101,6 +128,7 @@ export declare const SeedFileSchema: z.ZodObject<{
|
|
|
101
128
|
maxConcurrent: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
102
129
|
maxConcurrentPerRepo: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
103
130
|
affinityWindowMs: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
131
|
+
claimRetryAfterMs: z.ZodOptional<z.ZodNumber>;
|
|
104
132
|
gateTimeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
105
133
|
version: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
106
134
|
createdBy: z.ZodOptional<z.ZodString>;
|
|
@@ -111,6 +139,7 @@ export declare const SeedFileSchema: z.ZodObject<{
|
|
|
111
139
|
states: z.ZodArray<z.ZodObject<{
|
|
112
140
|
name: z.ZodString;
|
|
113
141
|
flowName: z.ZodString;
|
|
142
|
+
agentRole: z.ZodOptional<z.ZodString>;
|
|
114
143
|
modelTier: z.ZodOptional<z.ZodString>;
|
|
115
144
|
mode: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
|
|
116
145
|
passive: "passive";
|
|
@@ -123,12 +152,17 @@ export declare const SeedFileSchema: z.ZodObject<{
|
|
|
123
152
|
artifacts: z.ZodArray<z.ZodString>;
|
|
124
153
|
timeout_ms: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
125
154
|
}, z.core.$strip>>;
|
|
155
|
+
retryAfterMs: z.ZodOptional<z.ZodNumber>;
|
|
126
156
|
}, z.core.$strip>>;
|
|
127
157
|
gates: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
128
158
|
name: z.ZodString;
|
|
129
159
|
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
130
160
|
failurePrompt: z.ZodOptional<z.ZodString>;
|
|
131
161
|
timeoutPrompt: z.ZodOptional<z.ZodString>;
|
|
162
|
+
outcomes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
163
|
+
proceed: z.ZodOptional<z.ZodBoolean>;
|
|
164
|
+
toState: z.ZodOptional<z.ZodString>;
|
|
165
|
+
}, z.core.$strip>>>;
|
|
132
166
|
type: z.ZodLiteral<"command">;
|
|
133
167
|
command: z.ZodString;
|
|
134
168
|
}, z.core.$strip>, z.ZodObject<{
|
|
@@ -136,6 +170,10 @@ export declare const SeedFileSchema: z.ZodObject<{
|
|
|
136
170
|
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
137
171
|
failurePrompt: z.ZodOptional<z.ZodString>;
|
|
138
172
|
timeoutPrompt: z.ZodOptional<z.ZodString>;
|
|
173
|
+
outcomes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
174
|
+
proceed: z.ZodOptional<z.ZodBoolean>;
|
|
175
|
+
toState: z.ZodOptional<z.ZodString>;
|
|
176
|
+
}, z.core.$strip>>>;
|
|
139
177
|
type: z.ZodLiteral<"function">;
|
|
140
178
|
functionRef: z.ZodString;
|
|
141
179
|
}, z.core.$strip>, z.ZodObject<{
|
|
@@ -143,6 +181,10 @@ export declare const SeedFileSchema: z.ZodObject<{
|
|
|
143
181
|
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
144
182
|
failurePrompt: z.ZodOptional<z.ZodString>;
|
|
145
183
|
timeoutPrompt: z.ZodOptional<z.ZodString>;
|
|
184
|
+
outcomes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
185
|
+
proceed: z.ZodOptional<z.ZodBoolean>;
|
|
186
|
+
toState: z.ZodOptional<z.ZodString>;
|
|
187
|
+
}, z.core.$strip>>>;
|
|
146
188
|
type: z.ZodLiteral<"api">;
|
|
147
189
|
apiConfig: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
148
190
|
}, z.core.$strip>], "type">>>>;
|
|
@@ -10,6 +10,7 @@ export const FlowDefinitionSchema = z.object({
|
|
|
10
10
|
maxConcurrent: z.number().int().min(0).optional().default(0),
|
|
11
11
|
maxConcurrentPerRepo: z.number().int().min(0).optional().default(0),
|
|
12
12
|
affinityWindowMs: z.number().int().min(0).optional().default(300000),
|
|
13
|
+
claimRetryAfterMs: z.number().int().min(0).optional(),
|
|
13
14
|
gateTimeoutMs: z.number().int().min(1).optional(),
|
|
14
15
|
version: z.number().int().min(1).optional().default(1),
|
|
15
16
|
createdBy: z.string().optional(),
|
|
@@ -36,6 +37,7 @@ export const OnEnterSchema = z.object({
|
|
|
36
37
|
export const StateDefinitionSchema = z.object({
|
|
37
38
|
name: z.string().min(1),
|
|
38
39
|
flowName: z.string().min(1),
|
|
40
|
+
agentRole: z.string().optional(),
|
|
39
41
|
modelTier: z.string().optional(),
|
|
40
42
|
mode: z.enum(["passive", "active"]).optional().default("passive"),
|
|
41
43
|
promptTemplate: z
|
|
@@ -47,13 +49,19 @@ export const StateDefinitionSchema = z.object({
|
|
|
47
49
|
.optional(),
|
|
48
50
|
constraints: z.record(z.string(), z.unknown()).optional(),
|
|
49
51
|
onEnter: OnEnterSchema.optional(),
|
|
52
|
+
retryAfterMs: z.number().int().min(0).optional(),
|
|
50
53
|
});
|
|
51
54
|
// Gate: discriminated union on `type`
|
|
55
|
+
const GateOutcomeSchema = z.object({
|
|
56
|
+
proceed: z.boolean().optional(),
|
|
57
|
+
toState: z.string().min(1).optional(),
|
|
58
|
+
});
|
|
52
59
|
const BaseGateSchema = z.object({
|
|
53
60
|
name: z.string().min(1),
|
|
54
61
|
timeoutMs: z.number().int().min(1).optional(),
|
|
55
62
|
failurePrompt: z.string().optional(),
|
|
56
63
|
timeoutPrompt: z.string().min(1).optional(),
|
|
64
|
+
outcomes: z.record(z.string(), GateOutcomeSchema).optional(),
|
|
57
65
|
});
|
|
58
66
|
export const CommandGateSchema = BaseGateSchema.extend({
|
|
59
67
|
type: z.literal("command"),
|
|
@@ -54,7 +54,15 @@ export declare class Engine {
|
|
|
54
54
|
undrainWorker(workerId: string): void;
|
|
55
55
|
isDraining(workerId: string): boolean;
|
|
56
56
|
listDrainingWorkers(): string[];
|
|
57
|
+
emit(event: import("./event-types.js").EngineEvent): Promise<void>;
|
|
57
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;
|
|
58
66
|
createEntity(flowName: string, refs?: Record<string, {
|
|
59
67
|
adapter: string;
|
|
60
68
|
id: string;
|
|
@@ -39,6 +39,9 @@ export class Engine {
|
|
|
39
39
|
listDrainingWorkers() {
|
|
40
40
|
return Array.from(this.drainingWorkers);
|
|
41
41
|
}
|
|
42
|
+
async emit(event) {
|
|
43
|
+
await this.eventEmitter.emit(event);
|
|
44
|
+
}
|
|
42
45
|
async processSignal(entityId, signal, artifacts, triggeringInvocationId) {
|
|
43
46
|
// 1. Load entity
|
|
44
47
|
const entity = await this.entityRepo.get(entityId);
|
|
@@ -52,83 +55,20 @@ export class Engine {
|
|
|
52
55
|
const transition = findTransition(flow, entity.state, signal, { entity }, true, this.logger);
|
|
53
56
|
if (!transition)
|
|
54
57
|
throw new ValidationError(`No transition from "${entity.state}" on signal "${signal}" in flow "${flow.name}"`);
|
|
55
|
-
// 4. Evaluate gate
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const gateResult = await evaluateGate(gate, entity, this.gateRepo, flow.gateTimeoutMs);
|
|
62
|
-
if (!gateResult.passed) {
|
|
63
|
-
// Persist gate failure into entity artifacts for retry context
|
|
64
|
-
const priorFailures = Array.isArray(entity.artifacts?.gate_failures)
|
|
65
|
-
? entity.artifacts.gate_failures
|
|
66
|
-
: [];
|
|
67
|
-
await this.entityRepo.updateArtifacts(entityId, {
|
|
68
|
-
gate_failures: [
|
|
69
|
-
...priorFailures,
|
|
70
|
-
{
|
|
71
|
-
gateId: gate.id,
|
|
72
|
-
gateName: gate.name,
|
|
73
|
-
output: gateResult.output,
|
|
74
|
-
failedAt: new Date().toISOString(),
|
|
75
|
-
},
|
|
76
|
-
],
|
|
77
|
-
});
|
|
78
|
-
if (gateResult.timedOut) {
|
|
79
|
-
await this.eventEmitter.emit({
|
|
80
|
-
type: "gate.timedOut",
|
|
81
|
-
entityId,
|
|
82
|
-
gateId: gate.id,
|
|
83
|
-
emittedAt: new Date(),
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
else {
|
|
87
|
-
await this.eventEmitter.emit({
|
|
88
|
-
type: "gate.failed",
|
|
89
|
-
entityId,
|
|
90
|
-
gateId: gate.id,
|
|
91
|
-
emittedAt: new Date(),
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
let resolvedTimeoutPrompt;
|
|
95
|
-
if (gateResult.timedOut) {
|
|
96
|
-
const rawTemplate = gate.timeoutPrompt ?? flow.timeoutPrompt ?? DEFAULT_TIMEOUT_PROMPT;
|
|
97
|
-
try {
|
|
98
|
-
const hbs = getHandlebars();
|
|
99
|
-
const template = hbs.compile(rawTemplate);
|
|
100
|
-
resolvedTimeoutPrompt = template({
|
|
101
|
-
entity,
|
|
102
|
-
flow,
|
|
103
|
-
gate: { name: gate.name, output: gateResult.output },
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
catch (err) {
|
|
107
|
-
this.logger.error("[engine] Failed to render timeoutPrompt template:", err);
|
|
108
|
-
resolvedTimeoutPrompt = DEFAULT_TIMEOUT_PROMPT;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return {
|
|
112
|
-
gated: true,
|
|
113
|
-
gateTimedOut: gateResult.timedOut,
|
|
114
|
-
gateOutput: gateResult.output,
|
|
115
|
-
gateName: gate.name,
|
|
116
|
-
failurePrompt: gate.failurePrompt ?? undefined,
|
|
117
|
-
timeoutPrompt: resolvedTimeoutPrompt,
|
|
118
|
-
gatesPassed,
|
|
119
|
-
terminal: false,
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
gatesPassed.push(gate.name);
|
|
123
|
-
await this.eventEmitter.emit({
|
|
124
|
-
type: "gate.passed",
|
|
125
|
-
entityId,
|
|
126
|
-
gateId: gate.id,
|
|
127
|
-
emittedAt: new Date(),
|
|
128
|
-
});
|
|
58
|
+
// 4. Evaluate gate — returns a routing decision or a block result to return immediately.
|
|
59
|
+
const routing = transition.gateId
|
|
60
|
+
? await this.resolveGate(transition.gateId, entity, flow)
|
|
61
|
+
: { kind: "proceed", gatesPassed: [] };
|
|
62
|
+
if (routing.kind === "block") {
|
|
63
|
+
return { gated: true, ...routing, terminal: false };
|
|
129
64
|
}
|
|
65
|
+
// Gate passed or redirected — determine the actual destination.
|
|
66
|
+
const toState = routing.kind === "redirect" ? routing.toState : transition.toState;
|
|
67
|
+
const trigger = routing.kind === "redirect" ? routing.trigger : signal;
|
|
68
|
+
const spawnFlow = routing.kind === "redirect" ? null : transition.spawnFlow;
|
|
69
|
+
const { gatesPassed } = routing;
|
|
130
70
|
// 5. Transition entity
|
|
131
|
-
let updated = await this.entityRepo.transition(entityId,
|
|
71
|
+
let updated = await this.entityRepo.transition(entityId, toState, trigger, artifacts);
|
|
132
72
|
// Clear gate_failures on successful transition so stale failures don't bleed into future agent prompts
|
|
133
73
|
await this.entityRepo.updateArtifacts(entityId, { gate_failures: [] });
|
|
134
74
|
// Keep the in-memory entity in sync so buildInvocation sees the cleared failures
|
|
@@ -139,25 +79,25 @@ export class Engine {
|
|
|
139
79
|
entityId,
|
|
140
80
|
flowId: flow.id,
|
|
141
81
|
fromState: entity.state,
|
|
142
|
-
toState
|
|
143
|
-
trigger
|
|
82
|
+
toState,
|
|
83
|
+
trigger,
|
|
144
84
|
emittedAt: new Date(),
|
|
145
85
|
});
|
|
146
86
|
const result = {
|
|
147
|
-
newState:
|
|
87
|
+
newState: toState,
|
|
148
88
|
gatesPassed,
|
|
149
89
|
gated: false,
|
|
150
90
|
terminal: false,
|
|
151
91
|
};
|
|
152
92
|
// 6b. Execute onEnter hook if defined on the new state
|
|
153
|
-
const newStateDef = flow.states.find((s) => s.name ===
|
|
93
|
+
const newStateDef = flow.states.find((s) => s.name === toState);
|
|
154
94
|
if (newStateDef?.onEnter) {
|
|
155
95
|
const onEnterResult = await executeOnEnter(newStateDef.onEnter, updated, this.entityRepo);
|
|
156
96
|
if (onEnterResult.skipped) {
|
|
157
97
|
await this.eventEmitter.emit({
|
|
158
98
|
type: "onEnter.skipped",
|
|
159
99
|
entityId,
|
|
160
|
-
state:
|
|
100
|
+
state: toState,
|
|
161
101
|
emittedAt: new Date(),
|
|
162
102
|
});
|
|
163
103
|
}
|
|
@@ -165,20 +105,20 @@ export class Engine {
|
|
|
165
105
|
await this.eventEmitter.emit({
|
|
166
106
|
type: "onEnter.failed",
|
|
167
107
|
entityId,
|
|
168
|
-
state:
|
|
108
|
+
state: toState,
|
|
169
109
|
error: onEnterResult.error,
|
|
170
110
|
emittedAt: new Date(),
|
|
171
111
|
});
|
|
172
112
|
await this.transitionLogRepo.record({
|
|
173
113
|
entityId,
|
|
174
114
|
fromState: entity.state,
|
|
175
|
-
toState
|
|
176
|
-
trigger
|
|
115
|
+
toState,
|
|
116
|
+
trigger,
|
|
177
117
|
invocationId: triggeringInvocationId ?? null,
|
|
178
118
|
timestamp: new Date(),
|
|
179
119
|
});
|
|
180
120
|
return {
|
|
181
|
-
newState:
|
|
121
|
+
newState: toState,
|
|
182
122
|
gatesPassed,
|
|
183
123
|
gated: false,
|
|
184
124
|
onEnterFailed: true,
|
|
@@ -190,7 +130,7 @@ export class Engine {
|
|
|
190
130
|
await this.eventEmitter.emit({
|
|
191
131
|
type: "onEnter.completed",
|
|
192
132
|
entityId,
|
|
193
|
-
state:
|
|
133
|
+
state: toState,
|
|
194
134
|
artifacts: onEnterResult.artifacts ?? {},
|
|
195
135
|
emittedAt: new Date(),
|
|
196
136
|
});
|
|
@@ -211,15 +151,15 @@ export class Engine {
|
|
|
211
151
|
]);
|
|
212
152
|
const enriched = { ...updated, invocations, gateResults };
|
|
213
153
|
const build = await buildInvocation(newStateDef, enriched, this.adapters, flow, this.logger);
|
|
214
|
-
const invocation = await this.invocationRepo.create(entityId,
|
|
154
|
+
const invocation = await this.invocationRepo.create(entityId, toState, build.prompt, build.mode, undefined, build.systemPrompt || build.userContent
|
|
215
155
|
? { systemPrompt: build.systemPrompt, userContent: build.userContent }
|
|
216
|
-
: undefined);
|
|
156
|
+
: undefined, newStateDef.agentRole ?? null);
|
|
217
157
|
result.invocationId = invocation.id;
|
|
218
158
|
await this.eventEmitter.emit({
|
|
219
159
|
type: "invocation.created",
|
|
220
160
|
entityId,
|
|
221
161
|
invocationId: invocation.id,
|
|
222
|
-
stage:
|
|
162
|
+
stage: toState,
|
|
223
163
|
emittedAt: new Date(),
|
|
224
164
|
});
|
|
225
165
|
}
|
|
@@ -229,13 +169,13 @@ export class Engine {
|
|
|
229
169
|
await this.transitionLogRepo.record({
|
|
230
170
|
entityId,
|
|
231
171
|
fromState: entity.state,
|
|
232
|
-
toState
|
|
233
|
-
trigger
|
|
172
|
+
toState,
|
|
173
|
+
trigger,
|
|
234
174
|
invocationId: triggeringInvocationId ?? null,
|
|
235
175
|
timestamp: new Date(),
|
|
236
176
|
});
|
|
237
177
|
// 9. Spawn child flows
|
|
238
|
-
const spawned = await executeSpawn(
|
|
178
|
+
const spawned = await executeSpawn({ spawnFlow }, updated, this.flowRepo, this.entityRepo, this.logger);
|
|
239
179
|
if (spawned) {
|
|
240
180
|
result.spawned = [spawned.id];
|
|
241
181
|
await this.eventEmitter.emit({
|
|
@@ -246,14 +186,93 @@ export class Engine {
|
|
|
246
186
|
emittedAt: new Date(),
|
|
247
187
|
});
|
|
248
188
|
}
|
|
249
|
-
// 10. Mark terminal
|
|
250
|
-
|
|
251
|
-
if (isTerminal(flow, transition.toState)) {
|
|
189
|
+
// 10. Mark terminal
|
|
190
|
+
if (isTerminal(flow, toState)) {
|
|
252
191
|
result.terminal = true;
|
|
253
192
|
result.spawned = result.spawned ?? [];
|
|
254
193
|
}
|
|
255
194
|
return result;
|
|
256
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* Evaluate a gate and return a routing decision:
|
|
198
|
+
* - `proceed` — gate passed, continue to transition.toState
|
|
199
|
+
* - `redirect` — gate outcome maps to a different toState
|
|
200
|
+
* - `block` — gate failed or timed out; caller should return this as the signal result
|
|
201
|
+
*/
|
|
202
|
+
async resolveGate(gateId, entity, flow) {
|
|
203
|
+
const gate = await this.gateRepo.get(gateId);
|
|
204
|
+
if (!gate)
|
|
205
|
+
throw new NotFoundError(`Gate "${gateId}" not found`);
|
|
206
|
+
const gateResult = await evaluateGate(gate, entity, this.gateRepo, flow.gateTimeoutMs);
|
|
207
|
+
const namedOutcome = gateResult.outcome && gate.outcomes ? gate.outcomes[gateResult.outcome] : undefined;
|
|
208
|
+
if (namedOutcome?.toState) {
|
|
209
|
+
const outcomeLabel = gateResult.outcome ?? gate.name;
|
|
210
|
+
await this.eventEmitter.emit({
|
|
211
|
+
type: "gate.redirected",
|
|
212
|
+
entityId: entity.id,
|
|
213
|
+
gateId: gate.id,
|
|
214
|
+
outcome: outcomeLabel,
|
|
215
|
+
toState: namedOutcome.toState,
|
|
216
|
+
emittedAt: new Date(),
|
|
217
|
+
});
|
|
218
|
+
return {
|
|
219
|
+
kind: "redirect",
|
|
220
|
+
toState: namedOutcome.toState,
|
|
221
|
+
trigger: `gate:${gate.name}:${outcomeLabel}`,
|
|
222
|
+
gatesPassed: [gate.name],
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
if (namedOutcome?.proceed || (!namedOutcome && gateResult.passed)) {
|
|
226
|
+
await this.eventEmitter.emit({
|
|
227
|
+
type: "gate.passed",
|
|
228
|
+
entityId: entity.id,
|
|
229
|
+
gateId: gate.id,
|
|
230
|
+
emittedAt: new Date(),
|
|
231
|
+
});
|
|
232
|
+
return { kind: "proceed", gatesPassed: [gate.name] };
|
|
233
|
+
}
|
|
234
|
+
// Gate failed — persist failure context and emit event
|
|
235
|
+
const priorFailures = Array.isArray(entity.artifacts?.gate_failures)
|
|
236
|
+
? entity.artifacts.gate_failures
|
|
237
|
+
: [];
|
|
238
|
+
await this.entityRepo.updateArtifacts(entity.id, {
|
|
239
|
+
gate_failures: [
|
|
240
|
+
...priorFailures,
|
|
241
|
+
{ gateId: gate.id, gateName: gate.name, output: gateResult.output, failedAt: new Date().toISOString() },
|
|
242
|
+
],
|
|
243
|
+
});
|
|
244
|
+
await this.eventEmitter.emit({
|
|
245
|
+
type: gateResult.timedOut ? "gate.timedOut" : "gate.failed",
|
|
246
|
+
entityId: entity.id,
|
|
247
|
+
gateId: gate.id,
|
|
248
|
+
emittedAt: new Date(),
|
|
249
|
+
});
|
|
250
|
+
let timeoutPrompt;
|
|
251
|
+
if (gateResult.timedOut) {
|
|
252
|
+
const rawTemplate = gate.timeoutPrompt ?? flow.timeoutPrompt ?? DEFAULT_TIMEOUT_PROMPT;
|
|
253
|
+
try {
|
|
254
|
+
const hbs = getHandlebars();
|
|
255
|
+
timeoutPrompt = hbs.compile(rawTemplate)({
|
|
256
|
+
entity,
|
|
257
|
+
flow,
|
|
258
|
+
gate: { name: gate.name, output: gateResult.output },
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
this.logger.error("[engine] Failed to render timeoutPrompt template:", err);
|
|
263
|
+
timeoutPrompt = DEFAULT_TIMEOUT_PROMPT;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
kind: "block",
|
|
268
|
+
gateTimedOut: gateResult.timedOut,
|
|
269
|
+
gateOutput: gateResult.output,
|
|
270
|
+
gateName: gate.name,
|
|
271
|
+
failurePrompt: gate.failurePrompt ?? undefined,
|
|
272
|
+
timeoutPrompt,
|
|
273
|
+
gatesPassed: [],
|
|
274
|
+
};
|
|
275
|
+
}
|
|
257
276
|
async createEntity(flowName, refs, payload) {
|
|
258
277
|
const flow = await this.flowRepo.getByName(flowName);
|
|
259
278
|
if (!flow)
|
|
@@ -312,7 +331,7 @@ export class Engine {
|
|
|
312
331
|
const build = await buildInvocation(initialState, enriched, this.adapters, flow, this.logger);
|
|
313
332
|
await this.invocationRepo.create(entity.id, flow.initialState, build.prompt, build.mode, undefined, build.systemPrompt || build.userContent
|
|
314
333
|
? { systemPrompt: build.systemPrompt, userContent: build.userContent }
|
|
315
|
-
: undefined);
|
|
334
|
+
: undefined, initialState.agentRole ?? null);
|
|
316
335
|
}
|
|
317
336
|
return entity;
|
|
318
337
|
}
|
|
@@ -484,7 +503,7 @@ export class Engine {
|
|
|
484
503
|
const build = await this.buildPromptForEntity(state, claimed, flow);
|
|
485
504
|
const invocation = await this.invocationRepo.create(claimed.id, state.name, build.prompt, build.mode, undefined, build.systemPrompt || build.userContent
|
|
486
505
|
? { systemPrompt: build.systemPrompt, userContent: build.userContent }
|
|
487
|
-
: undefined);
|
|
506
|
+
: undefined, state.agentRole ?? null);
|
|
488
507
|
const entityClaimToken = worker_id ?? `agent:${role}`;
|
|
489
508
|
let claimedInvocation;
|
|
490
509
|
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;
|
|
@@ -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 {
|
|
@@ -104,7 +104,7 @@ export class ActiveRunner {
|
|
|
104
104
|
await this.entityRepo.updateArtifacts(invocation.entityId, { stuck: true, stuckAt: new Date().toISOString() });
|
|
105
105
|
return;
|
|
106
106
|
}
|
|
107
|
-
await this.invocationRepo.create(invocation.entityId, invocation.stage, invocation.prompt, invocation.mode, undefined, { ...(invocation.context ?? {}), retryCount: retryCount + 1 });
|
|
107
|
+
await this.invocationRepo.create(invocation.entityId, invocation.stage, invocation.prompt, invocation.mode, undefined, { ...(invocation.context ?? {}), retryCount: retryCount + 1 }, invocation.agentRole);
|
|
108
108
|
return;
|
|
109
109
|
}
|
|
110
110
|
// Gate timed out — re-queue after backoff instead of failing.
|
|
@@ -113,7 +113,7 @@ export class ActiveRunner {
|
|
|
113
113
|
if (result.gated && result.gateTimedOut) {
|
|
114
114
|
const retryAfterMs = 30000;
|
|
115
115
|
this.logger.info(`[active-runner] gate timed out for entity ${invocation.entityId}, re-queuing after ${retryAfterMs}ms`);
|
|
116
|
-
await this.invocationRepo.create(invocation.entityId, invocation.stage, invocation.prompt, invocation.mode, undefined, invocation.context ?? undefined);
|
|
116
|
+
await this.invocationRepo.create(invocation.entityId, invocation.stage, invocation.prompt, invocation.mode, undefined, invocation.context ?? undefined, invocation.agentRole);
|
|
117
117
|
await sleep(retryAfterMs, signal);
|
|
118
118
|
return;
|
|
119
119
|
}
|