@nt-ai-lab/deterministic-agent-workflow-engine 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.
package/dist/index.d.ts CHANGED
@@ -2,6 +2,8 @@ export type { BaseWorkflowState } from './platform/domain/workflow-state';
2
2
  export { WorkflowStateError } from './platform/domain/workflow-state';
3
3
  export type { BaseEvent } from './platform/domain/base-event';
4
4
  export { baseEventSchema } from './platform/domain/base-event';
5
+ export type { EventEnvelope, StoredEvent, } from './platform/domain/stored-event';
6
+ export { flattenStoredEvent, stripEnvelopeKeys, toPayload, } from './platform/domain/stored-event';
5
7
  export { engineEventSchema } from './platform/domain/engine-events';
6
8
  export type { EngineEvent, SessionStartedEvent, TransitionedEvent, AgentRegisteredEvent, AgentShutDownEvent, JournalEntryEvent, WriteCheckedEvent, BashCheckedEvent, PluginReadCheckedEvent, IdleCheckedEvent, IdentityVerifiedEvent, ContextRequestedEvent, } from './platform/domain/engine-events';
7
9
  export { repositoryMetadataEventSchema } from './platform/domain/repository-tracking-events';
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export { WorkflowStateError } from './platform/domain/workflow-state.js';
2
2
  export { baseEventSchema } from './platform/domain/base-event.js';
3
+ export { flattenStoredEvent, stripEnvelopeKeys, toPayload, } from './platform/domain/stored-event.js';
3
4
  export { engineEventSchema } from './platform/domain/engine-events.js';
4
5
  export { repositoryMetadataEventSchema } from './platform/domain/repository-tracking-events.js';
5
6
  export { checkBashCommand } from './platform/domain/bash-enforcement.js';
@@ -0,0 +1,28 @@
1
+ import type { BaseEvent } from './base-event';
2
+ /** @riviere-role value-object */
3
+ export interface EventEnvelope {
4
+ readonly type: string;
5
+ readonly at: string;
6
+ readonly state: string | undefined;
7
+ }
8
+ /**
9
+ * Platform wire / storage shape for events.
10
+ *
11
+ * Envelope fields are stamped by the platform at persist time (see
12
+ * `WorkflowEngine.persistEvents`). Workflow authors never construct
13
+ * `StoredEvent` directly — they call `appendEvent(BaseEvent)` with flat
14
+ * domain events. Only the engine and direct event-store readers (e.g. the
15
+ * control-center UI) observe this shape.
16
+ *
17
+ * @riviere-role value-object
18
+ */
19
+ export interface StoredEvent {
20
+ readonly envelope: EventEnvelope;
21
+ readonly payload: Readonly<Record<string, unknown>>;
22
+ }
23
+ /** @riviere-role domain-service */
24
+ export declare function flattenStoredEvent(stored: StoredEvent): BaseEvent;
25
+ /** @riviere-role domain-service */
26
+ export declare function toPayload(event: BaseEvent): Record<string, unknown>;
27
+ /** @riviere-role domain-service */
28
+ export declare function stripEnvelopeKeys(record: Record<string, unknown>): Record<string, unknown>;
@@ -0,0 +1,22 @@
1
+ /** @riviere-role domain-service */
2
+ export function flattenStoredEvent(stored) {
3
+ return {
4
+ ...stripEnvelopeKeys(stored.payload),
5
+ type: stored.envelope.type,
6
+ at: stored.envelope.at,
7
+ };
8
+ }
9
+ /** @riviere-role domain-service */
10
+ export function toPayload(event) {
11
+ return stripEnvelopeKeys(event);
12
+ }
13
+ /** @riviere-role domain-service */
14
+ export function stripEnvelopeKeys(record) {
15
+ const result = {};
16
+ for (const [key, value] of Object.entries(record)) {
17
+ if (key === 'type' || key === 'at')
18
+ continue;
19
+ result[key] = value;
20
+ }
21
+ return result;
22
+ }
@@ -1,5 +1,6 @@
1
1
  import type { ZodType } from 'zod';
2
2
  import type { BaseEvent } from './base-event';
3
+ import type { StoredEvent } from './stored-event';
3
4
  import type { PreconditionResult } from './precondition-result';
4
5
  import type { TranscriptReader } from '../infra/external-clients/transcript/transcript-reader';
5
6
  import type { BaseWorkflowState } from './workflow-state';
@@ -39,8 +40,8 @@ export interface WorkflowDefinition<TWorkflow extends RehydratableWorkflow<TStat
39
40
  }
40
41
  /** @riviere-role value-object */
41
42
  export interface WorkflowEventStore {
42
- readEvents(sessionId: string): readonly BaseEvent[];
43
- appendEvents(sessionId: string, events: readonly BaseEvent[]): void;
43
+ readEvents(sessionId: string): readonly StoredEvent[];
44
+ appendEvents(sessionId: string, events: readonly StoredEvent[]): void;
44
45
  sessionExists(sessionId: string): boolean;
45
46
  hasSessionStarted(sessionId: string): boolean;
46
47
  }
@@ -19,5 +19,6 @@ export declare class WorkflowEngine<TWorkflow extends RehydratableWorkflow<TStat
19
19
  private requireSession;
20
20
  private rehydrateFromEvents;
21
21
  private persistEvents;
22
+ private wrapEvents;
22
23
  private verifyIdentity;
23
24
  }
@@ -2,6 +2,7 @@ import { checkBashCommand } from './bash-enforcement.js';
2
2
  import { checkIdentity } from './identity-verification.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
6
  import { WorkflowStateError } from './workflow-state.js';
6
7
  /** @riviere-role domain-service */
7
8
  export class WorkflowEngine {
@@ -27,7 +28,7 @@ export class WorkflowEngine {
27
28
  const registry = this.factory.getRegistry();
28
29
  const stateNames = Object.keys(registry);
29
30
  const pendingEvents = enrichSessionStartedEvents(this.engineDeps, workflow.getPendingEvents(), transcriptPath, resolvedRepository, initialState.currentStateMachineState, stateNames);
30
- this.engineDeps.store.appendEvents(sessionId, pendingEvents);
31
+ this.engineDeps.store.appendEvents(sessionId, this.wrapEvents(pendingEvents, initialState));
31
32
  const procedureContent = this.engineDeps.readFile(buildProcedurePath(this.engineDeps, initialState.currentStateMachineState));
32
33
  const expectedPrefix = getExpectedPrefix(initialState.currentStateMachineState, registry);
33
34
  return {
@@ -155,9 +156,9 @@ export class WorkflowEngine {
155
156
  };
156
157
  }
157
158
  const exemptions = registry[currentStateName].allowForbidden?.bash ?? [];
158
- const result = checkBashCommand(command, bashForbidden, exemptions);
159
- if (!result.pass) {
160
- const reason = `Bash command blocked in ${currentStateName}. ${result.reason}`;
159
+ const bashCheckResult = checkBashCommand(command, bashForbidden, exemptions);
160
+ if (!bashCheckResult.pass) {
161
+ const reason = `Bash command blocked in ${currentStateName}. ${bashCheckResult.reason}`;
161
162
  workflow.appendEvent({
162
163
  type: 'bash-checked',
163
164
  at: this.engineDeps.now(),
@@ -287,15 +288,34 @@ export class WorkflowEngine {
287
288
  }
288
289
  }
289
290
  rehydrateFromEvents(sessionId) {
290
- const events = this.engineDeps.store.readEvents(sessionId);
291
+ const stored = this.engineDeps.store.readEvents(sessionId);
292
+ const events = stored.map(flattenStoredEvent);
291
293
  const state = events.reduce((accumulator, event) => this.factory.fold(accumulator, event), this.factory.initialState());
292
294
  return this.factory.buildWorkflow(state, this.workflowDeps);
293
295
  }
294
296
  persistEvents(sessionId, workflow) {
295
297
  const pending = workflow.getPendingEvents();
296
- if (pending.length > 0) {
297
- this.engineDeps.store.appendEvents(sessionId, pending);
298
- }
298
+ if (pending.length === 0)
299
+ return;
300
+ const preAppendState = this.rehydrateFromEvents(sessionId).getState();
301
+ this.engineDeps.store.appendEvents(sessionId, this.wrapEvents(pending, preAppendState));
302
+ }
303
+ wrapEvents(events, startState) {
304
+ const { stored } = events.reduce((accumulator, event) => ({
305
+ state: this.factory.fold(accumulator.state, event),
306
+ stored: [...accumulator.stored, {
307
+ envelope: {
308
+ type: event.type,
309
+ at: event.at,
310
+ state: accumulator.state.currentStateMachineState,
311
+ },
312
+ payload: toPayload(event),
313
+ }],
314
+ }), {
315
+ state: startState,
316
+ stored: [],
317
+ });
318
+ return stored;
299
319
  }
300
320
  verifyIdentity(sessionId, workflow) {
301
321
  const transcriptPath = workflow.getTranscriptPath();
@@ -304,14 +324,14 @@ export class WorkflowEngine {
304
324
  const expectedPrefix = getExpectedPrefix(state, registry);
305
325
  const pattern = buildPrefixPattern(registry);
306
326
  const messages = this.engineDeps.transcriptReader.readMessages(transcriptPath);
307
- const result = checkIdentity(messages, pattern);
308
- this.engineDeps.store.appendEvents(sessionId, [{
327
+ const identityCheckResult = checkIdentity(messages, pattern);
328
+ this.engineDeps.store.appendEvents(sessionId, this.wrapEvents([{
309
329
  type: 'identity-verified',
310
330
  at: this.engineDeps.now(),
311
- status: result.status,
331
+ status: identityCheckResult.status,
312
332
  transcriptPath,
313
- }]);
314
- if (result.status === 'lost') {
333
+ }], workflow.getState()));
334
+ if (identityCheckResult.status === 'lost') {
315
335
  return `You forgot. Next message MUST begin with: ${expectedPrefix}`;
316
336
  }
317
337
  return undefined;
@@ -1,4 +1,6 @@
1
1
  export declare const SEPARATOR = "----------------------------------------------------------------";
2
+ export declare const PLATFORM_NOTIFICATION_FENCE = "****************************************************************";
3
+ export declare const JOURNAL_GUIDANCE: string;
2
4
  /** @riviere-role cli-output-formatter */
3
5
  export declare function formatBlock(title: string, body: string): string;
4
6
  /** @riviere-role cli-output-formatter */
@@ -1,11 +1,26 @@
1
1
  export const SEPARATOR = '----------------------------------------------------------------';
2
+ export const PLATFORM_NOTIFICATION_FENCE = '****************************************************************';
3
+ export const JOURNAL_GUIDANCE = [
4
+ PLATFORM_NOTIFICATION_FENCE,
5
+ 'PLATFORM NOTIFICATION',
6
+ '',
7
+ 'Record your progress and reasoning as you work by calling:',
8
+ ' write-journal <agent-name> "<1\u20133 sentence note>"',
9
+ '',
10
+ 'Use it for key decisions, progress milestones, and blockers.',
11
+ 'Every session should have a journal trail of the work performed.',
12
+ PLATFORM_NOTIFICATION_FENCE,
13
+ ].join('\n');
2
14
  /** @riviere-role cli-output-formatter */
3
15
  export function formatBlock(title, body) {
4
16
  return `${title}\n${SEPARATOR}\n${body}`;
5
17
  }
18
+ function appendJournalGuidance(body) {
19
+ return `${body}\n\n${JOURNAL_GUIDANCE}`;
20
+ }
6
21
  /** @riviere-role cli-output-formatter */
7
22
  export function formatTransitionSuccess(title, procedureContent, expectedPrefix) {
8
- return formatBlock(title, `${procedureContent}\n\nNext message MUST begin with: ${expectedPrefix}`);
23
+ return formatBlock(title, appendJournalGuidance(`${procedureContent}\n\nNext message MUST begin with: ${expectedPrefix}`));
9
24
  }
10
25
  /** @riviere-role cli-output-formatter */
11
26
  export function formatTransitionError(to, reason, currentProcedure, expectedPrefix) {
@@ -25,5 +40,5 @@ export function formatOperationSuccess(op, body, expectedPrefix) {
25
40
  }
26
41
  /** @riviere-role cli-output-formatter */
27
42
  export function formatInitSuccess(procedureContent, expectedPrefix) {
28
- return formatBlock('Feature team initialized', `${procedureContent}\n\nNext message MUST begin with: ${expectedPrefix}`);
43
+ return formatBlock('Feature team initialized', appendJournalGuidance(`${procedureContent}\n\nNext message MUST begin with: ${expectedPrefix}`));
29
44
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nt-ai-lab/deterministic-agent-workflow-engine",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {