-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathstate-machine.zod.ts
More file actions
146 lines (124 loc) · 5.4 KB
/
Copy pathstate-machine.zod.ts
File metadata and controls
146 lines (124 loc) · 5.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
import { z } from 'zod';
import { SnakeCaseIdentifierSchema } from '../shared/identifiers.zod';
/**
* XState-inspired State Machine Protocol
* Used to define strict business logic constraints and lifecycle management.
* Prevent AI "hallucinations" by enforcing valid valid transitions.
*/
// --- Primitives ---
/**
* References a named action (side effect)
* Can be a script, a webhook, or a field update.
*/
import { lazySchema } from '../shared/lazy-schema';
export const ActionRefSchema = lazySchema(() => z.union([
z.string().describe('Action Name'),
z.object({
type: z.string(), // e.g., 'xstate.assign', 'log', 'email'
params: z.record(z.string(), z.unknown()).optional()
})
]));
/**
* References a named condition (guard)
* Must evaluate to true for the transition to occur.
*/
export const GuardRefSchema = lazySchema(() => z.union([
z.string().describe('Guard Name (e.g., "isManager", "amountGT1000")'),
z.object({
type: z.string(),
params: z.record(z.string(), z.unknown()).optional()
})
]));
// --- Core Structure ---
/**
* State Transition Definition
* "When EVENT happens, if GUARD is true, go to TARGET and run ACTIONS"
*/
export const TransitionSchema = lazySchema(() => z.object({
target: z.string().optional().describe('Target State ID'),
cond: GuardRefSchema.optional().describe('Condition (Guard) required to take this path'),
actions: z.array(ActionRefSchema).optional().describe('Actions to execute during transition'),
description: z.string().optional().describe('Human readable description of this rule'),
}));
// `EventSchema` (XState-style signal declaration `{ type, schema }`) was removed
// here in #4658 (dual-source ledger #4535 C6): nothing in this file — or any
// repo — ever referenced it. Event types on a state machine are the RECORD KEYS
// of `on:` (plain strings), not declared signal objects. The platform's one
// `EventSchema` is the event-bus envelope in `kernel/events/core.zod.ts`; the
// kernel-side analogue of a signal *declaration* is `EventTypeDefinitionSchema`
// in the same file.
export type ActionRef = z.infer<typeof ActionRefSchema>;
export type Transition = z.infer<typeof TransitionSchema>;
export type StateNodeConfig = {
type?: 'atomic' | 'compound' | 'parallel' | 'final' | 'history';
entry?: ActionRef[];
exit?: ActionRef[];
on?: Record<string, string | Transition | Transition[]>;
always?: Transition[];
initial?: string;
states?: Record<string, StateNodeConfig>;
meta?: {
label?: string;
description?: string;
color?: string;
aiInstructions?: string;
};
};
/**
* State Node Definition
*
* Both type arguments are given (#4195) so `z.input` is not `unknown` — a state
* machine is hand-authored, and an `unknown` input type means nothing checks
* what an author writes into `states`.
*
* Caveat worth knowing before trusting it: {@link StateNodeConfig} is written by
* hand in the AUTHORING shape (`type?` is optional), while `type` is
* `.default('atomic')` and so always present once parsed. Using it for both
* arguments is therefore exact on the input side and slightly loose on the
* output side — which is what this schema already claimed before, so nothing
* regressed. Making the output exact means re-deriving `StateNodeConfig` from
* the schema rather than maintaining it beside one; that is a separate change.
*/
export const StateNodeSchema: z.ZodType<StateNodeConfig, StateNodeConfig> = z.lazy(() => z.object({
/** Type of state */
type: z.enum(['atomic', 'compound', 'parallel', 'final', 'history']).default('atomic'),
/** Entry/Exit Actions */
entry: z.array(ActionRefSchema).optional().describe('Actions to run when entering this state'),
exit: z.array(ActionRefSchema).optional().describe('Actions to run when leaving this state'),
/** Transitions (Events) */
on: z.record(z.string(), z.union([
z.string(), // Shorthand target
TransitionSchema,
z.array(TransitionSchema)
])).optional().describe('Map of Event Type -> Transition Definition'),
/** Always Transitions (Eventless) */
always: z.array(TransitionSchema).optional(),
/** Nesting (Hierarchical States) */
initial: z.string().optional().describe('Initial child state (if compound)'),
states: z.record(z.string(), StateNodeSchema).optional(),
/** Metadata for UI/AI */
meta: z.object({
label: z.string().optional(),
description: z.string().optional(),
color: z.string().optional(), // For UI diagrams
// Instructions for AI Agent when in this state
aiInstructions: z.string().optional().describe('Specific instructions for AI when in this state'),
}).optional(),
}));
/**
* Top-Level State Machine Definition
*/
export const StateMachineSchema = lazySchema(() => z.object({
id: SnakeCaseIdentifierSchema.describe('Unique Machine ID'),
description: z.string().optional(),
/** Context (Memory) Schema */
contextSchema: z.record(z.string(), z.unknown()).optional().describe('Zod Schema for the machine context/memory'),
/** Initial State */
initial: z.string().describe('Initial State ID'),
/** State Definitions */
states: z.record(z.string(), StateNodeSchema).describe('State Nodes'),
/** Global Listeners */
on: z.record(z.string(), z.union([z.string(), TransitionSchema, z.array(TransitionSchema)])).optional(),
}));
export type StateMachineConfig = z.infer<typeof StateMachineSchema>;