2025-11-19 02:09:50 +08:00
import { streamText , wrapLanguageModel , type ModelMessage } from "ai"
2025-09-13 17:46:14 +08:00
import { Session } from "."
import { Identifier } from "../id/id"
import { Instance } from "../project/instance"
import { Provider } from "../provider/provider"
import { MessageV2 } from "./message-v2"
import { SystemPrompt } from "./system"
import { Bus } from "../bus"
2025-10-27 03:50:41 +08:00
import z from "zod"
2025-09-13 17:46:14 +08:00
import type { ModelsDev } from "../provider/models"
import { SessionPrompt } from "./prompt"
2025-09-13 17:55:04 +08:00
import { Flag } from "../flag/flag"
2025-09-16 16:52:34 +08:00
import { Token } from "../util/token"
import { Log } from "../util/log"
2025-10-22 00:08:21 +08:00
import { ProviderTransform } from "@/provider/transform"
2025-11-17 23:57:18 +08:00
import { SessionProcessor } from "./processor"
import { fn } from "@/util/fn"
2025-11-23 00:32:49 +08:00
import { mergeDeep , pipe } from "remeda"
2025-09-13 17:46:14 +08:00
export namespace SessionCompaction {
2025-09-16 16:52:34 +08:00
const log = Log . create ( { service : "session.compaction" } )
2025-09-13 17:46:14 +08:00
export const Event = {
Compacted : Bus.event (
"session.compacted" ,
z . object ( {
sessionID : z.string ( ) ,
} ) ,
) ,
}
2025-11-08 09:59:02 +08:00
export function isOverflow ( input : { tokens : MessageV2.Assistant [ "tokens" ] ; model : ModelsDev.Model } ) {
2025-09-13 17:55:04 +08:00
if ( Flag . OPENCODE_DISABLE_AUTOCOMPACT ) return false
2025-09-13 17:59:18 +08:00
const context = input . model . limit . context
if ( context === 0 ) return false
2025-09-13 17:46:14 +08:00
const count = input . tokens . input + input . tokens . cache . read + input . tokens . output
2025-11-08 09:59:02 +08:00
const output = Math . min ( input . model . limit . output , SessionPrompt . OUTPUT_TOKEN_MAX ) || SessionPrompt . OUTPUT_TOKEN_MAX
2025-09-13 17:59:18 +08:00
const usable = context - output
2025-09-13 17:53:03 +08:00
return count > usable
2025-09-13 17:46:14 +08:00
}
2025-09-17 15:07:24 +08:00
export const PRUNE_MINIMUM = 20 _000
export const PRUNE_PROTECT = 40 _000
2025-09-16 16:52:34 +08: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 } ) {
if ( Flag . OPENCODE_DISABLE_PRUNE ) return
log . info ( "pruning" )
2025-11-07 02:20:13 +08:00
const msgs = await Session . messages ( { sessionID : input.sessionID } )
2025-09-16 16:52:34 +08:00
let total = 0
let pruned = 0
const toPrune = [ ]
2025-09-17 15:07:24 +08:00
let turns = 0
2025-09-16 16:52:34 +08:00
2025-09-17 15:07:24 +08:00
loop : for ( let msgIndex = msgs . length - 1 ; msgIndex >= 0 ; msgIndex -- ) {
2025-09-16 16:52:34 +08:00
const msg = msgs [ msgIndex ]
2025-09-17 15:07:24 +08:00
if ( msg . info . role === "user" ) turns ++
if ( turns < 2 ) continue
if ( msg . info . role === "assistant" && msg . info . summary ) break loop
2025-09-16 16:52:34 +08: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" ) {
if ( part . state . time . compacted ) break loop
const estimate = Token . estimate ( part . state . output )
total += estimate
2025-09-17 15:07:24 +08:00
if ( total > PRUNE_PROTECT ) {
2025-09-16 16:52:34 +08:00
pruned += estimate
toPrune . push ( part )
}
}
}
}
log . info ( "found" , { pruned , total } )
2025-09-17 15:07:24 +08:00
if ( pruned > PRUNE_MINIMUM ) {
2025-09-16 16:52:34 +08: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 23:57:18 +08:00
export async function process ( input : {
parentID : string
messages : MessageV2.WithParts [ ]
sessionID : string
model : {
providerID : string
modelID : string
}
2025-11-21 16:13:10 +08:00
agent : string
2025-11-17 23:57:18 +08:00
abort : AbortSignal
2025-11-26 02:10:56 +08:00
auto : boolean
2025-11-17 23:57:18 +08:00
} ) {
const model = await Provider . getModel ( input . model . providerID , input . model . modelID )
2025-11-23 00:32:49 +08:00
const system = [ . . . SystemPrompt . compaction ( model . providerID ) ]
2025-09-13 17:46:14 +08:00
const msg = ( await Session . updateMessage ( {
id : Identifier.ascending ( "message" ) ,
role : "assistant" ,
2025-11-17 23:57:18 +08:00
parentID : input.parentID ,
2025-09-13 17:46:14 +08:00
sessionID : input.sessionID ,
2025-11-21 16:13:10 +08:00
mode : input.agent ,
2025-11-17 23:57:18 +08:00
summary : true ,
2025-09-13 17:46:14 +08:00
path : {
cwd : Instance.directory ,
root : Instance.worktree ,
} ,
cost : 0 ,
tokens : {
output : 0 ,
input : 0 ,
reasoning : 0 ,
cache : { read : 0 , write : 0 } ,
} ,
2025-11-17 23:57:18 +08:00
modelID : input.model.modelID ,
2025-09-13 17:46:14 +08:00
providerID : model.providerID ,
time : {
created : Date.now ( ) ,
} ,
} ) ) as MessageV2 . Assistant
2025-11-17 23:57:18 +08:00
const processor = SessionProcessor . create ( {
assistantMessage : msg ,
2025-10-16 02:44:16 +08:00
sessionID : input.sessionID ,
2025-11-17 23:57:18 +08:00
providerID : input.model.providerID ,
model : model.info ,
abort : input.abort ,
} )
const result = await processor . process ( ( ) = >
2025-10-23 07:31:36 +08:00
streamText ( {
2025-11-19 02:09:50 +08:00
onError ( error ) {
log . error ( "stream error" , {
error ,
} )
} ,
2025-10-23 07:31:36 +08:00
// set to 0, we handle loop
maxRetries : 0 ,
2025-11-23 00:32:49 +08:00
providerOptions : ProviderTransform.providerOptions (
model . npm ,
model . providerID ,
pipe (
{ } ,
mergeDeep ( ProviderTransform . options ( model . providerID , model . modelID , model . npm ? ? "" , input . sessionID ) ) ,
mergeDeep ( model . info . options ) ,
) ,
) ,
2025-10-31 12:35:26 +08:00
headers : model.info.headers ,
2025-11-17 23:57:18 +08:00
abortSignal : input.abort ,
2025-10-28 03:03:30 +08:00
tools : model.info.tool_call ? { } : undefined ,
2025-10-23 07:31:36 +08:00
messages : [
. . . system . map (
( x ) : ModelMessage = > ( {
role : "system" ,
content : x ,
} ) ,
) ,
2025-11-19 02:09:50 +08:00
. . . MessageV2 . toModelMessage (
input . messages . filter ( ( m ) = > {
if ( m . info . role !== "assistant" || m . info . error === undefined ) {
return true
}
if (
MessageV2 . AbortedError . isInstance ( m . info . error ) &&
m . parts . some ( ( part ) = > part . type !== "step-start" && part . type !== "reasoning" )
) {
return true
}
return false
} ) ,
) ,
2025-10-23 07:31:36 +08:00
{
role : "user" ,
content : [
{
type : "text" ,
2025-12-01 04:31:57 +08:00
text : "Summarize our conversation above. This summary will be the only context available when the conversation continues, so preserve critical information including: what was accomplished, current work in progress, files involved, next steps, and any key user requests or constraints. Be concise but detailed enough that work can continue seamlessly." ,
2025-10-23 07:31:36 +08:00
} ,
] ,
} ,
] ,
2025-11-19 02:09:50 +08:00
model : wrapLanguageModel ( {
model : model.language ,
middleware : [
{
async transformParams ( args ) {
if ( args . type === "stream" ) {
// @ts-expect-error
args . params . prompt = ProviderTransform . message ( args . params . prompt , model . providerID , model . modelID )
}
return args . params
} ,
} ,
] ,
} ) ,
2025-11-17 23:57:18 +08:00
} ) ,
)
2025-11-26 02:10:56 +08:00
if ( result === "continue" && input . auto ) {
2025-11-17 23:57:18 +08:00
const continueMsg = await Session . updateMessage ( {
id : Identifier.ascending ( "message" ) ,
role : "user" ,
sessionID : input.sessionID ,
time : {
created : Date.now ( ) ,
} ,
2025-11-21 16:13:10 +08:00
agent : input.agent ,
2025-11-17 23:57:18 +08:00
model : input.model ,
2025-10-23 07:31:36 +08:00
} )
2025-11-17 23:57:18 +08:00
await Session . updatePart ( {
id : Identifier.ascending ( "part" ) ,
messageID : continueMsg.id ,
2025-10-19 00:49:29 +08:00
sessionID : input.sessionID ,
2025-11-17 23:57:18 +08:00
type : "text" ,
synthetic : true ,
text : "Continue if you have next steps" ,
time : {
start : Date.now ( ) ,
end : Date.now ( ) ,
} ,
2025-10-19 00:49:29 +08:00
} )
}
2025-11-19 02:09:50 +08:00
if ( processor . message . error ) return "stop"
2025-11-26 03:47:10 +08:00
Bus . publish ( Event . Compacted , { sessionID : input.sessionID } )
2025-11-17 23:57:18 +08:00
return "continue"
2025-09-13 17:46:14 +08:00
}
2025-11-17 23:57:18 +08:00
export const create = fn (
z . object ( {
sessionID : Identifier.schema ( "session" ) ,
2025-11-21 16:13:10 +08:00
agent : z.string ( ) ,
2025-11-17 23:57:18 +08:00
model : z.object ( {
providerID : z.string ( ) ,
modelID : z.string ( ) ,
} ) ,
2025-11-26 02:10:56 +08:00
auto : z.boolean ( ) ,
2025-11-17 23:57:18 +08:00
} ) ,
async ( input ) = > {
const msg = await Session . updateMessage ( {
id : Identifier.ascending ( "message" ) ,
role : "user" ,
model : input.model ,
sessionID : input.sessionID ,
2025-11-21 16:13:10 +08:00
agent : input.agent ,
2025-11-17 23:57:18 +08:00
time : {
created : Date.now ( ) ,
} ,
} )
await Session . updatePart ( {
id : Identifier.ascending ( "part" ) ,
messageID : msg.id ,
sessionID : msg.sessionID ,
type : "compaction" ,
2025-11-26 02:10:56 +08:00
auto : input.auto ,
2025-11-17 23:57:18 +08:00
} )
} ,
)
2025-09-13 17:46:14 +08:00
}