@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 +2 -0
- package/dist/index.js +1 -0
- package/dist/platform/domain/stored-event.d.ts +28 -0
- package/dist/platform/domain/stored-event.js +22 -0
- package/dist/platform/domain/workflow-engine-types.d.ts +3 -2
- package/dist/platform/domain/workflow-engine.d.ts +1 -0
- package/dist/platform/domain/workflow-engine.js +33 -13
- package/dist/platform/infra/cli/presentation/output-guidance.d.ts +2 -0
- package/dist/platform/infra/cli/presentation/output-guidance.js +17 -2
- package/package.json +1 -1
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
|
|
43
|
-
appendEvents(sessionId: string, events: readonly
|
|
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
|
}
|
|
@@ -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
|
|
159
|
-
if (!
|
|
160
|
-
const reason = `Bash command blocked in ${currentStateName}. ${
|
|
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
|
|
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
|
|
297
|
-
|
|
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
|
|
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:
|
|
331
|
+
status: identityCheckResult.status,
|
|
312
332
|
transcriptPath,
|
|
313
|
-
}]);
|
|
314
|
-
if (
|
|
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
|
}
|