cluxion-agentplugin-preprocessing 0.2.0__py3-none-any.whl
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.
- cluxion_agentplugin_adapters/claude/.claude-plugin/plugin.json +8 -0
- cluxion_agentplugin_adapters/claude/skills/preprocess/SKILL.md +33 -0
- cluxion_agentplugin_adapters/codex/config-snippet.toml +5 -0
- cluxion_agentplugin_docs/cluxion-Docs/README.md +22 -0
- cluxion_agentplugin_docs/cluxion-Docs/architecture.md +36 -0
- cluxion_agentplugin_docs/cluxion-Docs/harness-logic.md +51 -0
- cluxion_agentplugin_docs/cluxion-Docs/honesty-preprocessing.md +40 -0
- cluxion_agentplugin_docs/cluxion-Docs/install-and-operations.md +36 -0
- cluxion_agentplugin_docs/cluxion-Docs/security.md +27 -0
- cluxion_agentplugin_docs/github-profile/README.md +67 -0
- cluxion_agentplugin_preprocessing/__init__.py +7 -0
- cluxion_agentplugin_preprocessing/cli.py +124 -0
- cluxion_agentplugin_preprocessing/hermes_config.py +163 -0
- cluxion_agentplugin_preprocessing/plugin.py +135 -0
- cluxion_agentplugin_preprocessing/plugin.yaml +13 -0
- cluxion_agentplugin_preprocessing/runner.py +241 -0
- cluxion_agentplugin_preprocessing/schemas.py +148 -0
- cluxion_agentplugin_preprocessing-0.2.0.dist-info/METADATA +115 -0
- cluxion_agentplugin_preprocessing-0.2.0.dist-info/RECORD +48 -0
- cluxion_agentplugin_preprocessing-0.2.0.dist-info/WHEEL +4 -0
- cluxion_agentplugin_preprocessing-0.2.0.dist-info/entry_points.txt +8 -0
- cluxion_agentplugin_preprocessing-0.2.0.dist-info/licenses/LICENSE +197 -0
- cluxion_runtime/__init__.py +16 -0
- cluxion_runtime/__main__.py +5 -0
- cluxion_runtime/adapters/__init__.py +25 -0
- cluxion_runtime/adapters/contract.py +82 -0
- cluxion_runtime/adapters/grok_build.py +35 -0
- cluxion_runtime/adapters/hermes.py +161 -0
- cluxion_runtime/adapters/spec.py +35 -0
- cluxion_runtime/bootstrap.py +270 -0
- cluxion_runtime/cli.py +282 -0
- cluxion_runtime/core/__init__.py +36 -0
- cluxion_runtime/core/clarification.py +192 -0
- cluxion_runtime/core/dispatch_store.py +270 -0
- cluxion_runtime/core/harness.py +320 -0
- cluxion_runtime/core/intent.py +55 -0
- cluxion_runtime/core/ledger.py +189 -0
- cluxion_runtime/core/ledger_codec.py +38 -0
- cluxion_runtime/core/plan_codec.py +121 -0
- cluxion_runtime/core/preprocess.py +497 -0
- cluxion_runtime/core/types.py +220 -0
- cluxion_runtime/core/work_queue.py +73 -0
- cluxion_runtime/models/__init__.py +15 -0
- cluxion_runtime/models/supervisor.py +156 -0
- cluxion_runtime/models/vllm_mlx.py +87 -0
- cluxion_runtime/resources/__init__.py +7 -0
- cluxion_runtime/resources/queue_bridge.py +128 -0
- cluxion_runtime/resources/rust_bridge.py +82 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cluxion-preprocess
|
|
3
|
+
description: Run Cluxion preprocessing before agent work. You (the connected AI) call cluxion tools/CLI. Use for task planning, long work queueing, or unclear user intent. Enforces honesty and clarification before queueing.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Cluxion Preprocessing — 연결된 AI 지시문
|
|
7
|
+
|
|
8
|
+
작업 전에 **당신(연결된 AI)** 이 plan·큐 도구를 호출합니다. 플러그인은 JSON 계약만 반환합니다.
|
|
9
|
+
|
|
10
|
+
## Plan 호출
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
cluxion-runtime plan --surface claude --json-stdin
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
stdin: `{"prompt": "<user request>", "cwd": "<workspace>"}`
|
|
17
|
+
|
|
18
|
+
Hermes에서는 `cluxion_plan` 도구를 동일 목적으로 사용합니다.
|
|
19
|
+
|
|
20
|
+
## 규칙
|
|
21
|
+
|
|
22
|
+
1. `clarification.required`이 true이면 사용자에게 질문하고 작업을 시작하지 말 것.
|
|
23
|
+
2. context가 부족하면 모른다고 말할 것 — 사실을 지어내지 말 것.
|
|
24
|
+
3. `queued` 모드: `cluxion_queue_next` → segment 처리 → `cluxion_queue_record` → `cluxion_queue_brief`.
|
|
25
|
+
4. 세션 맥락 보존이 필요하면 brief를 ForgetForge에 넘길 것: `forgetforge import-brief --source preprocessing --brief "<cluxion_queue_brief output>"` (또는 Hermes `forgetforge_import_brief`).
|
|
26
|
+
5. `answer_policy.required_checks`를 응답 전에 따를 것.
|
|
27
|
+
6. 플러그인이 대신 completion을 생성하지 않음 — **당신**이 계약에 맞게 응답할 것.
|
|
28
|
+
|
|
29
|
+
## 설치 확인
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
cluxion-preprocess check
|
|
33
|
+
```
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Legacy Docs (cluxion-Docs)
|
|
2
|
+
|
|
3
|
+
> **공개 문서 기준: [`../Docs/`](../Docs/)**
|
|
4
|
+
> 이 폴더는 이전 `hermes-cluxion` 패키징 시기의 **레거시 참고**입니다.
|
|
5
|
+
|
|
6
|
+
## 현재 방향 (요약)
|
|
7
|
+
|
|
8
|
+
- **연결된 AI**가 `cluxion_plan`·큐 도구를 호출하고 JSON 계약을 따릅니다.
|
|
9
|
+
- **모델·OAuth·provider**는 host agent가 소유합니다. 플러그인은 plan·게이트만 반환합니다.
|
|
10
|
+
- 플러그인 내부에서 **별도 LLM을 호출하지 않습니다.**
|
|
11
|
+
|
|
12
|
+
사용자·에이전트 연동 설명은 **`Docs/README.md`** 를 따르세요.
|
|
13
|
+
|
|
14
|
+
## 레거시 목차 (기술 참고)
|
|
15
|
+
|
|
16
|
+
| 문서 | 내용 |
|
|
17
|
+
|------|------|
|
|
18
|
+
| [architecture.md](./architecture.md) | 패키지 경계 (레거시 명칭 포함) |
|
|
19
|
+
| [harness-logic.md](./harness-logic.md) | intent·전처리·큐·resource |
|
|
20
|
+
| [honesty-preprocessing.md](./honesty-preprocessing.md) | answer_policy 계약 |
|
|
21
|
+
| [install-and-operations.md](./install-and-operations.md) | 설치 요약 → `Docs/installation.md` |
|
|
22
|
+
| [security.md](./security.md) | 공개 배포 보안 원칙 |
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Architecture (legacy reference)
|
|
2
|
+
|
|
3
|
+
> 최신 문서: [`../Docs/architecture.md`](../Docs/architecture.md)
|
|
4
|
+
|
|
5
|
+
## 패키지 구성
|
|
6
|
+
|
|
7
|
+
| 패키지 | 역할 |
|
|
8
|
+
|--------|------|
|
|
9
|
+
| `cluxion_agentplugin_preprocessing` | Hermes plugin, CLI, tool schema |
|
|
10
|
+
| `cluxion_runtime` | `cluxion-runtime plan`, harness 엔진 |
|
|
11
|
+
| `rust/cluxion_queue` | SQLite 작업큐 + dispatch (선택) |
|
|
12
|
+
|
|
13
|
+
## Host vs Cluxion
|
|
14
|
+
|
|
15
|
+
**Host agent (연결된 AI)**
|
|
16
|
+
|
|
17
|
+
- OAuth, provider, **모델 선택**
|
|
18
|
+
- tool 권한, completion, 최종 응답
|
|
19
|
+
- queued segment 처리·synthesis
|
|
20
|
+
|
|
21
|
+
**Cluxion preprocessing**
|
|
22
|
+
|
|
23
|
+
- `WorkItem` 정규화, intent 분류
|
|
24
|
+
- `answer_policy` · `host_execution` 계약
|
|
25
|
+
- 명확화·segment 큐·resource admission
|
|
26
|
+
- **추가 LLM 호출 없음**
|
|
27
|
+
|
|
28
|
+
## 등록 도구 (Hermes `cluxion` toolset)
|
|
29
|
+
|
|
30
|
+
| Tool | 용도 |
|
|
31
|
+
|------|------|
|
|
32
|
+
| `cluxion_plan` | 전처리·방향·큐·리소스 plan |
|
|
33
|
+
| `cluxion_clarify` | 명확화 질문 |
|
|
34
|
+
| `cluxion_queue_next` / `record` / `brief` | segment 큐 |
|
|
35
|
+
|
|
36
|
+
모델·provider 변경은 host agent 책임입니다. 전처리 플러그인은 실행 **전** 계약만 제공합니다.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Harness Logic (legacy reference)
|
|
2
|
+
|
|
3
|
+
> 최신 문서: [`../Docs/design.md`](../Docs/design.md), [`../Docs/tools.md`](../Docs/tools.md)
|
|
4
|
+
|
|
5
|
+
## 1. Adapter Payload
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cluxion-runtime plan --json-stdin --surface <surface>
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
필수: `prompt`
|
|
12
|
+
선택: `work_id`, `priority`, `cwd`, `metadata`, `expected_ram_mb`, `context_tokens`
|
|
13
|
+
|
|
14
|
+
## 2. Intent 분류
|
|
15
|
+
|
|
16
|
+
`classify_intent()` — **모델 호출 없음**, 결정론적.
|
|
17
|
+
|
|
18
|
+
- `category`, `operation`, `direction`, `confidence`, `signals`
|
|
19
|
+
- 방향은 host harness (`hermes_harness`, `claude_harness`, …) 또는 `host_managed`
|
|
20
|
+
|
|
21
|
+
## 3. 전처리 모드
|
|
22
|
+
|
|
23
|
+
| Mode | 용도 |
|
|
24
|
+
|------|------|
|
|
25
|
+
| `simple_answer` | 짧은 일반 질문 |
|
|
26
|
+
| `verification_answer` | 사실 확인 필요 짧은 질문 |
|
|
27
|
+
| `standard` | 코드·테스트·보안·문서 등 실질 작업 |
|
|
28
|
+
| `queued` | 긴 입력 segment 분할 |
|
|
29
|
+
| `needs_clarification` | 사용자 방향 확정 전 |
|
|
30
|
+
|
|
31
|
+
모든 모드에 `answer_policy` 포함.
|
|
32
|
+
|
|
33
|
+
## 4. 작업큐
|
|
34
|
+
|
|
35
|
+
`AgentWorkQueue` — priority + FIFO. Rust `cluxion-queue` 또는 Python fallback.
|
|
36
|
+
|
|
37
|
+
## 5. Host Execution
|
|
38
|
+
|
|
39
|
+
`host_execution` 계약을 **연결된 AI**가 읽고 실행:
|
|
40
|
+
|
|
41
|
+
- `current_turn_direct_answer`
|
|
42
|
+
- `current_turn_verify_then_answer`
|
|
43
|
+
- `single_host_task`
|
|
44
|
+
- `durable_segment_queue` + `cluxion_queue_*`
|
|
45
|
+
- `ask_user_before_queue`
|
|
46
|
+
|
|
47
|
+
## 6. Resource Admission
|
|
48
|
+
|
|
49
|
+
`collect_resource_snapshot()` + `capacity_decision()` — fail-closed RAM/CPU 게이트.
|
|
50
|
+
|
|
51
|
+
Queued segment content는 out-of-band store에 저장됩니다.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Honesty Preprocessing (legacy reference)
|
|
2
|
+
|
|
3
|
+
> 최신 문서: [`../Docs/design.md`](../Docs/design.md)
|
|
4
|
+
|
|
5
|
+
플러그인은 모델을 바꾸지 않습니다. **연결된 AI**가 따라야 하는 `answer_policy` 계약을 만듭니다.
|
|
6
|
+
|
|
7
|
+
## Goals
|
|
8
|
+
|
|
9
|
+
- 모르면 모른다고 말하게 함
|
|
10
|
+
- 확인하지 않은 실행·파일·환경 상태를 사실처럼 말하지 않게 함
|
|
11
|
+
- 확인한 사실 / 추론 / unknown 분리
|
|
12
|
+
- 짧은 질문에 heavy preprocessing 비용 최소화
|
|
13
|
+
|
|
14
|
+
## Modes
|
|
15
|
+
|
|
16
|
+
| Mode | 동작 |
|
|
17
|
+
|------|------|
|
|
18
|
+
| `simple_answer` | segment·resource snapshot 없음 |
|
|
19
|
+
| `verification_answer` | `verification_required` + `required_checks` |
|
|
20
|
+
| `standard` | segment·resource admission·evidence 기반 응답 |
|
|
21
|
+
| `queued` | checksum·순서 보존 합성 |
|
|
22
|
+
|
|
23
|
+
## Required Checks (대표)
|
|
24
|
+
|
|
25
|
+
- `verify_current_or_recent_fact`
|
|
26
|
+
- `cite_external_source_or_document`
|
|
27
|
+
- `inspect_runtime_state_before_claiming`
|
|
28
|
+
- `run_requested_check_or_state_not_run`
|
|
29
|
+
- `tie_claims_to_file_diff_or_command_output`
|
|
30
|
+
- `tie_security_claims_to_evidence`
|
|
31
|
+
- `preserve_segment_checksums_in_synthesis`
|
|
32
|
+
|
|
33
|
+
## Hard Boundaries
|
|
34
|
+
|
|
35
|
+
- 기억·추정만으로 현재 상태 단정 금지
|
|
36
|
+
- 실행하지 않은 테스트 통과 주장 금지
|
|
37
|
+
- 존재하지 않는 파일·URL·버전 날조 금지
|
|
38
|
+
- ambiguous prompt는 확정 표현 자제
|
|
39
|
+
|
|
40
|
+
검증·실행·응답은 **연결된 AI** 책임입니다.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Install And Operations (legacy reference)
|
|
2
|
+
|
|
3
|
+
> 최신 문서: [`../Docs/installation.md`](../Docs/installation.md)
|
|
4
|
+
|
|
5
|
+
## 설치
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install cluxion-agentplugin-preprocessing
|
|
9
|
+
cluxion-preprocess check
|
|
10
|
+
cluxion-preprocess enable # Hermes
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## 연결된 AI 연동
|
|
14
|
+
|
|
15
|
+
- Hermes: `cluxion_*` 도구 (plugin enable 후)
|
|
16
|
+
- Claude: `adapters/claude/skills/preprocess/SKILL.md`
|
|
17
|
+
- Codex: `adapters/codex/config-snippet.toml`
|
|
18
|
+
- 공통: `cluxion-runtime plan --surface <hermes|claude|codex|grok_build>`
|
|
19
|
+
|
|
20
|
+
## Rust 큐 (선택)
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
cargo build --release --manifest-path rust/cluxion_queue/Cargo.toml
|
|
24
|
+
export CLUXION_QUEUE_BIN=/path/to/cluxion-queue
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 배포 전 점검
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
uv run ruff check .
|
|
31
|
+
uv run pytest
|
|
32
|
+
cluxion-preprocess check
|
|
33
|
+
cluxion-runtime plan --surface hermes --prompt "smoke"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
모델 endpoint·provider 설정은 **host agent 문서**를 참고하세요. 전처리 플러그인은 모델을 소유하지 않습니다.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
> 공개 배포용 보안 원칙. 연동 설명은 [`../Docs/`](../Docs/) 참고.
|
|
4
|
+
|
|
5
|
+
## 원칙
|
|
6
|
+
|
|
7
|
+
- API 키·OAuth credential·secret 파일을 저장하지 않습니다.
|
|
8
|
+
- subprocess는 배열 인자만 사용 (`shell=True` 금지).
|
|
9
|
+
- Hermes config 변경 시 timestamp backup 후 atomic replace.
|
|
10
|
+
- 큐·dispatch 데이터는 **사용자 디스크 경로**에만 저장, 원격 전송 없음.
|
|
11
|
+
- host agent venv를 오염시키지 않도록 runtime 의존성은 격리 경로 사용 (코드 내부 정책).
|
|
12
|
+
|
|
13
|
+
## Host 경계
|
|
14
|
+
|
|
15
|
+
- 모델·provider·인증은 **연결된 AI / host agent** 소유.
|
|
16
|
+
- Cluxion은 plan·게이트 JSON만 반환.
|
|
17
|
+
|
|
18
|
+
## 배포 전 점검
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
uv run ruff check .
|
|
22
|
+
uv run pytest
|
|
23
|
+
uv run python -m build
|
|
24
|
+
uv run twine check dist/*
|
|
25
|
+
cluxion-preprocess check
|
|
26
|
+
cluxion-runtime plan --surface hermes --prompt "smoke"
|
|
27
|
+
```
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# hermes-cluxion
|
|
2
|
+
|
|
3
|
+
`hermes-cluxion` is a lightweight Cluxion harness plugin for Hermes Agent.
|
|
4
|
+
|
|
5
|
+
It keeps Hermes in control of OAuth, provider auth, active model selection, tools, permissions, and final AI completion calls. Cluxion adds deterministic work planning around Hermes: preprocessing, intent routing, honesty contracts, queued segment dispatch, resource admission, and optional local `vllm-mlx` endpoint setup.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
python -m pip install "hermes-cluxion==0.1.9"
|
|
11
|
+
hermes-cluxion enable
|
|
12
|
+
hermes-cluxion check
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Confirm the plugin is enabled:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
hermes tools list
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Expected:
|
|
22
|
+
|
|
23
|
+
```text
|
|
24
|
+
Plugin toolsets (cli):
|
|
25
|
+
✓ enabled cluxion 🔌 Cluxion
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Hermes Tools
|
|
29
|
+
|
|
30
|
+
- `cluxion_plan`
|
|
31
|
+
- `cluxion_bootstrap`
|
|
32
|
+
- `cluxion_serve_local`
|
|
33
|
+
- `cluxion_hermes_config`
|
|
34
|
+
- `cluxion_queue_next`
|
|
35
|
+
- `cluxion_queue_record`
|
|
36
|
+
- `cluxion_queue_brief`
|
|
37
|
+
|
|
38
|
+
## Execution Model
|
|
39
|
+
|
|
40
|
+
- Cloud AI calls stay inside Hermes.
|
|
41
|
+
- Local model calls also stay inside Hermes after switching to a custom local provider.
|
|
42
|
+
- Cluxion returns `host_execution`, `answer_policy`, queue metadata, and resource decisions.
|
|
43
|
+
- Short simple prompts do not pay queue or resource snapshot cost.
|
|
44
|
+
- Short verification prompts get required checks without heavy preprocessing.
|
|
45
|
+
- Long work uses durable queued segment dispatch and final briefing synthesis.
|
|
46
|
+
|
|
47
|
+
## Local MLX Flow
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
hermes-cluxion bootstrap --upgrade
|
|
51
|
+
hermes-cluxion hermes-config \
|
|
52
|
+
--model mlx-community/Qwen3.6-35B-A3B-OptiQ-4bit \
|
|
53
|
+
--port 23003 \
|
|
54
|
+
--context-length 131072
|
|
55
|
+
cluxion-runtime serve-local \
|
|
56
|
+
--model mlx-community/Qwen3.6-35B-A3B-OptiQ-4bit \
|
|
57
|
+
--port 23003 \
|
|
58
|
+
--upgrade-runtime
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Then switch Hermes:
|
|
62
|
+
|
|
63
|
+
```text
|
|
64
|
+
/model custom:cluxion-local:mlx-community/Qwen3.6-35B-A3B-OptiQ-4bit
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Full guide: [README](../../README.md)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""CLI helpers for the universal Cluxion preprocessing plugin."""
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from cluxion_agentplugin_preprocessing import __version__, hermes_config, runner
|
|
11
|
+
from cluxion_runtime.bootstrap import ensure_local_runtime
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from collections.abc import Sequence
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
18
|
+
"""Run cluxion-preprocess CLI."""
|
|
19
|
+
parser = _parser()
|
|
20
|
+
args = parser.parse_args(argv)
|
|
21
|
+
if args.command == "check":
|
|
22
|
+
return _check()
|
|
23
|
+
if args.command == "bootstrap":
|
|
24
|
+
return _bootstrap(args)
|
|
25
|
+
if args.command == "status":
|
|
26
|
+
return _status(args)
|
|
27
|
+
if args.command == "enable":
|
|
28
|
+
return _enable(args)
|
|
29
|
+
if args.command == "disable":
|
|
30
|
+
return _disable(args)
|
|
31
|
+
if args.command == "hermes-config":
|
|
32
|
+
return _hermes_config(args)
|
|
33
|
+
parser.print_help(sys.stderr)
|
|
34
|
+
return 2
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _parser() -> argparse.ArgumentParser:
|
|
38
|
+
parser = argparse.ArgumentParser(prog="cluxion-preprocess")
|
|
39
|
+
parser.add_argument("--version", action="version", version=f"cluxion-agentplugin-preprocessing {__version__}")
|
|
40
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
41
|
+
subparsers.add_parser("check", help="Check whether cluxion-runtime is visible")
|
|
42
|
+
bootstrap = subparsers.add_parser("bootstrap", help="Install or upgrade local runtime dependencies")
|
|
43
|
+
bootstrap.add_argument("--upgrade", action="store_true")
|
|
44
|
+
bootstrap.add_argument("--dry-run", action="store_true")
|
|
45
|
+
bootstrap.add_argument("--package", action="append", default=None)
|
|
46
|
+
status = subparsers.add_parser("status", help="Show whether Hermes config enables this plugin")
|
|
47
|
+
status.add_argument("--home", default=None)
|
|
48
|
+
enable = subparsers.add_parser("enable", help="Enable the pip-installed plugin in Hermes config")
|
|
49
|
+
enable.add_argument("--home", default=None)
|
|
50
|
+
enable.add_argument("--dry-run", action="store_true")
|
|
51
|
+
disable = subparsers.add_parser("disable", help="Disable the pip-installed plugin in Hermes config")
|
|
52
|
+
disable.add_argument("--home", default=None)
|
|
53
|
+
disable.add_argument("--dry-run", action="store_true")
|
|
54
|
+
config = subparsers.add_parser("hermes-config", help="Render Hermes local endpoint config through cluxion-runtime")
|
|
55
|
+
config.add_argument("--model", required=True)
|
|
56
|
+
config.add_argument("--host", default="127.0.0.1")
|
|
57
|
+
config.add_argument("--port", type=int, default=23003)
|
|
58
|
+
config.add_argument("--context-length", type=int, default=131_072)
|
|
59
|
+
config.add_argument("--provider-key", default="cluxion-local")
|
|
60
|
+
config.add_argument("--display-name", default="Cluxion Local")
|
|
61
|
+
return parser
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _check() -> int:
|
|
65
|
+
from cluxion_runtime.resources.queue_bridge import queue_available
|
|
66
|
+
|
|
67
|
+
payload = {
|
|
68
|
+
"plugin": "cluxion-agentplugin-preprocessing",
|
|
69
|
+
"version": __version__,
|
|
70
|
+
"cluxion_runtime_available": runner.runtime_available(),
|
|
71
|
+
"rust_queue_available": queue_available(),
|
|
72
|
+
}
|
|
73
|
+
print(json.dumps(payload, ensure_ascii=False, sort_keys=True))
|
|
74
|
+
return 0 if payload["cluxion_runtime_available"] else 1
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _bootstrap(args: argparse.Namespace) -> int:
|
|
78
|
+
result = ensure_local_runtime(
|
|
79
|
+
packages=tuple(args.package) if args.package else ("vllm-mlx",),
|
|
80
|
+
upgrade=bool(args.upgrade),
|
|
81
|
+
dry_run=bool(args.dry_run),
|
|
82
|
+
)
|
|
83
|
+
print(json.dumps(result.to_dict(), ensure_ascii=False, sort_keys=True))
|
|
84
|
+
return 0 if result.ok else 1
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _status(args: argparse.Namespace) -> int:
|
|
88
|
+
result = hermes_config.plugin_status(args.home)
|
|
89
|
+
print(json.dumps({"ok": True, **result.to_dict()}, ensure_ascii=False, sort_keys=True))
|
|
90
|
+
return 0 if result.enabled else 1
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _enable(args: argparse.Namespace) -> int:
|
|
94
|
+
result = hermes_config.enable_plugin(args.home, dry_run=bool(args.dry_run))
|
|
95
|
+
print(json.dumps({"ok": True, **result.to_dict()}, ensure_ascii=False, sort_keys=True))
|
|
96
|
+
return 0
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _disable(args: argparse.Namespace) -> int:
|
|
100
|
+
result = hermes_config.disable_plugin(args.home, dry_run=bool(args.dry_run))
|
|
101
|
+
print(json.dumps({"ok": True, **result.to_dict()}, ensure_ascii=False, sort_keys=True))
|
|
102
|
+
return 0
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _hermes_config(args: argparse.Namespace) -> int:
|
|
106
|
+
result = runner.hermes_config(
|
|
107
|
+
{
|
|
108
|
+
"model": str(args.model),
|
|
109
|
+
"host": str(args.host),
|
|
110
|
+
"port": int(args.port),
|
|
111
|
+
"context_length": int(args.context_length),
|
|
112
|
+
"provider_key": str(args.provider_key),
|
|
113
|
+
"display_name": str(args.display_name),
|
|
114
|
+
}
|
|
115
|
+
)
|
|
116
|
+
print(result.to_json())
|
|
117
|
+
return 0 if result.ok else 1
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
if __name__ == "__main__":
|
|
121
|
+
raise SystemExit(main())
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
__all__ = ["main"]
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Hermes config helpers for pip-installed entry point plugins."""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import tempfile
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from datetime import UTC, datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import yaml
|
|
14
|
+
|
|
15
|
+
PLUGIN_NAME = "cluxion-agentplugin-preprocessing"
|
|
16
|
+
LEGACY_PLUGIN_NAME = "hermes-cluxion"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class ConfigChange:
|
|
21
|
+
"""Result of a Hermes config update."""
|
|
22
|
+
|
|
23
|
+
config_path: Path
|
|
24
|
+
changed: bool
|
|
25
|
+
enabled: bool
|
|
26
|
+
backup_path: Path | None = None
|
|
27
|
+
|
|
28
|
+
def to_dict(self) -> dict[str, object]:
|
|
29
|
+
return {
|
|
30
|
+
"config_path": str(self.config_path),
|
|
31
|
+
"changed": self.changed,
|
|
32
|
+
"enabled": self.enabled,
|
|
33
|
+
"backup_path": str(self.backup_path) if self.backup_path else "",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def plugin_status(home: str | os.PathLike[str] | None = None) -> ConfigChange:
|
|
38
|
+
"""Return whether Hermes config enables this plugin."""
|
|
39
|
+
path = config_path(home)
|
|
40
|
+
config = _read_config(path)
|
|
41
|
+
return ConfigChange(path, False, _is_enabled(config))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def enable_plugin(home: str | os.PathLike[str] | None = None, *, dry_run: bool = False) -> ConfigChange:
|
|
45
|
+
"""Add hermes-cluxion to plugins.enabled and remove it from plugins.disabled."""
|
|
46
|
+
path = config_path(home)
|
|
47
|
+
config = _read_config(path)
|
|
48
|
+
changed = _set_enabled(config, enabled=True)
|
|
49
|
+
if dry_run or not changed:
|
|
50
|
+
return ConfigChange(path, changed, True)
|
|
51
|
+
backup = _write_config(path, config)
|
|
52
|
+
return ConfigChange(path, changed, True, backup)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def disable_plugin(home: str | os.PathLike[str] | None = None, *, dry_run: bool = False) -> ConfigChange:
|
|
56
|
+
"""Move hermes-cluxion from plugins.enabled to plugins.disabled."""
|
|
57
|
+
path = config_path(home)
|
|
58
|
+
config = _read_config(path)
|
|
59
|
+
changed = _set_enabled(config, enabled=False)
|
|
60
|
+
if dry_run or not changed:
|
|
61
|
+
return ConfigChange(path, changed, False)
|
|
62
|
+
backup = _write_config(path, config)
|
|
63
|
+
return ConfigChange(path, changed, False, backup)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def config_path(home: str | os.PathLike[str] | None = None) -> Path:
|
|
67
|
+
"""Resolve the Hermes config path without importing Hermes internals."""
|
|
68
|
+
if home is not None:
|
|
69
|
+
hermes_home = Path(home).expanduser()
|
|
70
|
+
else:
|
|
71
|
+
hermes_home = Path(os.environ.get("HERMES_HOME", "")).expanduser() if os.environ.get("HERMES_HOME") else Path.home() / ".hermes"
|
|
72
|
+
return hermes_home / "config.yaml"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _read_config(path: Path) -> dict[str, Any]:
|
|
76
|
+
if not path.exists():
|
|
77
|
+
return {}
|
|
78
|
+
raw = path.read_text(encoding="utf-8")
|
|
79
|
+
if not raw.strip():
|
|
80
|
+
return {}
|
|
81
|
+
loaded = yaml.safe_load(raw)
|
|
82
|
+
if loaded is None:
|
|
83
|
+
return {}
|
|
84
|
+
if not isinstance(loaded, dict):
|
|
85
|
+
raise ValueError(f"{path} must contain a YAML mapping")
|
|
86
|
+
return dict(loaded)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _is_enabled(config: dict[str, Any]) -> bool:
|
|
90
|
+
plugins = config.get("plugins")
|
|
91
|
+
if not isinstance(plugins, dict):
|
|
92
|
+
return False
|
|
93
|
+
enabled = plugins.get("enabled")
|
|
94
|
+
disabled = plugins.get("disabled")
|
|
95
|
+
return (_contains(enabled, PLUGIN_NAME) or _contains(enabled, LEGACY_PLUGIN_NAME)) and not _contains(
|
|
96
|
+
disabled, PLUGIN_NAME
|
|
97
|
+
) and not _contains(disabled, LEGACY_PLUGIN_NAME)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _set_enabled(config: dict[str, Any], *, enabled: bool) -> bool:
|
|
101
|
+
plugins = config.setdefault("plugins", {})
|
|
102
|
+
if not isinstance(plugins, dict):
|
|
103
|
+
raise ValueError("plugins must be a YAML mapping")
|
|
104
|
+
enabled_list = _as_list(plugins.get("enabled"))
|
|
105
|
+
disabled_list = _as_list(plugins.get("disabled"))
|
|
106
|
+
|
|
107
|
+
old_enabled = list(enabled_list)
|
|
108
|
+
old_disabled = list(disabled_list)
|
|
109
|
+
if enabled:
|
|
110
|
+
if PLUGIN_NAME not in enabled_list:
|
|
111
|
+
enabled_list.append(PLUGIN_NAME)
|
|
112
|
+
disabled_list = [item for item in disabled_list if item not in {PLUGIN_NAME, LEGACY_PLUGIN_NAME}]
|
|
113
|
+
else:
|
|
114
|
+
enabled_list = [item for item in enabled_list if item not in {PLUGIN_NAME, LEGACY_PLUGIN_NAME}]
|
|
115
|
+
if PLUGIN_NAME not in disabled_list:
|
|
116
|
+
disabled_list.append(PLUGIN_NAME)
|
|
117
|
+
|
|
118
|
+
plugins["enabled"] = enabled_list
|
|
119
|
+
if disabled_list:
|
|
120
|
+
plugins["disabled"] = disabled_list
|
|
121
|
+
else:
|
|
122
|
+
plugins.pop("disabled", None)
|
|
123
|
+
return enabled_list != old_enabled or disabled_list != old_disabled
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _as_list(value: object) -> list[str]:
|
|
127
|
+
if value is None:
|
|
128
|
+
return []
|
|
129
|
+
if not isinstance(value, list):
|
|
130
|
+
raise ValueError("plugins.enabled/plugins.disabled must be YAML lists")
|
|
131
|
+
items: list[str] = []
|
|
132
|
+
for item in value:
|
|
133
|
+
if not isinstance(item, str):
|
|
134
|
+
raise ValueError("plugins.enabled/plugins.disabled entries must be strings")
|
|
135
|
+
items.append(item)
|
|
136
|
+
return items
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _contains(value: object, item: str) -> bool:
|
|
140
|
+
return isinstance(value, list) and item in value
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _write_config(path: Path, config: dict[str, Any]) -> Path | None:
|
|
144
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
145
|
+
backup = _backup_config(path)
|
|
146
|
+
content = yaml.safe_dump(config, allow_unicode=True, sort_keys=False)
|
|
147
|
+
with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=path.parent, delete=False) as handle:
|
|
148
|
+
handle.write(content)
|
|
149
|
+
temp_name = handle.name
|
|
150
|
+
Path(temp_name).replace(path)
|
|
151
|
+
return backup
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _backup_config(path: Path) -> Path | None:
|
|
155
|
+
if not path.exists():
|
|
156
|
+
return None
|
|
157
|
+
stamp = datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ")
|
|
158
|
+
backup = path.with_name(f"{path.name}.bak-{stamp}")
|
|
159
|
+
shutil.copy2(path, backup)
|
|
160
|
+
return backup
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
__all__ = ["ConfigChange", "config_path", "disable_plugin", "enable_plugin", "plugin_status"]
|