@wopr-network/defcon 0.2.0 → 0.2.2
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/execution/cli.js +0 -0
- package/package.json +3 -2
- package/dist/api/router.d.ts +0 -24
- package/dist/api/router.js +0 -44
- package/dist/api/server.d.ts +0 -13
- package/dist/api/server.js +0 -280
- package/dist/api/wire-types.d.ts +0 -46
- package/dist/api/wire-types.js +0 -5
- package/dist/config/db-path.d.ts +0 -1
- package/dist/config/db-path.js +0 -1
- package/dist/config/exporter.d.ts +0 -3
- package/dist/config/exporter.js +0 -87
- package/dist/config/index.d.ts +0 -4
- package/dist/config/index.js +0 -4
- package/dist/config/seed-loader.d.ts +0 -10
- package/dist/config/seed-loader.js +0 -108
- package/dist/config/zod-schemas.d.ts +0 -165
- package/dist/config/zod-schemas.js +0 -283
- package/dist/cors.d.ts +0 -8
- package/dist/cors.js +0 -21
- package/dist/engine/constants.d.ts +0 -1
- package/dist/engine/constants.js +0 -1
- package/dist/engine/engine.d.ts +0 -69
- package/dist/engine/engine.js +0 -485
- package/dist/engine/event-emitter.d.ts +0 -9
- package/dist/engine/event-emitter.js +0 -19
- package/dist/engine/event-types.d.ts +0 -105
- package/dist/engine/event-types.js +0 -1
- package/dist/engine/flow-spawner.d.ts +0 -8
- package/dist/engine/flow-spawner.js +0 -28
- package/dist/engine/gate-command-validator.d.ts +0 -6
- package/dist/engine/gate-command-validator.js +0 -46
- package/dist/engine/gate-evaluator.d.ts +0 -12
- package/dist/engine/gate-evaluator.js +0 -233
- package/dist/engine/handlebars.d.ts +0 -9
- package/dist/engine/handlebars.js +0 -51
- package/dist/engine/index.d.ts +0 -12
- package/dist/engine/index.js +0 -7
- package/dist/engine/invocation-builder.d.ts +0 -18
- package/dist/engine/invocation-builder.js +0 -58
- package/dist/engine/on-enter.d.ts +0 -8
- package/dist/engine/on-enter.js +0 -102
- package/dist/engine/ssrf-guard.d.ts +0 -22
- package/dist/engine/ssrf-guard.js +0 -159
- package/dist/engine/state-machine.d.ts +0 -12
- package/dist/engine/state-machine.js +0 -74
- package/dist/execution/active-runner.d.ts +0 -45
- package/dist/execution/active-runner.js +0 -165
- package/dist/execution/admin-schemas.d.ts +0 -116
- package/dist/execution/admin-schemas.js +0 -125
- package/dist/execution/cli.d.ts +0 -57
- package/dist/execution/cli.js +0 -498
- package/dist/execution/handlers/admin.d.ts +0 -67
- package/dist/execution/handlers/admin.js +0 -200
- package/dist/execution/handlers/flow.d.ts +0 -25
- package/dist/execution/handlers/flow.js +0 -289
- package/dist/execution/handlers/query.d.ts +0 -31
- package/dist/execution/handlers/query.js +0 -64
- package/dist/execution/index.d.ts +0 -4
- package/dist/execution/index.js +0 -3
- package/dist/execution/mcp-helpers.d.ts +0 -42
- package/dist/execution/mcp-helpers.js +0 -23
- package/dist/execution/mcp-server.d.ts +0 -33
- package/dist/execution/mcp-server.js +0 -1020
- package/dist/execution/provision-worktree.d.ts +0 -16
- package/dist/execution/provision-worktree.js +0 -123
- package/dist/execution/tool-schemas.d.ts +0 -40
- package/dist/execution/tool-schemas.js +0 -44
- package/dist/logger.d.ts +0 -8
- package/dist/logger.js +0 -12
- package/dist/main.d.ts +0 -14
- package/dist/main.js +0 -28
- package/dist/repositories/drizzle/entity.repo.d.ts +0 -27
- package/dist/repositories/drizzle/entity.repo.js +0 -190
- package/dist/repositories/drizzle/event.repo.d.ts +0 -12
- package/dist/repositories/drizzle/event.repo.js +0 -24
- package/dist/repositories/drizzle/flow.repo.d.ts +0 -22
- package/dist/repositories/drizzle/flow.repo.js +0 -364
- package/dist/repositories/drizzle/gate.repo.d.ts +0 -16
- package/dist/repositories/drizzle/gate.repo.js +0 -98
- package/dist/repositories/drizzle/index.d.ts +0 -6
- package/dist/repositories/drizzle/index.js +0 -7
- package/dist/repositories/drizzle/invocation.repo.d.ts +0 -23
- package/dist/repositories/drizzle/invocation.repo.js +0 -199
- package/dist/repositories/drizzle/schema.d.ts +0 -1932
- package/dist/repositories/drizzle/schema.js +0 -155
- package/dist/repositories/drizzle/transition-log.repo.d.ts +0 -11
- package/dist/repositories/drizzle/transition-log.repo.js +0 -42
- package/dist/repositories/interfaces.d.ts +0 -321
- package/dist/repositories/interfaces.js +0 -2
- package/dist/utils/redact.d.ts +0 -2
- package/dist/utils/redact.js +0 -62
- package/gates/blocking-graph.d.ts +0 -26
- package/gates/blocking-graph.js +0 -102
- package/gates/test/bad-return-gate.d.ts +0 -1
- package/gates/test/bad-return-gate.js +0 -4
- package/gates/test/passing-gate.d.ts +0 -2
- package/gates/test/passing-gate.js +0 -3
- package/gates/test/slow-gate.d.ts +0 -2
- package/gates/test/slow-gate.js +0 -5
- package/gates/test/throwing-gate.d.ts +0 -1
- package/gates/test/throwing-gate.js +0 -3
package/dist/engine/engine.js
DELETED
|
@@ -1,485 +0,0 @@
|
|
|
1
|
-
import { consoleLogger } from "../logger.js";
|
|
2
|
-
import { DEFAULT_TIMEOUT_PROMPT } from "./constants.js";
|
|
3
|
-
import { executeSpawn } from "./flow-spawner.js";
|
|
4
|
-
import { evaluateGate } from "./gate-evaluator.js";
|
|
5
|
-
import { getHandlebars } from "./handlebars.js";
|
|
6
|
-
import { buildInvocation } from "./invocation-builder.js";
|
|
7
|
-
import { executeOnEnter } from "./on-enter.js";
|
|
8
|
-
import { findTransition, isTerminal } from "./state-machine.js";
|
|
9
|
-
export class Engine {
|
|
10
|
-
entityRepo;
|
|
11
|
-
flowRepo;
|
|
12
|
-
invocationRepo;
|
|
13
|
-
gateRepo;
|
|
14
|
-
transitionLogRepo;
|
|
15
|
-
adapters;
|
|
16
|
-
eventEmitter;
|
|
17
|
-
logger;
|
|
18
|
-
constructor(deps) {
|
|
19
|
-
this.entityRepo = deps.entityRepo;
|
|
20
|
-
this.flowRepo = deps.flowRepo;
|
|
21
|
-
this.invocationRepo = deps.invocationRepo;
|
|
22
|
-
this.gateRepo = deps.gateRepo;
|
|
23
|
-
this.transitionLogRepo = deps.transitionLogRepo;
|
|
24
|
-
this.adapters = deps.adapters;
|
|
25
|
-
this.eventEmitter = deps.eventEmitter;
|
|
26
|
-
this.logger = deps.logger ?? consoleLogger;
|
|
27
|
-
}
|
|
28
|
-
async processSignal(entityId, signal, artifacts, triggeringInvocationId) {
|
|
29
|
-
// 1. Load entity
|
|
30
|
-
const entity = await this.entityRepo.get(entityId);
|
|
31
|
-
if (!entity)
|
|
32
|
-
throw new Error(`Entity "${entityId}" not found`);
|
|
33
|
-
// 2. Load flow
|
|
34
|
-
const flow = await this.flowRepo.get(entity.flowId);
|
|
35
|
-
if (!flow)
|
|
36
|
-
throw new Error(`Flow "${entity.flowId}" not found`);
|
|
37
|
-
// 3. Find transition
|
|
38
|
-
const transition = findTransition(flow, entity.state, signal, { entity }, true, this.logger);
|
|
39
|
-
if (!transition)
|
|
40
|
-
throw new Error(`No transition from "${entity.state}" on signal "${signal}" in flow "${flow.name}"`);
|
|
41
|
-
// 4. Evaluate gate if present
|
|
42
|
-
const gatesPassed = [];
|
|
43
|
-
if (transition.gateId) {
|
|
44
|
-
const gate = await this.gateRepo.get(transition.gateId);
|
|
45
|
-
if (!gate)
|
|
46
|
-
throw new Error(`Gate "${transition.gateId}" not found`);
|
|
47
|
-
const gateResult = await evaluateGate(gate, entity, this.gateRepo, flow.gateTimeoutMs);
|
|
48
|
-
if (!gateResult.passed) {
|
|
49
|
-
// Persist gate failure into entity artifacts for retry context
|
|
50
|
-
const priorFailures = Array.isArray(entity.artifacts?.gate_failures)
|
|
51
|
-
? entity.artifacts.gate_failures
|
|
52
|
-
: [];
|
|
53
|
-
await this.entityRepo.updateArtifacts(entityId, {
|
|
54
|
-
gate_failures: [
|
|
55
|
-
...priorFailures,
|
|
56
|
-
{
|
|
57
|
-
gateId: gate.id,
|
|
58
|
-
gateName: gate.name,
|
|
59
|
-
output: gateResult.output,
|
|
60
|
-
failedAt: new Date().toISOString(),
|
|
61
|
-
},
|
|
62
|
-
],
|
|
63
|
-
});
|
|
64
|
-
if (gateResult.timedOut) {
|
|
65
|
-
await this.eventEmitter.emit({
|
|
66
|
-
type: "gate.timedOut",
|
|
67
|
-
entityId,
|
|
68
|
-
gateId: gate.id,
|
|
69
|
-
emittedAt: new Date(),
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
else {
|
|
73
|
-
await this.eventEmitter.emit({
|
|
74
|
-
type: "gate.failed",
|
|
75
|
-
entityId,
|
|
76
|
-
gateId: gate.id,
|
|
77
|
-
emittedAt: new Date(),
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
let resolvedTimeoutPrompt;
|
|
81
|
-
if (gateResult.timedOut) {
|
|
82
|
-
const rawTemplate = gate.timeoutPrompt ?? flow.timeoutPrompt ?? DEFAULT_TIMEOUT_PROMPT;
|
|
83
|
-
try {
|
|
84
|
-
const hbs = getHandlebars();
|
|
85
|
-
const template = hbs.compile(rawTemplate);
|
|
86
|
-
resolvedTimeoutPrompt = template({
|
|
87
|
-
entity,
|
|
88
|
-
flow,
|
|
89
|
-
gate: { name: gate.name, output: gateResult.output },
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
catch (err) {
|
|
93
|
-
this.logger.error("[engine] Failed to render timeoutPrompt template:", err);
|
|
94
|
-
resolvedTimeoutPrompt = DEFAULT_TIMEOUT_PROMPT;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return {
|
|
98
|
-
gated: true,
|
|
99
|
-
gateTimedOut: gateResult.timedOut,
|
|
100
|
-
gateOutput: gateResult.output,
|
|
101
|
-
gateName: gate.name,
|
|
102
|
-
failurePrompt: gate.failurePrompt ?? undefined,
|
|
103
|
-
timeoutPrompt: resolvedTimeoutPrompt,
|
|
104
|
-
gatesPassed,
|
|
105
|
-
terminal: false,
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
gatesPassed.push(gate.name);
|
|
109
|
-
await this.eventEmitter.emit({
|
|
110
|
-
type: "gate.passed",
|
|
111
|
-
entityId,
|
|
112
|
-
gateId: gate.id,
|
|
113
|
-
emittedAt: new Date(),
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
// 5. Transition entity
|
|
117
|
-
let updated = await this.entityRepo.transition(entityId, transition.toState, signal, artifacts);
|
|
118
|
-
// Clear gate_failures on successful transition so stale failures don't bleed into future agent prompts
|
|
119
|
-
await this.entityRepo.updateArtifacts(entityId, { gate_failures: [] });
|
|
120
|
-
// Keep the in-memory entity in sync so buildInvocation sees the cleared failures
|
|
121
|
-
updated = { ...updated, artifacts: { ...updated.artifacts, gate_failures: [] } };
|
|
122
|
-
// 6. Emit transition event
|
|
123
|
-
await this.eventEmitter.emit({
|
|
124
|
-
type: "entity.transitioned",
|
|
125
|
-
entityId,
|
|
126
|
-
flowId: flow.id,
|
|
127
|
-
fromState: entity.state,
|
|
128
|
-
toState: transition.toState,
|
|
129
|
-
trigger: signal,
|
|
130
|
-
emittedAt: new Date(),
|
|
131
|
-
});
|
|
132
|
-
const result = {
|
|
133
|
-
newState: transition.toState,
|
|
134
|
-
gatesPassed,
|
|
135
|
-
gated: false,
|
|
136
|
-
terminal: false,
|
|
137
|
-
};
|
|
138
|
-
// 6b. Execute onEnter hook if defined on the new state
|
|
139
|
-
const newStateDef = flow.states.find((s) => s.name === transition.toState);
|
|
140
|
-
if (newStateDef?.onEnter) {
|
|
141
|
-
const onEnterResult = await executeOnEnter(newStateDef.onEnter, updated, this.entityRepo);
|
|
142
|
-
if (onEnterResult.skipped) {
|
|
143
|
-
await this.eventEmitter.emit({
|
|
144
|
-
type: "onEnter.skipped",
|
|
145
|
-
entityId,
|
|
146
|
-
state: transition.toState,
|
|
147
|
-
emittedAt: new Date(),
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
else if (onEnterResult.error) {
|
|
151
|
-
await this.eventEmitter.emit({
|
|
152
|
-
type: "onEnter.failed",
|
|
153
|
-
entityId,
|
|
154
|
-
state: transition.toState,
|
|
155
|
-
error: onEnterResult.error,
|
|
156
|
-
emittedAt: new Date(),
|
|
157
|
-
});
|
|
158
|
-
await this.transitionLogRepo.record({
|
|
159
|
-
entityId,
|
|
160
|
-
fromState: entity.state,
|
|
161
|
-
toState: transition.toState,
|
|
162
|
-
trigger: signal,
|
|
163
|
-
invocationId: triggeringInvocationId ?? null,
|
|
164
|
-
timestamp: new Date(),
|
|
165
|
-
});
|
|
166
|
-
return {
|
|
167
|
-
newState: transition.toState,
|
|
168
|
-
gatesPassed,
|
|
169
|
-
gated: false,
|
|
170
|
-
onEnterFailed: true,
|
|
171
|
-
gateOutput: onEnterResult.error,
|
|
172
|
-
terminal: false,
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
else {
|
|
176
|
-
await this.eventEmitter.emit({
|
|
177
|
-
type: "onEnter.completed",
|
|
178
|
-
entityId,
|
|
179
|
-
state: transition.toState,
|
|
180
|
-
artifacts: onEnterResult.artifacts ?? {},
|
|
181
|
-
emittedAt: new Date(),
|
|
182
|
-
});
|
|
183
|
-
// Refresh entity so invocation builder sees new artifacts
|
|
184
|
-
const refreshed = await this.entityRepo.get(entityId);
|
|
185
|
-
if (refreshed) {
|
|
186
|
-
updated = refreshed;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
// 7. Create invocation if new state has a prompt template
|
|
191
|
-
if (newStateDef?.promptTemplate) {
|
|
192
|
-
const canCreate = await this.checkConcurrency(flow, entity);
|
|
193
|
-
if (canCreate) {
|
|
194
|
-
const [invocations, gateResults] = await Promise.all([
|
|
195
|
-
this.invocationRepo.findByEntity(updated.id),
|
|
196
|
-
this.gateRepo.resultsFor(updated.id),
|
|
197
|
-
]);
|
|
198
|
-
const enriched = { ...updated, invocations, gateResults };
|
|
199
|
-
const build = await buildInvocation(newStateDef, enriched, this.adapters, flow, this.logger);
|
|
200
|
-
const invocation = await this.invocationRepo.create(entityId, transition.toState, build.prompt, build.mode, undefined, build.systemPrompt || build.userContent
|
|
201
|
-
? { systemPrompt: build.systemPrompt, userContent: build.userContent }
|
|
202
|
-
: undefined);
|
|
203
|
-
result.invocationId = invocation.id;
|
|
204
|
-
await this.eventEmitter.emit({
|
|
205
|
-
type: "invocation.created",
|
|
206
|
-
entityId,
|
|
207
|
-
invocationId: invocation.id,
|
|
208
|
-
stage: transition.toState,
|
|
209
|
-
emittedAt: new Date(),
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
// 8. Record transition log with the TRIGGERING invocation (the one that reported the signal).
|
|
214
|
-
// The next invocation (result.invocationId) is already recorded in the invocations table.
|
|
215
|
-
await this.transitionLogRepo.record({
|
|
216
|
-
entityId,
|
|
217
|
-
fromState: entity.state,
|
|
218
|
-
toState: transition.toState,
|
|
219
|
-
trigger: signal,
|
|
220
|
-
invocationId: triggeringInvocationId ?? null,
|
|
221
|
-
timestamp: new Date(),
|
|
222
|
-
});
|
|
223
|
-
// 9. Spawn child flows
|
|
224
|
-
const spawned = await executeSpawn(transition, updated, this.flowRepo, this.entityRepo, this.logger);
|
|
225
|
-
if (spawned) {
|
|
226
|
-
result.spawned = [spawned.id];
|
|
227
|
-
await this.eventEmitter.emit({
|
|
228
|
-
type: "flow.spawned",
|
|
229
|
-
entityId,
|
|
230
|
-
flowId: flow.id,
|
|
231
|
-
spawnedFlowId: spawned.flowId,
|
|
232
|
-
emittedAt: new Date(),
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
// 10. Mark terminal — no invocation is created for terminal states (handled above),
|
|
236
|
-
// but we surface terminality in the result for callers.
|
|
237
|
-
if (isTerminal(flow, transition.toState)) {
|
|
238
|
-
result.terminal = true;
|
|
239
|
-
result.spawned = result.spawned ?? [];
|
|
240
|
-
}
|
|
241
|
-
return result;
|
|
242
|
-
}
|
|
243
|
-
async createEntity(flowName, refs) {
|
|
244
|
-
const flow = await this.flowRepo.getByName(flowName);
|
|
245
|
-
if (!flow)
|
|
246
|
-
throw new Error(`Flow "${flowName}" not found`);
|
|
247
|
-
const entity = await this.entityRepo.create(flow.id, flow.initialState, refs);
|
|
248
|
-
await this.eventEmitter.emit({
|
|
249
|
-
type: "entity.created",
|
|
250
|
-
entityId: entity.id,
|
|
251
|
-
flowId: flow.id,
|
|
252
|
-
payload: { refs: refs ?? null },
|
|
253
|
-
emittedAt: new Date(),
|
|
254
|
-
});
|
|
255
|
-
// Execute onEnter hook if defined on initial state
|
|
256
|
-
const initialState = flow.states.find((s) => s.name === flow.initialState);
|
|
257
|
-
if (initialState?.onEnter) {
|
|
258
|
-
const onEnterResult = await executeOnEnter(initialState.onEnter, entity, this.entityRepo);
|
|
259
|
-
if (onEnterResult.error) {
|
|
260
|
-
await this.eventEmitter.emit({
|
|
261
|
-
type: "onEnter.failed",
|
|
262
|
-
entityId: entity.id,
|
|
263
|
-
state: flow.initialState,
|
|
264
|
-
error: onEnterResult.error,
|
|
265
|
-
emittedAt: new Date(),
|
|
266
|
-
});
|
|
267
|
-
throw new Error(`onEnter failed for entity ${entity.id}: ${onEnterResult.error}`);
|
|
268
|
-
}
|
|
269
|
-
if (onEnterResult.artifacts) {
|
|
270
|
-
await this.eventEmitter.emit({
|
|
271
|
-
type: "onEnter.completed",
|
|
272
|
-
entityId: entity.id,
|
|
273
|
-
state: flow.initialState,
|
|
274
|
-
artifacts: onEnterResult.artifacts,
|
|
275
|
-
emittedAt: new Date(),
|
|
276
|
-
});
|
|
277
|
-
const refreshed = await this.entityRepo.get(entity.id);
|
|
278
|
-
if (refreshed) {
|
|
279
|
-
Object.assign(entity, refreshed);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
// Create invocation if initial state has a prompt template
|
|
284
|
-
if (initialState?.promptTemplate) {
|
|
285
|
-
const [invocations, gateResults] = await Promise.all([
|
|
286
|
-
this.invocationRepo.findByEntity(entity.id),
|
|
287
|
-
this.gateRepo.resultsFor(entity.id),
|
|
288
|
-
]);
|
|
289
|
-
const enriched = { ...entity, invocations, gateResults };
|
|
290
|
-
const build = await buildInvocation(initialState, enriched, this.adapters, flow, this.logger);
|
|
291
|
-
await this.invocationRepo.create(entity.id, flow.initialState, build.prompt, build.mode, undefined, build.systemPrompt || build.userContent
|
|
292
|
-
? { systemPrompt: build.systemPrompt, userContent: build.userContent }
|
|
293
|
-
: undefined);
|
|
294
|
-
}
|
|
295
|
-
return entity;
|
|
296
|
-
}
|
|
297
|
-
async claimWork(role, flowName, worker_id) {
|
|
298
|
-
let flows;
|
|
299
|
-
if (flowName) {
|
|
300
|
-
const flow = await this.flowRepo.getByName(flowName);
|
|
301
|
-
// Validate discipline match — null discipline flows are claimable by any role
|
|
302
|
-
flows = flow && (flow.discipline === null || flow.discipline === role) ? [flow] : [];
|
|
303
|
-
}
|
|
304
|
-
else {
|
|
305
|
-
const allFlows = await this.flowRepo.listAll();
|
|
306
|
-
flows = allFlows.filter((f) => f.discipline === null || f.discipline === role);
|
|
307
|
-
}
|
|
308
|
-
for (const flow of flows) {
|
|
309
|
-
// Try affinity match first if worker_id provided
|
|
310
|
-
if (worker_id) {
|
|
311
|
-
const affinityUnclaimed = await this.invocationRepo.findUnclaimedWithAffinity(flow.id, role, worker_id);
|
|
312
|
-
for (const pending of affinityUnclaimed) {
|
|
313
|
-
const claimed = await this.entityRepo.claimById(pending.entityId, `agent:${role}`);
|
|
314
|
-
if (!claimed)
|
|
315
|
-
continue;
|
|
316
|
-
const result = await this.tryClaimInvocation(pending, claimed, flow, role, worker_id);
|
|
317
|
-
if (result)
|
|
318
|
-
return result;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
// Prefer claiming an existing unclaimed invocation created by processSignal
|
|
322
|
-
// to avoid creating a duplicate. Fall back to creating a new one if none exist.
|
|
323
|
-
const unclaimed = await this.invocationRepo.findUnclaimedByFlow(flow.id);
|
|
324
|
-
for (const pending of unclaimed) {
|
|
325
|
-
const claimed = await this.entityRepo.claim(flow.id, pending.stage, `agent:${role}`);
|
|
326
|
-
if (!claimed)
|
|
327
|
-
continue;
|
|
328
|
-
const result = await this.tryClaimInvocation(pending, claimed, flow, role, worker_id);
|
|
329
|
-
if (result)
|
|
330
|
-
return result;
|
|
331
|
-
}
|
|
332
|
-
// No pre-existing unclaimed invocations — claim entity directly and create invocation
|
|
333
|
-
const claimableStates = flow.states.filter((s) => !!s.promptTemplate);
|
|
334
|
-
for (const state of claimableStates) {
|
|
335
|
-
const claimed = await this.entityRepo.claim(flow.id, state.name, `agent:${role}`);
|
|
336
|
-
if (!claimed)
|
|
337
|
-
continue;
|
|
338
|
-
await this.setAffinityIfNeeded(claimed.id, flow, role, worker_id);
|
|
339
|
-
const build = await this.buildPrompt(state, claimed, flow);
|
|
340
|
-
const invocation = await this.invocationRepo.create(claimed.id, state.name, build.prompt, build.mode, undefined, build.systemPrompt || build.userContent
|
|
341
|
-
? { systemPrompt: build.systemPrompt, userContent: build.userContent }
|
|
342
|
-
: undefined);
|
|
343
|
-
return this.emitAndReturn(claimed, invocation.id, build, flow, role);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
return null;
|
|
347
|
-
}
|
|
348
|
-
/**
|
|
349
|
-
* Try to claim an existing unclaimed invocation for an already-claimed entity.
|
|
350
|
-
* Handles the race condition where another worker claims the invocation first
|
|
351
|
-
* (releases the entity claim and returns null so the caller can try the next candidate).
|
|
352
|
-
*/
|
|
353
|
-
async tryClaimInvocation(pending, claimed, flow, role, worker_id) {
|
|
354
|
-
const claimedInvocation = await this.invocationRepo.claim(pending.id, `agent:${role}`);
|
|
355
|
-
if (!claimedInvocation) {
|
|
356
|
-
try {
|
|
357
|
-
await this.entityRepo.release(claimed.id, `agent:${role}`);
|
|
358
|
-
}
|
|
359
|
-
catch (err) {
|
|
360
|
-
this.logger.error(`release() failed for entity ${claimed.id}:`, err);
|
|
361
|
-
}
|
|
362
|
-
return null;
|
|
363
|
-
}
|
|
364
|
-
await this.setAffinityIfNeeded(claimed.id, flow, role, worker_id);
|
|
365
|
-
const state = flow.states.find((s) => s.name === pending.stage);
|
|
366
|
-
const build = state ? await this.buildPrompt(state, claimed, flow) : { prompt: pending.prompt, context: null };
|
|
367
|
-
return this.emitAndReturn(claimed, claimedInvocation.id, build, flow, role);
|
|
368
|
-
}
|
|
369
|
-
async setAffinityIfNeeded(entityId, flow, role, worker_id) {
|
|
370
|
-
if (!worker_id)
|
|
371
|
-
return;
|
|
372
|
-
const affinityWindow = flow.affinityWindowMs ?? 300000;
|
|
373
|
-
try {
|
|
374
|
-
await this.entityRepo.setAffinity(entityId, worker_id, role, new Date(Date.now() + affinityWindow));
|
|
375
|
-
}
|
|
376
|
-
catch (err) {
|
|
377
|
-
this.logger.warn(`setAffinity failed for entity ${entityId} worker ${worker_id} — continuing:`, err);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
async buildPrompt(state, entity, flow) {
|
|
381
|
-
const [invocations, gateResults] = await Promise.all([
|
|
382
|
-
this.invocationRepo.findByEntity(entity.id),
|
|
383
|
-
this.gateRepo.resultsFor(entity.id),
|
|
384
|
-
]);
|
|
385
|
-
const enriched = { ...entity, invocations, gateResults };
|
|
386
|
-
return buildInvocation(state, enriched, this.adapters, flow, this.logger);
|
|
387
|
-
}
|
|
388
|
-
async emitAndReturn(entity, invocationId, build, flow, role) {
|
|
389
|
-
await this.eventEmitter.emit({
|
|
390
|
-
type: "entity.claimed",
|
|
391
|
-
entityId: entity.id,
|
|
392
|
-
flowId: flow.id,
|
|
393
|
-
agentId: `agent:${role}`,
|
|
394
|
-
emittedAt: new Date(),
|
|
395
|
-
});
|
|
396
|
-
return {
|
|
397
|
-
entityId: entity.id,
|
|
398
|
-
invocationId,
|
|
399
|
-
prompt: build.prompt,
|
|
400
|
-
context: build.context,
|
|
401
|
-
};
|
|
402
|
-
}
|
|
403
|
-
async getStatus() {
|
|
404
|
-
const allFlows = await this.flowRepo.listAll();
|
|
405
|
-
const statusData = {};
|
|
406
|
-
let activeInvocations = 0;
|
|
407
|
-
let pendingClaims = 0;
|
|
408
|
-
for (const flow of allFlows) {
|
|
409
|
-
const stateEntries = await Promise.all(flow.states.map(async (state) => {
|
|
410
|
-
const entities = await this.entityRepo.findByFlowAndState(flow.id, state.name);
|
|
411
|
-
return [state.name, entities.length];
|
|
412
|
-
}));
|
|
413
|
-
statusData[flow.id] = Object.fromEntries(stateEntries);
|
|
414
|
-
const [active, pending] = await Promise.all([
|
|
415
|
-
this.invocationRepo.countActiveByFlow(flow.id),
|
|
416
|
-
this.invocationRepo.countPendingByFlow(flow.id),
|
|
417
|
-
]);
|
|
418
|
-
activeInvocations += active;
|
|
419
|
-
pendingClaims += pending;
|
|
420
|
-
}
|
|
421
|
-
return { flows: statusData, activeInvocations, pendingClaims };
|
|
422
|
-
}
|
|
423
|
-
startReaper(intervalMs, entityTtlMs = 60_000) {
|
|
424
|
-
let tickInFlight = false;
|
|
425
|
-
let stopped = false;
|
|
426
|
-
const tick = async () => {
|
|
427
|
-
const expired = await this.invocationRepo.reapExpired();
|
|
428
|
-
for (const inv of expired) {
|
|
429
|
-
await this.eventEmitter.emit({
|
|
430
|
-
type: "invocation.expired",
|
|
431
|
-
entityId: inv.entityId,
|
|
432
|
-
invocationId: inv.id,
|
|
433
|
-
emittedAt: new Date(),
|
|
434
|
-
});
|
|
435
|
-
}
|
|
436
|
-
await this.entityRepo.reapExpired(entityTtlMs);
|
|
437
|
-
await this.entityRepo.clearExpiredAffinity();
|
|
438
|
-
};
|
|
439
|
-
let currentTickPromise = Promise.resolve();
|
|
440
|
-
const timer = setInterval(() => {
|
|
441
|
-
if (stopped || tickInFlight)
|
|
442
|
-
return;
|
|
443
|
-
tickInFlight = true;
|
|
444
|
-
currentTickPromise = tick()
|
|
445
|
-
.catch((err) => {
|
|
446
|
-
this.logger.error("[reaper] error:", err);
|
|
447
|
-
})
|
|
448
|
-
.finally(() => {
|
|
449
|
-
tickInFlight = false;
|
|
450
|
-
// Reset chain head so completed ticks don't accumulate in memory
|
|
451
|
-
currentTickPromise = Promise.resolve();
|
|
452
|
-
});
|
|
453
|
-
}, intervalMs);
|
|
454
|
-
return async () => {
|
|
455
|
-
stopped = true;
|
|
456
|
-
clearInterval(timer);
|
|
457
|
-
await currentTickPromise;
|
|
458
|
-
};
|
|
459
|
-
}
|
|
460
|
-
async checkConcurrency(flow, entity) {
|
|
461
|
-
if (flow.maxConcurrent <= 0 && flow.maxConcurrentPerRepo <= 0)
|
|
462
|
-
return true;
|
|
463
|
-
const allInvocations = await this.invocationRepo.findByFlow(flow.id);
|
|
464
|
-
// Count active AND pending (unclaimed, not yet started) invocations
|
|
465
|
-
const activeOrPending = allInvocations.filter((i) => !i.completedAt && !i.failedAt);
|
|
466
|
-
if (flow.maxConcurrent > 0 && activeOrPending.length >= flow.maxConcurrent)
|
|
467
|
-
return false;
|
|
468
|
-
if (flow.maxConcurrentPerRepo > 0 && entity.refs) {
|
|
469
|
-
// Identify invocations for entities sharing the same repo ref as this entity.
|
|
470
|
-
// Fetch each unique entity involved in active/pending invocations to compare refs.
|
|
471
|
-
const uniqueEntityIds = [...new Set(activeOrPending.map((i) => i.entityId))];
|
|
472
|
-
const peerEntities = await Promise.all(uniqueEntityIds.map((id) => this.entityRepo.get(id)));
|
|
473
|
-
const repoCount = peerEntities.filter((peer) => {
|
|
474
|
-
if (!peer?.refs || !entity.refs)
|
|
475
|
-
return false;
|
|
476
|
-
// Two entities share the same repo if any ref adapter+id pair matches
|
|
477
|
-
const peerRefs = peer.refs;
|
|
478
|
-
return Object.values(entity.refs).some((ref) => Object.values(peerRefs).some((peerRef) => peerRef.adapter === ref.adapter && peerRef.id === ref.id));
|
|
479
|
-
}).length;
|
|
480
|
-
if (repoCount >= flow.maxConcurrentPerRepo)
|
|
481
|
-
return false;
|
|
482
|
-
}
|
|
483
|
-
return true;
|
|
484
|
-
}
|
|
485
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { Logger } from "../logger.js";
|
|
2
|
-
import type { EngineEvent, IEventBusAdapter } from "./event-types.js";
|
|
3
|
-
export declare class EventEmitter implements IEventBusAdapter {
|
|
4
|
-
private adapters;
|
|
5
|
-
private logger;
|
|
6
|
-
constructor(logger?: Logger);
|
|
7
|
-
register(adapter: IEventBusAdapter): void;
|
|
8
|
-
emit(event: EngineEvent): Promise<void>;
|
|
9
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { consoleLogger } from "../logger.js";
|
|
2
|
-
export class EventEmitter {
|
|
3
|
-
adapters = [];
|
|
4
|
-
logger;
|
|
5
|
-
constructor(logger) {
|
|
6
|
-
this.logger = logger ?? consoleLogger;
|
|
7
|
-
}
|
|
8
|
-
register(adapter) {
|
|
9
|
-
this.adapters.push(adapter);
|
|
10
|
-
}
|
|
11
|
-
async emit(event) {
|
|
12
|
-
const results = await Promise.allSettled(this.adapters.map((a) => Promise.resolve().then(() => a.emit(event))));
|
|
13
|
-
for (const r of results) {
|
|
14
|
-
if (r.status === "rejected") {
|
|
15
|
-
this.logger.error("[EventEmitter] adapter error:", r.reason);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
}
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
/** Event emitted by the engine during state-machine operations. */
|
|
2
|
-
export type EngineEvent = {
|
|
3
|
-
type: "entity.created";
|
|
4
|
-
entityId: string;
|
|
5
|
-
flowId: string;
|
|
6
|
-
payload: Record<string, unknown>;
|
|
7
|
-
emittedAt: Date;
|
|
8
|
-
} | {
|
|
9
|
-
type: "entity.transitioned";
|
|
10
|
-
entityId: string;
|
|
11
|
-
flowId: string;
|
|
12
|
-
fromState: string;
|
|
13
|
-
toState: string;
|
|
14
|
-
trigger: string;
|
|
15
|
-
emittedAt: Date;
|
|
16
|
-
} | {
|
|
17
|
-
type: "entity.claimed";
|
|
18
|
-
entityId: string;
|
|
19
|
-
flowId: string;
|
|
20
|
-
agentId: string;
|
|
21
|
-
emittedAt: Date;
|
|
22
|
-
} | {
|
|
23
|
-
type: "entity.released";
|
|
24
|
-
entityId: string;
|
|
25
|
-
flowId: string;
|
|
26
|
-
emittedAt: Date;
|
|
27
|
-
} | {
|
|
28
|
-
type: "invocation.created";
|
|
29
|
-
entityId: string;
|
|
30
|
-
invocationId: string;
|
|
31
|
-
stage: string;
|
|
32
|
-
emittedAt: Date;
|
|
33
|
-
} | {
|
|
34
|
-
type: "invocation.claimed";
|
|
35
|
-
entityId: string;
|
|
36
|
-
invocationId: string;
|
|
37
|
-
agentId: string;
|
|
38
|
-
emittedAt: Date;
|
|
39
|
-
} | {
|
|
40
|
-
type: "invocation.completed";
|
|
41
|
-
entityId: string;
|
|
42
|
-
invocationId: string;
|
|
43
|
-
signal: string;
|
|
44
|
-
emittedAt: Date;
|
|
45
|
-
} | {
|
|
46
|
-
type: "invocation.failed";
|
|
47
|
-
entityId: string;
|
|
48
|
-
invocationId: string;
|
|
49
|
-
error: string;
|
|
50
|
-
emittedAt: Date;
|
|
51
|
-
} | {
|
|
52
|
-
type: "invocation.expired";
|
|
53
|
-
entityId: string;
|
|
54
|
-
invocationId: string;
|
|
55
|
-
emittedAt: Date;
|
|
56
|
-
} | {
|
|
57
|
-
type: "gate.passed";
|
|
58
|
-
entityId: string;
|
|
59
|
-
gateId: string;
|
|
60
|
-
emittedAt: Date;
|
|
61
|
-
} | {
|
|
62
|
-
type: "gate.failed";
|
|
63
|
-
entityId: string;
|
|
64
|
-
gateId: string;
|
|
65
|
-
emittedAt: Date;
|
|
66
|
-
} | {
|
|
67
|
-
type: "gate.timedOut";
|
|
68
|
-
entityId: string;
|
|
69
|
-
gateId: string;
|
|
70
|
-
emittedAt: Date;
|
|
71
|
-
} | {
|
|
72
|
-
type: "flow.spawned";
|
|
73
|
-
entityId: string;
|
|
74
|
-
flowId: string;
|
|
75
|
-
spawnedFlowId: string;
|
|
76
|
-
emittedAt: Date;
|
|
77
|
-
} | {
|
|
78
|
-
type: "definition.changed";
|
|
79
|
-
flowId: string;
|
|
80
|
-
tool: string;
|
|
81
|
-
payload: Record<string, unknown>;
|
|
82
|
-
emittedAt: Date;
|
|
83
|
-
} | {
|
|
84
|
-
type: "onEnter.completed";
|
|
85
|
-
entityId: string;
|
|
86
|
-
state: string;
|
|
87
|
-
artifacts: Record<string, unknown>;
|
|
88
|
-
emittedAt: Date;
|
|
89
|
-
} | {
|
|
90
|
-
type: "onEnter.failed";
|
|
91
|
-
entityId: string;
|
|
92
|
-
state: string;
|
|
93
|
-
error: string;
|
|
94
|
-
emittedAt: Date;
|
|
95
|
-
} | {
|
|
96
|
-
type: "onEnter.skipped";
|
|
97
|
-
entityId: string;
|
|
98
|
-
state: string;
|
|
99
|
-
emittedAt: Date;
|
|
100
|
-
};
|
|
101
|
-
/** Adapter for broadcasting engine events to external systems. */
|
|
102
|
-
export interface IEventBusAdapter {
|
|
103
|
-
/** Emit an engine event to subscribed listeners. */
|
|
104
|
-
emit(event: EngineEvent): Promise<void>;
|
|
105
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { Logger } from "../logger.js";
|
|
2
|
-
import type { Entity, IEntityRepository, IFlowRepository, Transition } from "../repositories/interfaces.js";
|
|
3
|
-
/**
|
|
4
|
-
* If the transition has a spawnFlow, look up that flow and create a new entity in it.
|
|
5
|
-
* The spawned entity inherits the parent entity's refs.
|
|
6
|
-
* Returns the spawned entity, or null if no spawn is configured.
|
|
7
|
-
*/
|
|
8
|
-
export declare function executeSpawn(transition: Transition, parentEntity: Entity, flowRepo: IFlowRepository, entityRepo: IEntityRepository, logger?: Logger): Promise<Entity | null>;
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { consoleLogger } from "../logger.js";
|
|
2
|
-
/**
|
|
3
|
-
* If the transition has a spawnFlow, look up that flow and create a new entity in it.
|
|
4
|
-
* The spawned entity inherits the parent entity's refs.
|
|
5
|
-
* Returns the spawned entity, or null if no spawn is configured.
|
|
6
|
-
*/
|
|
7
|
-
export async function executeSpawn(transition, parentEntity, flowRepo, entityRepo, logger = consoleLogger) {
|
|
8
|
-
if (!transition.spawnFlow)
|
|
9
|
-
return null;
|
|
10
|
-
const flow = await flowRepo.getByName(transition.spawnFlow);
|
|
11
|
-
if (!flow)
|
|
12
|
-
throw new Error(`Spawn flow "${transition.spawnFlow}" not found`);
|
|
13
|
-
const childEntity = await entityRepo.create(flow.id, flow.initialState, parentEntity.refs ?? undefined);
|
|
14
|
-
try {
|
|
15
|
-
await entityRepo.appendSpawnedChild(parentEntity.id, {
|
|
16
|
-
childId: childEntity.id,
|
|
17
|
-
childFlow: transition.spawnFlow,
|
|
18
|
-
spawnedAt: new Date().toISOString(),
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
catch (err) {
|
|
22
|
-
// Log orphan so it can be manually cleaned up.
|
|
23
|
-
// The child entity is real and functional; only parent artifact bookkeeping failed.
|
|
24
|
-
logger.error(`[flow-spawner] ORPHAN child entity ${childEntity.id} (flow: ${transition.spawnFlow}) — ` +
|
|
25
|
-
`failed to register on parent ${parentEntity.id}: ${String(err)}`);
|
|
26
|
-
}
|
|
27
|
-
return childEntity;
|
|
28
|
-
}
|