@nt-ai-lab/deterministic-agent-workflow-engine 0.2.1 → 0.3.0

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.
@@ -0,0 +1,72 @@
1
+ import { z } from 'zod';
2
+ export const reflectionCategorySchema = z.enum([
3
+ 'state-efficiency',
4
+ 'review-rework',
5
+ 'quality-gates',
6
+ 'tooling',
7
+ 'workflow-design',
8
+ ]);
9
+ export const reflectionConfidenceSchema = z.enum(['low', 'medium', 'high']);
10
+ const evidenceBaseSchema = z.object({ label: z.string().min(1).optional(), });
11
+ export const reflectionEvidenceSchema = z.discriminatedUnion('kind', [
12
+ evidenceBaseSchema.extend({
13
+ kind: z.literal('state-period'),
14
+ state: z.string().min(1),
15
+ startedAt: z.string().min(1).optional(),
16
+ endedAt: z.string().min(1).optional(),
17
+ }),
18
+ evidenceBaseSchema.extend({
19
+ kind: z.literal('event'),
20
+ seq: z.number().int().positive(),
21
+ }),
22
+ evidenceBaseSchema.extend({
23
+ kind: z.literal('event-range'),
24
+ startSeq: z.number().int().positive(),
25
+ endSeq: z.number().int().positive(),
26
+ }),
27
+ evidenceBaseSchema.extend({
28
+ kind: z.literal('journal-entry'),
29
+ at: z.string().min(1),
30
+ agentName: z.string().min(1).optional(),
31
+ }),
32
+ evidenceBaseSchema.extend({
33
+ kind: z.literal('transcript-range'),
34
+ startIndex: z.number().int().nonnegative(),
35
+ endIndex: z.number().int().nonnegative(),
36
+ }),
37
+ evidenceBaseSchema.extend({
38
+ kind: z.literal('tool-activity'),
39
+ state: z.string().min(1).optional(),
40
+ toolName: z.string().min(1).optional(),
41
+ metric: z.string().min(1).optional(),
42
+ }),
43
+ ]);
44
+ export const reflectionFindingSchema = z.object({
45
+ title: z.string().min(1),
46
+ category: reflectionCategorySchema,
47
+ opportunity: z.string().min(1),
48
+ likelyCause: z.string().min(1),
49
+ suggestedChange: z.string().min(1),
50
+ expectedImpact: z.string().min(1),
51
+ confidence: reflectionConfidenceSchema.optional(),
52
+ evidence: z.array(reflectionEvidenceSchema).min(1),
53
+ });
54
+ export const reflectionPayloadSchema = z.object({
55
+ summary: z.string().min(1).optional(),
56
+ findings: z.array(reflectionFindingSchema).max(10),
57
+ }).strict();
58
+ export const recordReflectionInputSchema = z.object({
59
+ label: z.string().min(1).optional(),
60
+ agentName: z.string().min(1).optional(),
61
+ sourceState: z.string().min(1).optional(),
62
+ reflection: reflectionPayloadSchema,
63
+ }).strict();
64
+ export const storedReflectionSchema = z.object({
65
+ id: z.number().int().positive(),
66
+ sessionId: z.string().min(1),
67
+ createdAt: z.string().min(1),
68
+ label: z.string().min(1).optional(),
69
+ agentName: z.string().min(1).optional(),
70
+ sourceState: z.string().min(1).optional(),
71
+ reflection: reflectionPayloadSchema,
72
+ }).strict();
@@ -1,5 +1,6 @@
1
1
  import type { ZodType } from 'zod';
2
2
  import type { BaseEvent } from './base-event';
3
+ import type { RecordReflectionInput, StoredReflection } from './reflection-types';
3
4
  import type { StoredEvent } from './stored-event';
4
5
  import type { PreconditionResult } from './precondition-result';
5
6
  import type { TranscriptReader } from '../infra/external-clients/transcript/transcript-reader';
@@ -44,6 +45,8 @@ export interface WorkflowEventStore {
44
45
  appendEvents(sessionId: string, events: readonly StoredEvent[]): void;
45
46
  sessionExists(sessionId: string): boolean;
46
47
  hasSessionStarted(sessionId: string): boolean;
48
+ recordReflection(sessionId: string, createdAt: string, input: RecordReflectionInput): StoredReflection;
49
+ listReflections(sessionId: string): readonly StoredReflection[];
47
50
  }
48
51
  /** @riviere-role value-object */
49
52
  export type WorkflowEngineDeps = {
@@ -1,7 +1,7 @@
1
1
  import type { PreconditionResult } from './precondition-result';
2
2
  import type { BashForbiddenConfig } from './workflow-registry';
3
3
  import type { EngineResult, RehydratableWorkflow, WorkflowDefinition, WorkflowEngineDeps } from './workflow-engine-types';
4
- import type { BaseWorkflowState } from './workflow-state';
4
+ import { type BaseWorkflowState } from './workflow-state';
5
5
  /** @riviere-role domain-service */
6
6
  export declare class WorkflowEngine<TWorkflow extends RehydratableWorkflow<TState>, TState extends BaseWorkflowState<TStateName>, TDeps, TStateName extends string = string, TOperation extends string = string> {
7
7
  private readonly factory;
@@ -13,6 +13,7 @@ export declare class WorkflowEngine<TWorkflow extends RehydratableWorkflow<TStat
13
13
  transition(sessionId: string, target: TStateName): EngineResult;
14
14
  checkBash(sessionId: string, toolName: string, command: string, bashForbidden: BashForbiddenConfig): EngineResult;
15
15
  checkWrite(sessionId: string, toolName: string, filePath: string, isWriteAllowed: (filePath: string, state: TState) => boolean): EngineResult;
16
+ getState(sessionId: string): EngineResult;
16
17
  persistSessionId(sessionId: string): void;
17
18
  hasSession(sessionId: string): boolean;
18
19
  hasSessionStarted(sessionId: string): boolean;
@@ -20,5 +21,6 @@ export declare class WorkflowEngine<TWorkflow extends RehydratableWorkflow<TStat
20
21
  private rehydrateFromEvents;
21
22
  private persistEvents;
22
23
  private wrapEvents;
24
+ private applyIdentityGate;
23
25
  private verifyIdentity;
24
26
  }
@@ -2,8 +2,9 @@ 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';
6
- import { WorkflowStateError } from './workflow-state.js';
5
+ import { flattenStoredEvent, toPayload, } from './stored-event.js';
6
+ import { WorkflowStateError, } from './workflow-state.js';
7
+ import { serializeWorkflowState } from './workflow-state-serialization.js';
7
8
  /** @riviere-role domain-service */
8
9
  export class WorkflowEngine {
9
10
  factory;
@@ -40,15 +41,9 @@ export class WorkflowEngine {
40
41
  this.requireSession(sessionId);
41
42
  const workflow = this.rehydrateFromEvents(sessionId);
42
43
  const registry = this.factory.getRegistry();
43
- const identityResult = this.verifyIdentity(sessionId, workflow);
44
- if (identityResult !== undefined) {
45
- this.persistEvents(sessionId, workflow);
46
- const currentPrefix = getExpectedPrefix(workflow.getState().currentStateMachineState, registry);
47
- return {
48
- type: 'blocked',
49
- output: formatOperationGateError(op, identityResult, currentPrefix)
50
- };
51
- }
44
+ const gate = this.applyIdentityGate(sessionId, workflow, op);
45
+ if (gate !== undefined)
46
+ return gate;
52
47
  const result = fn(workflow);
53
48
  this.persistEvents(sessionId, workflow);
54
49
  const currentPrefix = getExpectedPrefix(workflow.getState().currentStateMachineState, registry);
@@ -70,15 +65,9 @@ export class WorkflowEngine {
70
65
  const state = workflow.getState();
71
66
  const currentStateName = state.currentStateMachineState;
72
67
  const registry = this.factory.getRegistry();
73
- const identityResult = this.verifyIdentity(sessionId, workflow);
74
- if (identityResult !== undefined) {
75
- this.persistEvents(sessionId, workflow);
76
- const currentPrefix = getExpectedPrefix(currentStateName, registry);
77
- return {
78
- type: 'blocked',
79
- output: formatOperationGateError('transition', identityResult, currentPrefix)
80
- };
81
- }
68
+ const gate = this.applyIdentityGate(sessionId, workflow, 'transition');
69
+ if (gate !== undefined)
70
+ return gate;
82
71
  const currentDef = registry[currentStateName];
83
72
  if (!currentDef.canTransitionTo.includes(target)) {
84
73
  const legalTargets = currentDef.canTransitionTo;
@@ -133,14 +122,9 @@ export class WorkflowEngine {
133
122
  const registry = this.factory.getRegistry();
134
123
  const currentStateName = workflow.getState().currentStateMachineState;
135
124
  const currentPrefix = getExpectedPrefix(currentStateName, registry);
136
- const identityResult = this.verifyIdentity(sessionId, workflow);
137
- if (identityResult !== undefined) {
138
- this.persistEvents(sessionId, workflow);
139
- return {
140
- type: 'blocked',
141
- output: formatOperationGateError('bash-check', identityResult, currentPrefix)
142
- };
143
- }
125
+ const gate = this.applyIdentityGate(sessionId, workflow, 'bash-check');
126
+ if (gate !== undefined)
127
+ return gate;
144
128
  if (toolName !== 'Bash') {
145
129
  workflow.appendEvent({
146
130
  type: 'bash-checked',
@@ -192,14 +176,9 @@ export class WorkflowEngine {
192
176
  const registry = this.factory.getRegistry();
193
177
  const currentStateName = workflow.getState().currentStateMachineState;
194
178
  const currentPrefix = getExpectedPrefix(currentStateName, registry);
195
- const identityResult = this.verifyIdentity(sessionId, workflow);
196
- if (identityResult !== undefined) {
197
- this.persistEvents(sessionId, workflow);
198
- return {
199
- type: 'blocked',
200
- output: formatOperationGateError('write-check', identityResult, currentPrefix)
201
- };
202
- }
179
+ const gate = this.applyIdentityGate(sessionId, workflow, 'write-check');
180
+ if (gate !== undefined)
181
+ return gate;
203
182
  const writeTools = new Set(['Write', 'Edit', 'NotebookEdit']);
204
183
  if (!writeTools.has(toolName)) {
205
184
  workflow.appendEvent({
@@ -273,6 +252,10 @@ export class WorkflowEngine {
273
252
  output: ''
274
253
  };
275
254
  }
255
+ getState(sessionId) {
256
+ this.requireSession(sessionId);
257
+ return serializeWorkflowState(this.rehydrateFromEvents(sessionId).getState());
258
+ }
276
259
  persistSessionId(sessionId) {
277
260
  this.engineDeps.appendToFile(this.engineDeps.getEnvFilePath(), `export CLAUDE_SESSION_ID='${sessionId}'\n`);
278
261
  }
@@ -317,6 +300,17 @@ export class WorkflowEngine {
317
300
  });
318
301
  return stored;
319
302
  }
303
+ applyIdentityGate(sessionId, workflow, op) {
304
+ const identityResult = this.verifyIdentity(sessionId, workflow);
305
+ if (identityResult === undefined)
306
+ return undefined;
307
+ this.persistEvents(sessionId, workflow);
308
+ const currentPrefix = getExpectedPrefix(workflow.getState().currentStateMachineState, this.factory.getRegistry());
309
+ return {
310
+ type: 'blocked',
311
+ output: formatOperationGateError(op, identityResult, currentPrefix),
312
+ };
313
+ }
320
314
  verifyIdentity(sessionId, workflow) {
321
315
  const transcriptPath = workflow.getTranscriptPath();
322
316
  const state = workflow.getState().currentStateMachineState;
@@ -0,0 +1,3 @@
1
+ import type { EngineResult } from './workflow-engine-types';
2
+ /** @riviere-role domain-service */
3
+ export declare function serializeWorkflowState(state: unknown): EngineResult;
@@ -0,0 +1,16 @@
1
+ /** @riviere-role domain-service */
2
+ export function serializeWorkflowState(state) {
3
+ try {
4
+ return {
5
+ type: 'success',
6
+ output: JSON.stringify(state, null, 2),
7
+ };
8
+ }
9
+ catch (error) {
10
+ const message = error instanceof Error ? error.message : String(error);
11
+ return {
12
+ type: 'error',
13
+ output: `Failed to serialize workflow state: ${message}`,
14
+ };
15
+ }
16
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nt-ai-lab/deterministic-agent-workflow-engine",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {