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" ,
text : "Provide a detailed but concise summary of 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." ,
} ,
] ,
} ,
] ,
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-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
}