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
6 changes: 4 additions & 2 deletions agent/auto_compact.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ async fn AgentLoop::checkpoint_at_ceiling(
}
}
let item = session.summary_item(
content=summary,
content=summary.content,
from_sequence=1,
to_sequence~,
)
Expand All @@ -152,7 +152,9 @@ async fn AgentLoop::checkpoint_at_ceiling(
"event": "auto_compaction_finished",
"from_sequence": 1,
"to_sequence": to_sequence,
"summary": summary,
"summary": summary.content,
"usage": summary.usage,
"duration_ms": summary.duration_ms,
}
(compacted, true)
}
Expand Down
90 changes: 88 additions & 2 deletions agent/auto_compact_test.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,18 @@ fn sse_final_with_reasoning(
}

///|
fn checkpoint_body(summary : String) -> Json {
{ "choices": [{ "message": { "role": "assistant", "content": summary } }] }
fn checkpoint_body(summary : String, usage? : Json) -> Json {
match usage {
Some(usage) =>
{
"choices": [{ "message": { "role": "assistant", "content": summary } }],
"usage": usage,
}
None =>
{
"choices": [{ "message": { "role": "assistant", "content": summary } }],
}
}
}

///|
Expand Down Expand Up @@ -1478,3 +1488,79 @@ async test "an oversized tool result is clamped at the loop" {
assert_true(clamped)
}
}

///|
/// The checkpoint request is the most expensive call the loop makes, so its
/// response telemetry must reach the `auto_compaction_finished` log: token
/// usage with the provider's prompt-cache hit/miss counters, plus the
/// request's wall-clock duration.
async test "ceiling checkpoint logs summary usage and duration" {
@async.with_task_group() <| group => {
let checkpoint_requests : Array[Json] = []
let handle = request => {
if is_checkpoint_request(request) {
checkpoint_requests.push(request)
Body(
checkpoint_body("CEILING-SUMMARY", usage={
"prompt_tokens": 700_123,
"completion_tokens": 456,
"total_tokens": 700_579,
"prompt_cache_hit_tokens": 690_000,
"prompt_cache_miss_tokens": 10_123,
}),
)
} else {
Sse(sse_tool_call("call_1", "probe", 900_000))
}
}
guard auto_compact_test_server(group, handle) is Some(url) else { return }
let entries : Array[Json] = []
let logger = @xlog.global()
defer {
logger.set_handler(@xlog.Stdout())
logger.set_config(Config())
}
logger.set_handler(MemoryLogHandler::{ entries, })
logger.set_level(Info)
let session = @agent_session.Session(
SessionId("checkpoint-telemetry"),
system_prompt="system",
)
let result = @agent.run_turn_in_scope(
runtime=AgentRuntime(),
scope=AgentTaskScope(group),
api_key="test-key",
model=Deepseek(V4Flash),
session~,
task="keep grinding",
append_item=(session, item) => session.append(item),
api_url=url,
tools=probe_tools(["result-1"]),
)
assert_eq(checkpoint_requests.length(), 1)
assert_eq(summary_before_terminal(result).content(), "CEILING-SUMMARY")
guard entries.filter(entry => log_event(entry) == "auto_compaction_finished")
is [finished] else {
fail("expected exactly one auto_compaction_finished log")
}
guard finished
is {
"usage": {
"prompt_tokens": Number(prompt_tokens, ..),
"prompt_cache_hit_tokens": Number(cache_hit, ..),
"prompt_cache_miss_tokens": Number(cache_miss, ..),
..
},
"duration_ms": Number(duration_ms, ..),
..
} else {
fail(
"expected usage telemetry on auto_compaction_finished: \{finished.stringify()}",
)
}
assert_eq(prompt_tokens.to_int(), 700_123)
assert_eq(cache_hit.to_int(), 690_000)
assert_eq(cache_miss.to_int(), 10_123)
assert_true(duration_ms >= 0)
}
}
30 changes: 24 additions & 6 deletions agent_session/compact/compact.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,20 @@ fn compaction_messages(
messages..push(ChatMessage(User, content=CompactionUserPrompt))
}

///|
/// A checkpoint summary plus the telemetry of the request that generated it.
/// The summary request resends the whole projected history, making it the
/// most expensive single request an agent issues: `usage` carries the
/// provider's token counts including prompt-cache hit/miss (zero when the
/// provider omits them) and `duration_ms` the request's wall-clock latency,
/// so callers log what a checkpoint actually cost instead of discarding the
/// response metadata.
pub struct CompactionSummary {
content : String
usage : @deepseek.Usage
duration_ms : Int
}

///|
/// Generate a handoff summary of the session's projected history through one
/// model call, failing when the endpoint returns an empty summary. Pass a
Expand All @@ -47,13 +61,15 @@ fn compaction_messages(
pub async fn generate_compaction_summary(
client~ : @client.Client,
session : @agent_session.Session,
) -> String {
) -> CompactionSummary {
let started_ms = @env.now()
let response = client.chat(compaction_messages(session))
let summary = response.content.trim().to_owned()
if summary.is_empty() {
let duration_ms = (@env.now() - started_ms).reinterpret_as_int64().to_int()
let content = response.content.trim().to_owned()
if content.is_empty() {
fail("compaction summary was empty")
}
summary
{ content, usage: response.usage, duration_ms }
}

///|
Expand Down Expand Up @@ -102,7 +118,7 @@ pub async fn compact_session(
let next = append_compaction_summary(
store?,
session,
summary~,
summary=summary.content,
from_sequence~,
to_sequence~,
)
Expand All @@ -111,7 +127,9 @@ pub async fn compact_session(
"event": "compaction_finished",
"from_sequence": from_sequence,
"to_sequence": to_sequence,
"summary": summary,
"summary": summary.content,
"usage": summary.usage,
"duration_ms": summary.duration_ms,
}
next
}
7 changes: 6 additions & 1 deletion agent_session/compact/pkg.generated.mbti
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,16 @@ import {
// Values
pub async fn compact_session(store? : @store.SessionStore, @agent_session.Session, api_key~ : String, model~ : @deepseek.Model, api_url? : String) -> @agent_session.Session

pub async fn generate_compaction_summary(client~ : @client.Client, @agent_session.Session) -> String
pub async fn generate_compaction_summary(client~ : @client.Client, @agent_session.Session) -> CompactionSummary

// Errors

// Types and methods
pub struct CompactionSummary {
content : String
usage : @deepseek.Usage
duration_ms : Int
}

// Type aliases

Expand Down
Loading