2025-12-09 19:32:04 +00:00
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
2025-09-13 09:46:14 +00:00
import { Session } from "."
import { Identifier } from "../id/id"
import { Instance } from "../project/instance"
import { Provider } from "../provider/provider"
import { MessageV2 } from "./message-v2"
2025-10-26 19:50:41 +00:00
import z from "zod"
2025-09-16 08:52:34 +00:00
import { Token } from "../util/token"
import { Log } from "../util/log"
2025-11-17 15:57:18 +00:00
import { SessionProcessor } from "./processor"
import { fn } from "@/util/fn"
2025-12-15 02:11:30 +00:00
import { Agent } from "@/agent/agent"
2025-12-17 19:57:09 +00:00
import { Plugin } from "@/plugin"
2025-12-27 00:31:42 +00:00
import { Config } from "@/config/config"
2026-02-11 01:55:22 +00:00
import { ProviderTransform } from "@/provider/transform"
2025-09-13 09:46:14 +00:00
export namespace SessionCompaction {
2025-09-16 08:52:34 +00:00
const log = Log . create ( { service : "session.compaction" } )
2025-09-13 09:46:14 +00:00
export const Event = {
2025-12-09 19:32:04 +00:00
Compacted : BusEvent.define (
2025-09-13 09:46:14 +00:00
"session.compacted" ,
z . object ( {
sessionID : z.string ( ) ,
} ) ,
) ,
}
2026-02-11 01:55:22 +00:00
const COMPACTION_BUFFER = 20 _000
2025-12-27 00:31:42 +00:00
export async function isOverflow ( input : { tokens : MessageV2.Assistant [ "tokens" ] ; model : Provider.Model } ) {
const config = await Config . get ( )
2025-12-27 00:48:56 +00:00
if ( config . compaction ? . auto === false ) return false
2025-09-13 09:59:18 +00:00
const context = input . model . limit . context
if ( context === 0 ) return false
2026-02-11 01:55:22 +00:00
const count =
input . tokens . total ||
input . tokens . input + input . tokens . output + input . tokens . cache . read + input . tokens . cache . write
const reserved =
config . compaction ? . reserved ? ? Math . min ( COMPACTION_BUFFER , ProviderTransform . maxOutputTokens ( input . model ) )
2026-02-11 21:43:04 +00:00
const usable = input . model . limit . input
? input . model . limit . input - reserved
: context - ProviderTransform . maxOutputTokens ( input . model )
2026-02-11 01:55:22 +00:00
return count >= usable
2025-09-13 09:46:14 +00:00
}
2025-09-17 07:07:24 +00:00
export const PRUNE_MINIMUM = 20 _000
export const PRUNE_PROTECT = 40 _000
2025-12-22 23:24:06 +00:00
const PRUNE_PROTECTED_TOOLS = [ "skill" ]
2025-09-16 08:52:34 +00:00
// goes backwards through parts until there are 40_000 tokens worth of tool
// calls. then erases output of previous tool calls. idea is to throw away old
// tool calls that are no longer relevant.
export async function prune ( input : { sessionID : string } ) {
2025-12-27 00:31:42 +00:00
const config = await Config . get ( )
2025-12-27 00:48:56 +00:00
if ( config . compaction ? . prune === false ) return
2025-09-16 08:52:34 +00:00
log . info ( "pruning" )
2025-11-06 18:20:13 +00:00
const msgs = await Session . messages ( { sessionID : input.sessionID } )
2025-09-16 08:52:34 +00:00
let total = 0
let pruned = 0
const toPrune = [ ]
2025-09-17 07:07:24 +00:00
let turns = 0
2025-09-16 08:52:34 +00:00
2025-09-17 07:07:24 +00:00
loop : for ( let msgIndex = msgs . length - 1 ; msgIndex >= 0 ; msgIndex -- ) {
2025-09-16 08:52:34 +00:00
const msg = msgs [ msgIndex ]
2025-09-17 07:07:24 +00:00
if ( msg . info . role === "user" ) turns ++
if ( turns < 2 ) continue
if ( msg . info . role === "assistant" && msg . info . summary ) break loop
2025-09-16 08:52:34 +00:00
for ( let partIndex = msg . parts . length - 1 ; partIndex >= 0 ; partIndex -- ) {
const part = msg . parts [ partIndex ]
if ( part . type === "tool" )
if ( part . state . status === "completed" ) {
2025-12-22 23:24:06 +00:00
if ( PRUNE_PROTECTED_TOOLS . includes ( part . tool ) ) continue
2025-09-16 08:52:34 +00:00
if ( part . state . time . compacted ) break loop
const estimate = Token . estimate ( part . state . output )
total += estimate
2025-09-17 07:07:24 +00:00
if ( total > PRUNE_PROTECT ) {
2025-09-16 08:52:34 +00:00
pruned += estimate
toPrune . push ( part )
}
}
}
}
log . info ( "found" , { pruned , total } )
2025-09-17 07:07:24 +00:00
if ( pruned > PRUNE_MINIMUM ) {
2025-09-16 08:52:34 +00:00
for ( const part of toPrune ) {
if ( part . state . status === "completed" ) {
part . state . time . compacted = Date . now ( )
await Session . updatePart ( part )
}
}
log . info ( "pruned" , { count : toPrune.length } )
}
}
2025-11-17 15:57:18 +00:00
export async function process ( input : {
parentID : string
messages : MessageV2.WithParts [ ]
sessionID : string
abort : AbortSignal
2025-11-25 18:10:56 +00:00
auto : boolean
2025-11-17 15:57:18 +00:00
} ) {
2025-12-15 02:11:30 +00:00
const userMessage = input . messages . findLast ( ( m ) = > m . info . id === input . parentID ) ! . info as MessageV2 . User
const agent = await Agent . get ( "compaction" )
const model = agent . model
? await Provider . getModel ( agent . model . providerID , agent . model . modelID )
: await Provider . getModel ( userMessage . model . providerID , userMessage . model . modelID )
2025-09-13 09:46:14 +00:00
const msg = ( await Session . updateMessage ( {
id : Identifier.ascending ( "message" ) ,
role : "assistant" ,
2025-11-17 15:57:18 +00:00
parentID : input.parentID ,
2025-09-13 09:46:14 +00:00
sessionID : input.sessionID ,
2025-12-15 02:11:30 +00:00
mode : "compaction" ,
agent : "compaction" ,
2026-02-06 22:33:47 +00:00
variant : userMessage.variant ,
2025-11-17 15:57:18 +00:00
summary : true ,
2025-09-13 09:46:14 +00:00
path : {
cwd : Instance.directory ,
root : Instance.worktree ,
} ,
cost : 0 ,
tokens : {
output : 0 ,
input : 0 ,
reasoning : 0 ,
cache : { read : 0 , write : 0 } ,
} ,
2025-12-15 02:11:30 +00:00
modelID : model.id ,
2025-09-13 09:46:14 +00:00
providerID : model.providerID ,
time : {
created : Date.now ( ) ,
} ,
} ) ) as MessageV2 . Assistant
2025-11-17 15:57:18 +00:00
const processor = SessionProcessor . create ( {
assistantMessage : msg ,
2025-10-15 18:44:16 +00:00
sessionID : input.sessionID ,
2025-12-15 02:11:30 +00:00
model ,
2025-11-17 15:57:18 +00:00
abort : input.abort ,
} )
2025-12-23 04:19:14 +00:00
// Allow plugins to inject context or replace compaction prompt
2025-12-17 19:57:09 +00:00
const compacting = await Plugin . trigger (
"experimental.session.compacting" ,
{ sessionID : input.sessionID } ,
2025-12-23 04:19:14 +00:00
{ context : [ ] , prompt : undefined } ,
2025-12-17 19:57:09 +00:00
)
2026-02-11 01:55:22 +00:00
const defaultPrompt = ` Provide a detailed prompt for continuing our conversation above.
Focus on information that would be helpful for continuing the conversation , including what we did , what we 're doing, which files we' re working on , and what we ' re going to do next .
The summary that you construct will be used so that another agent can read it and continue the work .
When constructing the summary , try to stick to this template :
-- -
# # Goal
[ What goal ( s ) is the user trying to accomplish ? ]
# # Instructions
- [ What important instructions did the user give you that are relevant ]
- [ If there is a plan or spec , include information about it so next agent can continue using it ]
# # Discoveries
[ What notable things were learned during this conversation that would be useful for the next agent to know when continuing the work ]
# # Accomplished
[ What work has been completed , what work is still in progress , and what work is left ? ]
# # Relevant files / directories
[ Construct a structured list of relevant files that have been read , edited , or created that pertain to the task at hand . If all the files in a directory are relevant , include the path to the directory . ]
-- - `
2025-12-23 04:19:14 +00:00
const promptText = compacting . prompt ? ? [ defaultPrompt , . . . compacting . context ] . join ( "\n\n" )
2025-12-04 02:09:03 +00:00
const result = await processor . process ( {
2025-12-15 02:11:30 +00:00
user : userMessage ,
agent ,
abort : input.abort ,
sessionID : input.sessionID ,
tools : { } ,
system : [ ] ,
2025-12-04 02:09:03 +00:00
messages : [
2026-01-20 22:39:00 +00:00
. . . MessageV2 . toModelMessages ( input . messages , model ) ,
2025-12-04 02:09:03 +00:00
{
role : "user" ,
content : [
2025-11-18 18:09:50 +00:00
{
2025-12-04 02:09:03 +00:00
type : "text" ,
2025-12-23 04:19:14 +00:00
text : promptText ,
2025-11-18 18:09:50 +00:00
} ,
] ,
2025-12-04 02:09:03 +00:00
} ,
] ,
2025-12-15 02:11:30 +00:00
model ,
2025-12-04 02:09:03 +00:00
} )
2025-12-15 02:11:30 +00:00
2025-11-25 18:10:56 +00:00
if ( result === "continue" && input . auto ) {
2025-11-17 15:57:18 +00:00
const continueMsg = await Session . updateMessage ( {
id : Identifier.ascending ( "message" ) ,
role : "user" ,
sessionID : input.sessionID ,
time : {
created : Date.now ( ) ,
} ,
2025-12-15 02:11:30 +00:00
agent : userMessage.agent ,
model : userMessage.model ,
2025-10-22 23:31:36 +00:00
} )
2025-11-17 15:57:18 +00:00
await Session . updatePart ( {
id : Identifier.ascending ( "part" ) ,
messageID : continueMsg.id ,
2025-10-18 16:49:29 +00:00
sessionID : input.sessionID ,
2025-11-17 15:57:18 +00:00
type : "text" ,
synthetic : true ,
2026-02-11 01:55:22 +00:00
text : "Continue if you have next steps, or stop and ask for clarification if you are unsure how to proceed." ,
2025-11-17 15:57:18 +00:00
time : {
start : Date.now ( ) ,
end : Date.now ( ) ,
} ,
2025-10-18 16:49:29 +00:00
} )
}
2025-11-18 18:09:50 +00:00
if ( processor . message . error ) return "stop"
2025-11-25 19:47:10 +00:00
Bus . publish ( Event . Compacted , { sessionID : input.sessionID } )
2025-11-17 15:57:18 +00:00
return "continue"
2025-09-13 09:46:14 +00:00
}
2025-11-17 15:57:18 +00:00
export const create = fn (
z . object ( {
sessionID : Identifier.schema ( "session" ) ,
2025-11-21 08:13:10 +00:00
agent : z.string ( ) ,
2025-11-17 15:57:18 +00:00
model : z.object ( {
providerID : z.string ( ) ,
modelID : z.string ( ) ,
} ) ,
2025-11-25 18:10:56 +00:00
auto : z.boolean ( ) ,
2025-11-17 15:57:18 +00:00
} ) ,
async ( input ) = > {
const msg = await Session . updateMessage ( {
id : Identifier.ascending ( "message" ) ,
role : "user" ,
model : input.model ,
sessionID : input.sessionID ,
2025-11-21 08:13:10 +00:00
agent : input.agent ,
2025-11-17 15:57:18 +00:00
time : {
created : Date.now ( ) ,
} ,
} )
await Session . updatePart ( {
id : Identifier.ascending ( "part" ) ,
messageID : msg.id ,
sessionID : msg.sessionID ,
type : "compaction" ,
2025-11-25 18:10:56 +00:00
auto : input.auto ,
2025-11-17 15:57:18 +00:00
} )
} ,
)
2025-09-13 09:46:14 +00:00
}