diff --git a/R/compact-turn.R b/R/compact-turn.R new file mode 100644 index 0000000..f979b2e --- /dev/null +++ b/R/compact-turn.R @@ -0,0 +1,312 @@ +# Compaction for turn_session history. +# +# Long-running subagents (and the parent chat) can build up multi- +# tens-of-thousands of tokens in `session$history`. Compaction asks +# the LLM to summarize the older slice and replaces it with a single +# assistant message holding the summary — keeping the most recent +# turn(s) verbatim so in-flight reasoning isn't truncated. +# +# Two principles: +# - Disk space is cheap; context is expensive. The on-disk +# transcript is durable (see subagent_spawn / subagent_turn_prompt +# persistence). Compaction only mutates the live in-memory +# history sent to the model. +# - Never compact mid-turn or when there's an unfinished +# tool_use → tool_result pair, because the LLM would see a +# dangling tool_use and refuse. + +#' Resolve the effective compaction threshold for a subagent. +#' +#' Returns a numeric percent. NULL means "compaction off for this +#' child" — caller skips entirely. +#' @param config Full corteza config (post-defaults). +#' @return Numeric percent in (0, 100], or NULL. +#' @keywords internal +subagent_compact_threshold <- function(config) { + cc <- config$subagents$context_compaction %||% list() + mode <- cc$mode %||% "inherit_strict" + if (identical(mode, "off")) { + return(NULL) + } + parent_pct <- as.numeric(config$context_compact_pct %||% 90L) + child_pct <- as.numeric(cc$compact_pct %||% 75L) + if (identical(mode, "inherit")) { + return(parent_pct) + } + # inherit_strict (default): child threshold can only be + # equal-or-lower than parent's. Async work shouldn't die because + # a quietly-growing child filled its window past the parent's + # tolerance. + min(parent_pct, child_pct) +} + +#' Find the largest cut point in `history` that doesn't split a +#' tool_use / tool_result pair. +#' +#' Returns the number of entries that can safely be summarized +#' (entries `1..cut`). Entries `cut+1..end` are preserved verbatim. +#' Returns 0 when no safe cut is available. +#' +#' Strategy: start from the maximum cut that leaves `keep_recent_turns` +#' user-prompt boundaries intact, then walk back as needed so the cut +#' doesn't land between a tool_use and the tool_result that satisfies +#' it. +#' @param history Live in-memory history list. +#' @param keep_recent_turns Number of recent user→assistant turns to +#' keep verbatim (a turn starts at a user message). +#' @keywords internal +compact_find_cut <- function(history, keep_recent_turns = 1L) { + n <- length(history) + if (n == 0L) { + return(0L) + } + # Walk from the end; find the start index of the (keep_recent + + # 1)th-from-last user turn. Everything before that is summarizable. + # + # Anthropic-style tool_result messages also have role == "user", + # but they're the second half of a tool_use round-trip — not a + # new user turn. Filter those out so the boundary lands on real + # human prompts. + user_starts <- integer(0) + for (i in seq_len(n)) { + role <- history[[i]]$role %||% "" + if (identical(role, "user") && + !compact_entry_is_tool_result_only(history[[i]])) { + user_starts <- c(user_starts, i) + } + } + if (length(user_starts) <= as.integer(keep_recent_turns)) { + return(0L) + } + # Cut just before the start of the (keep_recent + 1)th-from-last + # user turn (i.e., the boundary is the first kept user turn). + keep <- as.integer(keep_recent_turns) + boundary <- user_starts[length(user_starts) - keep + 1L] + cut <- boundary - 1L + if (cut <= 0L) { + return(0L) + } + # Don't split any tool_use / tool_result pair. Walk the cut back + # until every tool_use in the prefix `history[1..cut]` has its + # matching tool_result also in that prefix — i.e., no dangling + # tool_use whose tool_result lives in the kept tail. + while (cut > 0L && + compact_prefix_has_unmatched_tool_use(history, cut)) { + cut <- cut - 1L + } + as.integer(cut) +} + +#' Does a user-role entry contain only tool_result blocks? +#' +#' Anthropic-style chat history puts tool_result blocks inside a +#' user message; this helps `compact_find_cut` avoid treating them +#' as user-turn boundaries. +#' @noRd +compact_entry_is_tool_result_only <- function(entry) { + cnt <- entry$content + if (!is.list(cnt) || length(cnt) == 0L) { + return(FALSE) + } + for (block in cnt) { + bt <- block$type %||% "" + if (!identical(bt, "tool_result")) { + return(FALSE) + } + } + TRUE +} + +#' Does any tool_use in `history[1..cut]` have its matching +#' tool_result in `history[(cut+1):n]`? +#' @noRd +compact_prefix_has_unmatched_tool_use <- function(history, cut) { + n <- length(history) + if (cut <= 0L || cut >= n) { + return(FALSE) + } + # Collect tool_use ids in prefix. + prefix_uses <- character(0) + for (i in seq_len(cut)) { + c2 <- history[[i]]$content + if (!is.list(c2)) next + for (block in c2) { + if (identical(block$type %||% "", "tool_use")) { + tid <- block$id %||% "" + if (nzchar(tid)) prefix_uses <- c(prefix_uses, tid) + } + } + } + if (length(prefix_uses) == 0L) { + return(FALSE) + } + # Collect tool_result ids in prefix to remove already-matched ones. + prefix_results <- character(0) + for (i in seq_len(cut)) { + c2 <- history[[i]]$content + if (!is.list(c2)) next + for (block in c2) { + if (identical(block$type %||% "", "tool_result")) { + tid <- block$tool_use_id %||% "" + if (nzchar(tid)) prefix_results <- c(prefix_results, tid) + } + } + } + open <- setdiff(prefix_uses, prefix_results) + length(open) > 0L +} + +# Stripped-down summarization prompt — same shape the CLI uses. +.compact_summary_prompt <- paste( + "Summarize this conversation concisely, preserving:", + "1. What was accomplished (completed tasks, files modified)", + "2. Current work in progress", + "3. Key decisions and constraints", + "4. Pending tasks or next steps", + "5. Any errors encountered and their resolution", + "", + "Be specific about file names, function names, and technical details.", + "Format as a structured summary the assistant can use to continue the work.", + sep = "\n" +) + +#' Summarize the prefix of a history slice via the LLM. +#' +#' Returns the summary text on success or NULL on any error +#' (including timeout). Caller leaves history intact on NULL. +#' @param slice List of history entries to summarize (the part being +#' compacted; the recent tail is excluded). +#' @param provider Provider name. +#' @param model Model name. +#' @param timeout_seconds Hard wall on the summarizer call. +#' @keywords internal +compact_summarize_slice <- function(slice, provider = "anthropic", + model = NULL, timeout_seconds = 60L) { + if (length(slice) == 0L) { + return(NULL) + } + conv_text <- vapply(slice, function(entry) { + sprintf("[%s]: %s", entry$role %||% "?", + archival_history_entry_to_text(entry)) + }, character(1)) + conv_text <- paste(conv_text, collapse = "\n\n") + prompt <- sprintf("%s\n\n---\nConversation to summarize:\n%s", + .compact_summary_prompt, conv_text) + setTimeLimit(elapsed = timeout_seconds, transient = TRUE) + on.exit(setTimeLimit(elapsed = Inf, transient = FALSE), add = TRUE) + result <- tryCatch( + llm.api::chat( + prompt = prompt, + provider = provider, + model = model, + system = paste("You are a helpful assistant that creates", + "concise conversation summaries."), + temperature = 0.3 + ), + error = function(e) { + log_event("subagent_compact_failed", + reason = "summarizer_error", + error = conditionMessage(e), level = "warn") + NULL + } + ) + if (is.null(result)) { + return(NULL) + } + as.character(result$content %||% "") +} + +#' Replace the compacted prefix of a session's history with a +#' single assistant summary message. +#' +#' Pure function: returns the new history list, doesn't mutate +#' anything. The summary is wrapped in `[compacted history]\n\n...` +#' so it's visually distinct in the transcript. +#' @keywords internal +compact_rewrite_history <- function(history, cut, summary) { + if (cut <= 0L || cut >= length(history)) { + return(history) + } + kept <- history[(cut + 1L):length(history)] + summary_entry <- list( + role = "assistant", + content = sprintf("[compacted history]\n\n%s", summary) + ) + c(list(summary_entry), kept) +} + +#' Maybe compact a turn_session's in-memory history. +#' +#' Decision points: +#' - Compaction mode off → return invisibly without checking. +#' - History shorter than `min_messages` → skip (nothing to gain). +#' - Live token usage below threshold → skip. +#' - No safe cut available (e.g. open tool_use) → skip. +#' - Summarizer fails → log and leave history intact. +#' +#' On success, mutates `session$history` in place. Returns invisibly +#' TRUE if compaction ran successfully, FALSE otherwise. +#' +#' @param session A turn_session (`new_session()`). +#' @param config Full corteza config (post-defaults). +#' @param kind Optional marker. "archive_holder" skips compaction +#' entirely so seeded transcript history is preserved. +#' @keywords internal +maybe_compact_turn_session <- function(session, config, kind = NULL) { + if (identical(kind, "archive_holder")) { + return(invisible(FALSE)) + } + cc <- config$subagents$context_compaction %||% list() + threshold <- subagent_compact_threshold(config) + if (is.null(threshold)) { + return(invisible(FALSE)) + } + history <- session$history %||% list() + min_messages <- as.integer(cc$min_messages %||% 6L) + if (length(history) < min_messages) { + return(invisible(FALSE)) + } + model <- session$model_map$cloud %||% NULL + if (is.null(model)) { + model <- switch(session$provider %||% "anthropic", + anthropic = "claude-sonnet-4-20250514", + openai = "gpt-4o", + moonshot = "moonshot-v1-8k", + NULL) + } + # Estimate against the same tools turn() will send. turn() + # resolves tools from session$tools_filter when tools is NULL, + # so passing NULL here would undercount the live context for any + # subagent with an active tool filter. + tools_for_estimate <- tryCatch( + skills_as_api_tools(session$tools_filter), + error = function(e) NULL) + pct <- context_usage_pct(list(history = history), model = model, + system_prompt = session$system, + tools = tools_for_estimate) + if (pct < threshold) { + return(invisible(FALSE)) + } + cut <- compact_find_cut(history, + keep_recent_turns = cc$keep_recent_turns %||% 1L) + if (cut <= 0L) { + log_event("subagent_compact_skipped", + reason = "no_safe_cut", history_len = length(history)) + return(invisible(FALSE)) + } + slice <- history[seq_len(cut)] + summary <- compact_summarize_slice( + slice, provider = session$provider %||% "anthropic", + model = model, + timeout_seconds = as.integer(cc$timeout_seconds %||% 60L)) + if (is.null(summary) || !nzchar(summary)) { + return(invisible(FALSE)) + } + session$history <- compact_rewrite_history(history, cut, summary) + log_event("subagent_compact_applied", + before_len = length(history), + after_len = length(session$history), + threshold_pct = threshold, + pre_pct = pct) + invisible(TRUE) +} diff --git a/R/config.R b/R/config.R index 526b684..5be90b4 100644 --- a/R/config.R +++ b/R/config.R @@ -194,6 +194,32 @@ load_config <- function(cwd = getwd()) { if (is.null(sub$base_port)) { sub$base_port <- 7851L } + # Child context compaction. Working subagents (not archive holders) + # may compact their own in-memory history when it grows past the + # effective threshold. The on-disk transcript is unaffected. + if (is.null(sub$context_compaction)) { + sub$context_compaction <- list() + } + cc <- sub$context_compaction + if (is.null(cc$mode)) { + # inherit_strict: effective threshold = min(parent, child). + # inherit: use parent's context_compact_pct verbatim. + # off: never compact. + cc$mode <- "inherit_strict" + } + if (is.null(cc$compact_pct)) { + cc$compact_pct <- 75L + } + if (is.null(cc$keep_recent_turns)) { + cc$keep_recent_turns <- 1L + } + if (is.null(cc$min_messages)) { + cc$min_messages <- 6L + } + if (is.null(cc$timeout_seconds)) { + cc$timeout_seconds <- 60L + } + sub$context_compaction <- cc config$subagents <- sub # Archival (retroactive-extraction) configuration. Default off so diff --git a/R/subagent.R b/R/subagent.R index 78a0d98..0ae8ce0 100644 --- a/R/subagent.R +++ b/R/subagent.R @@ -158,6 +158,11 @@ subagent_seed_history <- function(history) { stop("Subagent turn session not initialized", call. = FALSE) } .subagent_state$session$history <- history + # Mark this child as an archive holder: its seeded history is the + # whole point of the subagent, so context compaction must not + # touch it. + .subagent_state$kind <- "archive_holder" + .subagent_state$protected_history_len <- length(history) invisible(TRUE) } @@ -251,6 +256,21 @@ subagent_turn_prompt <- function(prompt) { } } + # Context compaction. Runs after the turn (never mid-turn) and + # after archival has had its shot at the slice. The on-disk + # transcript already holds the full record; compaction only + # rewrites the in-memory history sent to the model on the next + # query. Archive holders are skipped via the kind marker. + tryCatch( + maybe_compact_turn_session( + .subagent_state$session, cfg, + kind = .subagent_state$kind), + error = function(e) { + log_event("subagent_compact_failed", + reason = "unexpected_error", + error = conditionMessage(e), level = "warn") + }) + as.character(result$reply %||% "") } diff --git a/inst/tinytest/test_compact_turn.R b/inst/tinytest/test_compact_turn.R new file mode 100644 index 0000000..9882f68 --- /dev/null +++ b/inst/tinytest/test_compact_turn.R @@ -0,0 +1,190 @@ +# Pure-function tests for the subagent compaction helpers. These +# exercise the policy resolution, the cut-point finder, and the +# pure history-rewrite. The summarizer call itself is gated to +# at_home in test_subagent_callr.R. + +# Threshold resolution -------- + +cfg_base <- list( + context_compact_pct = 90L, + subagents = list( + context_compaction = list( + mode = "inherit_strict", + compact_pct = 75L, + keep_recent_turns = 1L, + min_messages = 6L, + timeout_seconds = 60L))) + +# inherit_strict: effective threshold is the min of parent and child. +expect_equal(corteza:::subagent_compact_threshold(cfg_base), 75) + +# inherit: parent's threshold wins. +cfg_inherit <- cfg_base +cfg_inherit$subagents$context_compaction$mode <- "inherit" +expect_equal(corteza:::subagent_compact_threshold(cfg_inherit), 90) + +# off: NULL means caller should skip entirely. +cfg_off <- cfg_base +cfg_off$subagents$context_compaction$mode <- "off" +expect_null(corteza:::subagent_compact_threshold(cfg_off)) + +# inherit_strict still wins when child is set higher than parent +# (strict means equal-or-lower). +cfg_high <- cfg_base +cfg_high$subagents$context_compaction$compact_pct <- 95L +expect_equal(corteza:::subagent_compact_threshold(cfg_high), 90) + +# compact_find_cut -------- + +# Empty history: cut at 0. +expect_equal(corteza:::compact_find_cut(list()), 0L) + +# Need at least keep_recent_turns + 1 user turns to compact anything. +one_turn <- list( + list(role = "user", content = "first"), + list(role = "assistant", content = "reply")) +expect_equal(corteza:::compact_find_cut(one_turn, keep_recent_turns = 1L), 0L) + +# With three turns and keep_recent_turns = 1, the cut lands just +# before the start of the last user turn (index 5 = last user +# message, so cut = 4). +three_turns <- list( + list(role = "user", content = "q1"), # 1 + list(role = "assistant", content = "a1"), # 2 + list(role = "user", content = "q2"), # 3 + list(role = "assistant", content = "a2"), # 4 + list(role = "user", content = "q3"), # 5 + list(role = "assistant", content = "a3")) # 6 +expect_equal(corteza:::compact_find_cut(three_turns, keep_recent_turns = 1L), + 4L) + +# keep_recent_turns = 2: keep last two turns, cut after entry 2. +expect_equal(corteza:::compact_find_cut(three_turns, keep_recent_turns = 2L), + 2L) + +# Open tool_use/tool_result pair: cut walks back so the pair stays +# together. Construct: user, assistant-with-tool_use, tool_result, +# user, assistant. With keep_recent_turns=1 the natural cut is at 3 +# (before the last user), but entry 2 (assistant tool_use) pairs +# with entry 3 (tool_result inside the kept tail), so the cut must +# walk back below 2. +toolchain <- list( + list(role = "user", content = "do a thing"), + list(role = "assistant", + content = list(list(type = "tool_use", + id = "tu_1", name = "x", input = list()))), + list(role = "user", + content = list(list(type = "tool_result", + tool_use_id = "tu_1", content = "ok"))), + list(role = "user", content = "next"), + list(role = "assistant", content = "reply")) +cut_toolchain <- corteza:::compact_find_cut(toolchain, keep_recent_turns = 1L) +# Critical: tool_result messages (role == "user" but content is a +# tool_result block) must NOT count as user-turn boundaries — they +# are the second half of the previous assistant tool_use. The only +# real user turns here are entry 1 ("do a thing") and entry 4 +# ("next"). With keep_recent_turns = 1 the safe answer is cut = 3 +# (summarize entries 1-3, keeping the tool_use/tool_result pair +# intact in the prefix). The unsafe cut is 2, which would split +# the pair across the boundary. +expect_true(cut_toolchain != 2L, + info = "cut must not split tool_use / tool_result pair") +expect_equal(cut_toolchain, 3L, + info = "cut sits after the tool_use pair, before the next user turn") + +# Regression for the more subtle P1: a long turn with multiple +# tool_use/tool_result rounds before the final assistant. None of +# the user-tagged tool_result messages should look like a new user +# turn, so with keep_recent_turns = 1 the entire turn must stay +# together (cut = 0). +multi_tool_turn <- list( + list(role = "user", content = "do it"), # 1 real user + list(role = "assistant", + content = list(list(type = "tool_use", + id = "tu_a", name = "x", input = list()))), + list(role = "user", # 3 tool_result, NOT a turn + content = list(list(type = "tool_result", + tool_use_id = "tu_a", content = "..."))), + list(role = "assistant", + content = list(list(type = "tool_use", + id = "tu_b", name = "y", input = list()))), + list(role = "user", # 5 tool_result, NOT a turn + content = list(list(type = "tool_result", + tool_use_id = "tu_b", content = "..."))), + list(role = "assistant", content = "all done")) +expect_equal( + corteza:::compact_find_cut(multi_tool_turn, keep_recent_turns = 1L), + 0L, + info = "tool_result-only user msgs must not split a single turn") + +# compact_entry_is_tool_result_only -------- + +expect_false(corteza:::compact_entry_is_tool_result_only( + list(role = "user", content = "plain text"))) +expect_false(corteza:::compact_entry_is_tool_result_only( + list(role = "user", + content = list(list(type = "text", text = "hi"))))) +expect_true(corteza:::compact_entry_is_tool_result_only( + list(role = "user", + content = list(list(type = "tool_result", + tool_use_id = "tu_1", content = "ok"))))) +# Mixed content (text + tool_result) is not tool_result-only. +expect_false(corteza:::compact_entry_is_tool_result_only( + list(role = "user", + content = list(list(type = "text", text = "hi"), + list(type = "tool_result", tool_use_id = "tu_1", + content = "ok"))))) + +# compact_rewrite_history -------- + +# Pure rewrite: returns a new list with one summary entry prepended +# to the kept tail; doesn't mutate the input. +hist <- three_turns +new_hist <- corteza:::compact_rewrite_history(hist, cut = 4L, + summary = "summary text") +expect_equal(length(new_hist), 3L, + info = "rewrite leaves 1 summary + 2 kept entries") +expect_equal(new_hist[[1]]$role, "assistant") +expect_true(grepl("compacted history", new_hist[[1]]$content, fixed = TRUE)) +expect_true(grepl("summary text", new_hist[[1]]$content, fixed = TRUE)) +expect_equal(new_hist[[2]]$content, "q3") +expect_equal(new_hist[[3]]$content, "a3") +# Original untouched. +expect_equal(length(hist), 6L) + +# cut at 0 or >= length leaves history unchanged. +expect_identical( + corteza:::compact_rewrite_history(hist, cut = 0L, summary = "s"), + hist) +expect_identical( + corteza:::compact_rewrite_history(hist, cut = length(hist), + summary = "s"), + hist) + +# maybe_compact_turn_session: archive holders are skipped -------- + +# When kind == "archive_holder", the helper bails immediately even +# if the threshold would otherwise trigger. We don't need a real +# LLM call to verify this. +fake_session <- new.env(parent = emptyenv()) +fake_session$history <- three_turns +fake_session$provider <- "anthropic" +expect_false( + isTRUE(corteza:::maybe_compact_turn_session( + fake_session, cfg_base, kind = "archive_holder"))) +# History untouched. +expect_equal(length(fake_session$history), 6L) + +# Mode "off" also bails immediately. +expect_false( + isTRUE(corteza:::maybe_compact_turn_session( + fake_session, cfg_off))) +expect_equal(length(fake_session$history), 6L) + +# History shorter than min_messages bails (no LLM call). +short <- one_turn +fake_session$history <- short +expect_false( + isTRUE(corteza:::maybe_compact_turn_session( + fake_session, cfg_base))) +expect_equal(length(fake_session$history), 2L) diff --git a/man/compact_find_cut.Rd b/man/compact_find_cut.Rd new file mode 100644 index 0000000..8d8d01d --- /dev/null +++ b/man/compact_find_cut.Rd @@ -0,0 +1,24 @@ +% tinyrox says don't edit this manually, but it can't stop you! +\name{compact_find_cut} +\alias{compact_find_cut} +\title{Find the largest cut point in `history` that doesn't split a +tool_use / tool_result pair.} +\usage{ +compact_find_cut(history, keep_recent_turns = 1L) +} +\arguments{ +\item{history}{Live in-memory history list.} + +\item{keep_recent_turns}{Number of recent user→assistant turns to +keep verbatim (a turn starts at a user message).} +} +\description{ +Returns the number of entries that can safely be summarized +(entries `1..cut`). Entries `cut+1..end` are preserved verbatim. +Returns 0 when no safe cut is available. +Strategy: start from the maximum cut that leaves `keep_recent_turns` +user-prompt boundaries intact, then walk back as needed so the cut +doesn't land between a tool_use and the tool_result that satisfies +it. +} +\keyword{internal} diff --git a/man/compact_rewrite_history.Rd b/man/compact_rewrite_history.Rd new file mode 100644 index 0000000..d85d5f0 --- /dev/null +++ b/man/compact_rewrite_history.Rd @@ -0,0 +1,14 @@ +% tinyrox says don't edit this manually, but it can't stop you! +\name{compact_rewrite_history} +\alias{compact_rewrite_history} +\title{Replace the compacted prefix of a session's history with a +single assistant summary message.} +\usage{ +compact_rewrite_history(history, cut, summary) +} +\description{ +Pure function: returns the new history list, doesn't mutate +anything. The summary is wrapped in `[compacted history]\n\n...` +so it's visually distinct in the transcript. +} +\keyword{internal} diff --git a/man/compact_summarize_slice.Rd b/man/compact_summarize_slice.Rd new file mode 100644 index 0000000..5a49d36 --- /dev/null +++ b/man/compact_summarize_slice.Rd @@ -0,0 +1,23 @@ +% tinyrox says don't edit this manually, but it can't stop you! +\name{compact_summarize_slice} +\alias{compact_summarize_slice} +\title{Summarize the prefix of a history slice via the LLM.} +\usage{ +compact_summarize_slice(slice, provider = "anthropic", model = NULL, + timeout_seconds = 60L) +} +\arguments{ +\item{slice}{List of history entries to summarize (the part being +compacted; the recent tail is excluded).} + +\item{provider}{Provider name.} + +\item{model}{Model name.} + +\item{timeout_seconds}{Hard wall on the summarizer call.} +} +\description{ +Returns the summary text on success or NULL on any error +(including timeout). Caller leaves history intact on NULL. +} +\keyword{internal} diff --git a/man/maybe_compact_turn_session.Rd b/man/maybe_compact_turn_session.Rd new file mode 100644 index 0000000..2c3b90a --- /dev/null +++ b/man/maybe_compact_turn_session.Rd @@ -0,0 +1,26 @@ +% tinyrox says don't edit this manually, but it can't stop you! +\name{maybe_compact_turn_session} +\alias{maybe_compact_turn_session} +\title{Maybe compact a turn_session's in-memory history.} +\usage{ +maybe_compact_turn_session(session, config, kind = NULL) +} +\arguments{ +\item{session}{A turn_session (`new_session()`).} + +\item{config}{Full corteza config (post-defaults).} + +\item{kind}{Optional marker. "archive_holder" skips compaction +entirely so seeded transcript history is preserved.} +} +\description{ +Decision points: +- Compaction mode off → return invisibly without checking. +- History shorter than `min_messages` → skip (nothing to gain). +- Live token usage below threshold → skip. +- No safe cut available (e.g. open tool_use) → skip. +- Summarizer fails → log and leave history intact. +On success, mutates `session$history` in place. Returns invisibly +TRUE if compaction ran successfully, FALSE otherwise. +} +\keyword{internal} diff --git a/man/subagent_compact_threshold.Rd b/man/subagent_compact_threshold.Rd new file mode 100644 index 0000000..58b5610 --- /dev/null +++ b/man/subagent_compact_threshold.Rd @@ -0,0 +1,18 @@ +% tinyrox says don't edit this manually, but it can't stop you! +\name{subagent_compact_threshold} +\alias{subagent_compact_threshold} +\title{Resolve the effective compaction threshold for a subagent.} +\usage{ +subagent_compact_threshold(config) +} +\arguments{ +\item{config}{Full corteza config (post-defaults).} +} +\value{ +Numeric percent in (0, 100], or NULL. +} +\description{ +Returns a numeric percent. NULL means "compaction off for this +child" — caller skips entirely. +} +\keyword{internal}