@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
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { getHandlebars } from "./handlebars.js";
|
|
3
|
+
export async function executeOnExit(onExit, entity) {
|
|
4
|
+
const hbs = getHandlebars();
|
|
5
|
+
let renderedCommand;
|
|
6
|
+
// Merge artifact refs into entity.refs (same pattern as on-enter.ts)
|
|
7
|
+
const artifactRefs = entity.artifacts !== null &&
|
|
8
|
+
typeof entity.artifacts === "object" &&
|
|
9
|
+
"refs" in entity.artifacts &&
|
|
10
|
+
entity.artifacts.refs !== null &&
|
|
11
|
+
typeof entity.artifacts.refs === "object"
|
|
12
|
+
? entity.artifacts.refs
|
|
13
|
+
: {};
|
|
14
|
+
const entityForContext = { ...entity, refs: { ...artifactRefs, ...(entity.refs ?? {}) } };
|
|
15
|
+
try {
|
|
16
|
+
renderedCommand = hbs.compile(onExit.command)({ entity: entityForContext });
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
const error = `onExit template error: ${err instanceof Error ? err.message : String(err)}`;
|
|
20
|
+
return { error, timedOut: false };
|
|
21
|
+
}
|
|
22
|
+
const timeoutMs = onExit.timeout_ms ?? 30000;
|
|
23
|
+
const { exitCode, stdout, stderr, timedOut } = await runCommand(renderedCommand, timeoutMs);
|
|
24
|
+
if (timedOut) {
|
|
25
|
+
return { error: `onExit command timed out after ${timeoutMs}ms`, timedOut: true };
|
|
26
|
+
}
|
|
27
|
+
if (exitCode !== 0) {
|
|
28
|
+
return { error: `onExit command exited with code ${exitCode}: ${stderr || stdout}`, timedOut: false };
|
|
29
|
+
}
|
|
30
|
+
return { error: null, timedOut: false };
|
|
31
|
+
}
|
|
32
|
+
function runCommand(command, timeoutMs) {
|
|
33
|
+
return new Promise((resolve) => {
|
|
34
|
+
execFile("/bin/sh", ["-c", command], { timeout: timeoutMs }, (error, stdout, stderr) => {
|
|
35
|
+
const execErr = error;
|
|
36
|
+
const timedOut = execErr !== null && execErr.killed === true;
|
|
37
|
+
resolve({
|
|
38
|
+
exitCode: execErr ? (typeof execErr.code === "number" ? execErr.code : 1) : 0,
|
|
39
|
+
stdout: stdout.trim(),
|
|
40
|
+
stderr: stderr.trim(),
|
|
41
|
+
timedOut,
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -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,11 +9,13 @@ 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,
|
|
15
16
|
constraints: r.constraints,
|
|
16
17
|
onEnter: r.onEnter ?? null,
|
|
18
|
+
onExit: r.onExit ?? null,
|
|
17
19
|
retryAfterMs: r.retryAfterMs ?? null,
|
|
18
20
|
};
|
|
19
21
|
}
|
|
@@ -167,12 +169,13 @@ export class DrizzleFlowRepository {
|
|
|
167
169
|
id,
|
|
168
170
|
flowId,
|
|
169
171
|
name: state.name,
|
|
170
|
-
agentRole: null,
|
|
172
|
+
agentRole: state.agentRole || null,
|
|
171
173
|
modelTier: state.modelTier ?? null,
|
|
172
174
|
mode: state.mode ?? "passive",
|
|
173
175
|
promptTemplate: state.promptTemplate ?? null,
|
|
174
176
|
constraints: (state.constraints ?? null),
|
|
175
177
|
onEnter: (state.onEnter ?? null),
|
|
178
|
+
onExit: (state.onExit ?? null),
|
|
176
179
|
retryAfterMs: state.retryAfterMs ?? null,
|
|
177
180
|
};
|
|
178
181
|
this.db.transaction((tx) => {
|
|
@@ -188,6 +191,8 @@ export class DrizzleFlowRepository {
|
|
|
188
191
|
const updateValues = {};
|
|
189
192
|
if (changes.name !== undefined)
|
|
190
193
|
updateValues.name = changes.name;
|
|
194
|
+
if (changes.agentRole !== undefined)
|
|
195
|
+
updateValues.agentRole = changes.agentRole;
|
|
191
196
|
if (changes.modelTier !== undefined)
|
|
192
197
|
updateValues.modelTier = changes.modelTier;
|
|
193
198
|
if (changes.mode !== undefined)
|
|
@@ -198,6 +203,8 @@ export class DrizzleFlowRepository {
|
|
|
198
203
|
updateValues.constraints = changes.constraints;
|
|
199
204
|
if (changes.onEnter !== undefined)
|
|
200
205
|
updateValues.onEnter = changes.onEnter;
|
|
206
|
+
if (changes.onExit !== undefined)
|
|
207
|
+
updateValues.onExit = changes.onExit;
|
|
201
208
|
if (changes.retryAfterMs !== undefined)
|
|
202
209
|
updateValues.retryAfterMs = changes.retryAfterMs;
|
|
203
210
|
if (Object.keys(updateValues).length > 0) {
|
|
@@ -329,11 +336,13 @@ export class DrizzleFlowRepository {
|
|
|
329
336
|
id: s.id,
|
|
330
337
|
flowId,
|
|
331
338
|
name: s.name,
|
|
339
|
+
agentRole: s.agentRole || null,
|
|
332
340
|
modelTier: s.modelTier,
|
|
333
341
|
mode: s.mode ?? "passive",
|
|
334
342
|
promptTemplate: s.promptTemplate,
|
|
335
343
|
constraints: s.constraints,
|
|
336
344
|
onEnter: (s.onEnter ?? null),
|
|
345
|
+
onExit: (s.onExit ?? null),
|
|
337
346
|
retryAfterMs: s.retryAfterMs ?? null,
|
|
338
347
|
})
|
|
339
348
|
.run();
|
|
@@ -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,
|
|
@@ -498,6 +498,23 @@ export declare const stateDefinitions: import("drizzle-orm/sqlite-core").SQLiteT
|
|
|
498
498
|
identity: undefined;
|
|
499
499
|
generated: undefined;
|
|
500
500
|
}, {}, {}>;
|
|
501
|
+
onExit: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
502
|
+
name: "on_exit";
|
|
503
|
+
tableName: "state_definitions";
|
|
504
|
+
dataType: "json";
|
|
505
|
+
columnType: "SQLiteTextJson";
|
|
506
|
+
data: unknown;
|
|
507
|
+
driverParam: string;
|
|
508
|
+
notNull: false;
|
|
509
|
+
hasDefault: false;
|
|
510
|
+
isPrimaryKey: false;
|
|
511
|
+
isAutoincrement: false;
|
|
512
|
+
hasRuntimeDefault: false;
|
|
513
|
+
enumValues: undefined;
|
|
514
|
+
baseColumn: never;
|
|
515
|
+
identity: undefined;
|
|
516
|
+
generated: undefined;
|
|
517
|
+
}, {}, {}>;
|
|
501
518
|
retryAfterMs: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
502
519
|
name: "retry_after_ms";
|
|
503
520
|
tableName: "state_definitions";
|
|
@@ -689,6 +706,23 @@ export declare const gateDefinitions: import("drizzle-orm/sqlite-core").SQLiteTa
|
|
|
689
706
|
}, {}, {
|
|
690
707
|
length: number | undefined;
|
|
691
708
|
}>;
|
|
709
|
+
outcomes: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
710
|
+
name: "outcomes";
|
|
711
|
+
tableName: "gate_definitions";
|
|
712
|
+
dataType: "json";
|
|
713
|
+
columnType: "SQLiteTextJson";
|
|
714
|
+
data: unknown;
|
|
715
|
+
driverParam: string;
|
|
716
|
+
notNull: false;
|
|
717
|
+
hasDefault: false;
|
|
718
|
+
isPrimaryKey: false;
|
|
719
|
+
isAutoincrement: false;
|
|
720
|
+
hasRuntimeDefault: false;
|
|
721
|
+
enumValues: undefined;
|
|
722
|
+
baseColumn: never;
|
|
723
|
+
identity: undefined;
|
|
724
|
+
generated: undefined;
|
|
725
|
+
}, {}, {}>;
|
|
692
726
|
};
|
|
693
727
|
dialect: "sqlite";
|
|
694
728
|
}>;
|
|
@@ -32,6 +32,7 @@ export const stateDefinitions = sqliteTable("state_definitions", {
|
|
|
32
32
|
promptTemplate: text("prompt_template"),
|
|
33
33
|
constraints: text("constraints", { mode: "json" }),
|
|
34
34
|
onEnter: text("on_enter", { mode: "json" }),
|
|
35
|
+
onExit: text("on_exit", { mode: "json" }),
|
|
35
36
|
retryAfterMs: integer("retry_after_ms"),
|
|
36
37
|
}, (table) => ({
|
|
37
38
|
flowNameUnique: uniqueIndex("state_definitions_flow_name_unique").on(table.flowId, table.name),
|
|
@@ -46,6 +47,7 @@ export const gateDefinitions = sqliteTable("gate_definitions", {
|
|
|
46
47
|
timeoutMs: integer("timeout_ms"),
|
|
47
48
|
failurePrompt: text("failure_prompt"),
|
|
48
49
|
timeoutPrompt: text("timeout_prompt"),
|
|
50
|
+
outcomes: text("outcomes", { mode: "json" }),
|
|
49
51
|
});
|
|
50
52
|
export const transitionRules = sqliteTable("transition_rules", {
|
|
51
53
|
id: text("id").primaryKey(),
|
|
@@ -12,6 +12,11 @@ export interface OnEnterConfig {
|
|
|
12
12
|
artifacts: string[];
|
|
13
13
|
timeout_ms?: number;
|
|
14
14
|
}
|
|
15
|
+
/** Configuration for running a cleanup command when an entity exits a state */
|
|
16
|
+
export interface OnExitConfig {
|
|
17
|
+
command: string;
|
|
18
|
+
timeout_ms?: number;
|
|
19
|
+
}
|
|
15
20
|
/** Invocation execution mode */
|
|
16
21
|
export type Mode = "active" | "passive";
|
|
17
22
|
/** Runtime entity tracked through a flow */
|
|
@@ -36,6 +41,7 @@ export interface Invocation {
|
|
|
36
41
|
id: string;
|
|
37
42
|
entityId: string;
|
|
38
43
|
stage: string;
|
|
44
|
+
agentRole: string | null;
|
|
39
45
|
mode: Mode;
|
|
40
46
|
prompt: string;
|
|
41
47
|
context: Record<string, unknown> | null;
|
|
@@ -82,11 +88,14 @@ export interface State {
|
|
|
82
88
|
id: string;
|
|
83
89
|
flowId: string;
|
|
84
90
|
name: string;
|
|
91
|
+
/** Agent type identifier — maps to an agent MD file (e.g. "wopr-architect" → ~/.claude/agents/wopr-architect.md). */
|
|
92
|
+
agentRole: string | null;
|
|
85
93
|
modelTier: string | null;
|
|
86
94
|
mode: Mode;
|
|
87
95
|
promptTemplate: string | null;
|
|
88
96
|
constraints: Record<string, unknown> | null;
|
|
89
97
|
onEnter: OnEnterConfig | null;
|
|
98
|
+
onExit: OnExitConfig | null;
|
|
90
99
|
/** Override check_back delay for workers claiming this state. Falls back to Flow.claimRetryAfterMs. */
|
|
91
100
|
retryAfterMs: number | null;
|
|
92
101
|
}
|
|
@@ -115,6 +124,13 @@ export interface Gate {
|
|
|
115
124
|
timeoutMs: number | null;
|
|
116
125
|
failurePrompt: string | null;
|
|
117
126
|
timeoutPrompt: string | null;
|
|
127
|
+
/** Named outcome map from structured gate output. Keys are outcome names; values declare
|
|
128
|
+
* where the entity goes. `proceed: true` means the original transition continues.
|
|
129
|
+
* `toState` redirects to a different state. */
|
|
130
|
+
outcomes: Record<string, {
|
|
131
|
+
proceed?: boolean;
|
|
132
|
+
toState?: string;
|
|
133
|
+
}> | null;
|
|
118
134
|
}
|
|
119
135
|
/** A complete flow definition with its states and transitions */
|
|
120
136
|
export interface Flow {
|
|
@@ -170,11 +186,13 @@ export interface CreateFlowInput {
|
|
|
170
186
|
/** Input for adding a state to a flow */
|
|
171
187
|
export interface CreateStateInput {
|
|
172
188
|
name: string;
|
|
189
|
+
agentRole?: string;
|
|
173
190
|
modelTier?: string;
|
|
174
191
|
mode?: Mode;
|
|
175
192
|
promptTemplate?: string;
|
|
176
193
|
constraints?: Record<string, unknown>;
|
|
177
194
|
onEnter?: OnEnterConfig;
|
|
195
|
+
onExit?: OnExitConfig;
|
|
178
196
|
retryAfterMs?: number;
|
|
179
197
|
}
|
|
180
198
|
/** Input for adding a transition rule */
|
|
@@ -198,6 +216,10 @@ export interface CreateGateInput {
|
|
|
198
216
|
timeoutMs?: number;
|
|
199
217
|
failurePrompt?: string;
|
|
200
218
|
timeoutPrompt?: string;
|
|
219
|
+
outcomes?: Record<string, {
|
|
220
|
+
proceed?: boolean;
|
|
221
|
+
toState?: string;
|
|
222
|
+
}>;
|
|
201
223
|
}
|
|
202
224
|
/** Data-access contract for entity lifecycle operations. */
|
|
203
225
|
export interface IEntityRepository {
|
|
@@ -274,7 +296,7 @@ export interface IFlowRepository {
|
|
|
274
296
|
/** Data-access contract for invocation lifecycle and claiming. */
|
|
275
297
|
export interface IInvocationRepository {
|
|
276
298
|
/** Create a new invocation for an entity at a given stage. */
|
|
277
|
-
create(entityId: string, stage: string, prompt: string, mode: Mode, ttlMs
|
|
299
|
+
create(entityId: string, stage: string, prompt: string, mode: Mode, ttlMs: number | undefined, context: Record<string, unknown> | undefined, agentRole: string | null): Promise<Invocation>;
|
|
278
300
|
/** Get an invocation by ID, or null if not found. */
|
|
279
301
|
get(id: string): Promise<Invocation | null>;
|
|
280
302
|
/** 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 +351,7 @@ export interface IGateRepository {
|
|
|
329
351
|
/** Get all gate results for a given entity. */
|
|
330
352
|
resultsFor(entityId: string): Promise<GateResult[]>;
|
|
331
353
|
/** Update mutable fields on a gate definition. */
|
|
332
|
-
update(id: string, changes: Partial<Pick<Gate, "command" | "functionRef" | "apiConfig" | "timeoutMs" | "failurePrompt" | "timeoutPrompt">>): Promise<Gate>;
|
|
354
|
+
update(id: string, changes: Partial<Pick<Gate, "command" | "functionRef" | "apiConfig" | "timeoutMs" | "failurePrompt" | "timeoutPrompt" | "outcomes">>): Promise<Gate>;
|
|
333
355
|
/** Delete the gate result for a specific entity+gate combination. */
|
|
334
356
|
clearResult(entityId: string, gateId: string): Promise<void>;
|
|
335
357
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ALTER TABLE `gate_definitions` ADD `outcomes` text;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ALTER TABLE `state_definitions` ADD `on_exit` text;
|