Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 83 additions & 24 deletions crates/jp_cli/src/cmd/compact_flag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@
use std::str::FromStr;

use clap::{Arg, ArgAction, ArgMatches, Command};
use jp_config::conversation::compaction::{
CompactionConfig, CompactionRuleConfig, PartialCompactionRuleConfig, PartialSummaryConfig,
ReasoningMode, RuleBound, ToolCallsMode,
use jp_config::{
conversation::compaction::{
CompactionConfig, CompactionRuleConfig, PartialCompactionRuleConfig, PartialSummaryConfig,
ReasoningMode, RuleBound, ToolCallsMode,
},
types::{byte_size::ByteSize, policy_spec::PolicySpec},
};

/// Shared compaction flag that can be embedded in any command.
Expand Down Expand Up @@ -106,9 +109,13 @@ impl clap::Args for CompactFlag {
`r` / `reasoning`: strip reasoning blocks\n- `s` / `summarize`: generate an \
LLM summary\n- `t` / `tools` (or `t=MODE`): strip tool calls; bare strips \
both, or MODE is one of `strip`/`s`, `strip-requests`/`sreq`, \
`strip-responses`/`sres`, `omit`/`o`\n\nRange: FROM..TO (1-based, inclusive \
on both ends, so 1..5 is turns 1-5), single number, or .. for \
all\n\nExamples: s:..-3, r+t, t=sreq:5..-3, r:-20",
`strip-responses`/`sres`, `omit`/`o`\n\nA policy can carry options after a \
`,`:\n- `over=SIZE`: compact only items larger than SIZE (`512KB`, `1MB`, or \
a byte count). Each half of a tool call is judged on its own size; `omit` is \
judged on the pair combined. Not accepted on `summarize`.\n\nRange: FROM..TO \
(1-based, inclusive on both ends, so 1..5 is turns 1-5), single number, or \
.. for all\n\nExamples: s:..-3, r+t, t=sreq:5..-3, r:-20, t=sres,over=1mb, \
r,over=16kb+t=sres,over=1mb:..-3",
)
.action(ArgAction::Append)
.num_args(0..=1)
Expand Down Expand Up @@ -157,10 +164,13 @@ impl clap::FromArgMatches for CompactFlag {
/// A parsed compaction DSL spec: `POLICIES[:RANGE]`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct CompactSpec {
pub reasoning: bool,
/// `None` = no reasoning policy.
/// Carries the policy's own `over` threshold, if any.
pub reasoning: Option<PolicySpec<ReasoningMode>>,
/// `None` = no tool-call policy.
/// The mode mirrors the `--tools` flag.
pub tools: Option<ToolCallsMode>,
/// The mode mirrors the `--tools` flag, plus the policy's own `over`
/// threshold.
pub tools: Option<PolicySpec<ToolCallsMode>>,
pub summarize: bool,
/// `None` = use config defaults for range.
pub range: Option<DslRange>,
Expand All @@ -183,12 +193,12 @@ pub(crate) struct DslRange {

impl CompactSpec {
fn to_partial_rule(&self) -> PartialCompactionRuleConfig {
let mut rule = PartialCompactionRuleConfig::default();
let mut rule = PartialCompactionRuleConfig {
reasoning: self.reasoning,
tool_calls: self.tools,
..Default::default()
};

if self.reasoning {
rule.reasoning = Some(ReasoningMode::Strip);
}
rule.tool_calls = self.tools;
if self.summarize {
rule.summary = Some(PartialSummaryConfig::default());
}
Expand All @@ -214,12 +224,17 @@ impl FromStr for CompactSpec {
None => (s, None),
};

let mut reasoning = false;
let mut tools: Option<ToolCallsMode> = None;
let mut reasoning: Option<PolicySpec<ReasoningMode>> = None;
let mut tools: Option<PolicySpec<ToolCallsMode>> = None;
let mut summarize = false;

for policy in policies_str.split('+') {
let policy = policy.trim();
for term in policies_str.split('+') {
// Options bind to the policy they qualify, so `over` cannot be
// written without one: `r,over=16kb+t=sres,over=1mb`.
let mut parts = term.split(',');
let policy = parts.next().unwrap_or_default().trim();
let over = parse_policy_options(parts)?;

let (key, value) = match policy.split_once('=') {
Some((k, v)) => (k.trim(), Some(v.trim())),
None => (policy, None),
Expand All @@ -230,27 +245,39 @@ impl FromStr for CompactSpec {
if value.is_some() {
return Err("`reasoning` does not take a value".into());
}
reasoning = true;
reasoning = Some(PolicySpec {
policy: ReasoningMode::Strip,
over,
});
}
"s" | "summarize" => {
if value.is_some() {
return Err("`summarize` does not take a value".into());
}
if over.is_some() {
// A summary replaces its whole range rather than acting
// per item, so there is nothing for a size threshold to
// select.
return Err("`summarize` does not take an `over` threshold".into());
}
summarize = true;
}
"t" | "tools" => {
tools = Some(match value {
Some(v) => v.parse().map_err(|e| format!("{e}"))?,
// Bare `t` mirrors `--tools` without a value.
None => ToolCallsMode::Strip,
tools = Some(PolicySpec {
policy: match value {
Some(v) => v.parse().map_err(|e| format!("{e}"))?,
// Bare `t` mirrors `--tools` without a value.
None => ToolCallsMode::Strip,
},
over,
});
}
"" => return Err("empty policy".into()),
other => return Err(format!("unknown policy '{other}'")),
}
}

if !reasoning && tools.is_none() && !summarize {
if reasoning.is_none() && tools.is_none() && !summarize {
return Err("at least one policy required (r, t=MODE, s)".into());
}

Expand All @@ -265,6 +292,38 @@ impl FromStr for CompactSpec {
}
}

/// Parse the `key=value` options trailing a DSL policy term.
///
/// `over` is the only option today, so the result is just its value.
fn parse_policy_options<'a>(
options: impl Iterator<Item = &'a str>,
) -> Result<Option<ByteSize>, String> {
let mut over = None;

for option in options {
let (key, value) = option.split_once('=').ok_or_else(|| {
format!(
"invalid policy option '{}': expected `key=value`",
option.trim()
)
})?;

match key.trim() {
"over" => {
over = Some(
value
.trim()
.parse::<ByteSize>()
.map_err(|e| format!("{e}"))?,
);
}
other => return Err(format!("unknown policy option '{other}'")),
}
}

Ok(over)
}

/// Parse one DSL range bound: a positive integer is a 1-based absolute turn
/// index, a negative integer is an offset from the end.
fn parse_dsl_bound(s: &str) -> Result<RuleBound, String> {
Expand Down
Loading
Loading