@nt-ai-lab/deterministic-agent-workflow-engine 0.3.4 → 0.3.6

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/index.d.ts CHANGED
@@ -8,8 +8,10 @@ export { reflectionCategorySchema, reflectionConfidenceSchema, reflectionEvidenc
8
8
  export type { ReflectionCategory, ReflectionEvidence, ReflectionFinding, ReflectionPayload, RecordReflectionInput, StoredReflection, } from './platform/domain/reflection-types';
9
9
  export { reviewTypeSchema, reviewVerdictSchema, reviewFindingSeveritySchema, reviewFindingStatusSchema, reviewFindingSchema, reviewPayloadSchema, recordReviewInputSchema, storedReviewSchema, listedReviewSchema, reviewFiltersSchema, } from './platform/domain/review-types';
10
10
  export type { ReviewType, ReviewVerdict, ReviewFindingSeverity, ReviewFindingStatus, ReviewFinding, ReviewPayload, RecordReviewInput, StoredReview, ListedReview, ReviewFilters, } from './platform/domain/review-types';
11
- export { engineEventSchema } from './platform/domain/engine-events';
11
+ export { engineEventSchema, reviewRecordedEventSchema, } from './platform/domain/engine-events';
12
12
  export type { EngineEvent, SessionStartedEvent, TransitionedEvent, AgentRegisteredEvent, AgentShutDownEvent, JournalEntryEvent, WriteCheckedEvent, BashCheckedEvent, PluginReadCheckedEvent, IdleCheckedEvent, IdentityVerifiedEvent, ContextRequestedEvent, ReviewRecordedEvent, } from './platform/domain/engine-events';
13
+ export { isPlatformOwnedEventExcludedFromWorkflowState } from './platform/domain/engine-events';
14
+ export { reduceWorkflowStateFromStoredEvents } from './platform/domain/workflow-state-reducer';
13
15
  export { repositoryMetadataEventSchema } from './platform/domain/repository-tracking-events';
14
16
  export type { DomainMetadataEvent, IssueRecordedEvent, BranchRecordedEvent, PrRecordedEvent, } from './platform/domain/repository-tracking-events';
15
17
  export { checkBashCommand } from './platform/domain/bash-enforcement';
package/dist/index.js CHANGED
@@ -3,7 +3,9 @@ export { baseEventSchema } from './platform/domain/base-event.js';
3
3
  export { flattenStoredEvent, stripEnvelopeKeys, toPayload, } from './platform/domain/stored-event.js';
4
4
  export { reflectionCategorySchema, reflectionConfidenceSchema, reflectionEvidenceSchema, reflectionFindingSchema, reflectionPayloadSchema, recordReflectionInputSchema, storedReflectionSchema, } from './platform/domain/reflection-types.js';
5
5
  export { reviewTypeSchema, reviewVerdictSchema, reviewFindingSeveritySchema, reviewFindingStatusSchema, reviewFindingSchema, reviewPayloadSchema, recordReviewInputSchema, storedReviewSchema, listedReviewSchema, reviewFiltersSchema, } from './platform/domain/review-types.js';
6
- export { engineEventSchema } from './platform/domain/engine-events.js';
6
+ export { engineEventSchema, reviewRecordedEventSchema, } from './platform/domain/engine-events.js';
7
+ export { isPlatformOwnedEventExcludedFromWorkflowState } from './platform/domain/engine-events.js';
8
+ export { reduceWorkflowStateFromStoredEvents } from './platform/domain/workflow-state-reducer.js';
7
9
  export { repositoryMetadataEventSchema } from './platform/domain/repository-tracking-events.js';
8
10
  export { checkBashCommand } from './platform/domain/bash-enforcement.js';
9
11
  export { pass, fail } from './platform/domain/precondition-result.js';
@@ -208,7 +208,7 @@ declare const contextRequestedSchema: z.ZodObject<{
208
208
  at: string;
209
209
  agentName: string;
210
210
  }>;
211
- declare const reviewRecordedSchema: z.ZodObject<{
211
+ export declare const reviewRecordedEventSchema: z.ZodObject<{
212
212
  type: z.ZodLiteral<"review-recorded">;
213
213
  at: z.ZodString;
214
214
  reviewId: z.ZodNumber;
@@ -444,6 +444,8 @@ export declare const engineEventSchema: z.ZodDiscriminatedUnion<"type", [z.ZodOb
444
444
  reviewType: string;
445
445
  reviewId: number;
446
446
  }>]>;
447
+ /** @riviere-role domain-service */
448
+ export declare function isPlatformOwnedEventExcludedFromWorkflowState(type: string): boolean;
447
449
  /** @riviere-role value-object */
448
450
  export type EngineEvent = z.infer<typeof engineEventSchema>;
449
451
  /** @riviere-role value-object */
@@ -469,5 +471,5 @@ export type IdentityVerifiedEvent = z.infer<typeof identityVerifiedSchema>;
469
471
  /** @riviere-role value-object */
470
472
  export type ContextRequestedEvent = z.infer<typeof contextRequestedSchema>;
471
473
  /** @riviere-role value-object */
472
- export type ReviewRecordedEvent = z.infer<typeof reviewRecordedSchema>;
474
+ export type ReviewRecordedEvent = z.infer<typeof reviewRecordedEventSchema>;
473
475
  export {};
@@ -76,7 +76,7 @@ const contextRequestedSchema = z.object({
76
76
  at: z.string(),
77
77
  agentName: z.string(),
78
78
  });
79
- const reviewRecordedSchema = z.object({
79
+ export const reviewRecordedEventSchema = z.object({
80
80
  type: z.literal('review-recorded'),
81
81
  at: z.string(),
82
82
  reviewId: z.number().int().positive(),
@@ -95,5 +95,20 @@ export const engineEventSchema = z.discriminatedUnion('type', [
95
95
  idleCheckedSchema,
96
96
  identityVerifiedSchema,
97
97
  contextRequestedSchema,
98
- reviewRecordedSchema,
98
+ reviewRecordedEventSchema,
99
99
  ]);
100
+ const platformOwnedEventTypesExcludedFromWorkflowState = new Set([
101
+ 'agent-registered',
102
+ 'agent-shut-down',
103
+ 'journal-entry',
104
+ 'write-checked',
105
+ 'bash-checked',
106
+ 'plugin-read-checked',
107
+ 'idle-checked',
108
+ 'identity-verified',
109
+ 'context-requested',
110
+ ]);
111
+ /** @riviere-role domain-service */
112
+ export function isPlatformOwnedEventExcludedFromWorkflowState(type) {
113
+ return platformOwnedEventTypesExcludedFromWorkflowState.has(type);
114
+ }
@@ -0,0 +1,17 @@
1
+ import type { BaseWorkflowState } from './workflow-state';
2
+ import type { BashForbiddenConfig } from './workflow-registry';
3
+ import type { EngineResult, RehydratableWorkflow, WorkflowDefinition, WorkflowEngineDeps } from './workflow-engine-types';
4
+ type PlatformOperationContext<TWorkflow extends RehydratableWorkflow<TState>, TState extends BaseWorkflowState<TStateName>, TDeps, TStateName extends string, TOperation extends string> = {
5
+ readonly workflow: TWorkflow;
6
+ readonly engineDeps: WorkflowEngineDeps;
7
+ readonly factory: WorkflowDefinition<TWorkflow, TState, TDeps, TStateName, TOperation>;
8
+ readonly applyIdentityGate: (op: string) => EngineResult | undefined;
9
+ readonly persistPlatformEvent: (event: unknown) => void;
10
+ };
11
+ /** @riviere-role domain-service */
12
+ export declare function writeJournalWithPlatformEvents<TWorkflow extends RehydratableWorkflow<TState>, TState extends BaseWorkflowState<TStateName>, TDeps, TStateName extends string, TOperation extends string>(context: PlatformOperationContext<TWorkflow, TState, TDeps, TStateName, TOperation>, agentName: string, content: string): EngineResult;
13
+ /** @riviere-role domain-service */
14
+ export declare function checkBashWithPlatformEvents<TWorkflow extends RehydratableWorkflow<TState>, TState extends BaseWorkflowState<TStateName>, TDeps, TStateName extends string, TOperation extends string>(context: PlatformOperationContext<TWorkflow, TState, TDeps, TStateName, TOperation>, toolName: string, command: string, bashForbidden: BashForbiddenConfig): EngineResult;
15
+ /** @riviere-role domain-service */
16
+ export declare function checkWriteWithPlatformEvents<TWorkflow extends RehydratableWorkflow<TState>, TState extends BaseWorkflowState<TStateName>, TDeps, TStateName extends string, TOperation extends string>(context: PlatformOperationContext<TWorkflow, TState, TDeps, TStateName, TOperation>, toolName: string, filePath: string, isWriteAllowed: (filePath: string, state: TState) => boolean): EngineResult;
17
+ export {};
@@ -0,0 +1,147 @@
1
+ import { checkBashCommand } from './bash-enforcement.js';
2
+ import { formatOperationGateError, formatOperationSuccess, } from '../infra/cli/presentation/output-guidance.js';
3
+ import { getExpectedPrefix } from './workflow-engine-support.js';
4
+ /** @riviere-role domain-service */
5
+ export function writeJournalWithPlatformEvents(context, agentName, content) {
6
+ const gate = context.applyIdentityGate('write-journal');
7
+ if (gate !== undefined)
8
+ return gate;
9
+ context.persistPlatformEvent({
10
+ type: 'journal-entry',
11
+ at: context.engineDeps.now(),
12
+ agentName,
13
+ content,
14
+ });
15
+ const state = context.workflow.getState();
16
+ const body = context.factory.getOperationBody?.('write-journal', state) ?? 'Write journal entry';
17
+ return {
18
+ type: 'success',
19
+ output: formatOperationSuccess('write-journal', body, getExpectedPrefix(state.currentStateMachineState, context.factory.getRegistry())),
20
+ };
21
+ }
22
+ /** @riviere-role domain-service */
23
+ export function checkBashWithPlatformEvents(context, toolName, command, bashForbidden) {
24
+ const state = context.workflow.getState();
25
+ const currentStateName = state.currentStateMachineState;
26
+ const currentPrefix = getExpectedPrefix(currentStateName, context.factory.getRegistry());
27
+ const gate = context.applyIdentityGate('bash-check');
28
+ if (gate !== undefined)
29
+ return gate;
30
+ if (toolName !== 'Bash') {
31
+ context.persistPlatformEvent({
32
+ type: 'bash-checked',
33
+ at: context.engineDeps.now(),
34
+ tool: toolName,
35
+ command,
36
+ allowed: true,
37
+ });
38
+ return {
39
+ type: 'success',
40
+ output: '',
41
+ };
42
+ }
43
+ const exemptions = context.factory.getRegistry()[currentStateName].allowForbidden?.bash ?? [];
44
+ const bashCheckResult = checkBashCommand(command, bashForbidden, exemptions);
45
+ if (!bashCheckResult.pass) {
46
+ const reason = `Bash command blocked in ${currentStateName}. ${bashCheckResult.reason}`;
47
+ context.persistPlatformEvent({
48
+ type: 'bash-checked',
49
+ at: context.engineDeps.now(),
50
+ tool: toolName,
51
+ command,
52
+ allowed: false,
53
+ reason,
54
+ });
55
+ return {
56
+ type: 'blocked',
57
+ output: formatOperationGateError('bash-check', reason, currentPrefix),
58
+ };
59
+ }
60
+ context.persistPlatformEvent({
61
+ type: 'bash-checked',
62
+ at: context.engineDeps.now(),
63
+ tool: toolName,
64
+ command,
65
+ allowed: true,
66
+ });
67
+ return {
68
+ type: 'success',
69
+ output: '',
70
+ };
71
+ }
72
+ /** @riviere-role domain-service */
73
+ export function checkWriteWithPlatformEvents(context, toolName, filePath, isWriteAllowed) {
74
+ const state = context.workflow.getState();
75
+ const currentStateName = state.currentStateMachineState;
76
+ const currentPrefix = getExpectedPrefix(currentStateName, context.factory.getRegistry());
77
+ const gate = context.applyIdentityGate('write-check');
78
+ if (gate !== undefined)
79
+ return gate;
80
+ const writeTools = new Set(['Write', 'Edit', 'NotebookEdit']);
81
+ if (!writeTools.has(toolName)) {
82
+ context.persistPlatformEvent({
83
+ type: 'write-checked',
84
+ at: context.engineDeps.now(),
85
+ tool: toolName,
86
+ filePath,
87
+ allowed: true,
88
+ });
89
+ return {
90
+ type: 'success',
91
+ output: '',
92
+ };
93
+ }
94
+ const storePath = `${context.engineDeps.getPluginRoot()}/workflow.db`;
95
+ if (filePath === storePath) {
96
+ context.persistPlatformEvent({
97
+ type: 'write-checked',
98
+ at: context.engineDeps.now(),
99
+ tool: toolName,
100
+ filePath,
101
+ allowed: true,
102
+ });
103
+ return {
104
+ type: 'success',
105
+ output: '',
106
+ };
107
+ }
108
+ if (!(context.factory.getRegistry()[currentStateName].forbidden?.write ?? false)) {
109
+ context.persistPlatformEvent({
110
+ type: 'write-checked',
111
+ at: context.engineDeps.now(),
112
+ tool: toolName,
113
+ filePath,
114
+ allowed: true,
115
+ });
116
+ return {
117
+ type: 'success',
118
+ output: '',
119
+ };
120
+ }
121
+ if (!isWriteAllowed(filePath, state)) {
122
+ const reason = `Write to '${filePath}' is forbidden in state ${currentStateName}`;
123
+ context.persistPlatformEvent({
124
+ type: 'write-checked',
125
+ at: context.engineDeps.now(),
126
+ tool: toolName,
127
+ filePath,
128
+ allowed: false,
129
+ reason,
130
+ });
131
+ return {
132
+ type: 'blocked',
133
+ output: formatOperationGateError('write-check', reason, currentPrefix),
134
+ };
135
+ }
136
+ context.persistPlatformEvent({
137
+ type: 'write-checked',
138
+ at: context.engineDeps.now(),
139
+ tool: toolName,
140
+ filePath,
141
+ allowed: true,
142
+ });
143
+ return {
144
+ type: 'success',
145
+ output: '',
146
+ };
147
+ }
@@ -10,6 +10,7 @@ export declare class WorkflowEngine<TWorkflow extends RehydratableWorkflow<TStat
10
10
  constructor(factory: WorkflowDefinition<TWorkflow, TState, TDeps, TStateName, TOperation>, engineDeps: WorkflowEngineDeps, workflowDeps: TDeps);
11
11
  startSession(sessionId: string, transcriptPath: string, repository?: string): EngineResult;
12
12
  transaction(sessionId: string, op: string, fn: (workflow: TWorkflow) => PreconditionResult): EngineResult;
13
+ writeJournal(sessionId: string, agentName: string, content: string): EngineResult;
13
14
  transition(sessionId: string, target: TStateName): EngineResult;
14
15
  checkBash(sessionId: string, toolName: string, command: string, bashForbidden: BashForbiddenConfig): EngineResult;
15
16
  checkWrite(sessionId: string, toolName: string, filePath: string, isWriteAllowed: (filePath: string, state: TState) => boolean): EngineResult;
@@ -23,4 +24,6 @@ export declare class WorkflowEngine<TWorkflow extends RehydratableWorkflow<TStat
23
24
  private wrapEvents;
24
25
  private applyIdentityGate;
25
26
  private verifyIdentity;
27
+ private platformOperationContext;
28
+ private persistPlatformEvent;
26
29
  }
@@ -1,9 +1,11 @@
1
- import { checkBashCommand } from './bash-enforcement.js';
2
1
  import { checkIdentity } from './identity-verification.js';
2
+ import { checkBashWithPlatformEvents, checkWriteWithPlatformEvents, writeJournalWithPlatformEvents, } from './workflow-engine-platform-operations.js';
3
3
  import { buildPrefixPattern, buildProcedurePath, enrichSessionStartedEvents, getExpectedPrefix, readProcedure, } from './workflow-engine-support.js';
4
4
  import { formatIllegalTransitionError, formatInitSuccess, formatOperationGateError, formatOperationSuccess, formatTransitionError, formatTransitionSuccess, } from '../infra/cli/presentation/output-guidance.js';
5
- import { flattenStoredEvent, toPayload, } from './stored-event.js';
5
+ import { toPayload, } from './stored-event.js';
6
+ import { engineEventSchema } from './engine-events.js';
6
7
  import { WorkflowStateError, } from './workflow-state.js';
8
+ import { reduceWorkflowStateFromStoredEvents } from './workflow-state-reducer.js';
7
9
  import { serializeWorkflowState } from './workflow-state-serialization.js';
8
10
  /** @riviere-role domain-service */
9
11
  export class WorkflowEngine {
@@ -59,6 +61,11 @@ export class WorkflowEngine {
59
61
  output: formatOperationSuccess(op, body, currentPrefix)
60
62
  };
61
63
  }
64
+ writeJournal(sessionId, agentName, content) {
65
+ this.requireSession(sessionId);
66
+ const workflow = this.rehydrateFromEvents(sessionId);
67
+ return writeJournalWithPlatformEvents(this.platformOperationContext(sessionId, workflow), agentName, content);
68
+ }
62
69
  transition(sessionId, target) {
63
70
  this.requireSession(sessionId);
64
71
  const workflow = this.rehydrateFromEvents(sessionId);
@@ -119,138 +126,12 @@ export class WorkflowEngine {
119
126
  checkBash(sessionId, toolName, command, bashForbidden) {
120
127
  this.requireSession(sessionId);
121
128
  const workflow = this.rehydrateFromEvents(sessionId);
122
- const registry = this.factory.getRegistry();
123
- const currentStateName = workflow.getState().currentStateMachineState;
124
- const currentPrefix = getExpectedPrefix(currentStateName, registry);
125
- const gate = this.applyIdentityGate(sessionId, workflow, 'bash-check');
126
- if (gate !== undefined)
127
- return gate;
128
- if (toolName !== 'Bash') {
129
- workflow.appendEvent({
130
- type: 'bash-checked',
131
- at: this.engineDeps.now(),
132
- tool: toolName,
133
- command,
134
- allowed: true
135
- });
136
- this.persistEvents(sessionId, workflow);
137
- return {
138
- type: 'success',
139
- output: ''
140
- };
141
- }
142
- const exemptions = registry[currentStateName].allowForbidden?.bash ?? [];
143
- const bashCheckResult = checkBashCommand(command, bashForbidden, exemptions);
144
- if (!bashCheckResult.pass) {
145
- const reason = `Bash command blocked in ${currentStateName}. ${bashCheckResult.reason}`;
146
- workflow.appendEvent({
147
- type: 'bash-checked',
148
- at: this.engineDeps.now(),
149
- tool: toolName,
150
- command,
151
- allowed: false,
152
- reason,
153
- });
154
- this.persistEvents(sessionId, workflow);
155
- return {
156
- type: 'blocked',
157
- output: formatOperationGateError('bash-check', reason, currentPrefix)
158
- };
159
- }
160
- workflow.appendEvent({
161
- type: 'bash-checked',
162
- at: this.engineDeps.now(),
163
- tool: toolName,
164
- command,
165
- allowed: true
166
- });
167
- this.persistEvents(sessionId, workflow);
168
- return {
169
- type: 'success',
170
- output: ''
171
- };
129
+ return checkBashWithPlatformEvents(this.platformOperationContext(sessionId, workflow), toolName, command, bashForbidden);
172
130
  }
173
131
  checkWrite(sessionId, toolName, filePath, isWriteAllowed) {
174
132
  this.requireSession(sessionId);
175
133
  const workflow = this.rehydrateFromEvents(sessionId);
176
- const registry = this.factory.getRegistry();
177
- const currentStateName = workflow.getState().currentStateMachineState;
178
- const currentPrefix = getExpectedPrefix(currentStateName, registry);
179
- const gate = this.applyIdentityGate(sessionId, workflow, 'write-check');
180
- if (gate !== undefined)
181
- return gate;
182
- const writeTools = new Set(['Write', 'Edit', 'NotebookEdit']);
183
- if (!writeTools.has(toolName)) {
184
- workflow.appendEvent({
185
- type: 'write-checked',
186
- at: this.engineDeps.now(),
187
- tool: toolName,
188
- filePath,
189
- allowed: true
190
- });
191
- this.persistEvents(sessionId, workflow);
192
- return {
193
- type: 'success',
194
- output: ''
195
- };
196
- }
197
- const storePath = `${this.engineDeps.getPluginRoot()}/workflow.db`;
198
- if (filePath === storePath) {
199
- workflow.appendEvent({
200
- type: 'write-checked',
201
- at: this.engineDeps.now(),
202
- tool: toolName,
203
- filePath,
204
- allowed: true
205
- });
206
- this.persistEvents(sessionId, workflow);
207
- return {
208
- type: 'success',
209
- output: ''
210
- };
211
- }
212
- if (!(registry[currentStateName].forbidden?.write ?? false)) {
213
- workflow.appendEvent({
214
- type: 'write-checked',
215
- at: this.engineDeps.now(),
216
- tool: toolName,
217
- filePath,
218
- allowed: true
219
- });
220
- this.persistEvents(sessionId, workflow);
221
- return {
222
- type: 'success',
223
- output: ''
224
- };
225
- }
226
- if (!isWriteAllowed(filePath, workflow.getState())) {
227
- const reason = `Write to '${filePath}' is forbidden in state ${currentStateName}`;
228
- workflow.appendEvent({
229
- type: 'write-checked',
230
- at: this.engineDeps.now(),
231
- tool: toolName,
232
- filePath,
233
- allowed: false,
234
- reason,
235
- });
236
- this.persistEvents(sessionId, workflow);
237
- return {
238
- type: 'blocked',
239
- output: formatOperationGateError('write-check', reason, currentPrefix)
240
- };
241
- }
242
- workflow.appendEvent({
243
- type: 'write-checked',
244
- at: this.engineDeps.now(),
245
- tool: toolName,
246
- filePath,
247
- allowed: true
248
- });
249
- this.persistEvents(sessionId, workflow);
250
- return {
251
- type: 'success',
252
- output: ''
253
- };
134
+ return checkWriteWithPlatformEvents(this.platformOperationContext(sessionId, workflow), toolName, filePath, isWriteAllowed);
254
135
  }
255
136
  getState(sessionId) {
256
137
  this.requireSession(sessionId);
@@ -272,8 +153,7 @@ export class WorkflowEngine {
272
153
  }
273
154
  rehydrateFromEvents(sessionId) {
274
155
  const stored = this.engineDeps.store.readEvents(sessionId);
275
- const events = stored.map(flattenStoredEvent);
276
- const state = events.reduce((accumulator, event) => this.factory.fold(accumulator, event), this.factory.initialState());
156
+ const state = reduceWorkflowStateFromStoredEvents(this.factory, stored);
277
157
  return this.factory.buildWorkflow(state, this.workflowDeps);
278
158
  }
279
159
  persistEvents(sessionId, workflow) {
@@ -318,12 +198,12 @@ export class WorkflowEngine {
318
198
  const pattern = buildPrefixPattern(registry);
319
199
  const messages = this.engineDeps.transcriptReader.readMessages(transcriptPath);
320
200
  const identityCheckResult = checkIdentity(messages, pattern);
321
- this.engineDeps.store.appendEvents(sessionId, this.wrapEvents([{
322
- type: 'identity-verified',
323
- at: this.engineDeps.now(),
324
- status: identityCheckResult.status,
325
- transcriptPath,
326
- }], workflow.getState()));
201
+ this.persistPlatformEvent(sessionId, workflow.getState(), {
202
+ type: 'identity-verified',
203
+ at: this.engineDeps.now(),
204
+ status: identityCheckResult.status,
205
+ transcriptPath,
206
+ });
327
207
  if (identityCheckResult.status === 'lost') {
328
208
  const currentProcedure = readProcedure(this.engineDeps, state);
329
209
  return [
@@ -339,4 +219,24 @@ export class WorkflowEngine {
339
219
  }
340
220
  return undefined;
341
221
  }
222
+ platformOperationContext(sessionId, workflow) {
223
+ return {
224
+ workflow,
225
+ engineDeps: this.engineDeps,
226
+ factory: this.factory,
227
+ applyIdentityGate: (op) => this.applyIdentityGate(sessionId, workflow, op),
228
+ persistPlatformEvent: (event) => this.persistPlatformEvent(sessionId, workflow.getState(), event),
229
+ };
230
+ }
231
+ persistPlatformEvent(sessionId, state, event) {
232
+ const platformEvent = engineEventSchema.parse(event);
233
+ this.engineDeps.store.appendEvents(sessionId, [{
234
+ envelope: {
235
+ type: platformEvent.type,
236
+ at: platformEvent.at,
237
+ state: state.currentStateMachineState,
238
+ },
239
+ payload: toPayload(platformEvent),
240
+ }]);
241
+ }
342
242
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,305 @@
1
+ import { describe, expect, it, } from 'vitest';
2
+ import { z } from 'zod';
3
+ import { flattenStoredEvent, pass, reduceWorkflowStateFromStoredEvents, reviewRecordedEventSchema, WorkflowEngine, WorkflowStateError, } from '../../index.js';
4
+ function isSessionStartedEvent(event) {
5
+ return event.type === 'session-started';
6
+ }
7
+ function allowAgentRegistration(agentType, agentId) {
8
+ void agentType;
9
+ void agentId;
10
+ return pass();
11
+ }
12
+ function allowIdleCheck(agentName) {
13
+ void agentName;
14
+ return pass();
15
+ }
16
+ class StrictPlanningWorkflow {
17
+ state;
18
+ pendingEvents;
19
+ constructor(state, pendingEvents = []) {
20
+ this.state = state;
21
+ this.pendingEvents = pendingEvents;
22
+ }
23
+ getState() {
24
+ return this.state;
25
+ }
26
+ appendEvent(event) {
27
+ if (!isSessionStartedEvent(event)) {
28
+ throw new WorkflowStateError(`Unexpected event in appendEvent: ${event.type}`);
29
+ }
30
+ this.pendingEvents = [...this.pendingEvents, event];
31
+ this.state = {
32
+ ...this.state,
33
+ transcriptPath: event.transcriptPath,
34
+ };
35
+ }
36
+ getPendingEvents() {
37
+ return this.pendingEvents;
38
+ }
39
+ startSession(transcriptPath, repository) {
40
+ void repository;
41
+ this.state = {
42
+ ...this.state,
43
+ transcriptPath,
44
+ };
45
+ this.pendingEvents = [...this.pendingEvents, {
46
+ type: 'session-started',
47
+ at: '2026-01-01T00:00:00Z',
48
+ transcriptPath,
49
+ currentState: this.state.currentStateMachineState,
50
+ states: ['PLANNING'],
51
+ }];
52
+ }
53
+ getTranscriptPath() {
54
+ return this.state.transcriptPath;
55
+ }
56
+ registerAgent(agentType, agentId) {
57
+ return allowAgentRegistration(agentType, agentId);
58
+ }
59
+ handleTeammateIdle(agentName) {
60
+ return allowIdleCheck(agentName);
61
+ }
62
+ }
63
+ class ReviewTrackingWorkflow {
64
+ state;
65
+ constructor(state) {
66
+ this.state = state;
67
+ }
68
+ getState() {
69
+ return this.state;
70
+ }
71
+ appendEvent(event) {
72
+ void event;
73
+ throw new WorkflowStateError('appendEvent is not used in this test');
74
+ }
75
+ getPendingEvents() {
76
+ return [];
77
+ }
78
+ startSession(transcriptPath, repository) {
79
+ void transcriptPath;
80
+ void repository;
81
+ throw new WorkflowStateError('startSession is not used in this test');
82
+ }
83
+ getTranscriptPath() {
84
+ return '';
85
+ }
86
+ registerAgent(agentType, agentId) {
87
+ return allowAgentRegistration(agentType, agentId);
88
+ }
89
+ handleTeammateIdle(agentName) {
90
+ return allowIdleCheck(agentName);
91
+ }
92
+ }
93
+ class InMemoryWorkflowEventStore {
94
+ eventsBySessionId = new Map();
95
+ readEvents(sessionId) {
96
+ return this.eventsBySessionId.get(sessionId) ?? [];
97
+ }
98
+ appendEvents(sessionId, events) {
99
+ const existingEvents = this.eventsBySessionId.get(sessionId) ?? [];
100
+ this.eventsBySessionId.set(sessionId, [...existingEvents, ...events]);
101
+ }
102
+ sessionExists(sessionId) {
103
+ return this.eventsBySessionId.has(sessionId);
104
+ }
105
+ hasSessionStarted(sessionId) {
106
+ return this.readEvents(sessionId).some((event) => event.envelope.type === 'session-started');
107
+ }
108
+ recordReflection(sessionId, createdAt, input) {
109
+ void sessionId;
110
+ void createdAt;
111
+ void input;
112
+ throw new WorkflowStateError('Reflection storage is not configured for this test');
113
+ }
114
+ listReflections(sessionId) {
115
+ void sessionId;
116
+ return [];
117
+ }
118
+ recordReview(sessionId, createdAt, input) {
119
+ void sessionId;
120
+ void createdAt;
121
+ void input;
122
+ throw new WorkflowStateError('Review storage is not configured for this test');
123
+ }
124
+ recordReviewWithEvent(sessionId, createdAt, input, eventState) {
125
+ void sessionId;
126
+ void createdAt;
127
+ void input;
128
+ void eventState;
129
+ throw new WorkflowStateError('Review storage is not configured for this test');
130
+ }
131
+ listSessionReviews(sessionId) {
132
+ void sessionId;
133
+ return [];
134
+ }
135
+ listReviews(filters) {
136
+ void filters;
137
+ return [];
138
+ }
139
+ }
140
+ const workflowDefinition = {
141
+ fold(state, event) {
142
+ if (!isSessionStartedEvent(event)) {
143
+ throw new WorkflowStateError(`Unexpected event in fold: ${event.type}`);
144
+ }
145
+ return {
146
+ ...state,
147
+ transcriptPath: event.transcriptPath,
148
+ };
149
+ },
150
+ buildWorkflow(state) {
151
+ return new StrictPlanningWorkflow(state);
152
+ },
153
+ stateSchema: z.literal('PLANNING'),
154
+ initialState() {
155
+ return {
156
+ currentStateMachineState: 'PLANNING',
157
+ transcriptPath: '',
158
+ };
159
+ },
160
+ getRegistry() {
161
+ return {
162
+ PLANNING: {
163
+ emoji: '🧭',
164
+ agentInstructions: 'states/planning.md',
165
+ canTransitionTo: [],
166
+ allowedWorkflowOperations: ['write'],
167
+ forbidden: { write: true },
168
+ },
169
+ };
170
+ },
171
+ buildTransitionContext(state, from, to) {
172
+ return {
173
+ state,
174
+ from,
175
+ to,
176
+ gitInfo: {
177
+ currentBranch: 'main',
178
+ workingTreeClean: true,
179
+ headCommit: 'abc123',
180
+ changedFilesVsDefault: [],
181
+ hasCommitsVsDefault: false,
182
+ },
183
+ };
184
+ },
185
+ };
186
+ const reviewTrackingWorkflowDefinition = {
187
+ fold(state, event) {
188
+ const reviewRecordedEvent = reviewRecordedEventSchema.parse(event);
189
+ if (reviewRecordedEvent.reviewType !== 'code-review') {
190
+ return state;
191
+ }
192
+ return {
193
+ ...state,
194
+ codeReviewPassed: reviewRecordedEvent.verdict === 'PASS',
195
+ };
196
+ },
197
+ buildWorkflow(state) {
198
+ return new ReviewTrackingWorkflow(state);
199
+ },
200
+ stateSchema: z.literal('REVIEWING'),
201
+ initialState() {
202
+ return {
203
+ currentStateMachineState: 'REVIEWING',
204
+ codeReviewPassed: false,
205
+ };
206
+ },
207
+ getRegistry() {
208
+ return {
209
+ REVIEWING: {
210
+ emoji: '🔎',
211
+ agentInstructions: 'states/reviewing.md',
212
+ canTransitionTo: [],
213
+ allowedWorkflowOperations: ['record-review'],
214
+ },
215
+ };
216
+ },
217
+ buildTransitionContext(state, from, to) {
218
+ return {
219
+ state,
220
+ from,
221
+ to,
222
+ gitInfo: {
223
+ currentBranch: 'main',
224
+ workingTreeClean: true,
225
+ headCommit: 'abc123',
226
+ changedFilesVsDefault: [],
227
+ hasCommitsVsDefault: false,
228
+ },
229
+ };
230
+ },
231
+ };
232
+ function buildStoredEvent(type, at, state, payload) {
233
+ return {
234
+ envelope: {
235
+ type,
236
+ at,
237
+ state,
238
+ },
239
+ payload,
240
+ };
241
+ }
242
+ function createEngine() {
243
+ const store = new InMemoryWorkflowEventStore();
244
+ const engineDeps = {
245
+ store,
246
+ getPluginRoot: () => '/plugin-root',
247
+ getEnvFilePath: () => '/plugin-root/.env',
248
+ readFile: () => '',
249
+ appendToFile: () => undefined,
250
+ now: () => '2026-01-01T00:00:00Z',
251
+ transcriptReader: { readMessages: () => [] },
252
+ };
253
+ return {
254
+ store,
255
+ engine: new WorkflowEngine(workflowDefinition, engineDeps, {}),
256
+ };
257
+ }
258
+ describe('WorkflowEngine platform-owned events', () => {
259
+ it('persists journal and write-check events without routing them through the consumer workflow', () => {
260
+ const { engine, store, } = createEngine();
261
+ engine.startSession('session-1', '/transcripts/session-1.jsonl');
262
+ const journalResult = engine.writeJournal('session-1', 'gpt-5.4', 'Captured planning context.');
263
+ const writeCheckResult = engine.checkWrite('session-1', 'Read', '', () => true);
264
+ const stateResult = engine.getState('session-1');
265
+ expect(journalResult.type).toBe('success');
266
+ expect(writeCheckResult).toStrictEqual({
267
+ type: 'success',
268
+ output: '',
269
+ });
270
+ expect(stateResult).toStrictEqual({
271
+ type: 'success',
272
+ output: JSON.stringify({
273
+ currentStateMachineState: 'PLANNING',
274
+ transcriptPath: '/transcripts/session-1.jsonl',
275
+ }, null, 2),
276
+ });
277
+ expect(store.readEvents('session-1').map((event) => event.envelope.type)).toStrictEqual([
278
+ 'session-started',
279
+ 'identity-verified',
280
+ 'journal-entry',
281
+ 'identity-verified',
282
+ 'write-checked',
283
+ ]);
284
+ });
285
+ it('keeps review-recorded available to consumer workflow state reconstruction', () => {
286
+ const storedEvents = [buildStoredEvent('review-recorded', '2026-01-01T00:01:00Z', 'REVIEWING', {
287
+ reviewId: 7,
288
+ reviewType: 'code-review',
289
+ verdict: 'PASS',
290
+ })];
291
+ const state = reduceWorkflowStateFromStoredEvents(reviewTrackingWorkflowDefinition, storedEvents);
292
+ const flattenedEvent = flattenStoredEvent(storedEvents[0]);
293
+ expect(reviewRecordedEventSchema.parse(flattenedEvent)).toStrictEqual({
294
+ type: 'review-recorded',
295
+ at: '2026-01-01T00:01:00Z',
296
+ reviewId: 7,
297
+ reviewType: 'code-review',
298
+ verdict: 'PASS',
299
+ });
300
+ expect(state).toStrictEqual({
301
+ currentStateMachineState: 'REVIEWING',
302
+ codeReviewPassed: true,
303
+ });
304
+ });
305
+ });
@@ -0,0 +1,5 @@
1
+ import { type StoredEvent } from './stored-event';
2
+ import type { BaseWorkflowState } from './workflow-state';
3
+ import type { RehydratableWorkflow, WorkflowDefinition } from './workflow-engine-types';
4
+ /** @riviere-role domain-service */
5
+ export declare function reduceWorkflowStateFromStoredEvents<TWorkflow extends RehydratableWorkflow<TState>, TState extends BaseWorkflowState<TStateName>, TDeps, TStateName extends string, TOperation extends string>(workflowDefinition: WorkflowDefinition<TWorkflow, TState, TDeps, TStateName, TOperation>, storedEvents: readonly StoredEvent[]): TState;
@@ -0,0 +1,9 @@
1
+ import { isPlatformOwnedEventExcludedFromWorkflowState } from './engine-events.js';
2
+ import { flattenStoredEvent, } from './stored-event.js';
3
+ /** @riviere-role domain-service */
4
+ export function reduceWorkflowStateFromStoredEvents(workflowDefinition, storedEvents) {
5
+ return storedEvents
6
+ .map(flattenStoredEvent)
7
+ .filter((event) => !isPlatformOwnedEventExcludedFromWorkflowState(event.type))
8
+ .reduce((workflowState, event) => workflowDefinition.fold(workflowState, event), workflowDefinition.initialState());
9
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nt-ai-lab/deterministic-agent-workflow-engine",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {