2026-01-09 21:09:34 +01:00
import { Type } from "@sinclair/typebox" ;
import { normalizeGroupActivation } from "../../auto-reply/group-activation.js" ;
2026-01-14 14:31:43 +00:00
import { getFollowupQueueDepth , resolveQueueSettings } from "../../auto-reply/reply/queue.js" ;
2026-02-16 21:27:14 +00:00
import { buildStatusMessage , getTranscriptInfo } from "../../auto-reply/status.js" ;
2026-02-17 08:53:59 +09:00
import type { OpenClawConfig } from "../../config/config.js" ;
2026-01-09 21:09:34 +01:00
import { loadConfig } from "../../config/config.js" ;
import {
loadSessionStore ,
resolveStorePath ,
type SessionEntry ,
2026-01-15 23:06:42 +00:00
updateSessionStore ,
2026-01-09 21:09:34 +01:00
} from "../../config/sessions.js" ;
2026-02-01 10:03:47 +09:00
import { loadCombinedSessionStoreForGateway } from "../../gateway/session-utils.js" ;
2026-01-09 21:09:34 +01:00
import {
2026-01-16 00:24:31 +00:00
formatUsageWindowSummary ,
2026-01-09 21:09:34 +01:00
loadProviderUsageSummary ,
resolveUsageProviderId ,
} from "../../infra/provider-usage.js" ;
import {
buildAgentMainSessionKey ,
DEFAULT_AGENT_ID ,
resolveAgentIdFromSessionKey ,
} from "../../routing/session-key.js" ;
2026-01-21 06:00:16 +00:00
import { applyModelOverrideToSessionEntry } from "../../sessions/model-overrides.js" ;
2026-02-03 18:06:54 -08:00
import { resolveAgentDir } from "../agent-scope.js" ;
2026-02-01 10:03:47 +09:00
import { formatUserTime , resolveUserTimeFormat , resolveUserTimezone } from "../date-time.js" ;
2026-02-17 00:10:19 +00:00
import { resolveModelAuthLabel } from "../model-auth-label.js" ;
2026-02-03 18:06:54 -08:00
import { loadModelCatalog } from "../model-catalog.js" ;
import {
buildAllowedModelSet ,
buildModelAliasIndex ,
modelKey ,
resolveDefaultModelForAgent ,
resolveModelRefFromString ,
} from "../model-selection.js" ;
2026-02-17 08:53:59 +09:00
import type { AnyAgentTool } from "./common.js" ;
2026-01-09 21:09:34 +01:00
import { readStringParam } from "./common.js" ;
2026-01-24 11:09:06 +00:00
import {
shouldResolveSessionIdInput ,
resolveInternalSessionKey ,
resolveMainSessionAlias ,
createAgentToAgentPolicy ,
} from "./sessions-helpers.js" ;
2026-01-09 21:09:34 +01:00
const SessionStatusToolSchema = Type . Object ( {
sessionKey : Type.Optional ( Type . String ( ) ) ,
model : Type.Optional ( Type . String ( ) ) ,
} ) ;
function resolveSessionEntry ( params : {
store : Record < string , SessionEntry > ;
keyRaw : string ;
alias : string ;
mainKey : string ;
} ) : { key : string ; entry : SessionEntry } | null {
const keyRaw = params . keyRaw . trim ( ) ;
2026-01-31 16:19:20 +09:00
if ( ! keyRaw ) {
return null ;
}
2026-01-09 21:09:34 +01:00
const internal = resolveInternalSessionKey ( {
key : keyRaw ,
alias : params.alias ,
mainKey : params.mainKey ,
} ) ;
const candidates = new Set < string > ( [ keyRaw , internal ] ) ;
if ( ! keyRaw . startsWith ( "agent:" ) ) {
candidates . add ( ` agent: ${ DEFAULT_AGENT_ID } : ${ keyRaw } ` ) ;
candidates . add ( ` agent: ${ DEFAULT_AGENT_ID } : ${ internal } ` ) ;
}
if ( keyRaw === "main" ) {
candidates . add (
buildAgentMainSessionKey ( {
agentId : DEFAULT_AGENT_ID ,
mainKey : params.mainKey ,
} ) ,
) ;
}
for ( const key of candidates ) {
const entry = params . store [ key ] ;
2026-01-31 16:19:20 +09:00
if ( entry ) {
return { key , entry } ;
}
2026-01-09 21:09:34 +01:00
}
return null ;
}
2026-01-24 11:09:06 +00:00
function resolveSessionKeyFromSessionId ( params : {
2026-01-30 03:15:10 +01:00
cfg : OpenClawConfig ;
2026-01-24 11:09:06 +00:00
sessionId : string ;
agentId? : string ;
} ) : string | null {
const trimmed = params . sessionId . trim ( ) ;
2026-01-31 16:19:20 +09:00
if ( ! trimmed ) {
return null ;
}
2026-01-24 11:09:06 +00:00
const { store } = loadCombinedSessionStoreForGateway ( params . cfg ) ;
const match = Object . entries ( store ) . find ( ( [ key , entry ] ) = > {
2026-01-31 16:19:20 +09:00
if ( entry ? . sessionId !== trimmed ) {
return false ;
}
if ( ! params . agentId ) {
return true ;
}
2026-01-24 11:09:06 +00:00
return resolveAgentIdFromSessionKey ( key ) === params . agentId ;
} ) ;
return match ? . [ 0 ] ? ? null ;
}
2026-01-09 21:09:34 +01:00
async function resolveModelOverride ( params : {
2026-01-30 03:15:10 +01:00
cfg : OpenClawConfig ;
2026-01-09 21:09:34 +01:00
raw : string ;
sessionEntry? : SessionEntry ;
2026-01-20 19:04:25 +00:00
agentId : string ;
2026-01-09 21:09:34 +01:00
} ) : Promise <
| { kind : "reset" }
| {
kind : "set" ;
provider : string ;
model : string ;
isDefault : boolean ;
}
> {
const raw = params . raw . trim ( ) ;
2026-01-31 16:19:20 +09:00
if ( ! raw ) {
return { kind : "reset" } ;
}
if ( raw . toLowerCase ( ) === "default" ) {
return { kind : "reset" } ;
}
2026-01-09 21:09:34 +01:00
2026-01-20 19:04:25 +00:00
const configDefault = resolveDefaultModelForAgent ( {
2026-01-09 21:09:34 +01:00
cfg : params.cfg ,
2026-01-20 19:04:25 +00:00
agentId : params.agentId ,
2026-01-09 21:09:34 +01:00
} ) ;
2026-01-14 14:31:43 +00:00
const currentProvider = params . sessionEntry ? . providerOverride ? . trim ( ) || configDefault . provider ;
const currentModel = params . sessionEntry ? . modelOverride ? . trim ( ) || configDefault . model ;
2026-01-09 21:09:34 +01:00
const aliasIndex = buildModelAliasIndex ( {
cfg : params.cfg ,
defaultProvider : currentProvider ,
} ) ;
const catalog = await loadModelCatalog ( { config : params.cfg } ) ;
const allowed = buildAllowedModelSet ( {
cfg : params.cfg ,
catalog ,
defaultProvider : currentProvider ,
defaultModel : currentModel ,
} ) ;
const resolved = resolveModelRefFromString ( {
raw ,
defaultProvider : currentProvider ,
aliasIndex ,
} ) ;
if ( ! resolved ) {
throw new Error ( ` Unrecognized model " ${ raw } ". ` ) ;
}
const key = modelKey ( resolved . ref . provider , resolved . ref . model ) ;
if ( allowed . allowedKeys . size > 0 && ! allowed . allowedKeys . has ( key ) ) {
throw new Error ( ` Model " ${ key } " is not allowed. ` ) ;
}
const isDefault =
2026-01-14 14:31:43 +00:00
resolved . ref . provider === configDefault . provider && resolved . ref . model === configDefault . model ;
2026-01-09 21:09:34 +01:00
return {
kind : "set" ,
provider : resolved.ref.provider ,
model : resolved.ref.model ,
isDefault ,
} ;
}
export function createSessionStatusTool ( opts ? : {
agentSessionKey? : string ;
2026-01-30 03:15:10 +01:00
config? : OpenClawConfig ;
2026-01-09 21:09:34 +01:00
} ) : AnyAgentTool {
return {
label : "Session Status" ,
name : "session_status" ,
description :
2026-01-24 06:22:54 +00:00
"Show a /status-equivalent session status card (usage + time + cost when available). Use for model-use questions (📊 session_status). Optional: set per-session model override (model=default resets overrides)." ,
2026-01-09 21:09:34 +01:00
parameters : SessionStatusToolSchema ,
execute : async ( _toolCallId , args ) = > {
const params = args as Record < string , unknown > ;
const cfg = opts ? . config ? ? loadConfig ( ) ;
const { mainKey , alias } = resolveMainSessionAlias ( cfg ) ;
2026-01-24 11:09:06 +00:00
const a2aPolicy = createAgentToAgentPolicy ( cfg ) ;
2026-01-09 21:09:34 +01:00
2026-01-24 11:09:06 +00:00
const requestedKeyParam = readStringParam ( params , "sessionKey" ) ;
let requestedKeyRaw = requestedKeyParam ? ? opts ? . agentSessionKey ;
2026-01-09 21:09:34 +01:00
if ( ! requestedKeyRaw ? . trim ( ) ) {
throw new Error ( "sessionKey required" ) ;
}
2026-01-24 11:09:06 +00:00
const requesterAgentId = resolveAgentIdFromSessionKey (
opts ? . agentSessionKey ? ? requestedKeyRaw ,
) ;
const ensureAgentAccess = ( targetAgentId : string ) = > {
2026-01-31 16:19:20 +09:00
if ( targetAgentId === requesterAgentId ) {
return ;
}
2026-01-24 11:09:06 +00:00
// Gate cross-agent access behind tools.agentToAgent settings.
if ( ! a2aPolicy . enabled ) {
throw new Error (
"Agent-to-agent status is disabled. Set tools.agentToAgent.enabled=true to allow cross-agent access." ,
) ;
}
if ( ! a2aPolicy . isAllowed ( requesterAgentId , targetAgentId ) ) {
throw new Error ( "Agent-to-agent session status denied by tools.agentToAgent.allow." ) ;
}
} ;
if ( requestedKeyRaw . startsWith ( "agent:" ) ) {
ensureAgentAccess ( resolveAgentIdFromSessionKey ( requestedKeyRaw ) ) ;
}
2026-01-09 21:09:34 +01:00
2026-01-24 11:09:06 +00:00
const isExplicitAgentKey = requestedKeyRaw . startsWith ( "agent:" ) ;
let agentId = isExplicitAgentKey
? resolveAgentIdFromSessionKey ( requestedKeyRaw )
: requesterAgentId ;
let storePath = resolveStorePath ( cfg . session ? . store , { agentId } ) ;
let store = loadSessionStore ( storePath ) ;
// Resolve against the requester-scoped store first to avoid leaking default agent data.
let resolved = resolveSessionEntry ( {
2026-01-09 21:09:34 +01:00
store ,
keyRaw : requestedKeyRaw ,
alias ,
mainKey ,
} ) ;
2026-01-24 11:09:06 +00:00
if ( ! resolved && shouldResolveSessionIdInput ( requestedKeyRaw ) ) {
const resolvedKey = resolveSessionKeyFromSessionId ( {
cfg ,
sessionId : requestedKeyRaw ,
agentId : a2aPolicy.enabled ? undefined : requesterAgentId ,
} ) ;
if ( resolvedKey ) {
// If resolution points at another agent, enforce A2A policy before switching stores.
ensureAgentAccess ( resolveAgentIdFromSessionKey ( resolvedKey ) ) ;
requestedKeyRaw = resolvedKey ;
agentId = resolveAgentIdFromSessionKey ( resolvedKey ) ;
storePath = resolveStorePath ( cfg . session ? . store , { agentId } ) ;
store = loadSessionStore ( storePath ) ;
resolved = resolveSessionEntry ( {
store ,
keyRaw : requestedKeyRaw ,
alias ,
mainKey ,
} ) ;
}
}
2026-01-09 21:09:34 +01:00
if ( ! resolved ) {
2026-01-24 11:09:06 +00:00
const kind = shouldResolveSessionIdInput ( requestedKeyRaw ) ? "sessionId" : "sessionKey" ;
throw new Error ( ` Unknown ${ kind } : ${ requestedKeyRaw } ` ) ;
2026-01-09 21:09:34 +01:00
}
2026-01-21 06:00:16 +00:00
const configured = resolveDefaultModelForAgent ( { cfg , agentId } ) ;
2026-01-09 21:09:34 +01:00
const modelRaw = readStringParam ( params , "model" ) ;
let changedModel = false ;
if ( typeof modelRaw === "string" ) {
const selection = await resolveModelOverride ( {
cfg ,
raw : modelRaw ,
sessionEntry : resolved.entry ,
2026-01-20 19:04:25 +00:00
agentId ,
2026-01-09 21:09:34 +01:00
} ) ;
2026-01-21 06:00:16 +00:00
const nextEntry : SessionEntry = { . . . resolved . entry } ;
const applied = applyModelOverrideToSessionEntry ( {
entry : nextEntry ,
selection :
selection . kind === "reset"
? {
provider : configured.provider ,
model : configured.model ,
isDefault : true ,
}
: {
provider : selection.provider ,
model : selection.model ,
isDefault : selection.isDefault ,
} ,
2026-01-15 23:06:42 +00:00
} ) ;
2026-01-21 06:00:16 +00:00
if ( applied . updated ) {
store [ resolved . key ] = nextEntry ;
await updateSessionStore ( storePath , ( nextStore ) = > {
nextStore [ resolved . key ] = nextEntry ;
} ) ;
resolved . entry = nextEntry ;
changedModel = true ;
}
2026-01-09 21:09:34 +01:00
}
const agentDir = resolveAgentDir ( cfg , agentId ) ;
2026-01-14 14:31:43 +00:00
const providerForCard = resolved . entry . providerOverride ? . trim ( ) || configured . provider ;
2026-01-16 09:39:09 +00:00
const usageProvider = resolveUsageProviderId ( providerForCard ) ;
let usageLine : string | undefined ;
if ( usageProvider ) {
2026-01-09 21:09:34 +01:00
try {
const usageSummary = await loadProviderUsageSummary ( {
timeoutMs : 3500 ,
2026-01-16 09:39:09 +00:00
providers : [ usageProvider ] ,
2026-01-09 21:09:34 +01:00
agentDir ,
} ) ;
2026-01-16 11:30:04 +01:00
const snapshot = usageSummary . providers . find ( ( entry ) = > entry . provider === usageProvider ) ;
2026-01-16 09:39:09 +00:00
if ( snapshot ) {
2026-01-16 00:24:31 +00:00
const formatted = formatUsageWindowSummary ( snapshot , {
now : Date.now ( ) ,
maxWindows : 2 ,
2026-01-16 09:36:37 +00:00
includeResets : true ,
2026-01-16 00:24:31 +00:00
} ) ;
2026-01-16 09:39:09 +00:00
if ( formatted && ! formatted . startsWith ( "error:" ) ) {
usageLine = ` 📊 Usage: ${ formatted } ` ;
}
2026-01-16 00:24:31 +00:00
}
2026-01-09 21:09:34 +01:00
} catch {
// ignore
}
}
const isGroup =
resolved . entry . chatType === "group" ||
2026-01-17 04:04:05 +00:00
resolved . entry . chatType === "channel" ||
2026-01-09 21:09:34 +01:00
resolved . key . includes ( ":group:" ) ||
resolved . key . includes ( ":channel:" ) ;
const groupActivation = isGroup
2026-01-14 14:31:43 +00:00
? ( normalizeGroupActivation ( resolved . entry . groupActivation ) ? ? "mention" )
2026-01-09 21:09:34 +01:00
: undefined ;
const queueSettings = resolveQueueSettings ( {
cfg ,
2026-01-14 14:31:43 +00:00
channel : resolved.entry.channel ? ? resolved . entry . lastChannel ? ? "unknown" ,
2026-01-09 21:09:34 +01:00
sessionEntry : resolved.entry ,
} ) ;
const queueKey = resolved . key ? ? resolved . entry . sessionId ;
const queueDepth = queueKey ? getFollowupQueueDepth ( queueKey ) : 0 ;
const queueOverrides = Boolean (
2026-01-14 14:31:43 +00:00
resolved . entry . queueDebounceMs ? ? resolved . entry . queueCap ? ? resolved . entry . queueDrop ,
2026-01-09 21:09:34 +01:00
) ;
2026-01-24 06:22:54 +00:00
const userTimezone = resolveUserTimezone ( cfg . agents ? . defaults ? . userTimezone ) ;
const userTimeFormat = resolveUserTimeFormat ( cfg . agents ? . defaults ? . timeFormat ) ;
const userTime = formatUserTime ( new Date ( ) , userTimezone , userTimeFormat ) ;
const timeLine = userTime
? ` 🕒 Time: ${ userTime } ( ${ userTimezone } ) `
: ` 🕒 Time zone: ${ userTimezone } ` ;
2026-01-20 19:04:25 +00:00
const agentDefaults = cfg . agents ? . defaults ? ? { } ;
const defaultLabel = ` ${ configured . provider } / ${ configured . model } ` ;
const agentModel =
typeof agentDefaults . model === "object" && agentDefaults . model
? { . . . agentDefaults . model , primary : defaultLabel }
: { primary : defaultLabel } ;
2026-01-09 21:09:34 +01:00
const statusText = buildStatusMessage ( {
config : cfg ,
2026-01-20 19:04:25 +00:00
agent : {
. . . agentDefaults ,
model : agentModel ,
} ,
2026-02-13 14:17:24 +01:00
agentId ,
2026-01-09 21:09:34 +01:00
sessionEntry : resolved.entry ,
sessionKey : resolved.key ,
2026-02-12 23:23:12 -05:00
sessionStorePath : storePath ,
2026-01-09 21:09:34 +01:00
groupActivation ,
modelAuth : resolveModelAuthLabel ( {
provider : providerForCard ,
cfg ,
sessionEntry : resolved.entry ,
agentDir ,
} ) ,
usageLine ,
2026-01-24 06:22:54 +00:00
timeLine ,
2026-01-09 21:09:34 +01:00
queue : {
mode : queueSettings.mode ,
depth : queueDepth ,
debounceMs : queueSettings.debounceMs ,
cap : queueSettings.cap ,
dropPolicy : queueSettings.dropPolicy ,
showDetails : queueOverrides ,
} ,
includeTranscriptUsage : false ,
2026-02-16 21:27:14 +00:00
transcriptInfo : getTranscriptInfo ( {
sessionId : resolved.entry?.sessionId ,
sessionEntry : resolved.entry ,
agentId ,
sessionKey : resolved.key ,
storePath ,
} ) ,
2026-01-09 21:09:34 +01:00
} ) ;
return {
2026-01-16 09:39:09 +00:00
content : [ { type : "text" , text : statusText } ] ,
2026-01-09 21:09:34 +01:00
details : {
ok : true ,
sessionKey : resolved.key ,
changedModel ,
2026-01-16 09:39:09 +00:00
statusText ,
2026-01-09 21:09:34 +01:00
} ,
} ;
} ,
} ;
}