@wopr-network/defcon 0.2.0 → 0.2.1

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.
Files changed (102) hide show
  1. package/dist/src/execution/cli.js +0 -0
  2. package/package.json +3 -2
  3. package/dist/api/router.d.ts +0 -24
  4. package/dist/api/router.js +0 -44
  5. package/dist/api/server.d.ts +0 -13
  6. package/dist/api/server.js +0 -280
  7. package/dist/api/wire-types.d.ts +0 -46
  8. package/dist/api/wire-types.js +0 -5
  9. package/dist/config/db-path.d.ts +0 -1
  10. package/dist/config/db-path.js +0 -1
  11. package/dist/config/exporter.d.ts +0 -3
  12. package/dist/config/exporter.js +0 -87
  13. package/dist/config/index.d.ts +0 -4
  14. package/dist/config/index.js +0 -4
  15. package/dist/config/seed-loader.d.ts +0 -10
  16. package/dist/config/seed-loader.js +0 -108
  17. package/dist/config/zod-schemas.d.ts +0 -165
  18. package/dist/config/zod-schemas.js +0 -283
  19. package/dist/cors.d.ts +0 -8
  20. package/dist/cors.js +0 -21
  21. package/dist/engine/constants.d.ts +0 -1
  22. package/dist/engine/constants.js +0 -1
  23. package/dist/engine/engine.d.ts +0 -69
  24. package/dist/engine/engine.js +0 -485
  25. package/dist/engine/event-emitter.d.ts +0 -9
  26. package/dist/engine/event-emitter.js +0 -19
  27. package/dist/engine/event-types.d.ts +0 -105
  28. package/dist/engine/event-types.js +0 -1
  29. package/dist/engine/flow-spawner.d.ts +0 -8
  30. package/dist/engine/flow-spawner.js +0 -28
  31. package/dist/engine/gate-command-validator.d.ts +0 -6
  32. package/dist/engine/gate-command-validator.js +0 -46
  33. package/dist/engine/gate-evaluator.d.ts +0 -12
  34. package/dist/engine/gate-evaluator.js +0 -233
  35. package/dist/engine/handlebars.d.ts +0 -9
  36. package/dist/engine/handlebars.js +0 -51
  37. package/dist/engine/index.d.ts +0 -12
  38. package/dist/engine/index.js +0 -7
  39. package/dist/engine/invocation-builder.d.ts +0 -18
  40. package/dist/engine/invocation-builder.js +0 -58
  41. package/dist/engine/on-enter.d.ts +0 -8
  42. package/dist/engine/on-enter.js +0 -102
  43. package/dist/engine/ssrf-guard.d.ts +0 -22
  44. package/dist/engine/ssrf-guard.js +0 -159
  45. package/dist/engine/state-machine.d.ts +0 -12
  46. package/dist/engine/state-machine.js +0 -74
  47. package/dist/execution/active-runner.d.ts +0 -45
  48. package/dist/execution/active-runner.js +0 -165
  49. package/dist/execution/admin-schemas.d.ts +0 -116
  50. package/dist/execution/admin-schemas.js +0 -125
  51. package/dist/execution/cli.d.ts +0 -57
  52. package/dist/execution/cli.js +0 -498
  53. package/dist/execution/handlers/admin.d.ts +0 -67
  54. package/dist/execution/handlers/admin.js +0 -200
  55. package/dist/execution/handlers/flow.d.ts +0 -25
  56. package/dist/execution/handlers/flow.js +0 -289
  57. package/dist/execution/handlers/query.d.ts +0 -31
  58. package/dist/execution/handlers/query.js +0 -64
  59. package/dist/execution/index.d.ts +0 -4
  60. package/dist/execution/index.js +0 -3
  61. package/dist/execution/mcp-helpers.d.ts +0 -42
  62. package/dist/execution/mcp-helpers.js +0 -23
  63. package/dist/execution/mcp-server.d.ts +0 -33
  64. package/dist/execution/mcp-server.js +0 -1020
  65. package/dist/execution/provision-worktree.d.ts +0 -16
  66. package/dist/execution/provision-worktree.js +0 -123
  67. package/dist/execution/tool-schemas.d.ts +0 -40
  68. package/dist/execution/tool-schemas.js +0 -44
  69. package/dist/logger.d.ts +0 -8
  70. package/dist/logger.js +0 -12
  71. package/dist/main.d.ts +0 -14
  72. package/dist/main.js +0 -28
  73. package/dist/repositories/drizzle/entity.repo.d.ts +0 -27
  74. package/dist/repositories/drizzle/entity.repo.js +0 -190
  75. package/dist/repositories/drizzle/event.repo.d.ts +0 -12
  76. package/dist/repositories/drizzle/event.repo.js +0 -24
  77. package/dist/repositories/drizzle/flow.repo.d.ts +0 -22
  78. package/dist/repositories/drizzle/flow.repo.js +0 -364
  79. package/dist/repositories/drizzle/gate.repo.d.ts +0 -16
  80. package/dist/repositories/drizzle/gate.repo.js +0 -98
  81. package/dist/repositories/drizzle/index.d.ts +0 -6
  82. package/dist/repositories/drizzle/index.js +0 -7
  83. package/dist/repositories/drizzle/invocation.repo.d.ts +0 -23
  84. package/dist/repositories/drizzle/invocation.repo.js +0 -199
  85. package/dist/repositories/drizzle/schema.d.ts +0 -1932
  86. package/dist/repositories/drizzle/schema.js +0 -155
  87. package/dist/repositories/drizzle/transition-log.repo.d.ts +0 -11
  88. package/dist/repositories/drizzle/transition-log.repo.js +0 -42
  89. package/dist/repositories/interfaces.d.ts +0 -321
  90. package/dist/repositories/interfaces.js +0 -2
  91. package/dist/utils/redact.d.ts +0 -2
  92. package/dist/utils/redact.js +0 -62
  93. package/gates/blocking-graph.d.ts +0 -26
  94. package/gates/blocking-graph.js +0 -102
  95. package/gates/test/bad-return-gate.d.ts +0 -1
  96. package/gates/test/bad-return-gate.js +0 -4
  97. package/gates/test/passing-gate.d.ts +0 -2
  98. package/gates/test/passing-gate.js +0 -3
  99. package/gates/test/slow-gate.d.ts +0 -2
  100. package/gates/test/slow-gate.js +0 -5
  101. package/gates/test/throwing-gate.d.ts +0 -1
  102. package/gates/test/throwing-gate.js +0 -3
@@ -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
- }
@@ -1,6 +0,0 @@
1
- export interface GateCommandValidation {
2
- valid: boolean;
3
- resolvedPath: string | null;
4
- error: string | null;
5
- }
6
- export declare function validateGateCommand(command: string): GateCommandValidation;