cluxion-agentplugin-preprocessing 0.3.12__tar.gz → 0.3.14__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.
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/PKG-INFO +1 -1
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/pyproject.toml +1 -1
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/rust/cluxion_queue/src/guard.rs +260 -35
- cluxion_agentplugin_preprocessing-0.3.14/src/cluxion_agentplugin_preprocessing/__init__.py +12 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_agentplugin_preprocessing/guard_watch.py +5 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_agentplugin_preprocessing/plugin.py +64 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/core/context_compress.py +97 -14
- cluxion_agentplugin_preprocessing-0.3.14/src/cluxion_runtime/core/hybrid_forget.py +186 -0
- cluxion_agentplugin_preprocessing-0.3.14/src/cluxion_runtime/core/llm_compress.py +138 -0
- cluxion_agentplugin_preprocessing-0.3.14/src/cluxion_runtime/guard_daemon_host.py +259 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/resources/guard_bridge.py +15 -5
- cluxion_agentplugin_preprocessing-0.3.14/tests/runtime/test_auto_compress_middleware.py +118 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/tests/runtime/test_cluxion_runtime_spine.py +4 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/tests/runtime/test_context_compress.py +7 -0
- cluxion_agentplugin_preprocessing-0.3.14/tests/runtime/test_context_compress_llm_forget.py +202 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/tests/runtime/test_guard.py +30 -0
- cluxion_agentplugin_preprocessing-0.3.14/tests/runtime/test_guard_daemon_host.py +236 -0
- cluxion_agentplugin_preprocessing-0.3.12/src/cluxion_agentplugin_preprocessing/__init__.py +0 -7
- cluxion_agentplugin_preprocessing-0.3.12/src/cluxion_runtime/guard_daemon_host.py +0 -28
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/.github/profile/README.md +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/.gitignore +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/Docs/README.md +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/LICENSE +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/README.md +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/adapters/claude/.claude-plugin/plugin.json +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/adapters/claude/skills/preprocess/SKILL.md +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/adapters/codex/config-snippet.toml +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/cluxion-Docs/README.md +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/cluxion-Docs/architecture.md +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/cluxion-Docs/harness-logic.md +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/cluxion-Docs/honesty-preprocessing.md +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/cluxion-Docs/install-and-operations.md +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/cluxion-Docs/security.md +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/rust/cluxion_queue/Cargo.lock +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/rust/cluxion_queue/Cargo.toml +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/rust/cluxion_queue/pyproject.toml +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/rust/cluxion_queue/src/context.rs +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/rust/cluxion_queue/src/dispatch.rs +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/rust/cluxion_queue/src/lib.rs +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/rust/cluxion_queue/src/main.rs +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/rust/cluxion_queue/src/queue.rs +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/rust/cluxion_queue/src/types.rs +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_agentplugin_preprocessing/cli.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_agentplugin_preprocessing/doctor/__init__.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_agentplugin_preprocessing/doctor/catalog.json +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_agentplugin_preprocessing/doctor/framework.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_agentplugin_preprocessing/doctor/probes.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_agentplugin_preprocessing/hermes_config.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_agentplugin_preprocessing/plugin.yaml +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_agentplugin_preprocessing/runner.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_agentplugin_preprocessing/schemas.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/__init__.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/__main__.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/adapters/__init__.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/adapters/contract.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/adapters/grok_build.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/adapters/hermes.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/adapters/spec.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/bootstrap.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/cli.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/core/__init__.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/core/clarification.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/core/dispatch_store.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/core/harness.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/core/intent.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/core/ledger.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/core/ledger_codec.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/core/plan_codec.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/core/preprocess.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/core/types.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/core/work_queue.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/models/__init__.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/models/supervisor.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/models/vllm_mlx.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/resources/__init__.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/resources/py_queue.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/resources/queue_bridge.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/resources/rust_bridge.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/web/__init__.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/web/browser_bridge.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/tests/runtime/test_browser_bridge.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/tests/runtime/test_clarification.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/tests/runtime/test_contract.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/tests/runtime/test_dispatch_store.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/tests/runtime/test_ledger.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/tests/runtime/test_py_queue_concurrency.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/tests/runtime/test_queue_backends.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/tests/runtime/test_runtime_adapter_cli.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/tests/runtime/test_rust_queue.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/tests/runtime/test_supervisor.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/tests/test_bootstrap.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/tests/test_doctor.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/tests/test_guard_watch.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/tests/test_hermes_config.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/tests/test_packaging_policy.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/tests/test_plugin.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/tests/test_runner.py +0 -0
{cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cluxion-agentplugin-preprocessing
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.14
|
|
4
4
|
Summary: Universal agent plugin for Cluxion preprocessing, honesty contracts, clarification, Rust work queue, and resource-aware harness handoff.
|
|
5
5
|
Project-URL: Homepage, https://github.com/cluxion/cluxion-Agentplugin-preprocessing
|
|
6
6
|
Project-URL: Repository, https://github.com/cluxion/cluxion-Agentplugin-preprocessing
|
{cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/pyproject.toml
RENAMED
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cluxion-agentplugin-preprocessing"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.14"
|
|
8
8
|
description = "Universal agent plugin for Cluxion preprocessing, honesty contracts, clarification, Rust work queue, and resource-aware harness handoff."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -19,9 +19,13 @@ const DEFAULT_CPU_SAMPLE_MS: u64 = 100;
|
|
|
19
19
|
const MAX_REPORTED_PIDS: usize = 50;
|
|
20
20
|
const DEFAULT_CPU_HOT_THRESHOLD: f64 = 50.0;
|
|
21
21
|
const DEFAULT_RSS_HOT_THRESHOLD_MB: u64 = 1024;
|
|
22
|
-
pub const DEFAULT_DAEMON_INTERVAL_MS: u64 =
|
|
23
|
-
pub const DEFAULT_DAEMON_WINDOW: usize =
|
|
22
|
+
pub const DEFAULT_DAEMON_INTERVAL_MS: u64 = 1000;
|
|
23
|
+
pub const DEFAULT_DAEMON_WINDOW: usize = 10;
|
|
24
|
+
pub const PROC_SCAN_EVERY_N_TICKS: u64 = 5;
|
|
24
25
|
pub const STATE_FILE_NAME: &str = "guard_state.json";
|
|
26
|
+
pub const HEARTBEAT_FILE_NAME: &str = "guard_heartbeat";
|
|
27
|
+
pub const PID_FILE_NAME: &str = "guard_daemon.pid";
|
|
28
|
+
pub const DEFAULT_IDLE_TTL_MS: u64 = 600_000;
|
|
25
29
|
|
|
26
30
|
fn epoch_ms() -> u64 {
|
|
27
31
|
SystemTime::now()
|
|
@@ -30,6 +34,26 @@ fn epoch_ms() -> u64 {
|
|
|
30
34
|
.unwrap_or(0)
|
|
31
35
|
}
|
|
32
36
|
|
|
37
|
+
fn idle_ttl_ms() -> u64 {
|
|
38
|
+
std::env::var("CLUXION_GUARD_IDLE_TTL_MS")
|
|
39
|
+
.ok()
|
|
40
|
+
.and_then(|raw| raw.parse::<u64>().ok())
|
|
41
|
+
.unwrap_or(DEFAULT_IDLE_TTL_MS)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// True when the heartbeat mtime is older than `ttl_ms` relative to `now_ms`.
|
|
45
|
+
pub fn is_idle(heartbeat_mtime_ms: u64, now_ms: u64, ttl_ms: u64) -> bool {
|
|
46
|
+
now_ms.saturating_sub(heartbeat_mtime_ms) > ttl_ms
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
fn heartbeat_mtime_ms(path: &Path) -> Option<u64> {
|
|
50
|
+
let modified = std::fs::metadata(path).ok()?.modified().ok()?;
|
|
51
|
+
modified
|
|
52
|
+
.duration_since(UNIX_EPOCH)
|
|
53
|
+
.ok()
|
|
54
|
+
.map(|duration| duration.as_millis() as u64)
|
|
55
|
+
}
|
|
56
|
+
|
|
33
57
|
fn uint_field(payload: &Value, key: &str, default: u64) -> u64 {
|
|
34
58
|
payload.get(key).and_then(Value::as_u64).unwrap_or(default)
|
|
35
59
|
}
|
|
@@ -48,7 +72,14 @@ pub fn sample(payload: &Value) -> Result<Value, QueueError> {
|
|
|
48
72
|
Ok(sample_from(&sys))
|
|
49
73
|
}
|
|
50
74
|
|
|
51
|
-
|
|
75
|
+
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
76
|
+
struct ProcessScanCache {
|
|
77
|
+
process_count: usize,
|
|
78
|
+
zombie_count: usize,
|
|
79
|
+
zombie_pids: Vec<u64>,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
fn scan_process_fields(sys: &System) -> ProcessScanCache {
|
|
52
83
|
let mut zombie_pids: Vec<u64> = sys
|
|
53
84
|
.processes()
|
|
54
85
|
.iter()
|
|
@@ -58,19 +89,88 @@ fn sample_from(sys: &System) -> Value {
|
|
|
58
89
|
zombie_pids.sort_unstable();
|
|
59
90
|
let zombie_count = zombie_pids.len();
|
|
60
91
|
zombie_pids.truncate(MAX_REPORTED_PIDS);
|
|
92
|
+
ProcessScanCache {
|
|
93
|
+
process_count: sys.processes().len(),
|
|
94
|
+
zombie_count,
|
|
95
|
+
zombie_pids,
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
fn build_current_snapshot(sys: &System, process_cache: &ProcessScanCache) -> Value {
|
|
61
100
|
json!({
|
|
62
101
|
"ok": true,
|
|
63
102
|
"total_ram_mb": sys.total_memory() / 1_048_576,
|
|
64
103
|
"available_ram_mb": sys.available_memory() / 1_048_576,
|
|
65
104
|
"swap_used_mb": sys.used_swap() / 1_048_576,
|
|
66
105
|
"cpu_percent": f64::from(sys.global_cpu_usage()),
|
|
67
|
-
"process_count":
|
|
68
|
-
"zombie_count": zombie_count,
|
|
69
|
-
"zombie_pids": zombie_pids,
|
|
106
|
+
"process_count": process_cache.process_count,
|
|
107
|
+
"zombie_count": process_cache.zombie_count,
|
|
108
|
+
"zombie_pids": process_cache.zombie_pids.clone(),
|
|
70
109
|
"sampled_at_ms": epoch_ms(),
|
|
71
110
|
})
|
|
72
111
|
}
|
|
73
112
|
|
|
113
|
+
fn sample_from(sys: &System) -> Value {
|
|
114
|
+
build_current_snapshot(sys, &scan_process_fields(sys))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
fn push_window_sample(
|
|
118
|
+
cpu_window: &mut Vec<f64>,
|
|
119
|
+
ram_window: &mut Vec<u64>,
|
|
120
|
+
window: usize,
|
|
121
|
+
cpu: f64,
|
|
122
|
+
ram: u64,
|
|
123
|
+
) {
|
|
124
|
+
if cpu_window.len() == window {
|
|
125
|
+
cpu_window.remove(0);
|
|
126
|
+
ram_window.remove(0);
|
|
127
|
+
}
|
|
128
|
+
cpu_window.push(cpu);
|
|
129
|
+
ram_window.push(ram);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
fn build_daemon_state(
|
|
133
|
+
current: &Value,
|
|
134
|
+
cpu_window: &[f64],
|
|
135
|
+
ram_window: &[u64],
|
|
136
|
+
interval_ms: u64,
|
|
137
|
+
) -> Value {
|
|
138
|
+
json!({
|
|
139
|
+
"ok": true,
|
|
140
|
+
"current": current,
|
|
141
|
+
"window": {
|
|
142
|
+
"samples": cpu_window.len(),
|
|
143
|
+
"cpu_avg": cpu_window.iter().sum::<f64>() / cpu_window.len() as f64,
|
|
144
|
+
"cpu_peak": cpu_window.iter().cloned().fold(0.0, f64::max),
|
|
145
|
+
"min_available_ram_mb": ram_window.iter().min().copied().unwrap_or(0),
|
|
146
|
+
},
|
|
147
|
+
"interval_ms": interval_ms,
|
|
148
|
+
"updated_at_ms": epoch_ms(),
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
fn daemon_tick(
|
|
153
|
+
sys: &mut System,
|
|
154
|
+
process_cache: &mut ProcessScanCache,
|
|
155
|
+
cpu_window: &mut Vec<f64>,
|
|
156
|
+
ram_window: &mut Vec<u64>,
|
|
157
|
+
window: usize,
|
|
158
|
+
interval_ms: u64,
|
|
159
|
+
tick: u64,
|
|
160
|
+
) -> Value {
|
|
161
|
+
if tick % PROC_SCAN_EVERY_N_TICKS == 0 {
|
|
162
|
+
sys.refresh_processes(ProcessesToUpdate::All, true);
|
|
163
|
+
*process_cache = scan_process_fields(sys);
|
|
164
|
+
}
|
|
165
|
+
sys.refresh_memory();
|
|
166
|
+
sys.refresh_cpu_usage();
|
|
167
|
+
let current = build_current_snapshot(sys, process_cache);
|
|
168
|
+
let cpu = current["cpu_percent"].as_f64().unwrap_or(0.0);
|
|
169
|
+
let ram = current["available_ram_mb"].as_u64().unwrap_or(0);
|
|
170
|
+
push_window_sample(cpu_window, ram_window, window, cpu, ram);
|
|
171
|
+
build_daemon_state(¤t, cpu_window, ram_window, interval_ms)
|
|
172
|
+
}
|
|
173
|
+
|
|
74
174
|
/// Scan processes against registered owner roots. A process is `owned`
|
|
75
175
|
/// only when its parent lineage reaches one of `owned_roots`; everything
|
|
76
176
|
/// else — including processes whose lineage cannot be walked — is
|
|
@@ -157,50 +257,53 @@ fn is_owned(pid: u64, owned_roots: &[u64], parents: &HashMap<u64, u64>) -> bool
|
|
|
157
257
|
}
|
|
158
258
|
|
|
159
259
|
/// Polling daemon: refresh, fold into a rolling window, publish state
|
|
160
|
-
/// atomically (write to a temp file, then rename).
|
|
260
|
+
/// atomically (write to a temp file, then rename). Self-exits when the
|
|
261
|
+
/// heartbeat file is stale; otherwise runs until killed.
|
|
262
|
+
///
|
|
263
|
+
/// Cheap ticks refresh memory/CPU every `interval_ms`; full process scans run
|
|
264
|
+
/// every [`PROC_SCAN_EVERY_N_TICKS`] ticks and their results are cached for
|
|
265
|
+
/// intervening cheap ticks.
|
|
161
266
|
pub fn run_daemon(store_dir: &Path, interval_ms: u64, window: usize) -> Result<(), QueueError> {
|
|
162
267
|
std::fs::create_dir_all(store_dir)?;
|
|
163
268
|
let state_path = store_dir.join(STATE_FILE_NAME);
|
|
164
269
|
let tmp_path = store_dir.join(format!("{STATE_FILE_NAME}.tmp"));
|
|
270
|
+
let heartbeat_path = store_dir.join(HEARTBEAT_FILE_NAME);
|
|
271
|
+
let pid_path = store_dir.join(PID_FILE_NAME);
|
|
272
|
+
let idle_ttl = idle_ttl_ms();
|
|
165
273
|
let interval = std::time::Duration::from_millis(interval_ms.max(100));
|
|
166
274
|
let window = window.max(1);
|
|
275
|
+
let interval_ms = interval.as_millis() as u64;
|
|
167
276
|
|
|
168
277
|
let mut sys = System::new();
|
|
169
|
-
|
|
170
|
-
|
|
278
|
+
let mut process_cache = ProcessScanCache {
|
|
279
|
+
process_count: 0,
|
|
280
|
+
zombie_count: 0,
|
|
281
|
+
zombie_pids: Vec::new(),
|
|
282
|
+
};
|
|
171
283
|
let mut cpu_window: Vec<f64> = Vec::with_capacity(window);
|
|
172
284
|
let mut ram_window: Vec<u64> = Vec::with_capacity(window);
|
|
285
|
+
let mut tick: u64 = 0;
|
|
173
286
|
|
|
174
287
|
loop {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
let cpu = current["cpu_percent"].as_f64().unwrap_or(0.0);
|
|
182
|
-
let ram = current["available_ram_mb"].as_u64().unwrap_or(0);
|
|
183
|
-
if cpu_window.len() == window {
|
|
184
|
-
cpu_window.remove(0);
|
|
185
|
-
ram_window.remove(0);
|
|
288
|
+
if let Some(mtime) = heartbeat_mtime_ms(&heartbeat_path) {
|
|
289
|
+
if is_idle(mtime, epoch_ms(), idle_ttl) {
|
|
290
|
+
let _ = std::fs::remove_file(&pid_path);
|
|
291
|
+
return Ok(());
|
|
292
|
+
}
|
|
186
293
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
"cpu_peak": cpu_window.iter().cloned().fold(0.0, f64::max),
|
|
197
|
-
"min_available_ram_mb": ram_window.iter().min().copied().unwrap_or(0),
|
|
198
|
-
},
|
|
199
|
-
"interval_ms": interval.as_millis() as u64,
|
|
200
|
-
"updated_at_ms": epoch_ms(),
|
|
201
|
-
});
|
|
294
|
+
let state = daemon_tick(
|
|
295
|
+
&mut sys,
|
|
296
|
+
&mut process_cache,
|
|
297
|
+
&mut cpu_window,
|
|
298
|
+
&mut ram_window,
|
|
299
|
+
window,
|
|
300
|
+
interval_ms,
|
|
301
|
+
tick,
|
|
302
|
+
);
|
|
202
303
|
std::fs::write(&tmp_path, serde_json::to_vec(&state)?)?;
|
|
203
304
|
std::fs::rename(&tmp_path, &state_path)?;
|
|
305
|
+
tick += 1;
|
|
306
|
+
std::thread::sleep(interval);
|
|
204
307
|
}
|
|
205
308
|
}
|
|
206
309
|
|
|
@@ -262,4 +365,126 @@ mod tests {
|
|
|
262
365
|
// No roots registered -> nothing is owned.
|
|
263
366
|
assert!(!is_owned(10, &[], &parents));
|
|
264
367
|
}
|
|
368
|
+
|
|
369
|
+
#[test]
|
|
370
|
+
fn daemon_tick_caches_process_fields_between_scans() {
|
|
371
|
+
let mut sys = System::new();
|
|
372
|
+
sys.refresh_processes(ProcessesToUpdate::All, true);
|
|
373
|
+
let mut process_cache = scan_process_fields(&sys);
|
|
374
|
+
let mut cpu_window: Vec<f64> = Vec::with_capacity(3);
|
|
375
|
+
let mut ram_window: Vec<u64> = Vec::with_capacity(3);
|
|
376
|
+
|
|
377
|
+
let scan_tick_state = daemon_tick(
|
|
378
|
+
&mut sys,
|
|
379
|
+
&mut process_cache,
|
|
380
|
+
&mut cpu_window,
|
|
381
|
+
&mut ram_window,
|
|
382
|
+
3,
|
|
383
|
+
1000,
|
|
384
|
+
0,
|
|
385
|
+
);
|
|
386
|
+
let scan_process_count = scan_tick_state["current"]["process_count"]
|
|
387
|
+
.as_u64()
|
|
388
|
+
.expect("process_count");
|
|
389
|
+
assert!(scan_process_count > 0);
|
|
390
|
+
|
|
391
|
+
let stale_cache = ProcessScanCache {
|
|
392
|
+
process_count: 1,
|
|
393
|
+
zombie_count: 2,
|
|
394
|
+
zombie_pids: vec![99, 100],
|
|
395
|
+
};
|
|
396
|
+
process_cache = stale_cache.clone();
|
|
397
|
+
|
|
398
|
+
let cheap_tick_state = daemon_tick(
|
|
399
|
+
&mut sys,
|
|
400
|
+
&mut process_cache,
|
|
401
|
+
&mut cpu_window,
|
|
402
|
+
&mut ram_window,
|
|
403
|
+
3,
|
|
404
|
+
1000,
|
|
405
|
+
1,
|
|
406
|
+
);
|
|
407
|
+
assert_eq!(
|
|
408
|
+
cheap_tick_state["current"]["process_count"].as_u64(),
|
|
409
|
+
Some(1)
|
|
410
|
+
);
|
|
411
|
+
assert_eq!(
|
|
412
|
+
cheap_tick_state["current"]["zombie_count"].as_u64(),
|
|
413
|
+
Some(2)
|
|
414
|
+
);
|
|
415
|
+
assert_eq!(
|
|
416
|
+
cheap_tick_state["current"]["zombie_pids"],
|
|
417
|
+
json!([99, 100])
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
let rescan_tick_state = daemon_tick(
|
|
421
|
+
&mut sys,
|
|
422
|
+
&mut process_cache,
|
|
423
|
+
&mut cpu_window,
|
|
424
|
+
&mut ram_window,
|
|
425
|
+
3,
|
|
426
|
+
1000,
|
|
427
|
+
PROC_SCAN_EVERY_N_TICKS,
|
|
428
|
+
);
|
|
429
|
+
assert_ne!(process_cache, stale_cache);
|
|
430
|
+
assert!(process_cache.process_count > 0);
|
|
431
|
+
assert_eq!(
|
|
432
|
+
rescan_tick_state["current"]["process_count"].as_u64(),
|
|
433
|
+
Some(process_cache.process_count as u64)
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
#[test]
|
|
438
|
+
fn is_idle_detects_stale_and_fresh_heartbeats() {
|
|
439
|
+
let ttl = 600_000;
|
|
440
|
+
let now = 1_700_000_000_000u64;
|
|
441
|
+
assert!(is_idle(now - ttl - 1, now, ttl));
|
|
442
|
+
assert!(!is_idle(now - ttl, now, ttl));
|
|
443
|
+
assert!(!is_idle(now - 1, now, ttl));
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
#[test]
|
|
447
|
+
fn daemon_state_json_has_required_keys() {
|
|
448
|
+
let mut sys = System::new();
|
|
449
|
+
sys.refresh_processes(ProcessesToUpdate::All, true);
|
|
450
|
+
let mut process_cache = scan_process_fields(&sys);
|
|
451
|
+
let mut cpu_window: Vec<f64> = Vec::with_capacity(2);
|
|
452
|
+
let mut ram_window: Vec<u64> = Vec::with_capacity(2);
|
|
453
|
+
|
|
454
|
+
let state = daemon_tick(
|
|
455
|
+
&mut sys,
|
|
456
|
+
&mut process_cache,
|
|
457
|
+
&mut cpu_window,
|
|
458
|
+
&mut ram_window,
|
|
459
|
+
2,
|
|
460
|
+
1000,
|
|
461
|
+
0,
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
for key in ["ok", "current", "window", "interval_ms", "updated_at_ms"] {
|
|
465
|
+
assert!(state.get(key).is_some(), "missing top-level key: {key}");
|
|
466
|
+
}
|
|
467
|
+
for key in [
|
|
468
|
+
"ok",
|
|
469
|
+
"total_ram_mb",
|
|
470
|
+
"available_ram_mb",
|
|
471
|
+
"swap_used_mb",
|
|
472
|
+
"cpu_percent",
|
|
473
|
+
"process_count",
|
|
474
|
+
"zombie_count",
|
|
475
|
+
"zombie_pids",
|
|
476
|
+
"sampled_at_ms",
|
|
477
|
+
] {
|
|
478
|
+
assert!(
|
|
479
|
+
state["current"].get(key).is_some(),
|
|
480
|
+
"missing current key: {key}"
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
for key in ["samples", "cpu_avg", "cpu_peak", "min_available_ram_mb"] {
|
|
484
|
+
assert!(
|
|
485
|
+
state["window"].get(key).is_some(),
|
|
486
|
+
"missing window key: {key}"
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
265
490
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Universal Cluxion preprocessing agent plugin."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
__version__ = version("cluxion-agentplugin-preprocessing")
|
|
9
|
+
except PackageNotFoundError: # pragma: no cover
|
|
10
|
+
__version__ = "0.3.13"
|
|
11
|
+
|
|
12
|
+
__all__ = ["__version__"]
|
|
@@ -6,6 +6,7 @@ stderr and never raised into the host agent.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
import contextlib
|
|
9
10
|
import os
|
|
10
11
|
import sys
|
|
11
12
|
import threading
|
|
@@ -35,6 +36,7 @@ def on_session_start(**_: Any) -> None:
|
|
|
35
36
|
return
|
|
36
37
|
try:
|
|
37
38
|
result = guard_bridge.start_daemon()
|
|
39
|
+
guard_bridge.touch_heartbeat()
|
|
38
40
|
except Exception as exc:
|
|
39
41
|
_warn(f"cluxion guard autostart failed: {exc}")
|
|
40
42
|
return
|
|
@@ -53,6 +55,9 @@ def post_tool_call(**_: Any) -> None:
|
|
|
53
55
|
"""
|
|
54
56
|
global _last_warning_at, _last_watch_at
|
|
55
57
|
|
|
58
|
+
with contextlib.suppress(Exception):
|
|
59
|
+
guard_bridge.touch_heartbeat()
|
|
60
|
+
|
|
56
61
|
now = time.monotonic()
|
|
57
62
|
try:
|
|
58
63
|
with _lock:
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import importlib.resources
|
|
6
6
|
import json
|
|
7
|
+
import os
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
from typing import TYPE_CHECKING
|
|
9
10
|
|
|
@@ -26,6 +27,12 @@ from cluxion_agentplugin_preprocessing.schemas import (
|
|
|
26
27
|
SERVE_LOCAL_SCHEMA,
|
|
27
28
|
WEB_SEARCH_SCHEMA,
|
|
28
29
|
)
|
|
30
|
+
from cluxion_runtime.core.context_compress import (
|
|
31
|
+
DEFAULT_TRIGGER_RATIO,
|
|
32
|
+
_resolve_context_limit,
|
|
33
|
+
compress,
|
|
34
|
+
)
|
|
35
|
+
from cluxion_runtime.core.preprocess import estimate_tokens
|
|
29
36
|
|
|
30
37
|
if TYPE_CHECKING:
|
|
31
38
|
from collections.abc import Callable
|
|
@@ -38,6 +45,10 @@ def register(ctx: object) -> None:
|
|
|
38
45
|
register_hook("on_session_start", guard_watch.on_session_start)
|
|
39
46
|
register_hook("post_tool_call", guard_watch.post_tool_call)
|
|
40
47
|
|
|
48
|
+
register_mw = getattr(ctx, "register_middleware", None)
|
|
49
|
+
if callable(register_mw):
|
|
50
|
+
register_mw("llm_request", _auto_compress_middleware)
|
|
51
|
+
|
|
41
52
|
ctx.register_tool(
|
|
42
53
|
name="cluxion_plan",
|
|
43
54
|
toolset="cluxion",
|
|
@@ -275,4 +286,57 @@ def _json_result(callback: Callable[[], str]) -> str:
|
|
|
275
286
|
return json.dumps({"ok": False, "error": str(exc)}, ensure_ascii=False, sort_keys=True)
|
|
276
287
|
|
|
277
288
|
|
|
289
|
+
def _autocompress_enabled() -> bool:
|
|
290
|
+
value = os.environ.get("CLUXION_PREPROCESS_AUTOCOMPRESS", "1").strip().lower()
|
|
291
|
+
return value not in ("0", "false", "no", "off")
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _auto_compress_middleware(
|
|
295
|
+
request: dict[str, object],
|
|
296
|
+
original_request: object = None,
|
|
297
|
+
model: str | None = None,
|
|
298
|
+
**_: object,
|
|
299
|
+
) -> dict[str, object] | None:
|
|
300
|
+
del original_request
|
|
301
|
+
if os.environ.get("CLUXION_PREPROCESS_IN_COMPRESS") == "1":
|
|
302
|
+
return None
|
|
303
|
+
if not _autocompress_enabled():
|
|
304
|
+
return None
|
|
305
|
+
|
|
306
|
+
messages_key = "messages"
|
|
307
|
+
raw_messages = request.get("messages")
|
|
308
|
+
if not isinstance(raw_messages, list):
|
|
309
|
+
raw_messages = request.get("input")
|
|
310
|
+
messages_key = "input"
|
|
311
|
+
if not isinstance(raw_messages, list):
|
|
312
|
+
return None
|
|
313
|
+
|
|
314
|
+
try:
|
|
315
|
+
total_tokens = 0
|
|
316
|
+
for raw in raw_messages:
|
|
317
|
+
if isinstance(raw, dict):
|
|
318
|
+
total_tokens += estimate_tokens(str(raw.get("content", "")))
|
|
319
|
+
else:
|
|
320
|
+
total_tokens += estimate_tokens(str(raw))
|
|
321
|
+
|
|
322
|
+
limit_payload: dict[str, object] = {}
|
|
323
|
+
if model:
|
|
324
|
+
limit_payload["model"] = model
|
|
325
|
+
ctx_limit = request.get("context_limit_tokens")
|
|
326
|
+
if isinstance(ctx_limit, int) and not isinstance(ctx_limit, bool) and ctx_limit > 0:
|
|
327
|
+
limit_payload["context_limit_tokens"] = ctx_limit
|
|
328
|
+
context_limit = _resolve_context_limit(limit_payload)
|
|
329
|
+
if total_tokens / context_limit < DEFAULT_TRIGGER_RATIO:
|
|
330
|
+
return None
|
|
331
|
+
|
|
332
|
+
payload: dict[str, object] = {"messages": raw_messages, **limit_payload}
|
|
333
|
+
result = compress(payload)
|
|
334
|
+
shrunk = result.get("messages")
|
|
335
|
+
if not isinstance(shrunk, list):
|
|
336
|
+
return None
|
|
337
|
+
return {"request": {**request, messages_key: shrunk}, "source": "preprocessing"}
|
|
338
|
+
except Exception:
|
|
339
|
+
return None
|
|
340
|
+
|
|
341
|
+
|
|
278
342
|
__all__ = ["register"]
|
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Context compression: 70% trigger -> 30% target pipeline.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Stage 1 (deterministic) mirrors ``rust/cluxion_queue/src/context.rs`` — every
|
|
4
4
|
constant, threshold, ordering rule, and the token estimator must stay in
|
|
5
|
-
lockstep so the three backends produce identical output (parity-tested).
|
|
5
|
+
lockstep so the three backends produce identical Stage-1 output (parity-tested).
|
|
6
|
+
|
|
7
|
+
Stages 2 (LLM summarization via ``hermes -z``) and 3 (hybrid forgetting) are
|
|
8
|
+
Python-only; the Rust mirror intentionally does not replicate LLM or forgetforge
|
|
9
|
+
calls. Disable them with ``enable_llm_summary`` / ``enable_forget`` for Stage-1
|
|
10
|
+
parity.
|
|
6
11
|
|
|
7
12
|
What stays untouched: pinned messages (explicit ``pinned``, the first
|
|
8
13
|
user message = task intent, the most recent ``keep_recent`` turns).
|
|
9
|
-
Stages run oldest-first and stop as soon as usage reaches the target:
|
|
10
|
-
A. truncate long messages (head + tail excerpt)
|
|
11
|
-
B. drop exact duplicates (trimmed-content match)
|
|
12
|
-
C. fold remaining old turns into one-line digests
|
|
13
|
-
If the target is still not met the result carries ``ai_summary_request``
|
|
14
|
-
telling the host AI which messages to summarize and what to preserve.
|
|
15
14
|
"""
|
|
16
15
|
|
|
17
16
|
from __future__ import annotations
|
|
@@ -19,6 +18,8 @@ from __future__ import annotations
|
|
|
19
18
|
from dataclasses import dataclass
|
|
20
19
|
from typing import TYPE_CHECKING
|
|
21
20
|
|
|
21
|
+
from cluxion_runtime.core.hybrid_forget import apply_hybrid_forget
|
|
22
|
+
from cluxion_runtime.core.llm_compress import hermes_available, summarize_messages
|
|
22
23
|
from cluxion_runtime.core.preprocess import estimate_tokens
|
|
23
24
|
|
|
24
25
|
if TYPE_CHECKING:
|
|
@@ -70,6 +71,12 @@ def compress(payload: Mapping[str, object]) -> dict[str, object]:
|
|
|
70
71
|
trigger_ratio = _ratio(payload, "trigger_ratio", DEFAULT_TRIGGER_RATIO)
|
|
71
72
|
target_ratio = _ratio(payload, "target_ratio", DEFAULT_TARGET_RATIO)
|
|
72
73
|
keep_recent = _uint(payload, "keep_recent_turns", DEFAULT_KEEP_RECENT)
|
|
74
|
+
enable_llm = _bool_flag(payload, "enable_llm_summary", True)
|
|
75
|
+
enable_forget = _bool_flag(payload, "enable_forget", True)
|
|
76
|
+
model = payload.get("model") if isinstance(payload.get("model"), str) else None
|
|
77
|
+
session_id = payload.get("session_id") if isinstance(payload.get("session_id"), str) else None
|
|
78
|
+
timeout_raw = payload.get("llm_timeout_s")
|
|
79
|
+
timeout_s = float(timeout_raw) if isinstance(timeout_raw, (int, float)) and not isinstance(timeout_raw, bool) else 120.0
|
|
73
80
|
|
|
74
81
|
tokens_before = sum(estimate_tokens(m.content) for m in messages)
|
|
75
82
|
usage_before = tokens_before / context_limit
|
|
@@ -102,11 +109,69 @@ def compress(payload: Mapping[str, object]) -> dict[str, object]:
|
|
|
102
109
|
if changed:
|
|
103
110
|
stages.append("digest")
|
|
104
111
|
|
|
105
|
-
summary_request = None
|
|
106
|
-
|
|
112
|
+
summary_request: dict[str, object] | None = None
|
|
113
|
+
dropped_without_backup = False
|
|
114
|
+
over_target_pinned_only = False
|
|
115
|
+
forced_over_target = False
|
|
116
|
+
|
|
117
|
+
if total > target_tokens and enable_llm and hermes_available():
|
|
107
118
|
summary_request = _build_summary_request(messages, pinned, total, target_tokens)
|
|
119
|
+
indices = summary_request["summarize_indices"]
|
|
120
|
+
if isinstance(indices, list) and indices:
|
|
121
|
+
summaries = summarize_messages(
|
|
122
|
+
messages,
|
|
123
|
+
indices, # type: ignore[arg-type]
|
|
124
|
+
str(summary_request.get("instructions", "")),
|
|
125
|
+
model=model,
|
|
126
|
+
timeout_s=timeout_s,
|
|
127
|
+
)
|
|
128
|
+
if summaries is not None:
|
|
129
|
+
for idx, summary in summaries.items():
|
|
130
|
+
if idx in pinned or idx < 0 or idx >= len(messages):
|
|
131
|
+
continue
|
|
132
|
+
old_tokens = estimate_tokens(messages[idx].content)
|
|
133
|
+
messages[idx].content = summary
|
|
134
|
+
total = total - old_tokens + estimate_tokens(summary)
|
|
135
|
+
stages.append("llm_summary")
|
|
136
|
+
summary_request = None
|
|
137
|
+
# fail-safe: keep Stage-1 output and ai_summary_request on LLM failure
|
|
138
|
+
|
|
139
|
+
if total > target_tokens and enable_forget:
|
|
140
|
+
forget_result = apply_hybrid_forget(
|
|
141
|
+
messages,
|
|
142
|
+
pinned,
|
|
143
|
+
total,
|
|
144
|
+
target_tokens,
|
|
145
|
+
session_id=session_id,
|
|
146
|
+
)
|
|
147
|
+
messages = forget_result.messages
|
|
148
|
+
total = forget_result.tokens_after
|
|
149
|
+
dropped_without_backup = forget_result.dropped_without_backup
|
|
150
|
+
over_target_pinned_only = forget_result.over_target_pinned_only
|
|
151
|
+
if forget_result.dropped_indices:
|
|
152
|
+
stages.append("forget")
|
|
153
|
+
pinned = _pinned_indices(messages, keep_recent)
|
|
108
154
|
|
|
109
|
-
|
|
155
|
+
if total > target_tokens:
|
|
156
|
+
if summary_request is None:
|
|
157
|
+
summary_request = _build_summary_request(messages, pinned, total, target_tokens)
|
|
158
|
+
if over_target_pinned_only or not any(idx not in pinned for idx in range(len(messages))):
|
|
159
|
+
over_target_pinned_only = True
|
|
160
|
+
if total / context_limit > trigger_ratio:
|
|
161
|
+
forced_over_target = True
|
|
162
|
+
|
|
163
|
+
return _result_payload(
|
|
164
|
+
messages,
|
|
165
|
+
tokens_before,
|
|
166
|
+
total,
|
|
167
|
+
context_limit,
|
|
168
|
+
stages,
|
|
169
|
+
summary_request,
|
|
170
|
+
pinned,
|
|
171
|
+
dropped_without_backup=dropped_without_backup,
|
|
172
|
+
over_target_pinned_only=over_target_pinned_only,
|
|
173
|
+
forced_over_target=forced_over_target,
|
|
174
|
+
)
|
|
110
175
|
|
|
111
176
|
|
|
112
177
|
def _resolve_context_limit(payload: Mapping[str, object]) -> int:
|
|
@@ -136,6 +201,13 @@ def _uint(payload: Mapping[str, object], key: str, default: int) -> int:
|
|
|
136
201
|
return default
|
|
137
202
|
|
|
138
203
|
|
|
204
|
+
def _bool_flag(payload: Mapping[str, object], key: str, default: bool) -> bool:
|
|
205
|
+
value = payload.get(key)
|
|
206
|
+
if isinstance(value, bool):
|
|
207
|
+
return value
|
|
208
|
+
return default
|
|
209
|
+
|
|
210
|
+
|
|
139
211
|
def _pinned_indices(messages: list[_Msg], keep_recent: int) -> list[int]:
|
|
140
212
|
pinned = [idx for idx, msg in enumerate(messages) if msg.pinned]
|
|
141
213
|
first_user = next((idx for idx, msg in enumerate(messages) if msg.role == "user"), None)
|
|
@@ -235,8 +307,12 @@ def _result_payload(
|
|
|
235
307
|
stages: list[str],
|
|
236
308
|
summary_request: dict[str, object] | None,
|
|
237
309
|
pinned: list[int],
|
|
310
|
+
*,
|
|
311
|
+
dropped_without_backup: bool = False,
|
|
312
|
+
over_target_pinned_only: bool = False,
|
|
313
|
+
forced_over_target: bool = False,
|
|
238
314
|
) -> dict[str, object]:
|
|
239
|
-
|
|
315
|
+
result: dict[str, object] = {
|
|
240
316
|
"ok": True,
|
|
241
317
|
"compressed": bool(stages),
|
|
242
318
|
"tokens_before": tokens_before,
|
|
@@ -249,6 +325,13 @@ def _result_payload(
|
|
|
249
325
|
"messages": [{"role": m.role, "content": m.content, "pinned": m.pinned} for m in messages],
|
|
250
326
|
"ai_summary_request": summary_request,
|
|
251
327
|
}
|
|
328
|
+
if dropped_without_backup:
|
|
329
|
+
result["dropped_without_backup"] = True
|
|
330
|
+
if over_target_pinned_only:
|
|
331
|
+
result["over_target_pinned_only"] = True
|
|
332
|
+
if forced_over_target:
|
|
333
|
+
result["forced_over_target"] = True
|
|
334
|
+
return result
|
|
252
335
|
|
|
253
336
|
|
|
254
|
-
__all__ = ["compress", "estimate_tokens"]
|
|
337
|
+
__all__ = ["compress", "estimate_tokens"]
|