fluid-decisions 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.
Files changed (45) hide show
  1. fluid_decisions-0.1.0/PKG-INFO +196 -0
  2. fluid_decisions-0.1.0/README.md +172 -0
  3. fluid_decisions-0.1.0/config/models.yaml +15 -0
  4. fluid_decisions-0.1.0/fluid/__init__.py +3 -0
  5. fluid_decisions-0.1.0/fluid/audit.py +45 -0
  6. fluid_decisions-0.1.0/fluid/cli.py +151 -0
  7. fluid_decisions-0.1.0/fluid/client.py +376 -0
  8. fluid_decisions-0.1.0/fluid/hook.py +56 -0
  9. fluid_decisions-0.1.0/fluid/lanes.yaml +8 -0
  10. fluid_decisions-0.1.0/fluid/ledger.py +251 -0
  11. fluid_decisions-0.1.0/fluid/mcp_server.py +59 -0
  12. fluid_decisions-0.1.0/fluid/packs/classify.yaml +19 -0
  13. fluid_decisions-0.1.0/fluid/packs/gate.yaml +67 -0
  14. fluid_decisions-0.1.0/fluid/packs/route.yaml +17 -0
  15. fluid_decisions-0.1.0/fluid/packs/verify.yaml +11 -0
  16. fluid_decisions-0.1.0/fluid/policy.py +432 -0
  17. fluid_decisions-0.1.0/fluid/scoping.py +146 -0
  18. fluid_decisions-0.1.0/fluid/service.py +461 -0
  19. fluid_decisions-0.1.0/fluid/serving.py +304 -0
  20. fluid_decisions-0.1.0/fluid_decisions.egg-info/PKG-INFO +196 -0
  21. fluid_decisions-0.1.0/fluid_decisions.egg-info/SOURCES.txt +43 -0
  22. fluid_decisions-0.1.0/fluid_decisions.egg-info/dependency_links.txt +1 -0
  23. fluid_decisions-0.1.0/fluid_decisions.egg-info/entry_points.txt +3 -0
  24. fluid_decisions-0.1.0/fluid_decisions.egg-info/requires.txt +20 -0
  25. fluid_decisions-0.1.0/fluid_decisions.egg-info/top_level.txt +1 -0
  26. fluid_decisions-0.1.0/pyproject.toml +46 -0
  27. fluid_decisions-0.1.0/setup.cfg +4 -0
  28. fluid_decisions-0.1.0/tests/test_budget.py +192 -0
  29. fluid_decisions-0.1.0/tests/test_client.py +168 -0
  30. fluid_decisions-0.1.0/tests/test_client_parse.py +61 -0
  31. fluid_decisions-0.1.0/tests/test_download.py +38 -0
  32. fluid_decisions-0.1.0/tests/test_harness.py +268 -0
  33. fluid_decisions-0.1.0/tests/test_live.py +214 -0
  34. fluid_decisions-0.1.0/tests/test_mcp.py +42 -0
  35. fluid_decisions-0.1.0/tests/test_measure_scoping.py +275 -0
  36. fluid_decisions-0.1.0/tests/test_policy.py +300 -0
  37. fluid_decisions-0.1.0/tests/test_probe.py +108 -0
  38. fluid_decisions-0.1.0/tests/test_sandbox.py +119 -0
  39. fluid_decisions-0.1.0/tests/test_scoping.py +94 -0
  40. fluid_decisions-0.1.0/tests/test_scoping_fixtures.py +172 -0
  41. fluid_decisions-0.1.0/tests/test_service.py +349 -0
  42. fluid_decisions-0.1.0/tests/test_serving_contract.py +73 -0
  43. fluid_decisions-0.1.0/tests/test_storage.py +196 -0
  44. fluid_decisions-0.1.0/tests/test_surfaces.py +122 -0
  45. fluid_decisions-0.1.0/tests/test_worker.py +26 -0
@@ -0,0 +1,196 @@
1
+ Metadata-Version: 2.4
2
+ Name: fluid-decisions
3
+ Version: 0.1.0
4
+ Summary: Typed d1 decisions with policy, runtime hooks, CLI and MCP
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: httpx==0.28.1
8
+ Requires-Dist: PyYAML==6.0.3
9
+ Provides-Extra: mcp
10
+ Requires-Dist: mcp==2.3.0; extra == "mcp"
11
+ Provides-Extra: serve
12
+ Requires-Dist: huggingface-hub==2.2.0; extra == "serve"
13
+ Requires-Dist: transformers==5.19.0; extra == "serve"
14
+ Requires-Dist: torch==2.14.1; extra == "serve"
15
+ Requires-Dist: torchvision==0.29.1; extra == "serve"
16
+ Requires-Dist: Pillow==12.3.0; extra == "serve"
17
+ Requires-Dist: soundfile==0.14.0; extra == "serve"
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest==9.1.1; extra == "dev"
20
+ Requires-Dist: ruff==0.17.0; extra == "dev"
21
+ Requires-Dist: mypy==2.4.0; extra == "dev"
22
+ Requires-Dist: types-PyYAML==6.0.12.20260906; extra == "dev"
23
+ Requires-Dist: build==1.6.1; extra == "dev"
24
+
25
+ <div align="center">
26
+
27
+ <img src="assets/liquid-decisions.png" alt="Fluid Decisions logo" width="260">
28
+
29
+ # Fluid Decisions
30
+
31
+ **Safety classification, routing, tool gating and verification for coding agents and agentic workers.**
32
+
33
+ ![Status: MVP](https://img.shields.io/badge/status-MVP-FF093A?style=for-the-badge&labelColor=0D0D0D)
34
+
35
+ [The MVP](#what-it-does) · [How it works](#how-it-works) · [Self-hosting](#how-to-start) · [Roadmap](#roadmap) · [Project documents](#project-documents)
36
+
37
+ </div>
38
+
39
+ ---
40
+
41
+ > [!NOTE]
42
+ > **MVP with IP-003 result handling.** The Python library, CLI, runtime hook, MCP server and optional model download/serving tools are implemented. Unscored results are distinguished from flags, carried forward as pending taint, and can be rechecked. Policies are **uncalibrated**. See the [result-handling report](docs/c-completion_reports/CR-003_Unscored_Results.md) for live evidence and limitations. IDEA-001 and IDEA-002 remain the roadmap.
43
+
44
+ <a name="overview"></a>
45
+ ## <img src="https://get.abliter8.ai/doc-assets/overview-intro.png" alt="Overview" height="58">
46
+
47
+ Fluid Decisions is a self-hosted decision layer around Liquid AI's d1 models. It helps an agent check what it is about to do, choose where work should go, and verify its claims against supplied evidence.
48
+
49
+ The design separates three responsibilities: **the worker proposes, d1 returns probabilities, and policy code decides what happens next.** A runtime hook enforces the checks around tool execution. The worker's willingness to call a checking tool is not the enforcement mechanism.
50
+
51
+ The longer-term ambition is a reusable layer for checking agent behaviour, documents and proposed decisions against explicit rules. The first milestone is deliberately focused: prove classification, routing, gating and verification, then build on measured results.
52
+
53
+ <a name="what-it-does"></a>
54
+ ## <img src="https://get.abliter8.ai/doc-assets/features-whatitdoes.png" alt="What it does" height="58">
55
+
56
+ ### The MVP: four jobs, one policy layer
57
+
58
+ | Job | What it does | Example |
59
+ | --- | --- | --- |
60
+ | **Classify** | Check code, documents or screenshots against a named set of questions, with probabilities attached to each finding. | Flag code that appears to steal credentials or conceal build-time behaviour. |
61
+ | **Route** | Choose among defined destinations, alongside escalation and completeness checks. | Send a ticket to the right queue, or select a local model, a reasoner or a human. |
62
+ | **Gate** | Check a proposed tool call before execution and return **allow**, **ask** or **deny**. | Block a destructive shell pattern even when the model endpoint is unavailable. |
63
+ | **Verify** | Check claims in a finished answer against the supplied source and flag unsupported claims. | Catch an answer that contradicts a source without inventing a replacement sentence. |
64
+
65
+ One Python library supports a CLI, runtime hooks and a thin MCP server. The hook uses a [documented stdin JSON contract](docs/USAGE.md#runtime-hook-and-mandatory-enforcement). MCP exposes the same capabilities to workers that cannot install a hook; mandatory enforcement requires the host runtime to wire the checks into its workflow. The supplied `GuardedRuntime` wraps tool dispatch and withholds flagged results.
66
+
67
+ An unscored result is released with `checked: false` by default, and its uncertainty is carried to the next action through pending session taint; operators can select withholding instead.
68
+
69
+ The MVP also includes versioned YAML question packs and policies, a session ledger for flagged and pending results, and an audit log of verdicts, probabilities, lane, pack version and latency. Raw input state stays out of the audit log by default.
70
+
71
+ The full scope and acceptance criteria are in [IP-001: the kickoff plan](docs/b-implementation_plans/IP-001-KICKOFF.md).
72
+
73
+ <a name="how-it-works"></a>
74
+ ## <img src="https://get.abliter8.ai/doc-assets/howitworks-function.png" alt="How it works" height="58">
75
+
76
+ d1 answers typed questions over a state: **choice** selects among named options, **score** rates ordered levels, and **noul** gives the probability of yes. Related questions share one request; the optional IP-004 scoping mechanism can give a question a selected dotted-path view of the state and reports unresolved views instead of allowing a missing answer. The measured candidates failed adoption criteria, so the shipped packs remain unscoped. Policy code then applies rules and thresholds to the returned probabilities. See the [d1 reference skill](skills/liquid-d1/SKILL.md) for the API background.
77
+
78
+ The tool-gating path is:
79
+
80
+ ```mermaid
81
+ %%{init: {'theme':'base','themeVariables':{'primaryColor':'#FF093A','primaryTextColor':'#ffffff','primaryBorderColor':'#7a0620','lineColor':'#FF093A','secondaryColor':'#1b1b1b','secondaryTextColor':'#ffffff','tertiaryColor':'#1b1b1b','tertiaryTextColor':'#ffffff','clusterBkg':'#0d0d0d','clusterBorder':'#FF093A','fontFamily':'monospace'}}}%%
82
+ flowchart TD
83
+ A[Worker proposes a tool call] --> B[Runtime hook checks static rules]
84
+ B -->|Blocked pattern| C[Deny]
85
+ B -->|Needs semantic checks| D[d1 answers the question pack]
86
+ D -->|Probabilities| E[Policy applies thresholds]
87
+ D -->|Endpoint unavailable| F[Ask]
88
+ E --> G[Allow / ask / deny]
89
+ C --> H[Runtime enforces the outcome]
90
+ F --> H
91
+ G --> H
92
+ ```
93
+
94
+ The MVP controls are:
95
+
96
+ - **Static rules run first.** Known destructive patterns do not need a model verdict.
97
+ - **Unavailable means ask.** An endpoint failure or timeout cannot become permission to proceed.
98
+ - **Probabilities remain visible.** Full distributions, confidence and yes/no decisiveness stay distinct so callers can see uncertainty.
99
+ - **Some risks cannot be softened.** Authorisation or a user request cannot override secret exposure, constraint violations or unresolved choices.
100
+ - **Calibration follows evidence.** Initial thresholds are explicitly uncalibrated. An allow verdict is a policy outcome, not proof that arbitrary code is safe.
101
+ - **Every model call is budgeted.** Each check's time budget is derived from the lane's measured cost, so a large tool input completes instead of timing out to ask; a call whose expected cost exceeds the configured cap returns ask without being sent. Static denials run first and never wait on a budget.
102
+
103
+ After a tool returns, the hook checks its output for injection or canary content and retains session flags for later gates. Verification of a finished answer must likewise be triggered by the host workflow.
104
+
105
+ <a name="how-to-start"></a>
106
+ ## <img src="https://get.abliter8.ai/doc-assets/quickstart.png" alt="How to start" height="58">
107
+
108
+ ### Built for community self-hosting
109
+
110
+ The setup is **download a model → start its decision API → configure Fluid Decisions → connect a worker**. Follow the [self-hosting guide](docs/SELF_HOSTING.md) for the complete path and [usage guide](docs/USAGE.md) for the CLI, hooks and MCP.
111
+
112
+ | Model | Role | Upstream |
113
+ | --- | --- | --- |
114
+ | **d1-3B** | Default for text and image workflows. The core workflow can run with this model alone. | [Model and loading instructions](https://huggingface.co/LiquidAI/d1-3B) |
115
+ | **d1-omni-600M** | Optional endpoint for short audio inputs. Audio and images are kept in separate requests. | [Model and loading instructions](https://huggingface.co/LiquidAI/d1-omni-600M) |
116
+
117
+ Serve the models on your own hardware or connect to compatible endpoints you control. Public defaults use localhost. The client, CLI, hook and MCP have no PyTorch dependency; install the optional `serve` extra in the model-serving environment.
118
+
119
+ ```sh
120
+ git clone https://github.com/abliter8-ai/fluid-decisions.git
121
+ cd fluid-decisions
122
+ python3 -m venv .venv
123
+ .venv/bin/python -m pip install '.[serve,mcp]'
124
+ .venv/bin/python scripts/download_models.py d1-3B --local-dir models
125
+ .venv/bin/python scripts/serve_model.py --model d1-3B --model-path models/d1-3B
126
+ ```
127
+
128
+ In another terminal, from the same directory:
129
+
130
+ ```sh
131
+ .venv/bin/fluid gate --tool read_file --input '{"path":"README.md"}' \
132
+ --user-request 'Read the README'
133
+ ```
134
+
135
+ The model server stays in the first terminal. Model loading executes the pinned upstream custom code; read the model licence links below. The guide covers optional audio, device selection and timeout configuration for slower hardware.
136
+
137
+ The repository adapter has been tested with d1-3B revision `051bcc464b01b9f92942b364d9586b0ef5912432` on macOS 26.5 / Apple Silicon MPS / Python 3.14.8. Its text, image, batch and tool workflows passed in a bounded local run; see [self-hosting validation](docs/SELF_HOSTING.md#validation-status). Other platforms and local omni serving remain untested.
138
+
139
+ Development uses the maintainer's cluster and gateway. Community users need no access to either. Recorded checks cover the decision lanes and real Qwen/GLM worker calls; separate self-hosting evidence identifies the tested model revisions and environment.
140
+
141
+ <a name="roadmap"></a>
142
+ ## Roadmap
143
+
144
+ The following directions come from the two idea documents. They are an ambition and a backlog, not shipped features or a release schedule. Work on them starts after the MVP is accepted, with each selected idea getting its own implementation plan.
145
+
146
+ ### IDEA-001: deeper checks around agent workflows
147
+
148
+ [IDEA-001](docs/a-ideas/IDEA-001.md) expands the first four jobs into the surrounding agent session. The MVP establishes the initial ledger, audit trail and uncertainty handling; the roadmap develops those foundations into broader workflows.
149
+
150
+ | Direction | Where we hope to get |
151
+ | --- | --- |
152
+ | **Context retention and session memory** | Keep relevant tool traffic verbatim when compacting a session, and make flagged untrusted content visible across later actions. |
153
+ | **More useful findings and CI checks** | Turn uncertain classifications into focused second passes with a useful location, then apply the same checks to diffs and source trees. |
154
+ | **Model cascades and spoken intent** | Route work among local models, larger reasoners and people; explore direct intent routing for short voice clips. |
155
+ | **Visual workflows** | Choose among legal actions on a screenshot and inspect images for explicitly defined defects or regressions. |
156
+ | **Verification at completion** | Attach claim-level source checks when an answer is finished, as part of the runtime's delivery workflow. |
157
+ | **Calibration from real use** | Fit thresholds by action class and study disagreement between models as a signal of uncertainty, without averaging their probabilities. |
158
+
159
+ Context compaction is the first proposed expansion. It preserves retained text verbatim. Later priorities depend on observed reliability and useful audit traces.
160
+
161
+ ### IDEA-002: checks over candidates, documents and rules
162
+
163
+ [IDEA-002](docs/a-ideas/IDEA-002.md) applies the same machinery to work beyond coding agents. A caller supplies a proposed outcome and the rules or evidence to assess it against.
164
+
165
+ | Direction | What the check would do |
166
+ | --- | --- |
167
+ | **Candidate constraints** | Check a proposed assignment or plan against supplied constraints, such as capacity, rest periods or time windows. |
168
+ | **Document verification** | Check whether a packet is complete and internally consistent, and separately whether its claims are supported by its sources. |
169
+ | **Least-worst routes** | Compare a shortlist of already feasible, costed paths against explicit trade-offs. A router or solver produces the candidates. |
170
+ | **Employment-rule checks** | Flag possible conflicts between a rota, record or deduction and the supplied policy or contract for human review. |
171
+ | **Entitlement evidence checks** | Identify complete, incomplete or conflicting evidence against supplied rules, with a person owning the decision. |
172
+
173
+ These proposals assess candidates and evidence; they do not create schedules, solve routes or establish that a source is true. Employment and entitlement checks would remain review aids, with no autonomous hiring, dismissal, payment or benefit decisions.
174
+
175
+ The proposed order starts with document validity and candidate constraints, followed by route comparison. Employment and entitlement ideas come later, after calibration and review procedures are established.
176
+
177
+ <a name="project-documents"></a>
178
+ ## Project documents
179
+
180
+ | Document | Purpose |
181
+ | --- | --- |
182
+ | [IP-001](docs/b-implementation_plans/IP-001-KICKOFF.md) | MVP scope, build order and acceptance criteria |
183
+ | [Completion report](docs/c-completion_reports/CR-001_Fluid_Decisions.md) | Validation evidence and remaining limitations |
184
+ | [IP-002](docs/b-implementation_plans/IP-002-HARDENING.md) / [CR-002](docs/c-completion_reports/CR-002_Gate_Hardening.md) | Gate hardening, session recovery and real adapter validation |
185
+ | [IP-003](docs/b-implementation_plans/IP-003-UNSCORED-RESULTS.md) / [CR-003](docs/c-completion_reports/CR-003_Unscored_Results.md) | Unscored releases, pending taint, recheck and measured budgets |
186
+ | [Self-hosting](docs/SELF_HOSTING.md) | Download, serve and test your own models |
187
+ | [Usage](docs/USAGE.md) | CLI, Python runtime, hook and MCP contracts |
188
+ | [IDEA-001](docs/a-ideas/IDEA-001.md) | Agent workflow expansion backlog |
189
+ | [IDEA-002](docs/a-ideas/IDEA-002.md) | Candidate, document and rules-checking backlog |
190
+ | [d1 skill and API reference](skills/liquid-d1/SKILL.md) | Typed questions and response interpretation; live self-hosted responses must be verified during implementation |
191
+ | [Agent instructions](AGENTS.md) | Repository working rules and implementation boundaries |
192
+
193
+ <a name="licence"></a>
194
+ ## <img src="https://get.abliter8.ai/doc-assets/licence.png" alt="Licence" height="58">
195
+
196
+ A licence for this repository has not yet been added. The model checkpoints have their own upstream terms: [d1-3B licence](https://huggingface.co/LiquidAI/d1-3B/blob/main/LICENSE) and [d1-omni-600M licence](https://huggingface.co/LiquidAI/d1-omni-600M/blob/main/LICENSE). Model weights are downloaded separately and are not bundled here.
@@ -0,0 +1,172 @@
1
+ <div align="center">
2
+
3
+ <img src="assets/liquid-decisions.png" alt="Fluid Decisions logo" width="260">
4
+
5
+ # Fluid Decisions
6
+
7
+ **Safety classification, routing, tool gating and verification for coding agents and agentic workers.**
8
+
9
+ ![Status: MVP](https://img.shields.io/badge/status-MVP-FF093A?style=for-the-badge&labelColor=0D0D0D)
10
+
11
+ [The MVP](#what-it-does) · [How it works](#how-it-works) · [Self-hosting](#how-to-start) · [Roadmap](#roadmap) · [Project documents](#project-documents)
12
+
13
+ </div>
14
+
15
+ ---
16
+
17
+ > [!NOTE]
18
+ > **MVP with IP-003 result handling.** The Python library, CLI, runtime hook, MCP server and optional model download/serving tools are implemented. Unscored results are distinguished from flags, carried forward as pending taint, and can be rechecked. Policies are **uncalibrated**. See the [result-handling report](docs/c-completion_reports/CR-003_Unscored_Results.md) for live evidence and limitations. IDEA-001 and IDEA-002 remain the roadmap.
19
+
20
+ <a name="overview"></a>
21
+ ## <img src="https://get.abliter8.ai/doc-assets/overview-intro.png" alt="Overview" height="58">
22
+
23
+ Fluid Decisions is a self-hosted decision layer around Liquid AI's d1 models. It helps an agent check what it is about to do, choose where work should go, and verify its claims against supplied evidence.
24
+
25
+ The design separates three responsibilities: **the worker proposes, d1 returns probabilities, and policy code decides what happens next.** A runtime hook enforces the checks around tool execution. The worker's willingness to call a checking tool is not the enforcement mechanism.
26
+
27
+ The longer-term ambition is a reusable layer for checking agent behaviour, documents and proposed decisions against explicit rules. The first milestone is deliberately focused: prove classification, routing, gating and verification, then build on measured results.
28
+
29
+ <a name="what-it-does"></a>
30
+ ## <img src="https://get.abliter8.ai/doc-assets/features-whatitdoes.png" alt="What it does" height="58">
31
+
32
+ ### The MVP: four jobs, one policy layer
33
+
34
+ | Job | What it does | Example |
35
+ | --- | --- | --- |
36
+ | **Classify** | Check code, documents or screenshots against a named set of questions, with probabilities attached to each finding. | Flag code that appears to steal credentials or conceal build-time behaviour. |
37
+ | **Route** | Choose among defined destinations, alongside escalation and completeness checks. | Send a ticket to the right queue, or select a local model, a reasoner or a human. |
38
+ | **Gate** | Check a proposed tool call before execution and return **allow**, **ask** or **deny**. | Block a destructive shell pattern even when the model endpoint is unavailable. |
39
+ | **Verify** | Check claims in a finished answer against the supplied source and flag unsupported claims. | Catch an answer that contradicts a source without inventing a replacement sentence. |
40
+
41
+ One Python library supports a CLI, runtime hooks and a thin MCP server. The hook uses a [documented stdin JSON contract](docs/USAGE.md#runtime-hook-and-mandatory-enforcement). MCP exposes the same capabilities to workers that cannot install a hook; mandatory enforcement requires the host runtime to wire the checks into its workflow. The supplied `GuardedRuntime` wraps tool dispatch and withholds flagged results.
42
+
43
+ An unscored result is released with `checked: false` by default, and its uncertainty is carried to the next action through pending session taint; operators can select withholding instead.
44
+
45
+ The MVP also includes versioned YAML question packs and policies, a session ledger for flagged and pending results, and an audit log of verdicts, probabilities, lane, pack version and latency. Raw input state stays out of the audit log by default.
46
+
47
+ The full scope and acceptance criteria are in [IP-001: the kickoff plan](docs/b-implementation_plans/IP-001-KICKOFF.md).
48
+
49
+ <a name="how-it-works"></a>
50
+ ## <img src="https://get.abliter8.ai/doc-assets/howitworks-function.png" alt="How it works" height="58">
51
+
52
+ d1 answers typed questions over a state: **choice** selects among named options, **score** rates ordered levels, and **noul** gives the probability of yes. Related questions share one request; the optional IP-004 scoping mechanism can give a question a selected dotted-path view of the state and reports unresolved views instead of allowing a missing answer. The measured candidates failed adoption criteria, so the shipped packs remain unscoped. Policy code then applies rules and thresholds to the returned probabilities. See the [d1 reference skill](skills/liquid-d1/SKILL.md) for the API background.
53
+
54
+ The tool-gating path is:
55
+
56
+ ```mermaid
57
+ %%{init: {'theme':'base','themeVariables':{'primaryColor':'#FF093A','primaryTextColor':'#ffffff','primaryBorderColor':'#7a0620','lineColor':'#FF093A','secondaryColor':'#1b1b1b','secondaryTextColor':'#ffffff','tertiaryColor':'#1b1b1b','tertiaryTextColor':'#ffffff','clusterBkg':'#0d0d0d','clusterBorder':'#FF093A','fontFamily':'monospace'}}}%%
58
+ flowchart TD
59
+ A[Worker proposes a tool call] --> B[Runtime hook checks static rules]
60
+ B -->|Blocked pattern| C[Deny]
61
+ B -->|Needs semantic checks| D[d1 answers the question pack]
62
+ D -->|Probabilities| E[Policy applies thresholds]
63
+ D -->|Endpoint unavailable| F[Ask]
64
+ E --> G[Allow / ask / deny]
65
+ C --> H[Runtime enforces the outcome]
66
+ F --> H
67
+ G --> H
68
+ ```
69
+
70
+ The MVP controls are:
71
+
72
+ - **Static rules run first.** Known destructive patterns do not need a model verdict.
73
+ - **Unavailable means ask.** An endpoint failure or timeout cannot become permission to proceed.
74
+ - **Probabilities remain visible.** Full distributions, confidence and yes/no decisiveness stay distinct so callers can see uncertainty.
75
+ - **Some risks cannot be softened.** Authorisation or a user request cannot override secret exposure, constraint violations or unresolved choices.
76
+ - **Calibration follows evidence.** Initial thresholds are explicitly uncalibrated. An allow verdict is a policy outcome, not proof that arbitrary code is safe.
77
+ - **Every model call is budgeted.** Each check's time budget is derived from the lane's measured cost, so a large tool input completes instead of timing out to ask; a call whose expected cost exceeds the configured cap returns ask without being sent. Static denials run first and never wait on a budget.
78
+
79
+ After a tool returns, the hook checks its output for injection or canary content and retains session flags for later gates. Verification of a finished answer must likewise be triggered by the host workflow.
80
+
81
+ <a name="how-to-start"></a>
82
+ ## <img src="https://get.abliter8.ai/doc-assets/quickstart.png" alt="How to start" height="58">
83
+
84
+ ### Built for community self-hosting
85
+
86
+ The setup is **download a model → start its decision API → configure Fluid Decisions → connect a worker**. Follow the [self-hosting guide](docs/SELF_HOSTING.md) for the complete path and [usage guide](docs/USAGE.md) for the CLI, hooks and MCP.
87
+
88
+ | Model | Role | Upstream |
89
+ | --- | --- | --- |
90
+ | **d1-3B** | Default for text and image workflows. The core workflow can run with this model alone. | [Model and loading instructions](https://huggingface.co/LiquidAI/d1-3B) |
91
+ | **d1-omni-600M** | Optional endpoint for short audio inputs. Audio and images are kept in separate requests. | [Model and loading instructions](https://huggingface.co/LiquidAI/d1-omni-600M) |
92
+
93
+ Serve the models on your own hardware or connect to compatible endpoints you control. Public defaults use localhost. The client, CLI, hook and MCP have no PyTorch dependency; install the optional `serve` extra in the model-serving environment.
94
+
95
+ ```sh
96
+ git clone https://github.com/abliter8-ai/fluid-decisions.git
97
+ cd fluid-decisions
98
+ python3 -m venv .venv
99
+ .venv/bin/python -m pip install '.[serve,mcp]'
100
+ .venv/bin/python scripts/download_models.py d1-3B --local-dir models
101
+ .venv/bin/python scripts/serve_model.py --model d1-3B --model-path models/d1-3B
102
+ ```
103
+
104
+ In another terminal, from the same directory:
105
+
106
+ ```sh
107
+ .venv/bin/fluid gate --tool read_file --input '{"path":"README.md"}' \
108
+ --user-request 'Read the README'
109
+ ```
110
+
111
+ The model server stays in the first terminal. Model loading executes the pinned upstream custom code; read the model licence links below. The guide covers optional audio, device selection and timeout configuration for slower hardware.
112
+
113
+ The repository adapter has been tested with d1-3B revision `051bcc464b01b9f92942b364d9586b0ef5912432` on macOS 26.5 / Apple Silicon MPS / Python 3.14.8. Its text, image, batch and tool workflows passed in a bounded local run; see [self-hosting validation](docs/SELF_HOSTING.md#validation-status). Other platforms and local omni serving remain untested.
114
+
115
+ Development uses the maintainer's cluster and gateway. Community users need no access to either. Recorded checks cover the decision lanes and real Qwen/GLM worker calls; separate self-hosting evidence identifies the tested model revisions and environment.
116
+
117
+ <a name="roadmap"></a>
118
+ ## Roadmap
119
+
120
+ The following directions come from the two idea documents. They are an ambition and a backlog, not shipped features or a release schedule. Work on them starts after the MVP is accepted, with each selected idea getting its own implementation plan.
121
+
122
+ ### IDEA-001: deeper checks around agent workflows
123
+
124
+ [IDEA-001](docs/a-ideas/IDEA-001.md) expands the first four jobs into the surrounding agent session. The MVP establishes the initial ledger, audit trail and uncertainty handling; the roadmap develops those foundations into broader workflows.
125
+
126
+ | Direction | Where we hope to get |
127
+ | --- | --- |
128
+ | **Context retention and session memory** | Keep relevant tool traffic verbatim when compacting a session, and make flagged untrusted content visible across later actions. |
129
+ | **More useful findings and CI checks** | Turn uncertain classifications into focused second passes with a useful location, then apply the same checks to diffs and source trees. |
130
+ | **Model cascades and spoken intent** | Route work among local models, larger reasoners and people; explore direct intent routing for short voice clips. |
131
+ | **Visual workflows** | Choose among legal actions on a screenshot and inspect images for explicitly defined defects or regressions. |
132
+ | **Verification at completion** | Attach claim-level source checks when an answer is finished, as part of the runtime's delivery workflow. |
133
+ | **Calibration from real use** | Fit thresholds by action class and study disagreement between models as a signal of uncertainty, without averaging their probabilities. |
134
+
135
+ Context compaction is the first proposed expansion. It preserves retained text verbatim. Later priorities depend on observed reliability and useful audit traces.
136
+
137
+ ### IDEA-002: checks over candidates, documents and rules
138
+
139
+ [IDEA-002](docs/a-ideas/IDEA-002.md) applies the same machinery to work beyond coding agents. A caller supplies a proposed outcome and the rules or evidence to assess it against.
140
+
141
+ | Direction | What the check would do |
142
+ | --- | --- |
143
+ | **Candidate constraints** | Check a proposed assignment or plan against supplied constraints, such as capacity, rest periods or time windows. |
144
+ | **Document verification** | Check whether a packet is complete and internally consistent, and separately whether its claims are supported by its sources. |
145
+ | **Least-worst routes** | Compare a shortlist of already feasible, costed paths against explicit trade-offs. A router or solver produces the candidates. |
146
+ | **Employment-rule checks** | Flag possible conflicts between a rota, record or deduction and the supplied policy or contract for human review. |
147
+ | **Entitlement evidence checks** | Identify complete, incomplete or conflicting evidence against supplied rules, with a person owning the decision. |
148
+
149
+ These proposals assess candidates and evidence; they do not create schedules, solve routes or establish that a source is true. Employment and entitlement checks would remain review aids, with no autonomous hiring, dismissal, payment or benefit decisions.
150
+
151
+ The proposed order starts with document validity and candidate constraints, followed by route comparison. Employment and entitlement ideas come later, after calibration and review procedures are established.
152
+
153
+ <a name="project-documents"></a>
154
+ ## Project documents
155
+
156
+ | Document | Purpose |
157
+ | --- | --- |
158
+ | [IP-001](docs/b-implementation_plans/IP-001-KICKOFF.md) | MVP scope, build order and acceptance criteria |
159
+ | [Completion report](docs/c-completion_reports/CR-001_Fluid_Decisions.md) | Validation evidence and remaining limitations |
160
+ | [IP-002](docs/b-implementation_plans/IP-002-HARDENING.md) / [CR-002](docs/c-completion_reports/CR-002_Gate_Hardening.md) | Gate hardening, session recovery and real adapter validation |
161
+ | [IP-003](docs/b-implementation_plans/IP-003-UNSCORED-RESULTS.md) / [CR-003](docs/c-completion_reports/CR-003_Unscored_Results.md) | Unscored releases, pending taint, recheck and measured budgets |
162
+ | [Self-hosting](docs/SELF_HOSTING.md) | Download, serve and test your own models |
163
+ | [Usage](docs/USAGE.md) | CLI, Python runtime, hook and MCP contracts |
164
+ | [IDEA-001](docs/a-ideas/IDEA-001.md) | Agent workflow expansion backlog |
165
+ | [IDEA-002](docs/a-ideas/IDEA-002.md) | Candidate, document and rules-checking backlog |
166
+ | [d1 skill and API reference](skills/liquid-d1/SKILL.md) | Typed questions and response interpretation; live self-hosted responses must be verified during implementation |
167
+ | [Agent instructions](AGENTS.md) | Repository working rules and implementation boundaries |
168
+
169
+ <a name="licence"></a>
170
+ ## <img src="https://get.abliter8.ai/doc-assets/licence.png" alt="Licence" height="58">
171
+
172
+ A licence for this repository has not yet been added. The model checkpoints have their own upstream terms: [d1-3B licence](https://huggingface.co/LiquidAI/d1-3B/blob/main/LICENSE) and [d1-omni-600M licence](https://huggingface.co/LiquidAI/d1-omni-600M/blob/main/LICENSE). Model weights are downloaded separately and are not bundled here.
@@ -0,0 +1,15 @@
1
+ models:
2
+ d1-3B:
3
+ repo: LiquidAI/d1-3B
4
+ revision: 051bcc464b01b9f92942b364d9586b0ef5912432
5
+ modalities: [text, image]
6
+ port: 8774
7
+ upstream: https://huggingface.co/LiquidAI/d1-3B
8
+ license: https://huggingface.co/LiquidAI/d1-3B/blob/main/LICENSE
9
+ d1-omni-600M:
10
+ repo: LiquidAI/d1-omni-600M
11
+ revision: 02b55d7076f15129e59ab3f94783f32c4b088674
12
+ modalities: [text, image, audio]
13
+ port: 8775
14
+ upstream: https://huggingface.co/LiquidAI/d1-omni-600M
15
+ license: https://huggingface.co/LiquidAI/d1-omni-600M/blob/main/LICENSE
@@ -0,0 +1,3 @@
1
+ """Fluid Decisions: lightweight typed decisions with policy outside the model."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,45 @@
1
+ """Append-only decision records; state bodies are never persisted."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import json
6
+ import os
7
+ from datetime import datetime, timezone
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+
12
+ def data_directory() -> Path:
13
+ return Path(os.environ.get('FLUID_DATA_DIR', '~/.fluid-decisions')).expanduser()
14
+
15
+
16
+ def append_record(path: Path, record: dict[str, Any]) -> None:
17
+ path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
18
+ raw = (json.dumps(record, allow_nan=False, separators=(',', ':')) + '\n').encode()
19
+ fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600)
20
+ try:
21
+ # A single append avoids interleaving between concurrent short records.
22
+ if os.write(fd, raw) != len(raw):
23
+ raise OSError('incomplete audit append')
24
+ finally:
25
+ os.close(fd)
26
+
27
+
28
+ class Audit:
29
+ def __init__(self, path: Path | None = None):
30
+ self.path = path or data_directory() / 'audit.jsonl'
31
+
32
+ def record(self, decision: dict[str, Any], *, state: Any = None) -> None:
33
+ fields = ('job', 'lane', 'pack_version', 'verdict', 'answers', 'timings',
34
+ 'usage', 'reasons', 'passes', 'flagged', 'complete', 'taint_cleared',
35
+ 'clearance_reason', 'session_id_sha256', 'static_injection_rule',
36
+ 'windows', 'first_flagged_window', 'window_answers', 'session_assessment',
37
+ 'check', 'checked', 'budget_source', 'expected_ms', 'resolved', 'scoping',
38
+ 'budget')
39
+ record = {key: decision[key] for key in fields if key in decision}
40
+ record['timestamp'] = datetime.now(timezone.utc).isoformat()
41
+ if os.environ.get('FLUID_AUDIT_STATE') == '1':
42
+ raw = json.dumps(state, sort_keys=True, ensure_ascii=False).encode()
43
+ record['state'] = {'sha256': hashlib.sha256(raw).hexdigest(),
44
+ 'preview': '[state content redacted]', 'bytes': len(raw)}
45
+ append_record(self.path, record)
@@ -0,0 +1,151 @@
1
+ """Command line surfaces for Fluid Decisions.
2
+
3
+ The CLI deliberately has a small JSON contract: input and output are JSON, while
4
+ the exit status communicates the decision class to shell hooks.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import argparse
9
+ import base64
10
+ import json
11
+ import mimetypes
12
+ import sys
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ from .service import Decisions
17
+
18
+
19
+ def _service() -> Any:
20
+ return Decisions()
21
+
22
+
23
+ def _json(value: Any) -> None:
24
+ print(json.dumps(value, ensure_ascii=False, separators=(",", ":")))
25
+
26
+
27
+ def _read_json(value: str | None, filename: str | None, *, stdin: Any = None) -> Any:
28
+ if value is not None and filename is not None:
29
+ raise ValueError("use only one of --state and --file")
30
+ if filename:
31
+ text = Path(filename).read_text(encoding="utf-8")
32
+ elif value is not None:
33
+ text = value
34
+ else:
35
+ text = (sys.stdin if stdin is None else stdin).read()
36
+ if not text.strip():
37
+ raise ValueError("JSON input is required")
38
+ return json.loads(text)
39
+
40
+
41
+ def _media(paths: list[str], kind: str) -> list[str]:
42
+ result = []
43
+ for filename in paths:
44
+ content = Path(filename).read_bytes()
45
+ mime = mimetypes.guess_type(filename)[0] or ("image/png" if kind == "image" else "audio/wav")
46
+ result.append(f"data:{mime};base64,{base64.b64encode(content).decode('ascii')}")
47
+ return result
48
+
49
+
50
+ def _result_exit(result: Any, command: str) -> int:
51
+ if not isinstance(result, dict):
52
+ return 2
53
+ if result.get("complete") is False:
54
+ return 2
55
+ verdict = result.get("verdict")
56
+ if command == "verify":
57
+ findings = result.get("findings", result.get("claims", [])) or []
58
+ if verdict in {"deny", "high"} or any(
59
+ isinstance(item, dict) and item.get("verdict") in {"deny", "high", "flagged"}
60
+ for item in findings
61
+ ):
62
+ return 1
63
+ if verdict in {"deny", "high"}:
64
+ return 1
65
+ if verdict in {"ask", "incomplete"}:
66
+ return 2
67
+ if result.get("error") or result.get("incomplete"):
68
+ return 2
69
+ if command == "health" and any(
70
+ isinstance(result.get(lane), dict) and result[lane].get("error") for lane in ("max",)
71
+ ):
72
+ return 2
73
+ return 0
74
+
75
+
76
+ def _parser() -> argparse.ArgumentParser:
77
+ parser = argparse.ArgumentParser(prog="fluid")
78
+ sub = parser.add_subparsers(dest="command", required=True)
79
+ for name in ("classify", "route"):
80
+ command = sub.add_parser(name)
81
+ command.add_argument("--state")
82
+ command.add_argument("--file")
83
+ command.add_argument("--destinations", help="JSON object for route")
84
+ command.add_argument("--image", action="append", default=[])
85
+ command.add_argument("--audio")
86
+ gate = sub.add_parser("gate")
87
+ gate.add_argument("--tool", required=True)
88
+ gate.add_argument("--input", required=True, help="tool input JSON")
89
+ gate.add_argument("--user-request", default="")
90
+ gate.add_argument("--session")
91
+ verify = sub.add_parser("verify")
92
+ verify.add_argument("--source")
93
+ verify.add_argument("--source-file")
94
+ verify.add_argument("--claim", action="append", required=True)
95
+ session = sub.add_parser("session")
96
+ session_sub = session.add_subparsers(dest="session_command", required=True)
97
+ clear = session_sub.add_parser("clear")
98
+ clear.add_argument("--session", required=True)
99
+ clear.add_argument("--reason", default="operator clearance")
100
+ recheck = session_sub.add_parser("recheck")
101
+ recheck.add_argument("--session", required=True)
102
+ recheck.add_argument("--file", help="Original result text; stdin if omitted")
103
+ sub.add_parser("health")
104
+ return parser
105
+
106
+
107
+ def main(argv: list[str] | None = None) -> int:
108
+ parser = _parser()
109
+ try:
110
+ args = parser.parse_args(argv)
111
+ service = _service()
112
+ try:
113
+ if args.command in {"classify", "route"}:
114
+ state = _read_json(args.state, args.file)
115
+ images = _media(args.image, "image") if args.image else None
116
+ audio = _media([args.audio], "audio")[0] if args.audio else None
117
+ if args.command == "classify":
118
+ result = service.classify(state, images=images, audio=audio)
119
+ else:
120
+ destinations = json.loads(args.destinations) if args.destinations else None
121
+ result = service.route(state, destinations=destinations, images=images, audio=audio)
122
+ elif args.command == "gate":
123
+ result = service.gate(args.tool, json.loads(args.input), user_request=args.user_request, session_id=args.session)
124
+ elif args.command == "verify":
125
+ source = args.source or (Path(args.source_file).read_text(encoding="utf-8") if args.source_file else None)
126
+ if source is None:
127
+ raise ValueError("--source or --source-file is required")
128
+ result = service.verify(source, args.claim)
129
+ elif args.command == "session" and args.session_command == "clear":
130
+ result = service.clear_session(args.session, reason=args.reason)
131
+ elif args.command == "session" and args.session_command == "recheck":
132
+ if args.file:
133
+ text = Path(args.file).read_bytes().decode('utf-8')
134
+ elif hasattr(sys.stdin, 'buffer'):
135
+ text = sys.stdin.buffer.read().decode('utf-8')
136
+ else:
137
+ text = sys.stdin.read()
138
+ result = service.post_result(text, session_id=args.session, recheck=True)
139
+ else:
140
+ result = service.health()
141
+ finally:
142
+ service.close()
143
+ _json(result)
144
+ return _result_exit(result, args.command)
145
+ except Exception as exc:
146
+ _json({"verdict": "ask", "error": {"reason": "invalid_input", "message": str(exc)}})
147
+ return 2
148
+
149
+
150
+ if __name__ == "__main__": # pragma: no cover
151
+ raise SystemExit(main())