openoma 0.1.0__tar.gz
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.
- openoma-0.1.0/PKG-INFO +1009 -0
- openoma-0.1.0/README.md +1001 -0
- openoma-0.1.0/pyproject.toml +14 -0
- openoma-0.1.0/setup.cfg +4 -0
- openoma-0.1.0/src/openoma/__init__.py +62 -0
- openoma-0.1.0/src/openoma/adapter/__init__.py +4 -0
- openoma-0.1.0/src/openoma/adapter/base.py +82 -0
- openoma-0.1.0/src/openoma/adapter/protocol.py +42 -0
- openoma-0.1.0/src/openoma/builders/__init__.py +5 -0
- openoma-0.1.0/src/openoma/builders/contract.py +118 -0
- openoma-0.1.0/src/openoma/builders/flow.py +108 -0
- openoma-0.1.0/src/openoma/builders/work_block.py +78 -0
- openoma-0.1.0/src/openoma/core/__init__.py +29 -0
- openoma-0.1.0/src/openoma/core/condition.py +15 -0
- openoma-0.1.0/src/openoma/core/contract.py +40 -0
- openoma-0.1.0/src/openoma/core/flow.py +41 -0
- openoma-0.1.0/src/openoma/core/types.py +42 -0
- openoma-0.1.0/src/openoma/core/versioning.py +19 -0
- openoma-0.1.0/src/openoma/core/work_block.py +24 -0
- openoma-0.1.0/src/openoma/execution/__init__.py +27 -0
- openoma-0.1.0/src/openoma/execution/block_execution.py +50 -0
- openoma-0.1.0/src/openoma/execution/contract_execution.py +54 -0
- openoma-0.1.0/src/openoma/execution/events.py +48 -0
- openoma-0.1.0/src/openoma/execution/flow_execution.py +74 -0
- openoma-0.1.0/src/openoma/execution/types.py +34 -0
- openoma-0.1.0/src/openoma/store/__init__.py +4 -0
- openoma-0.1.0/src/openoma/store/memory.py +33 -0
- openoma-0.1.0/src/openoma/store/protocol.py +30 -0
- openoma-0.1.0/src/openoma/validation/__init__.py +29 -0
- openoma-0.1.0/src/openoma/validation/graph.py +70 -0
- openoma-0.1.0/src/openoma/validation/references.py +102 -0
- openoma-0.1.0/src/openoma.egg-info/PKG-INFO +1009 -0
- openoma-0.1.0/src/openoma.egg-info/SOURCES.txt +51 -0
- openoma-0.1.0/src/openoma.egg-info/dependency_links.txt +1 -0
- openoma-0.1.0/src/openoma.egg-info/requires.txt +1 -0
- openoma-0.1.0/src/openoma.egg-info/top_level.txt +1 -0
- openoma-0.1.0/tests/test_adapter.py +24 -0
- openoma-0.1.0/tests/test_block_execution.py +66 -0
- openoma-0.1.0/tests/test_builders.py +125 -0
- openoma-0.1.0/tests/test_condition.py +12 -0
- openoma-0.1.0/tests/test_contract.py +30 -0
- openoma-0.1.0/tests/test_contract_execution.py +41 -0
- openoma-0.1.0/tests/test_core_types.py +39 -0
- openoma-0.1.0/tests/test_events.py +24 -0
- openoma-0.1.0/tests/test_execution_types.py +22 -0
- openoma-0.1.0/tests/test_flow.py +43 -0
- openoma-0.1.0/tests/test_flow_execution.py +93 -0
- openoma-0.1.0/tests/test_smoke.py +49 -0
- openoma-0.1.0/tests/test_store.py +52 -0
- openoma-0.1.0/tests/test_validation_graph.py +90 -0
- openoma-0.1.0/tests/test_validation_references.py +96 -0
- openoma-0.1.0/tests/test_versioning.py +18 -0
- openoma-0.1.0/tests/test_work_block.py +31 -0
openoma-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,1009 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: openoma
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Opera Autonoma is designed to be a versatile and powerful tool for automating various tasks and processes. It provides a user-friendly interface and a wide range of features to help users streamline their workflows and increase productivity.
|
|
5
|
+
Requires-Python: >=3.13
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: pydantic>=2.12.5
|
|
8
|
+
|
|
9
|
+
# Openoma (Opera Autonoma) Specification
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
> **I vibe-coded this library via Claude Opus 4.6**
|
|
13
|
+
---
|
|
14
|
+
> **Open Operational Model Architecture** — A declarative framework for modeling,
|
|
15
|
+
> composing, and tracking operational processes in organizations.
|
|
16
|
+
|
|
17
|
+
**Version:** 0.1.0-draft
|
|
18
|
+
**Status:** Draft
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 1. Philosophy
|
|
23
|
+
|
|
24
|
+
Openoma separates **what work is** from **how work is done**.
|
|
25
|
+
|
|
26
|
+
The framework is built on three principles:
|
|
27
|
+
|
|
28
|
+
1. **Declarative over imperative.** Every entity — blocks, flows, contracts,
|
|
29
|
+
conditions, assessments — describes *what* should happen, never *how* to
|
|
30
|
+
execute it. Execution is a separate concern.
|
|
31
|
+
2. **Composability.** Small units compose into larger units. Work blocks compose
|
|
32
|
+
into flows. Flows compose into contracts. Assessment itself is a flow.
|
|
33
|
+
3. **Identity and immutability.** Every entity has a UUID. Every mutation
|
|
34
|
+
produces a new version. Executions reference a specific version, so the
|
|
35
|
+
definition of work never shifts under a running process.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 2. Core Module — `openoma.core`
|
|
40
|
+
|
|
41
|
+
The core module defines the logical model of operational processes. It contains
|
|
42
|
+
no execution semantics.
|
|
43
|
+
|
|
44
|
+
### 2.1 WorkBlock
|
|
45
|
+
|
|
46
|
+
A **WorkBlock** is the atomic unit of operational work. It declares what inputs
|
|
47
|
+
are needed and what outputs will be produced — like a single entry from a team's
|
|
48
|
+
runbook.
|
|
49
|
+
|
|
50
|
+
A WorkBlock is **reusable**: it can appear in many flows because it describes
|
|
51
|
+
*what* needs to be done, not a specific instance of doing it.
|
|
52
|
+
|
|
53
|
+
| Field | Type | Description |
|
|
54
|
+
|-------------------|-------------------------------|-----------------------------------------------------------------------------|
|
|
55
|
+
| `id` | `UUID` | Stable identity across versions. |
|
|
56
|
+
| `version` | `int` | Monotonically increasing. `(id, version)` is globally unique. |
|
|
57
|
+
| `name` | `str` | Human-readable name. |
|
|
58
|
+
| `description` | `str` | Natural-language description of the work to be performed. |
|
|
59
|
+
| `inputs` | `list[PortDescriptor]` | Declared input ports. |
|
|
60
|
+
| `outputs` | `list[PortDescriptor]` | Declared output ports. |
|
|
61
|
+
| `execution_hints` | `list[str]` | Optional capability tags (e.g. `"sql-access"`, `"api-gateway"`). Never names a specific executor. |
|
|
62
|
+
| `metadata` | `dict` | Arbitrary key-value pairs (tags, labels, ownership). |
|
|
63
|
+
|
|
64
|
+
#### PortDescriptor
|
|
65
|
+
|
|
66
|
+
A **PortDescriptor** names and describes a single input or output of a WorkBlock.
|
|
67
|
+
|
|
68
|
+
| Field | Type | Description |
|
|
69
|
+
|---------------|----------------|---------------------------------------------------------------|
|
|
70
|
+
| `name` | `str` | Identifier unique within the block's input or output list. |
|
|
71
|
+
| `description` | `str` | Natural-language description. |
|
|
72
|
+
| `required` | `bool` | Whether this port must be supplied / will always be produced. |
|
|
73
|
+
| `schema` | `dict \| None` | Optional. A JSON Schema document describing the expected shape of data on this port. The framework does not enforce it; consumers and adapters may validate against it. |
|
|
74
|
+
| `metadata` | `dict` | Optional. Hints for consumers (e.g. expected media type). |
|
|
75
|
+
|
|
76
|
+
> **Design decision — execution hints.** WorkBlocks may carry *capability hints*
|
|
77
|
+
> that describe what tooling or access is needed, but they never name a specific
|
|
78
|
+
> executor. The binding of a block to an executor is the responsibility of the
|
|
79
|
+
> Execution module. This keeps the core model clean while still allowing
|
|
80
|
+
> execution planners to filter and match blocks to capable executors.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
### 2.2 Condition
|
|
85
|
+
|
|
86
|
+
A **Condition** is a **value object** embedded in an edge. It governs traversal
|
|
87
|
+
between nodes in a flow. Conditions are declarative — the framework stores them
|
|
88
|
+
but never evaluates them. Evaluation is the responsibility of an external
|
|
89
|
+
execution engine.
|
|
90
|
+
|
|
91
|
+
Conditions are not first-class entities. They have no UUID and no version
|
|
92
|
+
lineage. They exist only as part of the edge that contains them. If the same
|
|
93
|
+
logical condition is needed on multiple edges, it is duplicated (or the
|
|
94
|
+
consuming application maintains a condition template library outside Openoma).
|
|
95
|
+
|
|
96
|
+
| Field | Type | Description |
|
|
97
|
+
|---------------|-----------------|--------------------------------------------------------------------|
|
|
98
|
+
| `description` | `str` | Natural-language description (prompt-like). Always present. |
|
|
99
|
+
| `predicate` | `dict \| None` | Optional structured predicate (e.g. JSONLogic, CEL expression, or a simple expression tree). |
|
|
100
|
+
| `metadata` | `dict` | Optional. |
|
|
101
|
+
|
|
102
|
+
A Condition with only a `description` is evaluated by a human or an LLM agent.
|
|
103
|
+
A Condition with a `predicate` can be evaluated programmatically. Both can
|
|
104
|
+
coexist — the description serves as documentation, the predicate as machine
|
|
105
|
+
logic.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
### 2.3 Flow
|
|
110
|
+
|
|
111
|
+
A **Flow** is a directed graph of nodes connected by edges with optional
|
|
112
|
+
conditions. It is the primary composition mechanism in Openoma.
|
|
113
|
+
|
|
114
|
+
Key properties:
|
|
115
|
+
|
|
116
|
+
- A flow is **not necessarily fully connected**. It may contain multiple
|
|
117
|
+
disconnected subgraphs representing independent work streams.
|
|
118
|
+
- A flow's nodes reference **WorkBlocks**.
|
|
119
|
+
- A flow can appear in multiple contracts.
|
|
120
|
+
|
|
121
|
+
#### 2.3.1 NodeReference
|
|
122
|
+
|
|
123
|
+
A **NodeReference** is a *slot* within a flow that points to a WorkBlock. The
|
|
124
|
+
same WorkBlock can be referenced multiple times within the same flow (e.g. two
|
|
125
|
+
parallel lanes doing the same type of work with different executors). Each slot
|
|
126
|
+
has its own UUID so that executions, edges, and conditions can address it
|
|
127
|
+
unambiguously.
|
|
128
|
+
|
|
129
|
+
| Field | Type | Description |
|
|
130
|
+
|------------------|----------------------------|------------------------------------------------------------------|
|
|
131
|
+
| `id` | `UUID` | Unique slot identifier within this flow. |
|
|
132
|
+
| `target_id` | `UUID` | The referenced WorkBlock ID. |
|
|
133
|
+
| `target_version` | `int` | The pinned version of the target. |
|
|
134
|
+
| `alias` | `str \| None` | Optional human label (e.g. `"primary-review"`, `"backup-review"`). |
|
|
135
|
+
| `metadata` | `dict` | Optional. |
|
|
136
|
+
|
|
137
|
+
> **Design decision — same block, two slots.** When the same task must be
|
|
138
|
+
> performed in parallel (e.g. by different teams), the flow creates two
|
|
139
|
+
> NodeReferences pointing to the same WorkBlock. The core model is identical;
|
|
140
|
+
> differentiation happens in the Execution module where each slot is assigned
|
|
141
|
+
> to a different executor.
|
|
142
|
+
|
|
143
|
+
#### 2.3.2 Edge
|
|
144
|
+
|
|
145
|
+
An **Edge** connects two nodes in the flow's directed graph.
|
|
146
|
+
|
|
147
|
+
| Field | Type | Description |
|
|
148
|
+
|-----------------|------------------------|---------------------------------------------------------------|
|
|
149
|
+
| `source_id` | `UUID \| None` | Source NodeReference ID. `None` marks an **entry edge** — the node is a starting point. |
|
|
150
|
+
| `target_id` | `UUID` | Target NodeReference ID. |
|
|
151
|
+
| `condition` | `Condition \| None` | Traversal condition. `None` means unconditional. |
|
|
152
|
+
| `port_mappings` | `list[PortMapping]` | How outputs of the source node map to inputs of the target node. Empty list if no explicit mapping (adapter uses convention). |
|
|
153
|
+
|
|
154
|
+
- Entry edges (`source_id = None`) designate starting nodes of the flow or of
|
|
155
|
+
a disconnected subgraph. Port mappings on entry edges describe how external
|
|
156
|
+
inputs to the flow are wired to the entry node's input ports.
|
|
157
|
+
- Terminal nodes are nodes with no outgoing edges.
|
|
158
|
+
|
|
159
|
+
#### PortMapping
|
|
160
|
+
|
|
161
|
+
A **PortMapping** connects an output port on the source node to an input port
|
|
162
|
+
on the target node.
|
|
163
|
+
|
|
164
|
+
| Field | Type | Description |
|
|
165
|
+
|---------------|--------|-------------------------------------------------------|
|
|
166
|
+
| `source_port` | `str` | Name of an output port on the source node. |
|
|
167
|
+
| `target_port` | `str` | Name of an input port on the target node. |
|
|
168
|
+
|
|
169
|
+
Port mappings are optional. When omitted, adapters may apply convention-based
|
|
170
|
+
wiring (e.g. matching port names). When present, they make data flow explicit
|
|
171
|
+
and portable across adapters.
|
|
172
|
+
|
|
173
|
+
#### 2.3.3 Flow Entity
|
|
174
|
+
|
|
175
|
+
| Field | Type | Description |
|
|
176
|
+
|--------------------|--------------------------|--------------------------------------------------------------------|
|
|
177
|
+
| `id` | `UUID` | Stable identity. |
|
|
178
|
+
| `version` | `int` | Monotonically increasing. |
|
|
179
|
+
| `name` | `str` | Human-readable name. |
|
|
180
|
+
| `description` | `str` | Natural-language description. |
|
|
181
|
+
| `nodes` | `list[NodeReference]` | All slots in this flow. |
|
|
182
|
+
| `edges` | `list[Edge]` | Directed edges between slots, with optional conditions. |
|
|
183
|
+
| `expected_outcome` | `Any \| None` | Opaque blob. Declared upfront: what this flow is meant to achieve. |
|
|
184
|
+
| `metadata` | `dict` | Optional. |
|
|
185
|
+
|
|
186
|
+
> **Interpreted outcome.** A flow's *interpreted outcome* is not stored in the
|
|
187
|
+
> core model. It is derived at execution time from the actual outputs of its
|
|
188
|
+
> terminal nodes. The core model only stores the *expected* outcome as a
|
|
189
|
+
> declarative goal.
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
### 2.4 Contract
|
|
194
|
+
|
|
195
|
+
A **Contract** is the highest-level organizational unit. It models an
|
|
196
|
+
organizational commitment — a mission, an SLA, a project charter — with two
|
|
197
|
+
sides:
|
|
198
|
+
|
|
199
|
+
| Side | Contains |
|
|
200
|
+
|-------------------|----------------------------------------------------------------------------|
|
|
201
|
+
| **Work side** | Workflows (FlowReferences) and/or sub-contracts (ContractReferences) that carry out the work. |
|
|
202
|
+
| **Outcome side** | Required outcomes (declarative goals) and assessment flows that evaluate whether goals are met. |
|
|
203
|
+
|
|
204
|
+
#### 2.4.1 RequiredOutcome
|
|
205
|
+
|
|
206
|
+
A **RequiredOutcome** is a named, opaque declaration of what the contract must
|
|
207
|
+
achieve.
|
|
208
|
+
|
|
209
|
+
| Field | Type | Description |
|
|
210
|
+
|---------------|----------|----------------------------------------------------|
|
|
211
|
+
| `id` | `UUID` | Identity. |
|
|
212
|
+
| `name` | `str` | Human label (e.g. `"99.9% uptime"`, `"Q3 revenue target"`). |
|
|
213
|
+
| `description` | `str` | Natural-language specification of the outcome. |
|
|
214
|
+
| `metadata` | `dict` | Optional. |
|
|
215
|
+
|
|
216
|
+
#### 2.4.2 Assessment Flows
|
|
217
|
+
|
|
218
|
+
Assessment is modeled as a **regular Flow** with one constraint:
|
|
219
|
+
|
|
220
|
+
> **An assessment flow must have exactly one terminal node.** The output of that
|
|
221
|
+
> terminal node is the assessment result for the required outcome it evaluates.
|
|
222
|
+
|
|
223
|
+
Assessment flows are first-class flows. They can contain WorkBlocks that pull
|
|
224
|
+
data from test executions, compare against required outcomes, compute scores,
|
|
225
|
+
and produce a final verdict. Because they are flows, they themselves can be
|
|
226
|
+
executed, tracked, versioned, and even assessed by higher-level contracts.
|
|
227
|
+
|
|
228
|
+
The Contract links each RequiredOutcome to one or more assessment flows. The
|
|
229
|
+
assessment flows may consume:
|
|
230
|
+
|
|
231
|
+
- Outputs from the contract's **work-side flows** (the work being assessed).
|
|
232
|
+
- Outputs from dedicated **test flows** (flows specifically designed to probe
|
|
233
|
+
outcomes, e.g. integration tests, audits, KPI computations).
|
|
234
|
+
- Assessment results from **sub-contracts**.
|
|
235
|
+
|
|
236
|
+
#### 2.4.3 ContractReference
|
|
237
|
+
|
|
238
|
+
| Field | Type | Description |
|
|
239
|
+
|--------------------|----------------|--------------------------------------|
|
|
240
|
+
| `contract_id` | `UUID` | Referenced sub-contract. |
|
|
241
|
+
| `contract_version` | `int` | Pinned version. |
|
|
242
|
+
| `alias` | `str \| None` | Optional human label. |
|
|
243
|
+
| `metadata` | `dict` | Optional. |
|
|
244
|
+
|
|
245
|
+
#### 2.4.4 AssessmentBinding
|
|
246
|
+
|
|
247
|
+
An **AssessmentBinding** links a RequiredOutcome to its assessment mechanism.
|
|
248
|
+
|
|
249
|
+
| Field | Type | Description |
|
|
250
|
+
|-----------------------|--------------------------|----------------------------------------------------------------|
|
|
251
|
+
| `required_outcome_id` | `UUID` | The outcome being assessed. |
|
|
252
|
+
| `assessment_flow_id` | `UUID` | The flow that performs the assessment. |
|
|
253
|
+
| `assessment_flow_version` | `int` | Pinned version. |
|
|
254
|
+
| `test_flow_refs` | `list[FlowReference]` | Flows whose execution outputs are supplied as inputs to the assessment flow's entry nodes. The adapter is responsible for wiring test flow terminal-node outputs to assessment flow entry-node inputs. |
|
|
255
|
+
| `metadata` | `dict` | Optional. |
|
|
256
|
+
|
|
257
|
+
#### 2.4.5 Contract Entity
|
|
258
|
+
|
|
259
|
+
| Field | Type | Description |
|
|
260
|
+
|------------------------|-----------------------------|-------------------------------------------------------|
|
|
261
|
+
| `id` | `UUID` | Stable identity. |
|
|
262
|
+
| `version` | `int` | Monotonically increasing. |
|
|
263
|
+
| `name` | `str` | Human-readable name. |
|
|
264
|
+
| `description` | `str` | Natural-language description. |
|
|
265
|
+
| `work_flows` | `list[FlowReference]` | Operational workflows on the work side. |
|
|
266
|
+
| `sub_contracts` | `list[ContractReference]` | Nested contracts on the work side. |
|
|
267
|
+
| `required_outcomes` | `list[RequiredOutcome]` | Declared goals on the outcome side. |
|
|
268
|
+
| `assessment_bindings` | `list[AssessmentBinding]` | Links outcomes to assessment flows and test flows. |
|
|
269
|
+
| `metadata` | `dict` | Optional. |
|
|
270
|
+
|
|
271
|
+
#### FlowReference
|
|
272
|
+
|
|
273
|
+
Used anywhere a flow is referenced by another entity.
|
|
274
|
+
|
|
275
|
+
| Field | Type | Description |
|
|
276
|
+
|----------------|----------------|---------------------------|
|
|
277
|
+
| `flow_id` | `UUID` | Referenced flow. |
|
|
278
|
+
| `flow_version` | `int` | Pinned version. |
|
|
279
|
+
| `alias` | `str \| None` | Optional human label to distinguish multiple references to the same flow. |
|
|
280
|
+
| `metadata` | `dict` | Optional. |
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
### 2.5 Versioning Model
|
|
285
|
+
|
|
286
|
+
All primary entities (WorkBlock, Flow, Contract) follow the same versioning
|
|
287
|
+
scheme:
|
|
288
|
+
|
|
289
|
+
- **`id`** (`UUID`) is the stable identity of the entity across all versions.
|
|
290
|
+
- **`version`** (`int`) starts at `1` and increments with each mutation.
|
|
291
|
+
- The pair `(id, version)` is globally unique and immutable once created.
|
|
292
|
+
- All references to an entity pin a specific version (`target_id` +
|
|
293
|
+
`target_version`, `flow_id` + `flow_version`, etc.).
|
|
294
|
+
- Executions always reference a pinned version, ensuring the definition of work
|
|
295
|
+
cannot change under a running process.
|
|
296
|
+
- Creating a new version does **not** invalidate existing versions. Old versions
|
|
297
|
+
remain accessible for auditing and for in-flight executions.
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
### 2.6 Entity Relationship Summary
|
|
302
|
+
|
|
303
|
+
```
|
|
304
|
+
Contract
|
|
305
|
+
├── work_flows ──────→ Flow (via FlowReference)
|
|
306
|
+
├── sub_contracts ───→ Contract (via ContractReference)
|
|
307
|
+
├── required_outcomes: [RequiredOutcome]
|
|
308
|
+
└── assessment_bindings
|
|
309
|
+
├── required_outcome_id → RequiredOutcome
|
|
310
|
+
├── assessment_flow_id → Flow (assessment flow, 1 terminal node)
|
|
311
|
+
└── test_flow_refs ───→ [Flow] (via FlowReference)
|
|
312
|
+
|
|
313
|
+
Flow
|
|
314
|
+
├── nodes: [NodeReference]
|
|
315
|
+
│ └── target_id → WorkBlock
|
|
316
|
+
└── edges: [Edge]
|
|
317
|
+
├── source_id → NodeReference (or None for entry)
|
|
318
|
+
├── target_id → NodeReference
|
|
319
|
+
├── condition → Condition (value object, optional)
|
|
320
|
+
└── port_mappings → [PortMapping] (source_port → target_port)
|
|
321
|
+
|
|
322
|
+
WorkBlock
|
|
323
|
+
├── inputs: [PortDescriptor] (each with optional schema)
|
|
324
|
+
├── outputs: [PortDescriptor] (each with optional schema)
|
|
325
|
+
└── execution_hints: [str]
|
|
326
|
+
|
|
327
|
+
Execution Interfaces
|
|
328
|
+
├── EventStore (protocol) ← storage backends implement
|
|
329
|
+
└── ExecutionAdapter (protocol, uses EventStore) ← orchestration engines implement
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## 3. Execution Module — `openoma.execution`
|
|
335
|
+
|
|
336
|
+
The execution module records **how** work defined in the core module was carried
|
|
337
|
+
out. It is strictly separate: the core model is a blueprint; the execution model
|
|
338
|
+
is the as-built record.
|
|
339
|
+
|
|
340
|
+
The execution module never modifies core entities. It only references them.
|
|
341
|
+
|
|
342
|
+
### 3.1 Event-Sourced Execution Log
|
|
343
|
+
|
|
344
|
+
All execution state is derived from an **append-only event log**. Execution
|
|
345
|
+
entities (BlockExecution, FlowExecution, ContractExecution) are projections
|
|
346
|
+
of the event stream — they are materialized views, not mutable records.
|
|
347
|
+
|
|
348
|
+
This design is motivated by a key principle from modern agent infrastructure:
|
|
349
|
+
**the session must outlive any single executor.** If an agent crashes, a human
|
|
350
|
+
goes off-shift, or an orchestration engine is swapped, the event log remains
|
|
351
|
+
the durable source of truth. A new executor can resume from the last event
|
|
352
|
+
without data loss.
|
|
353
|
+
|
|
354
|
+
#### ExecutionEvent
|
|
355
|
+
|
|
356
|
+
Every state change in the execution module is recorded as an immutable event.
|
|
357
|
+
|
|
358
|
+
| Field | Type | Description |
|
|
359
|
+
|--------------------|--------------------------|-------------------------------------------------------------------|
|
|
360
|
+
| `id` | `UUID` | Unique event identity. |
|
|
361
|
+
| `timestamp` | `datetime` | When the event was recorded. |
|
|
362
|
+
| `execution_id` | `UUID` | The BlockExecution, FlowExecution, or ContractExecution this event belongs to. |
|
|
363
|
+
| `event_type` | `ExecutionEventType` | The type of state transition (see below). |
|
|
364
|
+
| `executor` | `ExecutorInfo \| None` | The executor active at the time of this event (if applicable). |
|
|
365
|
+
| `payload` | `Any` | Event-specific data (opaque blob). |
|
|
366
|
+
| `metadata` | `dict` | Optional. |
|
|
367
|
+
|
|
368
|
+
#### ExecutionEventType
|
|
369
|
+
|
|
370
|
+
| Event Type | Description |
|
|
371
|
+
|-----------------------|-----------------------------------------------------------------------|
|
|
372
|
+
| `created` | Execution entity was initialized. |
|
|
373
|
+
| `executor_assigned` | An executor was assigned or reassigned. |
|
|
374
|
+
| `executor_released` | An executor released the execution (crash, handoff, shift end). |
|
|
375
|
+
| `started` | Work began. |
|
|
376
|
+
| `progress` | Intermediate progress update (opaque payload). |
|
|
377
|
+
| `outcome_produced` | An output was produced (payload contains the outcome blob). |
|
|
378
|
+
| `completed` | Execution finished successfully. |
|
|
379
|
+
| `failed` | Execution finished with an error. |
|
|
380
|
+
| `skipped` | Execution was deliberately skipped. |
|
|
381
|
+
| `cancelled` | Execution was aborted. |
|
|
382
|
+
|
|
383
|
+
#### ExecutionState (derived)
|
|
384
|
+
|
|
385
|
+
The current state of any execution entity is **derived** by replaying its event
|
|
386
|
+
stream. The canonical states are:
|
|
387
|
+
|
|
388
|
+
| State | Description |
|
|
389
|
+
|---------------|-------------------------------------------------------|
|
|
390
|
+
| `pending` | `created` event exists, no `started` event yet. |
|
|
391
|
+
| `in_progress` | `started` event exists, no terminal event yet. |
|
|
392
|
+
| `completed` | `completed` event exists. |
|
|
393
|
+
| `failed` | `failed` event exists. |
|
|
394
|
+
| `skipped` | `skipped` event exists. |
|
|
395
|
+
| `cancelled` | `cancelled` event exists. |
|
|
396
|
+
|
|
397
|
+
> **Resumability.** Because state is derived from events, any executor can
|
|
398
|
+
> pick up an execution by reading the event log. The `executor_assigned` and
|
|
399
|
+
> `executor_released` events make handoffs explicit. An orchestration engine
|
|
400
|
+
> that crashes simply stops emitting events; a replacement reads the log up
|
|
401
|
+
> to the last event and resumes.
|
|
402
|
+
|
|
403
|
+
### 3.2 ExecutorInfo
|
|
404
|
+
|
|
405
|
+
Executor identity is recorded **per event**, not per execution, because
|
|
406
|
+
executors can change over the lifetime of an execution (crash recovery, shift
|
|
407
|
+
handoff, escalation from agent to human).
|
|
408
|
+
|
|
409
|
+
| Field | Type | Description |
|
|
410
|
+
|--------------|---------------------------------|--------------------------------------------------|
|
|
411
|
+
| `type` | `"human" \| "agent" \| "system"` | Category of executor. |
|
|
412
|
+
| `identifier` | `str` | Unique identifier (user ID, agent name, system URI). |
|
|
413
|
+
| `metadata` | `dict` | Optional (team, role, model version, etc.). |
|
|
414
|
+
|
|
415
|
+
> **Executor continuity.** The execution log naturally records the full
|
|
416
|
+
> lineage of who worked on what. If agent A starts a block, crashes, and
|
|
417
|
+
> agent B resumes, the event stream contains:
|
|
418
|
+
> `executor_assigned(A)` → `started` → `executor_released(A)` →
|
|
419
|
+
> `executor_assigned(B)` → `completed`. No information is lost.
|
|
420
|
+
|
|
421
|
+
### 3.3 BlockExecution
|
|
422
|
+
|
|
423
|
+
A **BlockExecution** is a projection over the event stream for a single
|
|
424
|
+
WorkBlock execution within a specific flow context.
|
|
425
|
+
|
|
426
|
+
| Field | Type | Description |
|
|
427
|
+
|--------------------|--------------------------|-------------------------------------------------------------------|
|
|
428
|
+
| `id` | `UUID` | Unique execution identity. |
|
|
429
|
+
| `node_reference_id`| `UUID` | The NodeReference (slot) in the flow that was executed. |
|
|
430
|
+
| `work_block_id` | `UUID` | The WorkBlock that was executed. |
|
|
431
|
+
| `work_block_version` | `int` | Pinned version. |
|
|
432
|
+
| `state` | `ExecutionState` | Derived from event stream. |
|
|
433
|
+
| `events` | `list[ExecutionEvent]` | The ordered event log for this execution. |
|
|
434
|
+
|
|
435
|
+
Inputs, outcomes, executor assignments, timing, and attempt counts are all
|
|
436
|
+
read from the event stream rather than stored as top-level fields.
|
|
437
|
+
|
|
438
|
+
### 3.4 FlowExecution
|
|
439
|
+
|
|
440
|
+
A **FlowExecution** records the execution of a flow as a whole. Its state is
|
|
441
|
+
**derived** from its block executions — it is not set independently.
|
|
442
|
+
|
|
443
|
+
| Field | Type | Description |
|
|
444
|
+
|---------------------|-----------------------------|-----------------------------------------------------------------|
|
|
445
|
+
| `id` | `UUID` | Unique execution identity. |
|
|
446
|
+
| `flow_id` | `UUID` | The flow being executed. |
|
|
447
|
+
| `flow_version` | `int` | Pinned version. |
|
|
448
|
+
| `block_executions` | `list[UUID]` | BlockExecution IDs. **Many-to-many**: a BlockExecution can belong to multiple FlowExecutions. |
|
|
449
|
+
| `state` | `ExecutionState` | Derived from constituent block executions. |
|
|
450
|
+
| `events` | `list[ExecutionEvent]` | Flow-level events (e.g. flow started, flow completed). |
|
|
451
|
+
|
|
452
|
+
> **Shared execution outcomes.** A single BlockExecution (with its UUID and
|
|
453
|
+
> outcome) can be referenced by multiple FlowExecutions if the work it
|
|
454
|
+
> represents satisfies requirements in all of them. This avoids redundant
|
|
455
|
+
> execution of identical work.
|
|
456
|
+
|
|
457
|
+
#### FlowExecution State Derivation
|
|
458
|
+
|
|
459
|
+
A FlowExecution's state is derived deterministically from its block executions:
|
|
460
|
+
|
|
461
|
+
| Rule | Condition | Derived State |
|
|
462
|
+
|------|-----------|---------------|
|
|
463
|
+
| 1 | Any block is `failed`. | `failed` |
|
|
464
|
+
| 2 | Any block is `cancelled`. | `cancelled` |
|
|
465
|
+
| 3 | All **terminal nodes** are `completed` or `skipped`. | `completed` |
|
|
466
|
+
| 4 | Any block is `in_progress` or `pending` (and no rule 1–2 applies). | `in_progress` |
|
|
467
|
+
| 5 | All blocks are `pending`. | `pending` |
|
|
468
|
+
|
|
469
|
+
Rules are evaluated in priority order (1 is highest). A terminal node is a
|
|
470
|
+
node with no outgoing edges in the flow definition. Skipped terminal nodes
|
|
471
|
+
count as satisfied — they represent branches that were legitimately not taken.
|
|
472
|
+
|
|
473
|
+
### 3.5 ContractExecution
|
|
474
|
+
|
|
475
|
+
A **ContractExecution** records the execution state of a contract.
|
|
476
|
+
|
|
477
|
+
| Field | Type | Description |
|
|
478
|
+
|----------------------------|------------------------------|--------------------------------------------------------------|
|
|
479
|
+
| `id` | `UUID` | Unique execution identity. |
|
|
480
|
+
| `contract_id` | `UUID` | The contract being executed. |
|
|
481
|
+
| `contract_version` | `int` | Pinned version. |
|
|
482
|
+
| `flow_executions` | `list[UUID]` | FlowExecution IDs for work-side flows. **Many-to-many**: a FlowExecution can belong to multiple ContractExecutions. |
|
|
483
|
+
| `sub_contract_executions` | `list[UUID]` | ContractExecution IDs for sub-contracts. |
|
|
484
|
+
| `assessment_executions` | `list[AssessmentResult]` | Assessment flow execution results. |
|
|
485
|
+
| `state` | `ExecutionState` | Derived from flow and sub-contract executions. |
|
|
486
|
+
| `events` | `list[ExecutionEvent]` | Contract-level events. |
|
|
487
|
+
|
|
488
|
+
#### ContractExecution State Derivation
|
|
489
|
+
|
|
490
|
+
A ContractExecution's state is derived from its flow executions and
|
|
491
|
+
sub-contract executions:
|
|
492
|
+
|
|
493
|
+
| Rule | Condition | Derived State |
|
|
494
|
+
|------|-----------|---------------|
|
|
495
|
+
| 1 | Any flow execution or sub-contract execution is `failed`. | `failed` |
|
|
496
|
+
| 2 | Any flow execution or sub-contract execution is `cancelled`. | `cancelled` |
|
|
497
|
+
| 3 | All flow executions and sub-contract executions are `completed` or `skipped`. | `completed` |
|
|
498
|
+
| 4 | Any is `in_progress` or `pending` (and no rule 1–2 applies). | `in_progress` |
|
|
499
|
+
| 5 | All are `pending`. | `pending` |
|
|
500
|
+
|
|
501
|
+
Assessment flow executions are **not** factored into the contract's work-side
|
|
502
|
+
state derivation. They run independently and their results populate the
|
|
503
|
+
`assessment_executions` list. A contract can be `completed` (all work done)
|
|
504
|
+
but have failing assessments — this is an important distinction between
|
|
505
|
+
"work finished" and "goals met."
|
|
506
|
+
|
|
507
|
+
#### AssessmentResult
|
|
508
|
+
|
|
509
|
+
Links an assessment flow's execution to the required outcome it evaluated.
|
|
510
|
+
|
|
511
|
+
| Field | Type | Description |
|
|
512
|
+
|----------------------------|------------|----------------------------------------------------------|
|
|
513
|
+
| `required_outcome_id` | `UUID` | The outcome being assessed. |
|
|
514
|
+
| `assessment_flow_execution_id` | `UUID` | The FlowExecution of the assessment flow. |
|
|
515
|
+
| `result` | `Any` | The terminal node's output — the assessment verdict. |
|
|
516
|
+
|
|
517
|
+
---
|
|
518
|
+
|
|
519
|
+
## 4. Execution Interfaces — `openoma.adapter` and `openoma.store`
|
|
520
|
+
|
|
521
|
+
Openoma defines the **what** (core) and the **record** (execution). Two
|
|
522
|
+
interfaces connect it to the outside world:
|
|
523
|
+
|
|
524
|
+
- **Execution Adapter** — drives work (orchestration engines implement this).
|
|
525
|
+
- **Event Store** — persists and retrieves the event log (storage backends
|
|
526
|
+
implement this).
|
|
527
|
+
|
|
528
|
+
These are deliberately separate concerns. An orchestration engine (CrewAI,
|
|
529
|
+
Managed Agents, Temporal, a human task board) should not need to implement
|
|
530
|
+
storage. A storage backend (Kafka, EventStoreDB, a NoSQL collection, an
|
|
531
|
+
in-memory test store) should not need to know how work is executed.
|
|
532
|
+
|
|
533
|
+
### 4.1 Event Store Protocol
|
|
534
|
+
|
|
535
|
+
```python
|
|
536
|
+
class EventStore(Protocol):
|
|
537
|
+
"""Interface for persisting and retrieving the execution event log."""
|
|
538
|
+
|
|
539
|
+
def append(self, event: ExecutionEvent) -> None:
|
|
540
|
+
"""Append an event to the log. Immutable once appended."""
|
|
541
|
+
...
|
|
542
|
+
|
|
543
|
+
def get_events(
|
|
544
|
+
self,
|
|
545
|
+
execution_id: UUID,
|
|
546
|
+
after: datetime | None = None,
|
|
547
|
+
) -> list[ExecutionEvent]:
|
|
548
|
+
"""Retrieve events for an execution, optionally after a timestamp.
|
|
549
|
+
|
|
550
|
+
Returns events in chronological order.
|
|
551
|
+
"""
|
|
552
|
+
...
|
|
553
|
+
|
|
554
|
+
def get_latest_event(self, execution_id: UUID) -> ExecutionEvent | None:
|
|
555
|
+
"""Retrieve the most recent event for an execution.
|
|
556
|
+
|
|
557
|
+
Used for quick state checks and resumption without replaying
|
|
558
|
+
the full log.
|
|
559
|
+
"""
|
|
560
|
+
...
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
Openoma ships an `InMemoryEventStore` for testing and prototyping. Production
|
|
564
|
+
deployments implement the protocol with their storage backend of choice.
|
|
565
|
+
|
|
566
|
+
### 4.2 Execution Adapter Protocol
|
|
567
|
+
|
|
568
|
+
```python
|
|
569
|
+
class ExecutionAdapter(Protocol):
|
|
570
|
+
"""Interface that any orchestration engine implements to drive Openoma."""
|
|
571
|
+
|
|
572
|
+
def execute_block(
|
|
573
|
+
self,
|
|
574
|
+
block: WorkBlock,
|
|
575
|
+
node_ref: NodeReference,
|
|
576
|
+
inputs: Any,
|
|
577
|
+
store: EventStore,
|
|
578
|
+
) -> UUID:
|
|
579
|
+
"""Begin execution of a work block.
|
|
580
|
+
|
|
581
|
+
The adapter is responsible for:
|
|
582
|
+
- Selecting an appropriate executor based on execution_hints.
|
|
583
|
+
- Emitting ExecutionEvents via `store.append()`.
|
|
584
|
+
- Returning the BlockExecution UUID.
|
|
585
|
+
|
|
586
|
+
The adapter must NOT modify the WorkBlock or NodeReference.
|
|
587
|
+
"""
|
|
588
|
+
...
|
|
589
|
+
|
|
590
|
+
def resume_block(
|
|
591
|
+
self,
|
|
592
|
+
block_execution_id: UUID,
|
|
593
|
+
store: EventStore,
|
|
594
|
+
) -> None:
|
|
595
|
+
"""Resume a block execution from the last event in its log.
|
|
596
|
+
|
|
597
|
+
Called when an executor crashes or is replaced. The adapter reads
|
|
598
|
+
the event stream via `store.get_events()`, determines the last
|
|
599
|
+
known state, and continues.
|
|
600
|
+
"""
|
|
601
|
+
...
|
|
602
|
+
|
|
603
|
+
def evaluate_condition(
|
|
604
|
+
self,
|
|
605
|
+
condition: Condition,
|
|
606
|
+
context: Any,
|
|
607
|
+
) -> bool:
|
|
608
|
+
"""Evaluate a traversal condition given the current execution context.
|
|
609
|
+
|
|
610
|
+
For conditions with a structured predicate, the adapter may evaluate
|
|
611
|
+
it directly. For natural-language conditions, it may delegate to an
|
|
612
|
+
LLM, a human, or any other mechanism.
|
|
613
|
+
"""
|
|
614
|
+
...
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
### 4.3 Adapter and Store Semantics
|
|
618
|
+
|
|
619
|
+
- **The adapter drives; the store records.** The adapter decides *when* to
|
|
620
|
+
emit events. The store decides *where* events are persisted. Neither knows
|
|
621
|
+
the other's implementation.
|
|
622
|
+
- **The store is injected into the adapter.** Adapter methods receive the
|
|
623
|
+
store as a parameter. This allows the same adapter to work against different
|
|
624
|
+
storage backends without modification.
|
|
625
|
+
- **Multiple adapters can coexist.** A human-task adapter for manual work, an
|
|
626
|
+
agent adapter for AI-driven blocks, a CI/CD adapter for automated pipelines
|
|
627
|
+
— all writing to the same event store.
|
|
628
|
+
- The adapter is responsible for **executor selection**. It reads the
|
|
629
|
+
WorkBlock's `execution_hints` and matches them against available executors.
|
|
630
|
+
- The adapter is responsible for **condition evaluation**. It reads the
|
|
631
|
+
Condition's `description` and/or `predicate` and returns a boolean.
|
|
632
|
+
|
|
633
|
+
> **Analogy.** In Anthropic's Managed Agents architecture, the harness calls
|
|
634
|
+
> hands via `execute(name, input) → string` and records to a durable session
|
|
635
|
+
> log. In Openoma, the Execution Adapter is the harness interface, and the
|
|
636
|
+
> Event Store is the session log interface. Both are abstract — any
|
|
637
|
+
> orchestration engine implements the adapter, any storage backend implements
|
|
638
|
+
> the store.
|
|
639
|
+
|
|
640
|
+
---
|
|
641
|
+
|
|
642
|
+
## 5. Design Principles
|
|
643
|
+
|
|
644
|
+
### 5.1 Separation of Definition and Execution
|
|
645
|
+
|
|
646
|
+
The core module (`openoma.core`) and the execution module (`openoma.execution`)
|
|
647
|
+
are strictly separated. The core module defines the topology and intent of
|
|
648
|
+
work. The execution module records what actually happened. A third-party
|
|
649
|
+
orchestration engine reads the core model, decides how to execute it, and
|
|
650
|
+
writes results into the execution model via the Execution Adapter.
|
|
651
|
+
|
|
652
|
+
### 5.2 Event Sourcing
|
|
653
|
+
|
|
654
|
+
Execution state is never mutated in place. Every state transition is an
|
|
655
|
+
immutable event appended to a log. Current state is derived by replaying the
|
|
656
|
+
log. This guarantees:
|
|
657
|
+
|
|
658
|
+
- **Resumability.** Any executor can resume from the last event after a crash.
|
|
659
|
+
- **Auditability.** The complete history of who did what and when is preserved.
|
|
660
|
+
- **Executor independence.** The log outlives any single executor or
|
|
661
|
+
orchestration engine.
|
|
662
|
+
|
|
663
|
+
### 5.3 Self-Similar Assessment
|
|
664
|
+
|
|
665
|
+
Assessment is not a special mechanism — it is a regular flow with the constraint
|
|
666
|
+
of having exactly one terminal node. This means:
|
|
667
|
+
|
|
668
|
+
- Assessment flows can be versioned, executed, and tracked like any other flow.
|
|
669
|
+
- Assessment flows can themselves be assessed by higher-level contracts.
|
|
670
|
+
- The entire framework is recursively composable: contracts assess sub-contracts
|
|
671
|
+
whose assessments are themselves flows.
|
|
672
|
+
|
|
673
|
+
### 5.4 Graph Topology Freedom
|
|
674
|
+
|
|
675
|
+
Flows are directed graphs with no requirement for full connectivity. This
|
|
676
|
+
supports real-world operational structures where independent work streams
|
|
677
|
+
coexist within the same logical boundary without artificial sequencing.
|
|
678
|
+
|
|
679
|
+
### 5.5 Many-to-Many Execution Sharing
|
|
680
|
+
|
|
681
|
+
A BlockExecution can be claimed by multiple FlowExecutions, and a
|
|
682
|
+
FlowExecution can be claimed by multiple ContractExecutions. This models
|
|
683
|
+
reality: a single piece of completed work often satisfies obligations in
|
|
684
|
+
multiple contexts.
|
|
685
|
+
|
|
686
|
+
### 5.6 Opaque Outcomes
|
|
687
|
+
|
|
688
|
+
Outcomes (inputs, outputs, assessment verdicts) are opaque blobs. The framework
|
|
689
|
+
stores and routes them but does not inspect their contents. Schema enforcement,
|
|
690
|
+
if desired, is the responsibility of the execution engine or a validation layer
|
|
691
|
+
built on top of Openoma.
|
|
692
|
+
|
|
693
|
+
### 5.7 Orchestration Agnosticism
|
|
694
|
+
|
|
695
|
+
Openoma makes no assumptions about the orchestration engine. It does not
|
|
696
|
+
assume agents, does not assume humans, does not assume any particular
|
|
697
|
+
scheduling model. The Execution Adapter and Event Store protocols are the only
|
|
698
|
+
contracts between Openoma and the outside world. This ensures the framework
|
|
699
|
+
remains a stable substrate as orchestration tools evolve — today's harness
|
|
700
|
+
assumptions become tomorrow's dead weight, but the data model persists.
|
|
701
|
+
|
|
702
|
+
### 5.8 Flows as Templates, Executions as Instances
|
|
703
|
+
|
|
704
|
+
A Flow in the core model is a **template** — a reusable definition of work
|
|
705
|
+
structure. Each time a flow is run, a new **FlowExecution** is created in the
|
|
706
|
+
execution module. The same flow can produce many FlowExecutions (e.g. a sprint
|
|
707
|
+
cycle flow runs every two weeks).
|
|
708
|
+
|
|
709
|
+
Contracts reference flows (via FlowReference), not flow executions. A contract
|
|
710
|
+
does not model recurrence — it declares which flows constitute its work side.
|
|
711
|
+
Recurrence, scheduling, and triggering are the orchestration engine's concern.
|
|
712
|
+
Each run of a contract's workflows produces a new set of FlowExecutions, which
|
|
713
|
+
are grouped under a **ContractExecution**.
|
|
714
|
+
|
|
715
|
+
This means:
|
|
716
|
+
- The same Contract can have many ContractExecutions (one per evaluation
|
|
717
|
+
period, milestone, or triggered run).
|
|
718
|
+
- The same Flow can have many FlowExecutions across different
|
|
719
|
+
ContractExecutions or even the same one (e.g. a "daily health check" flow
|
|
720
|
+
executed multiple times within a single contract period).
|
|
721
|
+
- Openoma tracks every execution instance independently — the full history is
|
|
722
|
+
in the event log.
|
|
723
|
+
|
|
724
|
+
---
|
|
725
|
+
|
|
726
|
+
## 6. Illustrative Example
|
|
727
|
+
|
|
728
|
+
> An organization modeled end-to-end with Openoma.
|
|
729
|
+
|
|
730
|
+
### The Mission (Contract)
|
|
731
|
+
|
|
732
|
+
```
|
|
733
|
+
Contract: "Q3 Product Launch"
|
|
734
|
+
├── required_outcomes:
|
|
735
|
+
│ ├── "Feature X shipped to production"
|
|
736
|
+
│ └── "Customer satisfaction ≥ 4.5/5"
|
|
737
|
+
├── work_flows:
|
|
738
|
+
│ ├── Flow: "Engineering Sprint Cycle"
|
|
739
|
+
│ └── Flow: "QA Validation Pipeline"
|
|
740
|
+
├── sub_contracts:
|
|
741
|
+
│ └── Contract: "Infrastructure SLA"
|
|
742
|
+
│ ├── required_outcomes: ["99.9% uptime"]
|
|
743
|
+
│ ├── work_flows: [Flow: "Infra BAU"]
|
|
744
|
+
│ └── assessment_bindings: [→ Flow: "Uptime Monitor Assessment"]
|
|
745
|
+
└── assessment_bindings:
|
|
746
|
+
├── "Feature X shipped" → assessment Flow: "Release Verification"
|
|
747
|
+
│ └── test_flows: [Flow: "Smoke Test Suite"]
|
|
748
|
+
└── "Satisfaction ≥ 4.5" → assessment Flow: "CSAT Evaluator"
|
|
749
|
+
└── test_flows: [Flow: "Survey Collection"]
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
### A Workflow (Flow)
|
|
753
|
+
|
|
754
|
+
```
|
|
755
|
+
Flow: "Engineering Sprint Cycle"
|
|
756
|
+
├── nodes:
|
|
757
|
+
│ ├── [slot-A] → WorkBlock: "Ticket Triage"
|
|
758
|
+
│ ├── [slot-B] → WorkBlock: "Implementation"
|
|
759
|
+
│ ├── [slot-C] → WorkBlock: "Code Review"
|
|
760
|
+
│ └── [slot-D] → WorkBlock: "Implementation" ← same block, second slot
|
|
761
|
+
├── edges:
|
|
762
|
+
│ ├── None → slot-A (entry)
|
|
763
|
+
│ ├── slot-A → slot-B [condition: "ticket is approved"]
|
|
764
|
+
│ ├── slot-A → slot-D [condition: "ticket is fast-track"]
|
|
765
|
+
│ ├── slot-B → slot-C (unconditional)
|
|
766
|
+
│ └── slot-D → slot-C (unconditional)
|
|
767
|
+
└── expected_outcome: "All sprint tickets implemented and reviewed"
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
Note that `slot-B` and `slot-D` reference the same `WorkBlock: "Implementation"`
|
|
771
|
+
but represent two parallel lanes. At execution time, they may be assigned to
|
|
772
|
+
different engineers or agent systems.
|
|
773
|
+
|
|
774
|
+
### Daily Work (WorkBlock)
|
|
775
|
+
|
|
776
|
+
```
|
|
777
|
+
WorkBlock: "Code Review"
|
|
778
|
+
├── inputs:
|
|
779
|
+
│ ├── name: "pull_request_url", description: "URL of the PR to review"
|
|
780
|
+
│ └── name: "review_checklist", description: "Criteria for approval", required: false
|
|
781
|
+
├── outputs:
|
|
782
|
+
│ ├── name: "verdict", description: "approved | changes_requested | rejected"
|
|
783
|
+
│ └── name: "comments", description: "Review comments"
|
|
784
|
+
└── execution_hints: ["github-access", "codebase-read"]
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
### Assessment (Assessment Flow)
|
|
788
|
+
|
|
789
|
+
```
|
|
790
|
+
Flow: "Release Verification" [assessment flow — 1 terminal node]
|
|
791
|
+
├── nodes:
|
|
792
|
+
│ ├── [slot-1] → WorkBlock: "Check Deployment Status"
|
|
793
|
+
│ ├── [slot-2] → WorkBlock: "Run Smoke Tests"
|
|
794
|
+
│ └── [slot-3] → WorkBlock: "Produce Release Verdict" ← terminal
|
|
795
|
+
├── edges:
|
|
796
|
+
│ ├── None → slot-1
|
|
797
|
+
│ ├── slot-1 → slot-2
|
|
798
|
+
│ └── slot-2 → slot-3
|
|
799
|
+
└── expected_outcome: "Binary pass/fail on release readiness"
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
The terminal node (`slot-3`) produces the assessment result that the contract
|
|
803
|
+
uses to evaluate whether `"Feature X shipped to production"` is satisfied.
|
|
804
|
+
|
|
805
|
+
---
|
|
806
|
+
|
|
807
|
+
## 7. Module Boundary
|
|
808
|
+
|
|
809
|
+
```
|
|
810
|
+
openoma/
|
|
811
|
+
├── core/ # Core model — definitions only
|
|
812
|
+
│ ├── work_block.py
|
|
813
|
+
│ ├── condition.py
|
|
814
|
+
│ ├── flow.py
|
|
815
|
+
│ ├── contract.py
|
|
816
|
+
│ ├── types.py # PortDescriptor, PortMapping, FlowReference, ContractReference, etc.
|
|
817
|
+
│ └── versioning.py # Version management utilities
|
|
818
|
+
├── execution/ # Execution model — event-sourced records
|
|
819
|
+
│ ├── events.py # ExecutionEvent, ExecutionEventType
|
|
820
|
+
│ ├── block_execution.py
|
|
821
|
+
│ ├── flow_execution.py
|
|
822
|
+
│ ├── contract_execution.py
|
|
823
|
+
│ └── types.py # ExecutorInfo, ExecutionState, AssessmentResult, etc.
|
|
824
|
+
├── store/ # Event Store protocol + built-in implementations
|
|
825
|
+
│ ├── protocol.py # EventStore abstract interface
|
|
826
|
+
│ └── memory.py # InMemoryEventStore (for testing / prototyping)
|
|
827
|
+
├── adapter/ # Execution Adapter protocol
|
|
828
|
+
│ ├── protocol.py # ExecutionAdapter abstract interface
|
|
829
|
+
│ └── base.py # Optional base class with common event-emitting logic
|
|
830
|
+
├── validation/ # Invariant validators
|
|
831
|
+
│ ├── graph.py # Terminal node checks, edge validity
|
|
832
|
+
│ └── references.py # Version pin validation, reference integrity
|
|
833
|
+
├── builders/ # Fluent builder APIs for constructing entities
|
|
834
|
+
│ ├── work_block.py
|
|
835
|
+
│ ├── flow.py
|
|
836
|
+
│ └── contract.py
|
|
837
|
+
└── __init__.py
|
|
838
|
+
```
|
|
839
|
+
|
|
840
|
+
---
|
|
841
|
+
|
|
842
|
+
## 8. Constraints and Invariants
|
|
843
|
+
|
|
844
|
+
1. **Assessment terminal constraint.** Any flow used as an assessment flow in
|
|
845
|
+
an AssessmentBinding must have exactly one terminal node (a node with no
|
|
846
|
+
outgoing edges).
|
|
847
|
+
|
|
848
|
+
2. **Version pinning.** All cross-entity references must pin a specific
|
|
849
|
+
version. Unpinned references are not permitted.
|
|
850
|
+
|
|
851
|
+
3. **No circular sub-contract inclusion.** A contract may not include itself
|
|
852
|
+
(directly or transitively) as a sub-contract.
|
|
853
|
+
|
|
854
|
+
4. **NodeReference uniqueness.** Within a flow, each NodeReference has a unique
|
|
855
|
+
`id`, even if multiple references point to the same target entity.
|
|
856
|
+
|
|
857
|
+
5. **Edge validity.** Every `source_id` and `target_id` in an edge must
|
|
858
|
+
reference a NodeReference that exists in the same flow's `nodes` list.
|
|
859
|
+
|
|
860
|
+
6. **Event append-only.** Execution events are immutable once appended. Events
|
|
861
|
+
cannot be deleted or modified. State is always derived by replaying the log.
|
|
862
|
+
|
|
863
|
+
7. **Terminal state finality.** Once an execution's derived state reaches a
|
|
864
|
+
terminal state (`completed`, `failed`, `cancelled`), no further
|
|
865
|
+
non-administrative events may be appended. A new execution must be created
|
|
866
|
+
for retries.
|
|
867
|
+
|
|
868
|
+
8. **Adapter writes via store.** The Execution Adapter emits events
|
|
869
|
+
exclusively through the Event Store's `append()` method. The adapter
|
|
870
|
+
never writes directly to the underlying persistence layer.
|
|
871
|
+
|
|
872
|
+
9. **Port mapping validity.** Every `source_port` in a PortMapping must
|
|
873
|
+
reference an output port that exists on the source node's WorkBlock.
|
|
874
|
+
Every `target_port` must reference an input port on the target node's
|
|
875
|
+
WorkBlock.
|
|
876
|
+
|
|
877
|
+
---
|
|
878
|
+
|
|
879
|
+
## 9. Out of Scope (for this framework)
|
|
880
|
+
|
|
881
|
+
- **Orchestration / scheduling.** How flows get executed (task queues, agent
|
|
882
|
+
routing, human assignment) is the domain of an orchestration engine that
|
|
883
|
+
implements the Execution Adapter and drives Openoma models.
|
|
884
|
+
- **Harness implementation.** The specific agent loop, context engineering,
|
|
885
|
+
prompt management, and model selection are the orchestration engine's
|
|
886
|
+
concern. Openoma only defines the adapter interface.
|
|
887
|
+
- **Schema enforcement.** PortDescriptor may carry an optional `schema` field,
|
|
888
|
+
but the framework does not validate data against it. Enforcement is the
|
|
889
|
+
responsibility of adapters or validation layers.
|
|
890
|
+
- **Production persistence.** Openoma ships an in-memory EventStore for
|
|
891
|
+
testing. Production storage backends (NoSQL, EventStoreDB, Kafka, etc.)
|
|
892
|
+
implement the EventStore protocol externally.
|
|
893
|
+
- **Access control.** Authorization over who can view/edit/execute entities is
|
|
894
|
+
external.
|
|
895
|
+
|
|
896
|
+
---
|
|
897
|
+
|
|
898
|
+
## 10. Open Questions
|
|
899
|
+
|
|
900
|
+
1. **Condition expression language.** Should the spec mandate a specific
|
|
901
|
+
structured predicate format (JSONLogic, CEL, custom AST), or leave it as an
|
|
902
|
+
opaque dict for now? Current decision: opaque dict, with a recommended
|
|
903
|
+
format to be specified in a future RFC.
|
|
904
|
+
|
|
905
|
+
2. **Execution lineage.** Should the execution model track causal
|
|
906
|
+
relationships between block executions (e.g. "this block was triggered
|
|
907
|
+
because that block completed")? This could be a `triggered_by` field on the
|
|
908
|
+
`started` event's payload, enabling full provenance graphs.
|
|
909
|
+
|
|
910
|
+
3. **Event compaction.** For long-running contracts, the event log may grow
|
|
911
|
+
very large. Should the spec define a compaction/snapshotting mechanism
|
|
912
|
+
that preserves auditability while bounding read costs?
|
|
913
|
+
|
|
914
|
+
---
|
|
915
|
+
|
|
916
|
+
## 11. What Ships in the Package
|
|
917
|
+
|
|
918
|
+
When a consumer installs Openoma (`pip install openoma`), they get the
|
|
919
|
+
following:
|
|
920
|
+
|
|
921
|
+
### 11.1 Data Models
|
|
922
|
+
|
|
923
|
+
Immutable, serializable data classes (Pydantic or dataclasses) for every entity
|
|
924
|
+
in Sections 2 and 3:
|
|
925
|
+
|
|
926
|
+
- **Core:** `WorkBlock`, `PortDescriptor`, `PortMapping`, `Condition`,
|
|
927
|
+
`NodeReference`, `Edge`, `Flow`, `Contract`, `RequiredOutcome`,
|
|
928
|
+
`AssessmentBinding`, `FlowReference`, `ContractReference`.
|
|
929
|
+
- **Execution:** `ExecutionEvent`, `ExecutionEventType`, `ExecutorInfo`,
|
|
930
|
+
`ExecutionState`, `BlockExecution`, `FlowExecution`, `ContractExecution`,
|
|
931
|
+
`AssessmentResult`.
|
|
932
|
+
|
|
933
|
+
All models support serialization to/from JSON-compatible dicts (ready for
|
|
934
|
+
GraphQL resolvers, NoSQL documents, or API payloads).
|
|
935
|
+
|
|
936
|
+
### 11.2 Validators
|
|
937
|
+
|
|
938
|
+
Functions that enforce the invariants from Section 8:
|
|
939
|
+
|
|
940
|
+
- `validate_flow(flow)` — checks edge validity, NodeReference uniqueness,
|
|
941
|
+
and assessment terminal constraint.
|
|
942
|
+
- `validate_contract(contract)` — checks cycle detection (no circular
|
|
943
|
+
sub-contracts), version pin integrity, and assessment binding validity.
|
|
944
|
+
- `validate_port_mappings(flow)` — checks that all port mappings reference
|
|
945
|
+
ports that exist on the source and target nodes.
|
|
946
|
+
|
|
947
|
+
Validators raise structured errors with paths to the offending entity.
|
|
948
|
+
|
|
949
|
+
### 11.3 State Derivation
|
|
950
|
+
|
|
951
|
+
Pure functions that compute execution state from event streams:
|
|
952
|
+
|
|
953
|
+
- `derive_block_state(events) → ExecutionState`
|
|
954
|
+
- `derive_flow_state(block_executions) → ExecutionState`
|
|
955
|
+
- `derive_contract_state(flow_executions, sub_contract_executions) → ExecutionState`
|
|
956
|
+
|
|
957
|
+
These implement the rules from Sections 3.4 and 3.5.
|
|
958
|
+
|
|
959
|
+
### 11.4 Protocols
|
|
960
|
+
|
|
961
|
+
- `EventStore` protocol (Section 4.1) — for storage backends.
|
|
962
|
+
- `ExecutionAdapter` protocol (Section 4.2) — for orchestration engines.
|
|
963
|
+
|
|
964
|
+
### 11.5 Built-in Implementations
|
|
965
|
+
|
|
966
|
+
- `InMemoryEventStore` — a simple in-memory EventStore for testing,
|
|
967
|
+
prototyping, and local development. Not suitable for production.
|
|
968
|
+
|
|
969
|
+
### 11.6 Builder APIs
|
|
970
|
+
|
|
971
|
+
Fluent builder classes for constructing entities without error-prone dict
|
|
972
|
+
assembly:
|
|
973
|
+
|
|
974
|
+
```python
|
|
975
|
+
from openoma.builders import FlowBuilder, WorkBlockBuilder
|
|
976
|
+
|
|
977
|
+
block = (
|
|
978
|
+
WorkBlockBuilder("Code Review")
|
|
979
|
+
.input("pull_request_url", "URL of the PR to review")
|
|
980
|
+
.input("review_checklist", "Criteria for approval", required=False)
|
|
981
|
+
.output("verdict", "approved | changes_requested | rejected")
|
|
982
|
+
.output("comments", "Review comments")
|
|
983
|
+
.hint("github-access")
|
|
984
|
+
.build()
|
|
985
|
+
)
|
|
986
|
+
|
|
987
|
+
flow = (
|
|
988
|
+
FlowBuilder("Sprint Cycle")
|
|
989
|
+
.add_block(block, alias="review")
|
|
990
|
+
.add_block(impl_block, alias="impl-primary")
|
|
991
|
+
.add_block(impl_block, alias="impl-fast-track") # same block, two slots
|
|
992
|
+
.edge("impl-primary", "review")
|
|
993
|
+
.edge("impl-fast-track", "review")
|
|
994
|
+
.entry("impl-primary", condition=Condition(description="ticket is approved"))
|
|
995
|
+
.entry("impl-fast-track", condition=Condition(description="ticket is fast-track"))
|
|
996
|
+
.build() # runs validate_flow() automatically
|
|
997
|
+
)
|
|
998
|
+
```
|
|
999
|
+
|
|
1000
|
+
### 11.7 What Openoma Does NOT Ship
|
|
1001
|
+
|
|
1002
|
+
- **No orchestration engine.** No scheduler, no task queue, no agent loop.
|
|
1003
|
+
- **No production storage.** No database drivers, no Kafka integration.
|
|
1004
|
+
- **No UI.** No dashboard, no visualization.
|
|
1005
|
+
- **No network layer.** No REST/GraphQL server. (A separate `openoma-graphql`
|
|
1006
|
+
package may be created for this.)
|
|
1007
|
+
|
|
1008
|
+
Openoma is a **data model + validation + protocol** library. It is the stable
|
|
1009
|
+
kernel that other systems build on top of.
|