covalve 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.
- covalve-0.1.0/.gitignore +13 -0
- covalve-0.1.0/ADR/ADR-001.md +57 -0
- covalve-0.1.0/ADR/ADR-002.md +63 -0
- covalve-0.1.0/ADR/ADR-003.md +112 -0
- covalve-0.1.0/ADR/ADR-004.md +96 -0
- covalve-0.1.0/ADR/ADR-005.md +104 -0
- covalve-0.1.0/LICENSE +21 -0
- covalve-0.1.0/PKG-INFO +298 -0
- covalve-0.1.0/README.md +276 -0
- covalve-0.1.0/covalve/__init__.py +35 -0
- covalve-0.1.0/covalve/infrastructure/base/cache.py +12 -0
- covalve-0.1.0/covalve/infrastructure/base/guardrails.py +6 -0
- covalve-0.1.0/covalve/infrastructure/base/llm.py +11 -0
- covalve-0.1.0/covalve/infrastructure/base/log.py +7 -0
- covalve-0.1.0/covalve/infrastructure/base/memory.py +10 -0
- covalve-0.1.0/covalve/infrastructure/base/tools.py +7 -0
- covalve-0.1.0/covalve/infrastructure/contract.py +20 -0
- covalve-0.1.0/covalve/runtime/engine.py +98 -0
- covalve-0.1.0/covalve/runtime/executor/analyze_query.py +43 -0
- covalve-0.1.0/covalve/runtime/executor/error.py +14 -0
- covalve-0.1.0/covalve/runtime/executor/error_counter.py +18 -0
- covalve-0.1.0/covalve/runtime/executor/fallback.py +20 -0
- covalve-0.1.0/covalve/runtime/executor/guardrail.py +13 -0
- covalve-0.1.0/covalve/runtime/executor/main_llm.py +72 -0
- covalve-0.1.0/covalve/runtime/executor/retrieve_conv.py +15 -0
- covalve-0.1.0/covalve/runtime/executor/save_data.py +28 -0
- covalve-0.1.0/covalve/runtime/executor/tools_executor.py +65 -0
- covalve-0.1.0/covalve/runtime/executor/tools_mapper.py +18 -0
- covalve-0.1.0/covalve/runtime/hook/__init__.py +4 -0
- covalve-0.1.0/covalve/runtime/hook/context.py +11 -0
- covalve-0.1.0/covalve/runtime/hook/executor.py +32 -0
- covalve-0.1.0/covalve/runtime/hook/registry.py +43 -0
- covalve-0.1.0/covalve/runtime/init.py +76 -0
- covalve-0.1.0/covalve/runtime/models/context.py +60 -0
- covalve-0.1.0/covalve/runtime/models/infra.py +25 -0
- covalve-0.1.0/covalve/runtime/models/io.py +36 -0
- covalve-0.1.0/covalve/runtime/models/logs.py +14 -0
- covalve-0.1.0/covalve/runtime/models/metadata.py +33 -0
- covalve-0.1.0/covalve/runtime/pipeline.py +31 -0
- covalve-0.1.0/covalve/runtime/registry.py +23 -0
- covalve-0.1.0/covalve/runtime/validator/graph_traversal.py +44 -0
- covalve-0.1.0/covalve/schemas/__init__.py +0 -0
- covalve-0.1.0/covalve/schemas/schema.json +56 -0
- covalve-0.1.0/example/prompt-example/clarification_template.txt +13 -0
- covalve-0.1.0/example/prompt-example/main_llm_response.txt +14 -0
- covalve-0.1.0/example/prompt-example/query_analyze_prompt.txt +127 -0
- covalve-0.1.0/example/schemas-example/schema.json +56 -0
- covalve-0.1.0/example/schemas-example/tools_schema.json +7 -0
- covalve-0.1.0/pyproject.toml +35 -0
covalve-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# ADR-001: FSM-Based Pipeline Runtime
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
#Accepted
|
|
5
|
+
|
|
6
|
+
## Date
|
|
7
|
+
2026-05-06
|
|
8
|
+
|
|
9
|
+
## Context
|
|
10
|
+
When building an AI pipeline, several existing frameworks were evaluated,
|
|
11
|
+
including LangChain, LlamaIndex, and Haystack.
|
|
12
|
+
These frameworks are generally prompt-driven — routing, tool selection,
|
|
13
|
+
and decision-making are delegated to the LLM automatically. This approach
|
|
14
|
+
reduces the system's locus of control because control resides primarily in
|
|
15
|
+
the prompt rather than in the system itself.
|
|
16
|
+
|
|
17
|
+
The probabilistic and automated nature of routing makes pipeline behavior
|
|
18
|
+
difficult to predict and verify, even when additional stabilization
|
|
19
|
+
techniques are applied.
|
|
20
|
+
|
|
21
|
+
## Decision
|
|
22
|
+
Build a fully controllable, code-based orchestrator using a Deterministic
|
|
23
|
+
Finite Automaton (DFA) as the foundation of the pipeline runtime.
|
|
24
|
+
|
|
25
|
+
## Rationale
|
|
26
|
+
A DFA was chosen because it more accurately represents the execution
|
|
27
|
+
lifecycle of the pipeline compared to alternatives such as simple function
|
|
28
|
+
chains or DAG-based orchestration.
|
|
29
|
+
|
|
30
|
+
A DAG is inherently directional and does not naturally represent retry,
|
|
31
|
+
fallback, and error recovery behaviors, which are essential parts of this
|
|
32
|
+
pipeline's execution lifecycle. A DFA enables explicit states,
|
|
33
|
+
deterministic transitions, and a fully traceable lifecycle from start to
|
|
34
|
+
finish.
|
|
35
|
+
|
|
36
|
+
Compared to prompt-based frameworks, this approach provides full system
|
|
37
|
+
control — routing, error handling, and decision-making are implemented in
|
|
38
|
+
code rather than delegated to prompts. As a result, pipeline behavior
|
|
39
|
+
becomes predictable and verifiable because every state and transition is
|
|
40
|
+
defined explicitly.
|
|
41
|
+
|
|
42
|
+
## Consequences
|
|
43
|
+
|
|
44
|
+
### Positive
|
|
45
|
+
- Pipeline behavior is fully deterministic and auditable
|
|
46
|
+
- Error handling is explicit — every failure case has a defined execution path
|
|
47
|
+
- No dependency on external orchestration frameworks
|
|
48
|
+
- Easier tracing and debugging because every state transition is recorded
|
|
49
|
+
|
|
50
|
+
### Negative
|
|
51
|
+
- More complex developer experience — contributors must understand
|
|
52
|
+
state machines and pipeline design before contributing
|
|
53
|
+
- Less accessible for non-developers — there is no visual interface like
|
|
54
|
+
n8n or LangFlow for inspecting or modifying flows
|
|
55
|
+
- No real-time activity graph visualization
|
|
56
|
+
- Flow changes require updates to schemas and executors rather than
|
|
57
|
+
simply modifying prompts
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# ADR-002: Python as Implementation Language
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
#Accepted
|
|
5
|
+
|
|
6
|
+
## Date
|
|
7
|
+
2026-05-06
|
|
8
|
+
|
|
9
|
+
## Context
|
|
10
|
+
ml-runtime requires an implementation language that can integrate directly
|
|
11
|
+
with the AI ecosystem — including LLM SDKs, embedding models, and vector
|
|
12
|
+
stores.
|
|
13
|
+
|
|
14
|
+
The architectural foundation of ml-runtime traces back to StateFlowGuard,
|
|
15
|
+
a TypeScript library built previously. However, the decision to use
|
|
16
|
+
TypeScript/Node.js was never under consideration for ml-runtime — Python
|
|
17
|
+
was the baseline assumption from the start given its AI ecosystem maturity.
|
|
18
|
+
|
|
19
|
+
Rust was identified as the ideal systems language for a runtime of this
|
|
20
|
+
nature due to its memory safety guarantees, lack of GC, and true
|
|
21
|
+
parallelism. However, at this stage, the cost of integrating Rust with
|
|
22
|
+
the Python AI ecosystem outweighs the benefit. Rust AI clients are
|
|
23
|
+
largely community-maintained, and FFI boundary complexity via PyO3 adds
|
|
24
|
+
significant overhead for a project still in the pilot phase.
|
|
25
|
+
|
|
26
|
+
Python was chosen as the pragmatic baseline.
|
|
27
|
+
|
|
28
|
+
## Decision
|
|
29
|
+
Implement ml-runtime in Python, using asyncio as the concurrency model
|
|
30
|
+
and Pydantic for schema validation and typed context objects.
|
|
31
|
+
|
|
32
|
+
## Rationale
|
|
33
|
+
Python is the native language of the AI ecosystem. LLM SDKs, vector
|
|
34
|
+
stores, and embedding libraries all provide first-class Python support.
|
|
35
|
+
This eliminates integration friction at the AI layer.
|
|
36
|
+
|
|
37
|
+
asyncio provides sufficient concurrency for I/O-bound pipeline workloads
|
|
38
|
+
— LLM calls, vector store queries, and storage reads are all I/O-bound
|
|
39
|
+
operations where asyncio performs well.
|
|
40
|
+
|
|
41
|
+
Pydantic enforces schema contracts at runtime, partially mitigating
|
|
42
|
+
Python's lack of compile-time type enforcement.
|
|
43
|
+
|
|
44
|
+
## Consequences
|
|
45
|
+
|
|
46
|
+
### Positive
|
|
47
|
+
- Full compatibility with AI ecosystem libraries out of the box
|
|
48
|
+
- asyncio handles I/O-bound concurrency adequately for pipeline workloads
|
|
49
|
+
- Pydantic provides runtime type safety for PipelineContext and schemas
|
|
50
|
+
|
|
51
|
+
### Negative
|
|
52
|
+
- GIL prevents true parallelism — CPU-bound executor work will block
|
|
53
|
+
other coroutines unless explicitly offloaded via run_in_executor()
|
|
54
|
+
- GC pressure increases under high request volume due to Pydantic object
|
|
55
|
+
churn per pipeline execution
|
|
56
|
+
- Python's dynamic typing requires disciplined use of type hints and
|
|
57
|
+
Pydantic — enforcement is opt-in, not compile-time
|
|
58
|
+
|
|
59
|
+
## Future Consideration
|
|
60
|
+
Rust via PyO3 is the identified migration path if GIL or GC pressure
|
|
61
|
+
becomes a measurable bottleneck in production. The FSM core is a natural
|
|
62
|
+
candidate for extraction as a native extension while the Python AI layer
|
|
63
|
+
remains in place.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# ADR-003: Hook System Design
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
#Accepted
|
|
5
|
+
|
|
6
|
+
## Date
|
|
7
|
+
2026-05-19
|
|
8
|
+
|
|
9
|
+
## Context
|
|
10
|
+
As the runtime matured, there was a need to accommodate cross-cutting concerns
|
|
11
|
+
such as state transition logging, audit trails, and notifications without
|
|
12
|
+
coupling these concerns into executor logic. Executors should remain focused
|
|
13
|
+
on their single responsibility — processing a state and returning an event.
|
|
14
|
+
|
|
15
|
+
Hardcoding observability and extensibility logic directly into executors would
|
|
16
|
+
violate separation of concerns and make the runtime harder to maintain and extend.
|
|
17
|
+
|
|
18
|
+
The initial design used a `plugins.json` configuration file with two modes:
|
|
19
|
+
`hanging` (fire-and-forget) and `middleware` (blocking). After implementation,
|
|
20
|
+
this approach was replaced with a decorator-based `HookRegistry` for the
|
|
21
|
+
following reasons:
|
|
22
|
+
|
|
23
|
+
- `plugins.json` separates hook registration from hook implementation — a
|
|
24
|
+
contributor must read two files to understand one hook's behavior
|
|
25
|
+
- Decorator-based registration is more idiomatic in Python and easier to
|
|
26
|
+
trace via type checkers and IDE tooling
|
|
27
|
+
- The names `hanging` and `middleware` did not sufficiently express the
|
|
28
|
+
behavioral contract of each mode
|
|
29
|
+
|
|
30
|
+
## Decision
|
|
31
|
+
Introduce a hook system implemented via a decorator-based `HookRegistry`
|
|
32
|
+
with two hook types: **observer** and **interceptor**.
|
|
33
|
+
|
|
34
|
+
### Observer
|
|
35
|
+
Fire-and-forget. Can be attached to multiple nodes. Executed via
|
|
36
|
+
`asyncio.create_task()`. Used for observability concerns such as logging,
|
|
37
|
+
metrics, and audit trails that must not block pipeline execution.
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
@hooks.observer(nodes=["ANALYZE", "MAIN_LLM"], on=HookOn.EXIT)
|
|
41
|
+
async def fn(ctx: ReadOnlyContext) -> None:
|
|
42
|
+
...
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Interceptor
|
|
46
|
+
Blocking. Single node only. Executed via `await`. Returns `bool` — if
|
|
47
|
+
`False`, the runtime emits the `on_false` event and redirects accordingly.
|
|
48
|
+
Used for concerns that must intercept execution before or after a node runs
|
|
49
|
+
and may need to halt or redirect the pipeline.
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
@hooks.interceptor(node="GUARDRAIL", on=HookOn.ENTER, on_false="OUT_OF_SCOPE")
|
|
53
|
+
async def fn(ctx: ReadOnlyContext) -> bool:
|
|
54
|
+
...
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`on_false` is **required** on every interceptor. If no redirect is needed,
|
|
58
|
+
use an observer instead.
|
|
59
|
+
|
|
60
|
+
### Single-node constraint on interceptor
|
|
61
|
+
Interceptor is constrained to a single node because each node may have a
|
|
62
|
+
different set of valid outgoing events. Allowing multi-node interceptors
|
|
63
|
+
would require one `on_false` event to be valid across all attached nodes
|
|
64
|
+
simultaneously — this cannot be guaranteed and would produce ambiguous
|
|
65
|
+
wiring between nodes and their `on_false` transitions. The single-node
|
|
66
|
+
constraint removes this ambiguity entirely.
|
|
67
|
+
|
|
68
|
+
### ReadOnlyContext
|
|
69
|
+
All hooks receive a `ReadOnlyContext` — a subclass of `PipelineContext`
|
|
70
|
+
with `frozen=True`. Hooks observe pipeline state; they do not modify it.
|
|
71
|
+
This ensures hook side effects are bounded and cannot corrupt context.
|
|
72
|
+
|
|
73
|
+
### Startup validation
|
|
74
|
+
`init_hooks()` validates all registered `on_false` events against the
|
|
75
|
+
schema transitions at startup. An interceptor whose `on_false` event is
|
|
76
|
+
not a valid transition in `schema.json` will raise an error before the
|
|
77
|
+
pipeline accepts any request.
|
|
78
|
+
|
|
79
|
+
## Rationale
|
|
80
|
+
Hooks as a separate concern from executors keeps the core runtime clean.
|
|
81
|
+
Observability and extensibility are opt-in — implementors attach only what
|
|
82
|
+
they need without modifying executor code.
|
|
83
|
+
|
|
84
|
+
The event-based redirect mechanism for interceptors preserves graph
|
|
85
|
+
integrity. Interceptors cannot redirect to arbitrary nodes — they can only
|
|
86
|
+
emit events already defined in the schema, enforced at startup. This keeps
|
|
87
|
+
the FSM fully auditable.
|
|
88
|
+
|
|
89
|
+
`ReadOnlyContext` as the hook interface ensures that hook side effects are
|
|
90
|
+
predictable. Hooks observe state; they do not modify it.
|
|
91
|
+
|
|
92
|
+
Decorator-based registration keeps registration and implementation
|
|
93
|
+
co-located, reducing the cognitive overhead of understanding any given hook.
|
|
94
|
+
|
|
95
|
+
## Consequences
|
|
96
|
+
|
|
97
|
+
### Positive
|
|
98
|
+
- Observability concerns are decoupled from executor logic
|
|
99
|
+
- Implementors extend runtime behavior without touching core files
|
|
100
|
+
- Graph integrity is preserved — hooks cannot bypass schema-defined transitions
|
|
101
|
+
- Single-node constraint on interceptor eliminates `on_false` wiring ambiguity
|
|
102
|
+
- `on_false` startup validation catches misconfiguration before runtime
|
|
103
|
+
- Decorator-based registration is co-located with implementation — easier to read and trace
|
|
104
|
+
|
|
105
|
+
### Negative
|
|
106
|
+
- Additional complexity for contributors — hook lifecycle, observer vs interceptor
|
|
107
|
+
distinction, and `on_false` requirements must be understood before use
|
|
108
|
+
- Interceptors add latency to the nodes they are attached to
|
|
109
|
+
- `on_false` events must be explicitly defined in `schema.json` — implementors
|
|
110
|
+
must keep hook registrations and schema in sync
|
|
111
|
+
- Single-node interceptor constraint means multi-node interception requires
|
|
112
|
+
registering separate interceptors per node
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# ADR-004: Guardrail as an Optional Core Node
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
#Accepted
|
|
5
|
+
|
|
6
|
+
## Date
|
|
7
|
+
2026-05-19
|
|
8
|
+
|
|
9
|
+
## Context
|
|
10
|
+
The pipeline's ANALYZE state is responsible for classifying user queries into
|
|
11
|
+
structured intents. During development, domain boundary enforcement — rejecting
|
|
12
|
+
queries outside the operational scope of the runtime — was initially handled
|
|
13
|
+
inside ANALYZE by prompting the LLM to assign low confidence or an
|
|
14
|
+
`outofcontext` intent to out-of-scope queries.
|
|
15
|
+
|
|
16
|
+
This approach has several drawbacks:
|
|
17
|
+
|
|
18
|
+
- ANALYZE becomes responsible for two distinct concerns: intent classification
|
|
19
|
+
and domain boundary enforcement. This violates single responsibility.
|
|
20
|
+
- Domain context injected into the ANALYZE prompt increases token count on every
|
|
21
|
+
request, regardless of whether the query is out of scope.
|
|
22
|
+
- LLM-based boundary enforcement is probabilistic — as observed in testing,
|
|
23
|
+
out-of-scope queries occasionally pass through and receive full responses.
|
|
24
|
+
- Cost and latency scale with prompt size. Overloading ANALYZE with domain
|
|
25
|
+
guardrail logic compounds this as the domain context grows.
|
|
26
|
+
|
|
27
|
+
## Decision
|
|
28
|
+
Introduce a dedicated GUARDRAIL node as an optional core node, positioned
|
|
29
|
+
between `RETRIEVE_PREVIOUS_CONVERSATION` and `ANALYZE` in the pipeline graph
|
|
30
|
+
when included.
|
|
31
|
+
|
|
32
|
+
GUARDRAIL is optional — its presence is declared in `schema.json`. If the
|
|
33
|
+
node is absent from the schema, the runtime proceeds directly from
|
|
34
|
+
`RETRIEVE_PREVIOUS_CONVERSATION` to `ANALYZE` without error.
|
|
35
|
+
|
|
36
|
+
When included, GUARDRAIL's implementation is injected via a `GuardrailBase`
|
|
37
|
+
abstract interface defined in `infrastructure/adapters/`, following the same
|
|
38
|
+
pattern as `LLMBase` and `StorageBase`. The core runtime does not know the
|
|
39
|
+
domain rules — only that a guardrail check must pass before ANALYZE is invoked.
|
|
40
|
+
|
|
41
|
+
Graph with GUARDRAIL included:
|
|
42
|
+
```
|
|
43
|
+
RETRIEVE_PREVIOUS_CONVERSATION → GUARDRAIL → ANALYZE
|
|
44
|
+
↓
|
|
45
|
+
FALLBACK (OUT_OF_SCOPE)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Graph without GUARDRAIL:
|
|
49
|
+
```
|
|
50
|
+
RETRIEVE_PREVIOUS_CONVERSATION → ANALYZE
|
|
51
|
+
```
|
|
52
|
+
The short-term mitigation — handling `outofcontext` via domain prompt in
|
|
53
|
+
`main_llm_response.txt` — remains in place until GUARDRAIL is implemented.
|
|
54
|
+
|
|
55
|
+
## Rationale
|
|
56
|
+
Schema-driven optionality is made reliable by ADR-005 — because every executor
|
|
57
|
+
is a pure function that returns a new context via `model_copy` rather than
|
|
58
|
+
mutating the received instance, nodes can be added or removed from the graph
|
|
59
|
+
without implicit side effects on downstream nodes.
|
|
60
|
+
|
|
61
|
+
GUARDRAIL is optional because not every deployment context requires the same
|
|
62
|
+
boundary enforcement strategy. A prototype or internal tool may have no
|
|
63
|
+
out-of-scope risk, and forcing a mandatory node would add unnecessary
|
|
64
|
+
implementation burden. The core remains agnostic.
|
|
65
|
+
|
|
66
|
+
Despite being optional, GUARDRAIL is strongly recommended for any production
|
|
67
|
+
deployment. LLM-based boundary enforcement in ANALYZE is probabilistic — as
|
|
68
|
+
observed in testing, out-of-scope queries occasionally bypass prompt-level
|
|
69
|
+
filtering. GUARDRAIL provides a deterministic enforcement layer before any
|
|
70
|
+
LLM call, at zero token cost.
|
|
71
|
+
|
|
72
|
+
The `GuardrailBase` interface keeps the core agnostic to implementation
|
|
73
|
+
strategy. Whether the implementor uses keyword matching, embedding similarity,
|
|
74
|
+
or a rule engine is an implementation detail the core does not need to know.
|
|
75
|
+
|
|
76
|
+
## Consequences
|
|
77
|
+
|
|
78
|
+
### Positive
|
|
79
|
+
- ANALYZE is responsible only for intent classification
|
|
80
|
+
- Out-of-scope queries are rejected before any LLM call — zero token cost
|
|
81
|
+
- Schema-driven optionality is preserved — GUARDRAIL can be included or
|
|
82
|
+
excluded via `schema.json` without affecting other nodes
|
|
83
|
+
- Core remains agnostic to business domain and enforcement strategy
|
|
84
|
+
|
|
85
|
+
### Negative
|
|
86
|
+
- Optional status means production deployments may omit GUARDRAIL — the
|
|
87
|
+
runtime will not warn or block if the node is absent
|
|
88
|
+
- When included, adds one additional state to every pipeline execution,
|
|
89
|
+
including in-scope queries
|
|
90
|
+
- Implementors must understand that omitting GUARDRAIL delegates boundary
|
|
91
|
+
enforcement back to ANALYZE prompts, which is probabilistic
|
|
92
|
+
|
|
93
|
+
## Recommendation
|
|
94
|
+
Production deployments should always include GUARDRAIL. Omitting it is
|
|
95
|
+
only appropriate for prototypes or internal tools where domain boundary
|
|
96
|
+
enforcement is not a requirement.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# ADR-005: No Direct Mutation of PipelineContext
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
#Accepted
|
|
5
|
+
|
|
6
|
+
## Date
|
|
7
|
+
2026-05-21
|
|
8
|
+
|
|
9
|
+
## Context
|
|
10
|
+
`PipelineContext` is the primary communication object passed between nodes
|
|
11
|
+
in the pipeline. It carries the accumulated results of each executor to
|
|
12
|
+
the next state in the graph.
|
|
13
|
+
|
|
14
|
+
During the pilot phase, executors were written to mutate `PipelineContext`
|
|
15
|
+
directly:
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
ctx.context.metadata = metadata
|
|
19
|
+
ctx.context.is_clarification = True
|
|
20
|
+
ctx.context.tools_data = {}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
This approach works functionally but violates the intent of how nodes should
|
|
24
|
+
interact with context. Direct mutation introduces several risks:
|
|
25
|
+
|
|
26
|
+
- Executors can overwrite fields set by previous nodes without explicit intent
|
|
27
|
+
- Side effects are implicit — it is not clear from the function signature
|
|
28
|
+
what a node will change
|
|
29
|
+
- Node behavior becomes order-dependent in ways that are not visible in the
|
|
30
|
+
schema, making the pipeline harder to reason about
|
|
31
|
+
- Schema-driven optionality — the ability to compose pipelines by including
|
|
32
|
+
or excluding nodes via `schema.json` — is fragile if nodes have implicit
|
|
33
|
+
side effects on shared state
|
|
34
|
+
|
|
35
|
+
This principle is foundational to ADR-004 (Guardrail as Optional Node).
|
|
36
|
+
Schema-driven optionality only works reliably if every node is a pure
|
|
37
|
+
function — same input context produces predictable output context, with no
|
|
38
|
+
hidden side effects on the received instance.
|
|
39
|
+
|
|
40
|
+
## Decision
|
|
41
|
+
Executors must not mutate the `PipelineContext` instance they receive.
|
|
42
|
+
Instead, every executor must return a new context instance via Pydantic's
|
|
43
|
+
`model_copy(update={...})` pattern.
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
# before — direct mutation
|
|
47
|
+
ctx.context.metadata = metadata
|
|
48
|
+
return ReturnSchema(event="NEXT", context=ctx.context)
|
|
49
|
+
|
|
50
|
+
# after — return new instance
|
|
51
|
+
return ReturnSchema(
|
|
52
|
+
event="NEXT",
|
|
53
|
+
context=ctx.context.model_copy(update={"metadata": metadata})
|
|
54
|
+
)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`PipelineContext` remains a mutable Pydantic model — this is not full
|
|
58
|
+
immutability. The constraint is behavioral: no executor may call attribute
|
|
59
|
+
assignment on the context instance it receives. All changes must go through
|
|
60
|
+
`model_copy`.
|
|
61
|
+
|
|
62
|
+
## Rationale
|
|
63
|
+
`PipelineContext` is a message passed between nodes, not a shared global
|
|
64
|
+
state. Treating it as a message means each node is responsible for producing
|
|
65
|
+
the next version of that message explicitly — changes are visible in the
|
|
66
|
+
return value, not hidden as side effects.
|
|
67
|
+
|
|
68
|
+
This aligns with the functional pipeline principle: nodes are pure functions
|
|
69
|
+
that take context and return context. The FSM runtime loop is the only
|
|
70
|
+
entity that decides which context version becomes the active state.
|
|
71
|
+
|
|
72
|
+
This also enables schema-driven optionality — if a node can be added or
|
|
73
|
+
removed from the graph via `schema.json` without breaking other nodes,
|
|
74
|
+
each node must be self-contained and free of implicit dependencies on
|
|
75
|
+
mutation order.
|
|
76
|
+
|
|
77
|
+
## Consequences
|
|
78
|
+
|
|
79
|
+
### Positive
|
|
80
|
+
- Node behavior is explicit — all context changes are visible in the return value
|
|
81
|
+
- Nodes become self-contained — removing a node from the schema does not
|
|
82
|
+
silently break downstream nodes that depended on its mutations
|
|
83
|
+
- Easier to test — each executor can be tested in isolation with a fixed
|
|
84
|
+
input context
|
|
85
|
+
- Enables schema-driven optionality as described in ADR-004
|
|
86
|
+
|
|
87
|
+
### Negative
|
|
88
|
+
- All existing executors need to be refactored — direct mutations are
|
|
89
|
+
pervasive in the current codebase
|
|
90
|
+
- `model_copy` creates a new Pydantic object per executor call — minor
|
|
91
|
+
memory overhead per pipeline execution, acceptable at current scale
|
|
92
|
+
- Slightly more verbose executor code compared to direct assignment
|
|
93
|
+
|
|
94
|
+
## Refactor Scope
|
|
95
|
+
The following executors require refactoring as part of Phase 2:
|
|
96
|
+
- `handle_analyze` — mutates `metadata`, `error`, `last_error_emitted`
|
|
97
|
+
- `handle_fallback` — mutates `is_clarification`, `fallback_content`
|
|
98
|
+
- `handle_tools_mapper` — mutates `tool_list`
|
|
99
|
+
- `handle_execute_tools` — mutates `tools_data`, `tool_list`, `last_error_emitted`
|
|
100
|
+
- `handle_main_llm` — mutates `summarize`, `response`
|
|
101
|
+
- `handle_save_data_to_persistence` — no context mutation, compliant
|
|
102
|
+
- `handle_retrieve_previous_conversation` — mutates `background`
|
|
103
|
+
- `handle_error_counter` — mutates via redis, context passed through
|
|
104
|
+
- `handle_internal_server_error` — mutates `response`
|
covalve-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Candra Julius Indira Patty
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|