@wopr-network/defcon 1.4.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 +2 -0
- package/dist/src/config/zod-schemas.d.ts +38 -0
- package/dist/src/config/zod-schemas.js +6 -0
- package/dist/src/engine/engine.d.ts +7 -0
- package/dist/src/engine/engine.js +112 -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 +3 -2
- package/dist/src/execution/mcp-server.js +2 -2
- package/dist/src/repositories/drizzle/flow.repo.js +5 -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 +17 -0
- package/dist/src/repositories/drizzle/schema.js +1 -0
- package/dist/src/repositories/interfaces.d.ts +17 -2
- package/drizzle/0013_gate_outcomes.sql +1 -0
- package/drizzle/meta/_journal.json +7 -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,6 +87,7 @@ 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,
|
|
@@ -23,6 +23,7 @@ export declare const OnEnterSchema: z.ZodObject<{
|
|
|
23
23
|
export declare const StateDefinitionSchema: z.ZodObject<{
|
|
24
24
|
name: z.ZodString;
|
|
25
25
|
flowName: z.ZodString;
|
|
26
|
+
agentRole: z.ZodOptional<z.ZodString>;
|
|
26
27
|
modelTier: z.ZodOptional<z.ZodString>;
|
|
27
28
|
mode: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
|
|
28
29
|
passive: "passive";
|
|
@@ -42,6 +43,10 @@ export declare const CommandGateSchema: z.ZodObject<{
|
|
|
42
43
|
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
43
44
|
failurePrompt: z.ZodOptional<z.ZodString>;
|
|
44
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>>>;
|
|
45
50
|
type: z.ZodLiteral<"command">;
|
|
46
51
|
command: z.ZodString;
|
|
47
52
|
}, z.core.$strip>;
|
|
@@ -50,6 +55,10 @@ export declare const FunctionGateSchema: z.ZodObject<{
|
|
|
50
55
|
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
51
56
|
failurePrompt: z.ZodOptional<z.ZodString>;
|
|
52
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>>>;
|
|
53
62
|
type: z.ZodLiteral<"function">;
|
|
54
63
|
functionRef: z.ZodString;
|
|
55
64
|
}, z.core.$strip>;
|
|
@@ -58,6 +67,10 @@ export declare const ApiGateSchema: z.ZodObject<{
|
|
|
58
67
|
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
59
68
|
failurePrompt: z.ZodOptional<z.ZodString>;
|
|
60
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>>>;
|
|
61
74
|
type: z.ZodLiteral<"api">;
|
|
62
75
|
apiConfig: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
63
76
|
}, z.core.$strip>;
|
|
@@ -66,6 +79,10 @@ export declare const GateDefinitionSchema: z.ZodDiscriminatedUnion<[z.ZodObject<
|
|
|
66
79
|
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
67
80
|
failurePrompt: z.ZodOptional<z.ZodString>;
|
|
68
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>>>;
|
|
69
86
|
type: z.ZodLiteral<"command">;
|
|
70
87
|
command: z.ZodString;
|
|
71
88
|
}, z.core.$strip>, z.ZodObject<{
|
|
@@ -73,6 +90,10 @@ export declare const GateDefinitionSchema: z.ZodDiscriminatedUnion<[z.ZodObject<
|
|
|
73
90
|
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
74
91
|
failurePrompt: z.ZodOptional<z.ZodString>;
|
|
75
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>>>;
|
|
76
97
|
type: z.ZodLiteral<"function">;
|
|
77
98
|
functionRef: z.ZodString;
|
|
78
99
|
}, z.core.$strip>, z.ZodObject<{
|
|
@@ -80,6 +101,10 @@ export declare const GateDefinitionSchema: z.ZodDiscriminatedUnion<[z.ZodObject<
|
|
|
80
101
|
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
81
102
|
failurePrompt: z.ZodOptional<z.ZodString>;
|
|
82
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>>>;
|
|
83
108
|
type: z.ZodLiteral<"api">;
|
|
84
109
|
apiConfig: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
85
110
|
}, z.core.$strip>], "type">;
|
|
@@ -114,6 +139,7 @@ export declare const SeedFileSchema: z.ZodObject<{
|
|
|
114
139
|
states: z.ZodArray<z.ZodObject<{
|
|
115
140
|
name: z.ZodString;
|
|
116
141
|
flowName: z.ZodString;
|
|
142
|
+
agentRole: z.ZodOptional<z.ZodString>;
|
|
117
143
|
modelTier: z.ZodOptional<z.ZodString>;
|
|
118
144
|
mode: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
|
|
119
145
|
passive: "passive";
|
|
@@ -133,6 +159,10 @@ export declare const SeedFileSchema: z.ZodObject<{
|
|
|
133
159
|
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
134
160
|
failurePrompt: z.ZodOptional<z.ZodString>;
|
|
135
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>>>;
|
|
136
166
|
type: z.ZodLiteral<"command">;
|
|
137
167
|
command: z.ZodString;
|
|
138
168
|
}, z.core.$strip>, z.ZodObject<{
|
|
@@ -140,6 +170,10 @@ export declare const SeedFileSchema: z.ZodObject<{
|
|
|
140
170
|
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
141
171
|
failurePrompt: z.ZodOptional<z.ZodString>;
|
|
142
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>>>;
|
|
143
177
|
type: z.ZodLiteral<"function">;
|
|
144
178
|
functionRef: z.ZodString;
|
|
145
179
|
}, z.core.$strip>, z.ZodObject<{
|
|
@@ -147,6 +181,10 @@ export declare const SeedFileSchema: z.ZodObject<{
|
|
|
147
181
|
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
148
182
|
failurePrompt: z.ZodOptional<z.ZodString>;
|
|
149
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>>>;
|
|
150
188
|
type: z.ZodLiteral<"api">;
|
|
151
189
|
apiConfig: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
152
190
|
}, z.core.$strip>], "type">>>>;
|
|
@@ -37,6 +37,7 @@ export const OnEnterSchema = z.object({
|
|
|
37
37
|
export const StateDefinitionSchema = z.object({
|
|
38
38
|
name: z.string().min(1),
|
|
39
39
|
flowName: z.string().min(1),
|
|
40
|
+
agentRole: z.string().optional(),
|
|
40
41
|
modelTier: z.string().optional(),
|
|
41
42
|
mode: z.enum(["passive", "active"]).optional().default("passive"),
|
|
42
43
|
promptTemplate: z
|
|
@@ -51,11 +52,16 @@ export const StateDefinitionSchema = z.object({
|
|
|
51
52
|
retryAfterMs: z.number().int().min(0).optional(),
|
|
52
53
|
});
|
|
53
54
|
// Gate: discriminated union on `type`
|
|
55
|
+
const GateOutcomeSchema = z.object({
|
|
56
|
+
proceed: z.boolean().optional(),
|
|
57
|
+
toState: z.string().min(1).optional(),
|
|
58
|
+
});
|
|
54
59
|
const BaseGateSchema = z.object({
|
|
55
60
|
name: z.string().min(1),
|
|
56
61
|
timeoutMs: z.number().int().min(1).optional(),
|
|
57
62
|
failurePrompt: z.string().optional(),
|
|
58
63
|
timeoutPrompt: z.string().min(1).optional(),
|
|
64
|
+
outcomes: z.record(z.string(), GateOutcomeSchema).optional(),
|
|
59
65
|
});
|
|
60
66
|
export const CommandGateSchema = BaseGateSchema.extend({
|
|
61
67
|
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;
|
|
@@ -55,83 +55,20 @@ export class Engine {
|
|
|
55
55
|
const transition = findTransition(flow, entity.state, signal, { entity }, true, this.logger);
|
|
56
56
|
if (!transition)
|
|
57
57
|
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
|
-
const gateResult = await evaluateGate(gate, entity, this.gateRepo, flow.gateTimeoutMs);
|
|
65
|
-
if (!gateResult.passed) {
|
|
66
|
-
// Persist gate failure into entity artifacts for retry context
|
|
67
|
-
const priorFailures = Array.isArray(entity.artifacts?.gate_failures)
|
|
68
|
-
? entity.artifacts.gate_failures
|
|
69
|
-
: [];
|
|
70
|
-
await this.entityRepo.updateArtifacts(entityId, {
|
|
71
|
-
gate_failures: [
|
|
72
|
-
...priorFailures,
|
|
73
|
-
{
|
|
74
|
-
gateId: gate.id,
|
|
75
|
-
gateName: gate.name,
|
|
76
|
-
output: gateResult.output,
|
|
77
|
-
failedAt: new Date().toISOString(),
|
|
78
|
-
},
|
|
79
|
-
],
|
|
80
|
-
});
|
|
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
|
-
}
|
|
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
|
-
});
|
|
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 };
|
|
132
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;
|
|
133
70
|
// 5. Transition entity
|
|
134
|
-
let updated = await this.entityRepo.transition(entityId,
|
|
71
|
+
let updated = await this.entityRepo.transition(entityId, toState, trigger, artifacts);
|
|
135
72
|
// Clear gate_failures on successful transition so stale failures don't bleed into future agent prompts
|
|
136
73
|
await this.entityRepo.updateArtifacts(entityId, { gate_failures: [] });
|
|
137
74
|
// Keep the in-memory entity in sync so buildInvocation sees the cleared failures
|
|
@@ -142,25 +79,25 @@ export class Engine {
|
|
|
142
79
|
entityId,
|
|
143
80
|
flowId: flow.id,
|
|
144
81
|
fromState: entity.state,
|
|
145
|
-
toState
|
|
146
|
-
trigger
|
|
82
|
+
toState,
|
|
83
|
+
trigger,
|
|
147
84
|
emittedAt: new Date(),
|
|
148
85
|
});
|
|
149
86
|
const result = {
|
|
150
|
-
newState:
|
|
87
|
+
newState: toState,
|
|
151
88
|
gatesPassed,
|
|
152
89
|
gated: false,
|
|
153
90
|
terminal: false,
|
|
154
91
|
};
|
|
155
92
|
// 6b. Execute onEnter hook if defined on the new state
|
|
156
|
-
const newStateDef = flow.states.find((s) => s.name ===
|
|
93
|
+
const newStateDef = flow.states.find((s) => s.name === toState);
|
|
157
94
|
if (newStateDef?.onEnter) {
|
|
158
95
|
const onEnterResult = await executeOnEnter(newStateDef.onEnter, updated, this.entityRepo);
|
|
159
96
|
if (onEnterResult.skipped) {
|
|
160
97
|
await this.eventEmitter.emit({
|
|
161
98
|
type: "onEnter.skipped",
|
|
162
99
|
entityId,
|
|
163
|
-
state:
|
|
100
|
+
state: toState,
|
|
164
101
|
emittedAt: new Date(),
|
|
165
102
|
});
|
|
166
103
|
}
|
|
@@ -168,20 +105,20 @@ export class Engine {
|
|
|
168
105
|
await this.eventEmitter.emit({
|
|
169
106
|
type: "onEnter.failed",
|
|
170
107
|
entityId,
|
|
171
|
-
state:
|
|
108
|
+
state: toState,
|
|
172
109
|
error: onEnterResult.error,
|
|
173
110
|
emittedAt: new Date(),
|
|
174
111
|
});
|
|
175
112
|
await this.transitionLogRepo.record({
|
|
176
113
|
entityId,
|
|
177
114
|
fromState: entity.state,
|
|
178
|
-
toState
|
|
179
|
-
trigger
|
|
115
|
+
toState,
|
|
116
|
+
trigger,
|
|
180
117
|
invocationId: triggeringInvocationId ?? null,
|
|
181
118
|
timestamp: new Date(),
|
|
182
119
|
});
|
|
183
120
|
return {
|
|
184
|
-
newState:
|
|
121
|
+
newState: toState,
|
|
185
122
|
gatesPassed,
|
|
186
123
|
gated: false,
|
|
187
124
|
onEnterFailed: true,
|
|
@@ -193,7 +130,7 @@ export class Engine {
|
|
|
193
130
|
await this.eventEmitter.emit({
|
|
194
131
|
type: "onEnter.completed",
|
|
195
132
|
entityId,
|
|
196
|
-
state:
|
|
133
|
+
state: toState,
|
|
197
134
|
artifacts: onEnterResult.artifacts ?? {},
|
|
198
135
|
emittedAt: new Date(),
|
|
199
136
|
});
|
|
@@ -214,15 +151,15 @@ export class Engine {
|
|
|
214
151
|
]);
|
|
215
152
|
const enriched = { ...updated, invocations, gateResults };
|
|
216
153
|
const build = await buildInvocation(newStateDef, enriched, this.adapters, flow, this.logger);
|
|
217
|
-
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
|
|
218
155
|
? { systemPrompt: build.systemPrompt, userContent: build.userContent }
|
|
219
|
-
: undefined);
|
|
156
|
+
: undefined, newStateDef.agentRole ?? null);
|
|
220
157
|
result.invocationId = invocation.id;
|
|
221
158
|
await this.eventEmitter.emit({
|
|
222
159
|
type: "invocation.created",
|
|
223
160
|
entityId,
|
|
224
161
|
invocationId: invocation.id,
|
|
225
|
-
stage:
|
|
162
|
+
stage: toState,
|
|
226
163
|
emittedAt: new Date(),
|
|
227
164
|
});
|
|
228
165
|
}
|
|
@@ -232,13 +169,13 @@ export class Engine {
|
|
|
232
169
|
await this.transitionLogRepo.record({
|
|
233
170
|
entityId,
|
|
234
171
|
fromState: entity.state,
|
|
235
|
-
toState
|
|
236
|
-
trigger
|
|
172
|
+
toState,
|
|
173
|
+
trigger,
|
|
237
174
|
invocationId: triggeringInvocationId ?? null,
|
|
238
175
|
timestamp: new Date(),
|
|
239
176
|
});
|
|
240
177
|
// 9. Spawn child flows
|
|
241
|
-
const spawned = await executeSpawn(
|
|
178
|
+
const spawned = await executeSpawn({ spawnFlow }, updated, this.flowRepo, this.entityRepo, this.logger);
|
|
242
179
|
if (spawned) {
|
|
243
180
|
result.spawned = [spawned.id];
|
|
244
181
|
await this.eventEmitter.emit({
|
|
@@ -249,14 +186,93 @@ export class Engine {
|
|
|
249
186
|
emittedAt: new Date(),
|
|
250
187
|
});
|
|
251
188
|
}
|
|
252
|
-
// 10. Mark terminal
|
|
253
|
-
|
|
254
|
-
if (isTerminal(flow, transition.toState)) {
|
|
189
|
+
// 10. Mark terminal
|
|
190
|
+
if (isTerminal(flow, toState)) {
|
|
255
191
|
result.terminal = true;
|
|
256
192
|
result.spawned = result.spawned ?? [];
|
|
257
193
|
}
|
|
258
194
|
return result;
|
|
259
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
|
+
}
|
|
260
276
|
async createEntity(flowName, refs, payload) {
|
|
261
277
|
const flow = await this.flowRepo.getByName(flowName);
|
|
262
278
|
if (!flow)
|
|
@@ -315,7 +331,7 @@ export class Engine {
|
|
|
315
331
|
const build = await buildInvocation(initialState, enriched, this.adapters, flow, this.logger);
|
|
316
332
|
await this.invocationRepo.create(entity.id, flow.initialState, build.prompt, build.mode, undefined, build.systemPrompt || build.userContent
|
|
317
333
|
? { systemPrompt: build.systemPrompt, userContent: build.userContent }
|
|
318
|
-
: undefined);
|
|
334
|
+
: undefined, initialState.agentRole ?? null);
|
|
319
335
|
}
|
|
320
336
|
return entity;
|
|
321
337
|
}
|
|
@@ -487,7 +503,7 @@ export class Engine {
|
|
|
487
503
|
const build = await this.buildPromptForEntity(state, claimed, flow);
|
|
488
504
|
const invocation = await this.invocationRepo.create(claimed.id, state.name, build.prompt, build.mode, undefined, build.systemPrompt || build.userContent
|
|
489
505
|
? { systemPrompt: build.systemPrompt, userContent: build.userContent }
|
|
490
|
-
: undefined);
|
|
506
|
+
: undefined, state.agentRole ?? null);
|
|
491
507
|
const entityClaimToken = worker_id ?? `agent:${role}`;
|
|
492
508
|
let claimedInvocation;
|
|
493
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
|
}
|
|
@@ -203,6 +203,7 @@ export async function handleFlowClaim(deps, args) {
|
|
|
203
203
|
invocation_id: claimed.id,
|
|
204
204
|
flow: flow?.name ?? null,
|
|
205
205
|
stage: claimed.stage,
|
|
206
|
+
agent_role: claimed.agentRole || null,
|
|
206
207
|
prompt: claimed.prompt,
|
|
207
208
|
context: claimed.context,
|
|
208
209
|
});
|
|
@@ -260,7 +261,7 @@ export async function handleFlowReport(deps, args) {
|
|
|
260
261
|
// would regress the entity back to a stale state.
|
|
261
262
|
const entityAfter = await deps.entities.get(entityId).catch(() => null);
|
|
262
263
|
if (!entityAfter || entityAfter.state === activeInvocation.stage) {
|
|
263
|
-
await deps.invocations.create(entityId, activeInvocation.stage, activeInvocation.prompt, activeInvocation.mode, undefined, activeInvocation.context ?? undefined);
|
|
264
|
+
await deps.invocations.create(entityId, activeInvocation.stage, activeInvocation.prompt, activeInvocation.mode, undefined, activeInvocation.context ?? undefined, activeInvocation.agentRole);
|
|
264
265
|
}
|
|
265
266
|
return errorResult(message);
|
|
266
267
|
}
|
|
@@ -286,7 +287,7 @@ export async function handleFlowReport(deps, args) {
|
|
|
286
287
|
// check_back) finds an active invocation without requiring a round-trip through
|
|
287
288
|
// flow.claim. Workers on the "waiting" path will re-claim via flow.claim as usual.
|
|
288
289
|
if (result.gated) {
|
|
289
|
-
const replacement = await deps.invocations.create(entityId, activeInvocation.stage, activeInvocation.prompt, activeInvocation.mode, undefined, activeInvocation.context ?? undefined);
|
|
290
|
+
const replacement = await deps.invocations.create(entityId, activeInvocation.stage, activeInvocation.prompt, activeInvocation.mode, undefined, activeInvocation.context ?? undefined, activeInvocation.agentRole);
|
|
290
291
|
const claimedBy = activeInvocation.claimedBy;
|
|
291
292
|
if (claimedBy) {
|
|
292
293
|
await deps.invocations.claim(replacement.id, claimedBy).catch(() => {
|
|
@@ -556,7 +556,7 @@ async function handleFlowReport(deps, args) {
|
|
|
556
556
|
const flow = currentEntity ? await deps.flows.get(currentEntity.flowId) : null;
|
|
557
557
|
const entityIsTerminal = currentEntity && flow ? isTerminal(flow, currentEntity.state) : false;
|
|
558
558
|
if (!entityIsTerminal) {
|
|
559
|
-
const replacement = await deps.invocations.create(entityId, activeInvocation.stage, activeInvocation.prompt, activeInvocation.mode, undefined, activeInvocation.context ?? undefined);
|
|
559
|
+
const replacement = await deps.invocations.create(entityId, activeInvocation.stage, activeInvocation.prompt, activeInvocation.mode, undefined, activeInvocation.context ?? undefined, activeInvocation.agentRole);
|
|
560
560
|
// Claim the replacement for the same worker so it can retry immediately.
|
|
561
561
|
if (worker_id && replacement) {
|
|
562
562
|
try {
|
|
@@ -590,7 +590,7 @@ async function handleFlowReport(deps, args) {
|
|
|
590
590
|
// Gate blocked — create a replacement unclaimed invocation so the entity
|
|
591
591
|
// can be reclaimed; without it the entity would be permanently orphaned.
|
|
592
592
|
if (result.gated) {
|
|
593
|
-
await deps.invocations.create(entityId, activeInvocation.stage, activeInvocation.prompt, activeInvocation.mode, undefined, activeInvocation.context ?? undefined);
|
|
593
|
+
await deps.invocations.create(entityId, activeInvocation.stage, activeInvocation.prompt, activeInvocation.mode, undefined, activeInvocation.context ?? undefined, activeInvocation.agentRole);
|
|
594
594
|
if (result.gateTimedOut) {
|
|
595
595
|
const renderedPrompt = result.timeoutPrompt ?? DEFAULT_TIMEOUT_PROMPT;
|
|
596
596
|
return jsonResult({
|
|
@@ -9,6 +9,7 @@ function rowToState(r) {
|
|
|
9
9
|
id: r.id,
|
|
10
10
|
flowId: r.flowId,
|
|
11
11
|
name: r.name,
|
|
12
|
+
agentRole: r.agentRole ?? null,
|
|
12
13
|
modelTier: r.modelTier ?? null,
|
|
13
14
|
mode: (r.mode ?? "passive"),
|
|
14
15
|
promptTemplate: r.promptTemplate ?? null,
|
|
@@ -167,7 +168,7 @@ export class DrizzleFlowRepository {
|
|
|
167
168
|
id,
|
|
168
169
|
flowId,
|
|
169
170
|
name: state.name,
|
|
170
|
-
agentRole: null,
|
|
171
|
+
agentRole: state.agentRole || null,
|
|
171
172
|
modelTier: state.modelTier ?? null,
|
|
172
173
|
mode: state.mode ?? "passive",
|
|
173
174
|
promptTemplate: state.promptTemplate ?? null,
|
|
@@ -188,6 +189,8 @@ export class DrizzleFlowRepository {
|
|
|
188
189
|
const updateValues = {};
|
|
189
190
|
if (changes.name !== undefined)
|
|
190
191
|
updateValues.name = changes.name;
|
|
192
|
+
if (changes.agentRole !== undefined)
|
|
193
|
+
updateValues.agentRole = changes.agentRole;
|
|
191
194
|
if (changes.modelTier !== undefined)
|
|
192
195
|
updateValues.modelTier = changes.modelTier;
|
|
193
196
|
if (changes.mode !== undefined)
|
|
@@ -329,6 +332,7 @@ export class DrizzleFlowRepository {
|
|
|
329
332
|
id: s.id,
|
|
330
333
|
flowId,
|
|
331
334
|
name: s.name,
|
|
335
|
+
agentRole: s.agentRole || null,
|
|
332
336
|
modelTier: s.modelTier,
|
|
333
337
|
mode: s.mode ?? "passive",
|
|
334
338
|
promptTemplate: s.promptTemplate,
|
|
@@ -13,6 +13,7 @@ function toGate(row) {
|
|
|
13
13
|
timeoutMs: row.timeoutMs ?? null,
|
|
14
14
|
failurePrompt: row.failurePrompt ?? null,
|
|
15
15
|
timeoutPrompt: row.timeoutPrompt ?? null,
|
|
16
|
+
outcomes: row.outcomes ?? null,
|
|
16
17
|
};
|
|
17
18
|
}
|
|
18
19
|
function toGateResult(row) {
|
|
@@ -42,6 +43,7 @@ export class DrizzleGateRepository {
|
|
|
42
43
|
...(gate.timeoutMs != null ? { timeoutMs: gate.timeoutMs } : {}),
|
|
43
44
|
failurePrompt: gate.failurePrompt ?? null,
|
|
44
45
|
timeoutPrompt: gate.timeoutPrompt ?? null,
|
|
46
|
+
outcomes: gate.outcomes ?? null,
|
|
45
47
|
};
|
|
46
48
|
this.db.insert(gateDefinitions).values(values).run();
|
|
47
49
|
const row = this.db.select().from(gateDefinitions).where(eq(gateDefinitions.id, id)).get();
|
|
@@ -5,7 +5,7 @@ type Db = BetterSQLite3Database<typeof schema>;
|
|
|
5
5
|
export declare class DrizzleInvocationRepository implements IInvocationRepository {
|
|
6
6
|
private db;
|
|
7
7
|
constructor(db: Db);
|
|
8
|
-
create(entityId: string, stage: string, prompt: string, mode: Mode, ttlMs
|
|
8
|
+
create(entityId: string, stage: string, prompt: string, mode: Mode, ttlMs: number | undefined, context: Record<string, unknown> | undefined, agentRole: string | null): Promise<Invocation>;
|
|
9
9
|
get(id: string): Promise<Invocation | null>;
|
|
10
10
|
claim(invocationId: string, agentId: string): Promise<Invocation | null>;
|
|
11
11
|
complete(id: string, signal: string, artifacts?: Artifacts): Promise<Invocation>;
|
|
@@ -6,6 +6,7 @@ function toInvocation(row) {
|
|
|
6
6
|
id: row.id,
|
|
7
7
|
entityId: row.entityId,
|
|
8
8
|
stage: row.stage,
|
|
9
|
+
agentRole: row.agentRole ?? null,
|
|
9
10
|
mode: row.mode,
|
|
10
11
|
prompt: row.prompt,
|
|
11
12
|
context: row.context,
|
|
@@ -25,7 +26,7 @@ export class DrizzleInvocationRepository {
|
|
|
25
26
|
constructor(db) {
|
|
26
27
|
this.db = db;
|
|
27
28
|
}
|
|
28
|
-
async create(entityId, stage, prompt, mode, ttlMs, context) {
|
|
29
|
+
async create(entityId, stage, prompt, mode, ttlMs, context, agentRole) {
|
|
29
30
|
const id = crypto.randomUUID();
|
|
30
31
|
this.db
|
|
31
32
|
.insert(invocations)
|
|
@@ -33,6 +34,7 @@ export class DrizzleInvocationRepository {
|
|
|
33
34
|
id,
|
|
34
35
|
entityId,
|
|
35
36
|
stage,
|
|
37
|
+
agentRole: agentRole || null,
|
|
36
38
|
prompt,
|
|
37
39
|
mode,
|
|
38
40
|
ttlMs: ttlMs ?? 1800000,
|
|
@@ -689,6 +689,23 @@ export declare const gateDefinitions: import("drizzle-orm/sqlite-core").SQLiteTa
|
|
|
689
689
|
}, {}, {
|
|
690
690
|
length: number | undefined;
|
|
691
691
|
}>;
|
|
692
|
+
outcomes: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
693
|
+
name: "outcomes";
|
|
694
|
+
tableName: "gate_definitions";
|
|
695
|
+
dataType: "json";
|
|
696
|
+
columnType: "SQLiteTextJson";
|
|
697
|
+
data: unknown;
|
|
698
|
+
driverParam: string;
|
|
699
|
+
notNull: false;
|
|
700
|
+
hasDefault: false;
|
|
701
|
+
isPrimaryKey: false;
|
|
702
|
+
isAutoincrement: false;
|
|
703
|
+
hasRuntimeDefault: false;
|
|
704
|
+
enumValues: undefined;
|
|
705
|
+
baseColumn: never;
|
|
706
|
+
identity: undefined;
|
|
707
|
+
generated: undefined;
|
|
708
|
+
}, {}, {}>;
|
|
692
709
|
};
|
|
693
710
|
dialect: "sqlite";
|
|
694
711
|
}>;
|
|
@@ -46,6 +46,7 @@ export const gateDefinitions = sqliteTable("gate_definitions", {
|
|
|
46
46
|
timeoutMs: integer("timeout_ms"),
|
|
47
47
|
failurePrompt: text("failure_prompt"),
|
|
48
48
|
timeoutPrompt: text("timeout_prompt"),
|
|
49
|
+
outcomes: text("outcomes", { mode: "json" }),
|
|
49
50
|
});
|
|
50
51
|
export const transitionRules = sqliteTable("transition_rules", {
|
|
51
52
|
id: text("id").primaryKey(),
|
|
@@ -36,6 +36,7 @@ export interface Invocation {
|
|
|
36
36
|
id: string;
|
|
37
37
|
entityId: string;
|
|
38
38
|
stage: string;
|
|
39
|
+
agentRole: string | null;
|
|
39
40
|
mode: Mode;
|
|
40
41
|
prompt: string;
|
|
41
42
|
context: Record<string, unknown> | null;
|
|
@@ -82,6 +83,8 @@ export interface State {
|
|
|
82
83
|
id: string;
|
|
83
84
|
flowId: string;
|
|
84
85
|
name: string;
|
|
86
|
+
/** Agent type identifier — maps to an agent MD file (e.g. "wopr-architect" → ~/.claude/agents/wopr-architect.md). */
|
|
87
|
+
agentRole: string | null;
|
|
85
88
|
modelTier: string | null;
|
|
86
89
|
mode: Mode;
|
|
87
90
|
promptTemplate: string | null;
|
|
@@ -115,6 +118,13 @@ export interface Gate {
|
|
|
115
118
|
timeoutMs: number | null;
|
|
116
119
|
failurePrompt: string | null;
|
|
117
120
|
timeoutPrompt: string | null;
|
|
121
|
+
/** Named outcome map from structured gate output. Keys are outcome names; values declare
|
|
122
|
+
* where the entity goes. `proceed: true` means the original transition continues.
|
|
123
|
+
* `toState` redirects to a different state. */
|
|
124
|
+
outcomes: Record<string, {
|
|
125
|
+
proceed?: boolean;
|
|
126
|
+
toState?: string;
|
|
127
|
+
}> | null;
|
|
118
128
|
}
|
|
119
129
|
/** A complete flow definition with its states and transitions */
|
|
120
130
|
export interface Flow {
|
|
@@ -170,6 +180,7 @@ export interface CreateFlowInput {
|
|
|
170
180
|
/** Input for adding a state to a flow */
|
|
171
181
|
export interface CreateStateInput {
|
|
172
182
|
name: string;
|
|
183
|
+
agentRole?: string;
|
|
173
184
|
modelTier?: string;
|
|
174
185
|
mode?: Mode;
|
|
175
186
|
promptTemplate?: string;
|
|
@@ -198,6 +209,10 @@ export interface CreateGateInput {
|
|
|
198
209
|
timeoutMs?: number;
|
|
199
210
|
failurePrompt?: string;
|
|
200
211
|
timeoutPrompt?: string;
|
|
212
|
+
outcomes?: Record<string, {
|
|
213
|
+
proceed?: boolean;
|
|
214
|
+
toState?: string;
|
|
215
|
+
}>;
|
|
201
216
|
}
|
|
202
217
|
/** Data-access contract for entity lifecycle operations. */
|
|
203
218
|
export interface IEntityRepository {
|
|
@@ -274,7 +289,7 @@ export interface IFlowRepository {
|
|
|
274
289
|
/** Data-access contract for invocation lifecycle and claiming. */
|
|
275
290
|
export interface IInvocationRepository {
|
|
276
291
|
/** Create a new invocation for an entity at a given stage. */
|
|
277
|
-
create(entityId: string, stage: string, prompt: string, mode: Mode, ttlMs
|
|
292
|
+
create(entityId: string, stage: string, prompt: string, mode: Mode, ttlMs: number | undefined, context: Record<string, unknown> | undefined, agentRole: string | null): Promise<Invocation>;
|
|
278
293
|
/** Get an invocation by ID, or null if not found. */
|
|
279
294
|
get(id: string): Promise<Invocation | null>;
|
|
280
295
|
/** Atomically claim an unclaimed invocation for the specified agent. Uses compare-and-swap (UPDATE WHERE claimedBy IS NULL). Returns null if already claimed. */
|
|
@@ -329,7 +344,7 @@ export interface IGateRepository {
|
|
|
329
344
|
/** Get all gate results for a given entity. */
|
|
330
345
|
resultsFor(entityId: string): Promise<GateResult[]>;
|
|
331
346
|
/** Update mutable fields on a gate definition. */
|
|
332
|
-
update(id: string, changes: Partial<Pick<Gate, "command" | "functionRef" | "apiConfig" | "timeoutMs" | "failurePrompt" | "timeoutPrompt">>): Promise<Gate>;
|
|
347
|
+
update(id: string, changes: Partial<Pick<Gate, "command" | "functionRef" | "apiConfig" | "timeoutMs" | "failurePrompt" | "timeoutPrompt" | "outcomes">>): Promise<Gate>;
|
|
333
348
|
/** Delete the gate result for a specific entity+gate combination. */
|
|
334
349
|
clearResult(entityId: string, gateId: string): Promise<void>;
|
|
335
350
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ALTER TABLE `gate_definitions` ADD `outcomes` text;
|