@nt-ai-lab/deterministic-agent-workflow-engine 0.2.1 → 0.3.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 +2 -0
- package/dist/index.js +1 -0
- package/dist/platform/domain/reflection-types.d.ts +1349 -0
- package/dist/platform/domain/reflection-types.js +72 -0
- package/dist/platform/domain/workflow-engine-types.d.ts +3 -0
- package/dist/platform/domain/workflow-engine.d.ts +3 -1
- package/dist/platform/domain/workflow-engine.js +41 -38
- package/dist/platform/domain/workflow-state-serialization.d.ts +3 -0
- package/dist/platform/domain/workflow-state-serialization.js +16 -0
- package/dist/platform/infra/cli/presentation/output-guidance.spec.d.ts +1 -0
- package/dist/platform/infra/cli/presentation/output-guidance.spec.js +184 -0
- package/package.json +4 -1
|
@@ -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
|
|
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
|
|
44
|
-
if (
|
|
45
|
-
|
|
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
|
|
74
|
-
if (
|
|
75
|
-
|
|
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
|
|
137
|
-
if (
|
|
138
|
-
|
|
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
|
|
196
|
-
if (
|
|
197
|
-
|
|
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,11 +300,21 @@ 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;
|
|
323
317
|
const registry = this.factory.getRegistry();
|
|
324
|
-
const expectedPrefix = getExpectedPrefix(state, registry);
|
|
325
318
|
const pattern = buildPrefixPattern(registry);
|
|
326
319
|
const messages = this.engineDeps.transcriptReader.readMessages(transcriptPath);
|
|
327
320
|
const identityCheckResult = checkIdentity(messages, pattern);
|
|
@@ -332,7 +325,17 @@ export class WorkflowEngine {
|
|
|
332
325
|
transcriptPath,
|
|
333
326
|
}], workflow.getState()));
|
|
334
327
|
if (identityCheckResult.status === 'lost') {
|
|
335
|
-
|
|
328
|
+
const currentProcedure = readProcedure(this.engineDeps, state);
|
|
329
|
+
return [
|
|
330
|
+
'Your last message is missing the required state prefix.',
|
|
331
|
+
'',
|
|
332
|
+
`- send a new message starting with: ${getExpectedPrefix(state, registry)}`,
|
|
333
|
+
'- then continue with the current procedure',
|
|
334
|
+
'',
|
|
335
|
+
'Current procedure:',
|
|
336
|
+
'',
|
|
337
|
+
currentProcedure,
|
|
338
|
+
].join('\n');
|
|
336
339
|
}
|
|
337
340
|
return undefined;
|
|
338
341
|
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { WorkflowEngine, pass, } from '../../../../index.js';
|
|
3
|
+
import { SEPARATOR } from './output-guidance.js';
|
|
4
|
+
function isSessionStartedEvent(event) {
|
|
5
|
+
return event.type === 'session-started';
|
|
6
|
+
}
|
|
7
|
+
class PlanningWorkflow {
|
|
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
|
+
this.pendingEvents = [...this.pendingEvents, event];
|
|
19
|
+
if (isSessionStartedEvent(event)) {
|
|
20
|
+
this.state = {
|
|
21
|
+
...this.state,
|
|
22
|
+
transcriptPath: event.transcriptPath,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
getPendingEvents() {
|
|
27
|
+
return this.pendingEvents;
|
|
28
|
+
}
|
|
29
|
+
startSession(transcriptPath, _repository) {
|
|
30
|
+
void _repository;
|
|
31
|
+
this.state = {
|
|
32
|
+
...this.state,
|
|
33
|
+
transcriptPath,
|
|
34
|
+
};
|
|
35
|
+
this.pendingEvents = [...this.pendingEvents, {
|
|
36
|
+
type: 'session-started',
|
|
37
|
+
at: '2026-01-01T00:00:00Z',
|
|
38
|
+
transcriptPath,
|
|
39
|
+
currentState: this.state.currentStateMachineState,
|
|
40
|
+
states: ['PLANNING'],
|
|
41
|
+
}];
|
|
42
|
+
}
|
|
43
|
+
getTranscriptPath() {
|
|
44
|
+
return this.state.transcriptPath;
|
|
45
|
+
}
|
|
46
|
+
registerAgent(_agentType, _agentId) {
|
|
47
|
+
void _agentType;
|
|
48
|
+
void _agentId;
|
|
49
|
+
return pass();
|
|
50
|
+
}
|
|
51
|
+
handleTeammateIdle(_agentName) {
|
|
52
|
+
void _agentName;
|
|
53
|
+
return pass();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
class ReflectionStoreNotConfiguredError extends Error {
|
|
57
|
+
constructor() {
|
|
58
|
+
super('Reflection storage is not configured for this test');
|
|
59
|
+
this.name = 'ReflectionStoreNotConfiguredError';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
class MemoryStore {
|
|
63
|
+
eventsBySessionId = new Map();
|
|
64
|
+
readEvents(sessionId) {
|
|
65
|
+
return this.eventsBySessionId.get(sessionId) ?? [];
|
|
66
|
+
}
|
|
67
|
+
appendEvents(sessionId, events) {
|
|
68
|
+
const existingEvents = this.eventsBySessionId.get(sessionId) ?? [];
|
|
69
|
+
this.eventsBySessionId.set(sessionId, [...existingEvents, ...events]);
|
|
70
|
+
}
|
|
71
|
+
sessionExists(sessionId) {
|
|
72
|
+
return this.eventsBySessionId.has(sessionId);
|
|
73
|
+
}
|
|
74
|
+
hasSessionStarted(sessionId) {
|
|
75
|
+
return this.eventsBySessionId.get(sessionId)?.some((event) => {
|
|
76
|
+
return event.envelope.type === 'session-started';
|
|
77
|
+
}) ?? false;
|
|
78
|
+
}
|
|
79
|
+
recordReflection(_sessionId, _createdAt, _input) {
|
|
80
|
+
void _sessionId;
|
|
81
|
+
void _createdAt;
|
|
82
|
+
void _input;
|
|
83
|
+
throw new ReflectionStoreNotConfiguredError();
|
|
84
|
+
}
|
|
85
|
+
listReflections() {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const procedureContent = 'PLANNING instructions';
|
|
90
|
+
const workflowDefinition = {
|
|
91
|
+
fold(state, event) {
|
|
92
|
+
if (!isSessionStartedEvent(event)) {
|
|
93
|
+
return state;
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
...state,
|
|
97
|
+
transcriptPath: event.transcriptPath,
|
|
98
|
+
};
|
|
99
|
+
},
|
|
100
|
+
buildWorkflow(state, _deps) {
|
|
101
|
+
void _deps;
|
|
102
|
+
return new PlanningWorkflow(state);
|
|
103
|
+
},
|
|
104
|
+
stateSchema: z.literal('PLANNING'),
|
|
105
|
+
initialState() {
|
|
106
|
+
return {
|
|
107
|
+
currentStateMachineState: 'PLANNING',
|
|
108
|
+
transcriptPath: '',
|
|
109
|
+
};
|
|
110
|
+
},
|
|
111
|
+
getRegistry() {
|
|
112
|
+
return {
|
|
113
|
+
PLANNING: {
|
|
114
|
+
emoji: '🧭',
|
|
115
|
+
agentInstructions: procedureContent,
|
|
116
|
+
canTransitionTo: [],
|
|
117
|
+
allowedWorkflowOperations: ['write'],
|
|
118
|
+
forbidden: { write: true },
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
},
|
|
122
|
+
buildTransitionContext(state, from, to) {
|
|
123
|
+
return {
|
|
124
|
+
state,
|
|
125
|
+
from,
|
|
126
|
+
to,
|
|
127
|
+
gitInfo: {
|
|
128
|
+
currentBranch: 'main',
|
|
129
|
+
workingTreeClean: true,
|
|
130
|
+
headCommit: 'abc123',
|
|
131
|
+
changedFilesVsDefault: [],
|
|
132
|
+
hasCommitsVsDefault: false,
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
const transcriptReader = {
|
|
138
|
+
readMessages: () => [{
|
|
139
|
+
id: 'message-1',
|
|
140
|
+
textContent: 'plain text without the expected prefix',
|
|
141
|
+
}],
|
|
142
|
+
};
|
|
143
|
+
const engineDeps = {
|
|
144
|
+
store: new MemoryStore(),
|
|
145
|
+
getPluginRoot: () => '/plugin-root',
|
|
146
|
+
getEnvFilePath: () => '/plugin-root/.env',
|
|
147
|
+
readFile: (path) => {
|
|
148
|
+
if (path === '/plugin-root/states/planning.md') {
|
|
149
|
+
return procedureContent;
|
|
150
|
+
}
|
|
151
|
+
return '';
|
|
152
|
+
},
|
|
153
|
+
appendToFile: (filePath, content) => {
|
|
154
|
+
void filePath;
|
|
155
|
+
void content;
|
|
156
|
+
},
|
|
157
|
+
now: () => '2026-01-01T00:00:00Z',
|
|
158
|
+
transcriptReader,
|
|
159
|
+
};
|
|
160
|
+
it('reinserts the current procedure when identity verification fails', () => {
|
|
161
|
+
const emptyWorkflowDeps = {};
|
|
162
|
+
const engine = new WorkflowEngine(workflowDefinition, engineDeps, emptyWorkflowDeps);
|
|
163
|
+
engine.startSession('session-1', '/transcripts/session-1.jsonl');
|
|
164
|
+
const result = engine.checkWrite('session-1', 'Write', '/workspace/note.md', () => true);
|
|
165
|
+
const expectedOutput = [
|
|
166
|
+
'Cannot write-check',
|
|
167
|
+
SEPARATOR,
|
|
168
|
+
'Your last message is missing the required state prefix.',
|
|
169
|
+
'',
|
|
170
|
+
'- send a new message starting with: 🧭 PLANNING',
|
|
171
|
+
'- then continue with the current procedure',
|
|
172
|
+
'',
|
|
173
|
+
'Current procedure:',
|
|
174
|
+
'',
|
|
175
|
+
procedureContent,
|
|
176
|
+
'',
|
|
177
|
+
'Next message MUST begin with: 🧭 PLANNING',
|
|
178
|
+
].join('\n');
|
|
179
|
+
const expectedResult = {
|
|
180
|
+
type: 'blocked',
|
|
181
|
+
output: expectedOutput,
|
|
182
|
+
};
|
|
183
|
+
expect(result).toEqual(expectedResult);
|
|
184
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nt-ai-lab/deterministic-agent-workflow-engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"zod": "^3.25.76"
|
|
15
15
|
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"vitest": "^2.1.9"
|
|
18
|
+
},
|
|
16
19
|
"publishConfig": {
|
|
17
20
|
"access": "public"
|
|
18
21
|
}
|