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

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
@@ -10,6 +10,8 @@ export { reviewTypeSchema, reviewVerdictSchema, reviewFindingSeveritySchema, rev
10
10
  export type { ReviewType, ReviewVerdict, ReviewFindingSeverity, ReviewFindingStatus, ReviewFinding, ReviewPayload, RecordReviewInput, StoredReview, ListedReview, ReviewFilters, } from './platform/domain/review-types';
11
11
  export { engineEventSchema } 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
@@ -4,6 +4,8 @@ export { flattenStoredEvent, stripEnvelopeKeys, toPayload, } from './platform/do
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
6
  export { engineEventSchema } 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';
@@ -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 */
@@ -97,3 +97,19 @@ export const engineEventSchema = z.discriminatedUnion('type', [
97
97
  contextRequestedSchema,
98
98
  reviewRecordedSchema,
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
+ 'review-recorded',
111
+ ]);
112
+ /** @riviere-role domain-service */
113
+ export function isPlatformOwnedEventExcludedFromWorkflowState(type) {
114
+ return platformOwnedEventTypesExcludedFromWorkflowState.has(type);
115
+ }
@@ -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,193 @@
1
+ import { describe, expect, it, } from 'vitest';
2
+ import { z } from 'zod';
3
+ import { pass, WorkflowEngine, WorkflowStateError, } from '../../index.js';
4
+ function isSessionStartedEvent(event) {
5
+ return event.type === 'session-started';
6
+ }
7
+ class StrictPlanningWorkflow {
8
+ state;
9
+ pendingEvents;
10
+ constructor(state, pendingEvents = []) {
11
+ this.state = state;
12
+ this.pendingEvents = pendingEvents;
13
+ }
14
+ getState() {
15
+ return this.state;
16
+ }
17
+ appendEvent(event) {
18
+ if (!isSessionStartedEvent(event)) {
19
+ throw new WorkflowStateError(`Unexpected event in appendEvent: ${event.type}`);
20
+ }
21
+ this.pendingEvents = [...this.pendingEvents, event];
22
+ this.state = {
23
+ ...this.state,
24
+ transcriptPath: event.transcriptPath,
25
+ };
26
+ }
27
+ getPendingEvents() {
28
+ return this.pendingEvents;
29
+ }
30
+ startSession(transcriptPath, repository) {
31
+ void repository;
32
+ this.state = {
33
+ ...this.state,
34
+ transcriptPath,
35
+ };
36
+ this.pendingEvents = [...this.pendingEvents, {
37
+ type: 'session-started',
38
+ at: '2026-01-01T00:00:00Z',
39
+ transcriptPath,
40
+ currentState: this.state.currentStateMachineState,
41
+ states: ['PLANNING'],
42
+ }];
43
+ }
44
+ getTranscriptPath() {
45
+ return this.state.transcriptPath;
46
+ }
47
+ registerAgent(agentType, agentId) {
48
+ void agentType;
49
+ void agentId;
50
+ return pass();
51
+ }
52
+ handleTeammateIdle(agentName) {
53
+ void agentName;
54
+ return pass();
55
+ }
56
+ }
57
+ class InMemoryWorkflowEventStore {
58
+ eventsBySessionId = new Map();
59
+ readEvents(sessionId) {
60
+ return this.eventsBySessionId.get(sessionId) ?? [];
61
+ }
62
+ appendEvents(sessionId, events) {
63
+ const existingEvents = this.eventsBySessionId.get(sessionId) ?? [];
64
+ this.eventsBySessionId.set(sessionId, [...existingEvents, ...events]);
65
+ }
66
+ sessionExists(sessionId) {
67
+ return this.eventsBySessionId.has(sessionId);
68
+ }
69
+ hasSessionStarted(sessionId) {
70
+ return this.readEvents(sessionId).some((event) => event.envelope.type === 'session-started');
71
+ }
72
+ recordReflection(sessionId, createdAt, input) {
73
+ void sessionId;
74
+ void createdAt;
75
+ void input;
76
+ throw new WorkflowStateError('Reflection storage is not configured for this test');
77
+ }
78
+ listReflections(sessionId) {
79
+ void sessionId;
80
+ return [];
81
+ }
82
+ recordReview(sessionId, createdAt, input) {
83
+ void sessionId;
84
+ void createdAt;
85
+ void input;
86
+ throw new WorkflowStateError('Review storage is not configured for this test');
87
+ }
88
+ recordReviewWithEvent(sessionId, createdAt, input, eventState) {
89
+ void sessionId;
90
+ void createdAt;
91
+ void input;
92
+ void eventState;
93
+ throw new WorkflowStateError('Review storage is not configured for this test');
94
+ }
95
+ listSessionReviews(sessionId) {
96
+ void sessionId;
97
+ return [];
98
+ }
99
+ listReviews(filters) {
100
+ void filters;
101
+ return [];
102
+ }
103
+ }
104
+ const workflowDefinition = {
105
+ fold(state, event) {
106
+ if (!isSessionStartedEvent(event)) {
107
+ throw new WorkflowStateError(`Unexpected event in fold: ${event.type}`);
108
+ }
109
+ return {
110
+ ...state,
111
+ transcriptPath: event.transcriptPath,
112
+ };
113
+ },
114
+ buildWorkflow(state) {
115
+ return new StrictPlanningWorkflow(state);
116
+ },
117
+ stateSchema: z.literal('PLANNING'),
118
+ initialState() {
119
+ return {
120
+ currentStateMachineState: 'PLANNING',
121
+ transcriptPath: '',
122
+ };
123
+ },
124
+ getRegistry() {
125
+ return {
126
+ PLANNING: {
127
+ emoji: '🧭',
128
+ agentInstructions: 'states/planning.md',
129
+ canTransitionTo: [],
130
+ allowedWorkflowOperations: ['write'],
131
+ forbidden: { write: true },
132
+ },
133
+ };
134
+ },
135
+ buildTransitionContext(state, from, to) {
136
+ return {
137
+ state,
138
+ from,
139
+ to,
140
+ gitInfo: {
141
+ currentBranch: 'main',
142
+ workingTreeClean: true,
143
+ headCommit: 'abc123',
144
+ changedFilesVsDefault: [],
145
+ hasCommitsVsDefault: false,
146
+ },
147
+ };
148
+ },
149
+ };
150
+ function createEngine() {
151
+ const store = new InMemoryWorkflowEventStore();
152
+ const engineDeps = {
153
+ store,
154
+ getPluginRoot: () => '/plugin-root',
155
+ getEnvFilePath: () => '/plugin-root/.env',
156
+ readFile: () => '',
157
+ appendToFile: () => undefined,
158
+ now: () => '2026-01-01T00:00:00Z',
159
+ transcriptReader: { readMessages: () => [] },
160
+ };
161
+ return {
162
+ store,
163
+ engine: new WorkflowEngine(workflowDefinition, engineDeps, {}),
164
+ };
165
+ }
166
+ describe('WorkflowEngine platform-owned events', () => {
167
+ it('persists journal and write-check events without routing them through the consumer workflow', () => {
168
+ const { engine, store, } = createEngine();
169
+ engine.startSession('session-1', '/transcripts/session-1.jsonl');
170
+ const journalResult = engine.writeJournal('session-1', 'gpt-5.4', 'Captured planning context.');
171
+ const writeCheckResult = engine.checkWrite('session-1', 'Read', '', () => true);
172
+ const stateResult = engine.getState('session-1');
173
+ expect(journalResult.type).toBe('success');
174
+ expect(writeCheckResult).toStrictEqual({
175
+ type: 'success',
176
+ output: '',
177
+ });
178
+ expect(stateResult).toStrictEqual({
179
+ type: 'success',
180
+ output: JSON.stringify({
181
+ currentStateMachineState: 'PLANNING',
182
+ transcriptPath: '/transcripts/session-1.jsonl',
183
+ }, null, 2),
184
+ });
185
+ expect(store.readEvents('session-1').map((event) => event.envelope.type)).toStrictEqual([
186
+ 'session-started',
187
+ 'identity-verified',
188
+ 'journal-entry',
189
+ 'identity-verified',
190
+ 'write-checked',
191
+ ]);
192
+ });
193
+ });
@@ -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.5",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {