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.
Files changed (48) hide show
  1. cluxion_agentplugin_adapters/claude/.claude-plugin/plugin.json +8 -0
  2. cluxion_agentplugin_adapters/claude/skills/preprocess/SKILL.md +33 -0
  3. cluxion_agentplugin_adapters/codex/config-snippet.toml +5 -0
  4. cluxion_agentplugin_docs/cluxion-Docs/README.md +22 -0
  5. cluxion_agentplugin_docs/cluxion-Docs/architecture.md +36 -0
  6. cluxion_agentplugin_docs/cluxion-Docs/harness-logic.md +51 -0
  7. cluxion_agentplugin_docs/cluxion-Docs/honesty-preprocessing.md +40 -0
  8. cluxion_agentplugin_docs/cluxion-Docs/install-and-operations.md +36 -0
  9. cluxion_agentplugin_docs/cluxion-Docs/security.md +27 -0
  10. cluxion_agentplugin_docs/github-profile/README.md +67 -0
  11. cluxion_agentplugin_preprocessing/__init__.py +7 -0
  12. cluxion_agentplugin_preprocessing/cli.py +124 -0
  13. cluxion_agentplugin_preprocessing/hermes_config.py +163 -0
  14. cluxion_agentplugin_preprocessing/plugin.py +135 -0
  15. cluxion_agentplugin_preprocessing/plugin.yaml +13 -0
  16. cluxion_agentplugin_preprocessing/runner.py +241 -0
  17. cluxion_agentplugin_preprocessing/schemas.py +148 -0
  18. cluxion_agentplugin_preprocessing-0.2.0.dist-info/METADATA +115 -0
  19. cluxion_agentplugin_preprocessing-0.2.0.dist-info/RECORD +48 -0
  20. cluxion_agentplugin_preprocessing-0.2.0.dist-info/WHEEL +4 -0
  21. cluxion_agentplugin_preprocessing-0.2.0.dist-info/entry_points.txt +8 -0
  22. cluxion_agentplugin_preprocessing-0.2.0.dist-info/licenses/LICENSE +197 -0
  23. cluxion_runtime/__init__.py +16 -0
  24. cluxion_runtime/__main__.py +5 -0
  25. cluxion_runtime/adapters/__init__.py +25 -0
  26. cluxion_runtime/adapters/contract.py +82 -0
  27. cluxion_runtime/adapters/grok_build.py +35 -0
  28. cluxion_runtime/adapters/hermes.py +161 -0
  29. cluxion_runtime/adapters/spec.py +35 -0
  30. cluxion_runtime/bootstrap.py +270 -0
  31. cluxion_runtime/cli.py +282 -0
  32. cluxion_runtime/core/__init__.py +36 -0
  33. cluxion_runtime/core/clarification.py +192 -0
  34. cluxion_runtime/core/dispatch_store.py +270 -0
  35. cluxion_runtime/core/harness.py +320 -0
  36. cluxion_runtime/core/intent.py +55 -0
  37. cluxion_runtime/core/ledger.py +189 -0
  38. cluxion_runtime/core/ledger_codec.py +38 -0
  39. cluxion_runtime/core/plan_codec.py +121 -0
  40. cluxion_runtime/core/preprocess.py +497 -0
  41. cluxion_runtime/core/types.py +220 -0
  42. cluxion_runtime/core/work_queue.py +73 -0
  43. cluxion_runtime/models/__init__.py +15 -0
  44. cluxion_runtime/models/supervisor.py +156 -0
  45. cluxion_runtime/models/vllm_mlx.py +87 -0
  46. cluxion_runtime/resources/__init__.py +7 -0
  47. cluxion_runtime/resources/queue_bridge.py +128 -0
  48. cluxion_runtime/resources/rust_bridge.py +82 -0
@@ -0,0 +1,192 @@
1
+ from __future__ import annotations
2
+
3
+ """Deterministic ambiguity detection and user clarification before queueing."""
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from cluxion_runtime.core.types import WorkIntent, WorkItem
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class ClarificationQuestion:
12
+ """Single question the host agent must ask the user."""
13
+
14
+ question_id: str
15
+ prompt: str
16
+ why: str
17
+ blocking: bool = True
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class ClarificationResult:
22
+ """Whether work can proceed or must ask the user first."""
23
+
24
+ required: bool
25
+ ready_for_queue: bool
26
+ reason_codes: tuple[str, ...]
27
+ questions: tuple[ClarificationQuestion, ...]
28
+ resolved_direction: str = ""
29
+
30
+ def to_dict(self) -> dict[str, object]:
31
+ return {
32
+ "required": self.required,
33
+ "ready_for_queue": self.ready_for_queue,
34
+ "reason_codes": list(self.reason_codes),
35
+ "resolved_direction": self.resolved_direction,
36
+ "questions": [
37
+ {
38
+ "question_id": question.question_id,
39
+ "prompt": question.prompt,
40
+ "why": question.why,
41
+ "blocking": question.blocking,
42
+ }
43
+ for question in self.questions
44
+ ],
45
+ }
46
+
47
+
48
+ _AMBIGUOUS_KEYWORDS = (
49
+ "maybe",
50
+ "perhaps",
51
+ "either",
52
+ "or",
53
+ "not sure",
54
+ "unsure",
55
+ "아마",
56
+ "어느",
57
+ "둘 중",
58
+ "모르겠",
59
+ "헷갈",
60
+ "애매",
61
+ )
62
+ _SCOPE_KEYWORDS = ("all", "everything", "전부", "전체", "모든")
63
+ _TARGET_MISSING_KEYWORDS = ("fix", "implement", "refactor", "patch", "수정", "구현", "리팩터", "패치")
64
+ _LOW_CONFIDENCE_THRESHOLD = 0.62
65
+
66
+
67
+ def assess_clarification(item: WorkItem, intent: WorkIntent) -> ClarificationResult:
68
+ """Decide whether the host agent must ask the user before queueing work."""
69
+ text = item.prompt.lower()
70
+ reasons: list[str] = []
71
+ questions: list[ClarificationQuestion] = []
72
+
73
+ if intent.confidence < _LOW_CONFIDENCE_THRESHOLD and _needs_direction_confirmation(text, intent):
74
+ reasons.append("low_intent_confidence")
75
+ questions.append(
76
+ ClarificationQuestion(
77
+ "intent_direction",
78
+ "어떤 방향으로 진행할지 한 문장으로 확정해 주세요. (예: 버그 수정 / 문서 작성 / 조사만)",
79
+ "요청 의도가 여러 갈래로 해석될 수 있습니다.",
80
+ )
81
+ )
82
+
83
+ if _has_any(text, _AMBIGUOUS_KEYWORDS):
84
+ reasons.append("ambiguous_language")
85
+ questions.append(
86
+ ClarificationQuestion(
87
+ "disambiguate_choice",
88
+ "애매한 표현이 있습니다. 원하는 결과를 A/B 중 하나로 골라 주시거나, 우선순위를 명시해 주세요.",
89
+ "모호한 지시는 잘못된 작업큐로 이어질 수 있습니다.",
90
+ )
91
+ )
92
+
93
+ if intent.category == "engineering" and _looks_like_coding_without_target(text):
94
+ reasons.append("missing_target_scope")
95
+ questions.append(
96
+ ClarificationQuestion(
97
+ "target_scope",
98
+ "어떤 파일/모듈/기능을 대상으로 할까요? 경로나 심볼 이름을 알려 주세요.",
99
+ "코딩 작업인데 변경 대상이 명시되지 않았습니다.",
100
+ )
101
+ )
102
+
103
+ if _has_any(text, _SCOPE_KEYWORDS) and not _has_any(text, ("repo", "project", "directory", "레포", "프로젝트", "폴더")):
104
+ reasons.append("broad_scope_without_boundary")
105
+ questions.append(
106
+ ClarificationQuestion(
107
+ "scope_boundary",
108
+ "범위가 넓어 보입니다. 포함/제외할 경로나 컴포넌트를 지정해 주세요.",
109
+ "전체 범위 작업은 실패하거나 과도한 비용이 들 수 있습니다.",
110
+ )
111
+ )
112
+
113
+ if _conflicting_signals(text, intent):
114
+ reasons.append("conflicting_signals")
115
+ questions.append(
116
+ ClarificationQuestion(
117
+ "resolve_conflict",
118
+ "서로 다른 작업 유형 신호가 감지됐습니다. 이번 턴의 1순위 목표를 하나만 선택해 주세요.",
119
+ "동시에 여러 종류의 작업으로 해석됩니다.",
120
+ )
121
+ )
122
+
123
+ if item.metadata.get("clarification_answers"):
124
+ return ClarificationResult(
125
+ required=False,
126
+ ready_for_queue=True,
127
+ reason_codes=("user_clarified",),
128
+ questions=(),
129
+ resolved_direction=item.metadata.get("clarification_answers", ""),
130
+ )
131
+
132
+ if not questions:
133
+ return ClarificationResult(
134
+ required=False,
135
+ ready_for_queue=True,
136
+ reason_codes=("direction_clear",),
137
+ questions=(),
138
+ resolved_direction=intent.direction,
139
+ )
140
+
141
+ deduped = _dedupe_questions(questions)
142
+ return ClarificationResult(
143
+ required=True,
144
+ ready_for_queue=False,
145
+ reason_codes=tuple(dict.fromkeys(reasons)),
146
+ questions=tuple(deduped),
147
+ resolved_direction="",
148
+ )
149
+
150
+
151
+ def _needs_direction_confirmation(text: str, intent: WorkIntent) -> bool:
152
+ if intent.category == "general" and len(text) < 240:
153
+ return False
154
+ if _has_any(text, _AMBIGUOUS_KEYWORDS):
155
+ return True
156
+ if intent.category in {"engineering", "security", "documentation", "local_model"}:
157
+ return True
158
+ return len(text) > 400
159
+
160
+
161
+ def _looks_like_coding_without_target(text: str) -> bool:
162
+ if not _has_any(text, _TARGET_MISSING_KEYWORDS):
163
+ return False
164
+ target_markers = (".py", ".rs", ".ts", ".js", "/", "\\", "src/", "tests/", "파일", "모듈", "함수", "class ")
165
+ return not _has_any(text, target_markers)
166
+
167
+
168
+ def _conflicting_signals(text: str, intent: WorkIntent) -> bool:
169
+ coding = _has_any(text, ("code", "implement", "fix", "patch", "코드", "구현", "수정"))
170
+ docs = _has_any(text, ("docs", "readme", "문서", "가이드"))
171
+ security = _has_any(text, ("security", "audit", "보안", "취약점"))
172
+ local = intent.local_model_requested or _has_any(text, ("local model", "vllm", "로컬"))
173
+ active = sum(signal for signal in (coding, docs, security, local) if signal)
174
+ return active >= 2 and intent.confidence < 0.8
175
+
176
+
177
+ def _dedupe_questions(questions: list[ClarificationQuestion]) -> list[ClarificationQuestion]:
178
+ seen: set[str] = set()
179
+ unique: list[ClarificationQuestion] = []
180
+ for question in questions:
181
+ if question.question_id in seen:
182
+ continue
183
+ seen.add(question.question_id)
184
+ unique.append(question)
185
+ return unique
186
+
187
+
188
+ def _has_any(text: str, needles: tuple[str, ...]) -> bool:
189
+ return any(needle in text for needle in needles)
190
+
191
+
192
+ __all__ = ["ClarificationQuestion", "ClarificationResult", "assess_clarification"]
@@ -0,0 +1,270 @@
1
+ from __future__ import annotations
2
+
3
+ """Hermes host-model segment dispatch를 위한 작은 파일 기반 저장소."""
4
+
5
+ import json
6
+ import os
7
+ import time
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from tempfile import NamedTemporaryFile
11
+ from typing import TYPE_CHECKING
12
+
13
+ if TYPE_CHECKING:
14
+ from cluxion_runtime.core.types import HarnessPlan, QueueSegment
15
+
16
+ DISPATCH_DIR_ENV = "CLUXION_PREPROCESS_DISPATCH_DIR"
17
+ _LEGACY_DISPATCH_DIR_ENV = "HERMES_CLUXION_DISPATCH_DIR"
18
+ _DEFAULT_BASE_DIR = Path.home() / ".local" / "share" / "cluxion-agentplugin-preprocessing" / "queue" / "dispatch"
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class DispatchStepRecord:
23
+ """저장된 segment 실행 단계."""
24
+
25
+ step_id: str
26
+ segment_id: str
27
+ checksum: str
28
+ token_estimate: int
29
+ content: str
30
+ status: str
31
+ result: str = ""
32
+ error: str = ""
33
+
34
+
35
+ class DispatchStoreError(RuntimeError):
36
+ """dispatch bundle이 없거나 손상된 경우."""
37
+
38
+
39
+ def default_dispatch_dir() -> Path:
40
+ """사용자별 dispatch 저장 경로를 반환한다."""
41
+ for env_name in (DISPATCH_DIR_ENV, _LEGACY_DISPATCH_DIR_ENV):
42
+ value = os.environ.get(env_name, "").strip()
43
+ if value:
44
+ return Path(value).expanduser()
45
+ return _DEFAULT_BASE_DIR
46
+
47
+
48
+ def persist_dispatch_bundle(plan: HarnessPlan, *, dispatch_dir: Path | None = None) -> Path | None:
49
+ """queue가 필요한 plan만 segment content를 별도 bundle로 저장한다."""
50
+ if not plan.execution.queue_required:
51
+ return None
52
+ bundle = _bundle_from_plan(plan)
53
+ target_dir = default_dispatch_dir() if dispatch_dir is None else dispatch_dir
54
+ if plan.queue_backend == "rust" and dispatch_dir is None and not _custom_dispatch_dir_configured():
55
+ try:
56
+ from cluxion_runtime.resources.queue_bridge import default_store_dir
57
+ from cluxion_runtime.resources.queue_bridge import persist_dispatch_bundle as rust_persist
58
+
59
+ result = rust_persist(plan.item.work_id, bundle, store_dir=default_store_dir())
60
+ if result.get("ok") and result.get("stored"):
61
+ return Path(str(result.get("path", "")))
62
+ except RuntimeError:
63
+ pass
64
+ target_dir.mkdir(parents=True, exist_ok=True)
65
+ path = _bundle_path(plan.item.work_id, target_dir)
66
+ _atomic_write_json(path, bundle)
67
+ return path
68
+
69
+
70
+ def load_dispatch_bundle(work_id: str, *, dispatch_dir: Path | None = None) -> dict[str, object]:
71
+ """work_id에 해당하는 dispatch bundle을 읽는다."""
72
+ path = _bundle_path(work_id, default_dispatch_dir() if dispatch_dir is None else dispatch_dir)
73
+ if not path.exists():
74
+ raise DispatchStoreError(f"dispatch bundle not found: {work_id}")
75
+ try:
76
+ payload = json.loads(path.read_text(encoding="utf-8"))
77
+ except json.JSONDecodeError as exc:
78
+ raise DispatchStoreError(f"dispatch bundle is invalid JSON: {work_id}") from exc
79
+ if not isinstance(payload, dict):
80
+ raise DispatchStoreError(f"dispatch bundle must be an object: {work_id}")
81
+ return payload
82
+
83
+
84
+ def next_dispatch_step(work_id: str, *, dispatch_dir: Path | None = None) -> dict[str, object]:
85
+ """다음 queued segment를 running으로 표시하고 Hermes가 처리할 payload를 반환한다."""
86
+ target_dir = default_dispatch_dir() if dispatch_dir is None else dispatch_dir
87
+ bundle = load_dispatch_bundle(work_id, dispatch_dir=target_dir)
88
+ steps = _steps(bundle)
89
+ for step in steps:
90
+ if step.get("status") in {"queued", "retry_wait"}:
91
+ step["status"] = "running"
92
+ step["updated_at"] = time.time()
93
+ _atomic_write_json(_bundle_path(work_id, target_dir), bundle)
94
+ return {
95
+ "work_id": work_id,
96
+ "ready": True,
97
+ "step": _public_step(step),
98
+ "remaining": _remaining_count(steps),
99
+ "synthesis_ready": False,
100
+ }
101
+ return {
102
+ "work_id": work_id,
103
+ "ready": False,
104
+ "step": {},
105
+ "remaining": _remaining_count(steps),
106
+ "synthesis_ready": all(step.get("status") == "succeeded" for step in steps),
107
+ }
108
+
109
+
110
+ def record_dispatch_result(
111
+ work_id: str,
112
+ step_id: str,
113
+ *,
114
+ result: str = "",
115
+ error: str = "",
116
+ succeeded: bool = True,
117
+ dispatch_dir: Path | None = None,
118
+ ) -> dict[str, object]:
119
+ """Hermes 모델이 처리한 segment 결과를 저장한다."""
120
+ target_dir = default_dispatch_dir() if dispatch_dir is None else dispatch_dir
121
+ bundle = load_dispatch_bundle(work_id, dispatch_dir=target_dir)
122
+ steps = _steps(bundle)
123
+ for step in steps:
124
+ if step.get("step_id") == step_id:
125
+ step["status"] = "succeeded" if succeeded else "failed"
126
+ step["result"] = result
127
+ step["error"] = error
128
+ step["updated_at"] = time.time()
129
+ _atomic_write_json(_bundle_path(work_id, target_dir), bundle)
130
+ return {
131
+ "work_id": work_id,
132
+ "step_id": step_id,
133
+ "recorded": True,
134
+ "status": step["status"],
135
+ "remaining": _remaining_count(steps),
136
+ "synthesis_ready": all(item.get("status") == "succeeded" for item in steps),
137
+ }
138
+ raise DispatchStoreError(f"dispatch step not found: {work_id}/{step_id}")
139
+
140
+
141
+ def build_briefing_payload(work_id: str, *, dispatch_dir: Path | None = None) -> dict[str, object]:
142
+ """모든 segment 결과를 최종 보고용 synthesis prompt로 묶는다."""
143
+ bundle = load_dispatch_bundle(work_id, dispatch_dir=dispatch_dir)
144
+ steps = _steps(bundle)
145
+ missing = [str(step.get("step_id", "")) for step in steps if step.get("status") != "succeeded"]
146
+ if missing:
147
+ return {"work_id": work_id, "ready": False, "missing_steps": missing, "briefing_prompt": ""}
148
+ prompt = _briefing_prompt(bundle, steps)
149
+ return {
150
+ "work_id": work_id,
151
+ "ready": True,
152
+ "missing_steps": [],
153
+ "briefing_prompt": prompt,
154
+ "result_count": len(steps),
155
+ }
156
+
157
+
158
+ def _bundle_from_plan(plan: HarnessPlan) -> dict[str, object]:
159
+ return {
160
+ "schema_version": 1,
161
+ "created_at": time.time(),
162
+ "work_id": plan.item.work_id,
163
+ "surface": plan.item.surface.value,
164
+ "original_prompt_preview": plan.preprocessing.normalized_prompt,
165
+ "answer_policy": {
166
+ "response_contract": plan.preprocessing.answer_policy.response_contract,
167
+ "required_checks": list(plan.preprocessing.answer_policy.required_checks),
168
+ "rules": list(plan.preprocessing.answer_policy.rules),
169
+ },
170
+ "steps": [_step_from_segment(segment) for segment in plan.preprocessing.segments],
171
+ }
172
+
173
+
174
+ def _step_from_segment(segment: QueueSegment) -> dict[str, object]:
175
+ return {
176
+ "step_id": f"exec_{segment.segment_id}",
177
+ "segment_id": segment.segment_id,
178
+ "checksum": segment.checksum,
179
+ "token_estimate": segment.token_estimate,
180
+ "content": segment.content,
181
+ "status": "queued",
182
+ "result": "",
183
+ "error": "",
184
+ "updated_at": time.time(),
185
+ }
186
+
187
+
188
+ def _public_step(step: dict[str, object]) -> dict[str, object]:
189
+ content = str(step.get("content", ""))
190
+ return {
191
+ "step_id": str(step.get("step_id", "")),
192
+ "segment_id": str(step.get("segment_id", "")),
193
+ "checksum": str(step.get("checksum", "")),
194
+ "token_estimate": int(step.get("token_estimate", 0)),
195
+ "content": content,
196
+ "instruction": (
197
+ "Process this segment with the current Hermes model. Preserve the checksum, "
198
+ "return only segment-grounded findings, and do not claim checks were run unless they were run."
199
+ ),
200
+ }
201
+
202
+
203
+ def _briefing_prompt(bundle: dict[str, object], steps: list[dict[str, object]]) -> str:
204
+ lines = [
205
+ "[cluxion_final_briefing]",
206
+ f"work_id={bundle.get('work_id', '')}",
207
+ "Synthesize the ordered segment results into a concise user-facing briefing.",
208
+ "Separate verified facts, tool results, inferences, missing checks, and remaining risks.",
209
+ "[segment_results]",
210
+ ]
211
+ for step in steps:
212
+ lines.append(
213
+ "\n".join(
214
+ [
215
+ f"step_id={step.get('step_id', '')}",
216
+ f"segment_id={step.get('segment_id', '')}",
217
+ f"checksum={step.get('checksum', '')}",
218
+ str(step.get("result", "")),
219
+ ]
220
+ )
221
+ )
222
+ return "\n\n".join(lines)
223
+
224
+
225
+ def _steps(bundle: dict[str, object]) -> list[dict[str, object]]:
226
+ steps = bundle.get("steps")
227
+ if not isinstance(steps, list):
228
+ raise DispatchStoreError("dispatch bundle has no steps array")
229
+ if not all(isinstance(step, dict) for step in steps):
230
+ raise DispatchStoreError("dispatch bundle steps must be objects")
231
+ return steps
232
+
233
+
234
+ def _remaining_count(steps: list[dict[str, object]]) -> int:
235
+ return sum(1 for step in steps if step.get("status") in {"queued", "retry_wait", "running"})
236
+
237
+
238
+ def _custom_dispatch_dir_configured() -> bool:
239
+ return bool(os.environ.get(DISPATCH_DIR_ENV, "").strip() or os.environ.get(_LEGACY_DISPATCH_DIR_ENV, "").strip())
240
+
241
+
242
+ def _bundle_path(work_id: str, dispatch_dir: Path) -> Path:
243
+ safe = "".join(ch for ch in work_id if ch.isalnum() or ch in {"-", "_"})
244
+ if not safe:
245
+ raise DispatchStoreError("work_id is empty")
246
+ return dispatch_dir / f"{safe}.json"
247
+
248
+
249
+ def _atomic_write_json(path: Path, payload: dict[str, object]) -> None:
250
+ path.parent.mkdir(parents=True, exist_ok=True)
251
+ with NamedTemporaryFile("w", encoding="utf-8", dir=path.parent, delete=False) as handle:
252
+ json.dump(payload, handle, ensure_ascii=False, sort_keys=True)
253
+ handle.write("\n")
254
+ handle.flush()
255
+ os.fsync(handle.fileno())
256
+ temporary = Path(handle.name)
257
+ temporary.replace(path)
258
+
259
+
260
+ __all__ = [
261
+ "DISPATCH_DIR_ENV",
262
+ "DispatchStepRecord",
263
+ "DispatchStoreError",
264
+ "build_briefing_payload",
265
+ "default_dispatch_dir",
266
+ "load_dispatch_bundle",
267
+ "next_dispatch_step",
268
+ "persist_dispatch_bundle",
269
+ "record_dispatch_result",
270
+ ]