@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.
- 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 +30 -36
- package/dist/platform/domain/workflow-state-serialization.d.ts +3 -0
- package/dist/platform/domain/workflow-state-serialization.js +16 -0
- package/package.json +1 -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,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,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
|
+
}
|