Files
tutti/packages/agent/daemon/runtime/codex_appserver_settings.go
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

323 lines
12 KiB
Go

package agentruntime
import (
"context"
"fmt"
"slices"
"strings"
"github.com/tutti-os/tutti/packages/agent/store-sqlite/canonical"
)
func (a *CodexAppServerAdapter) ApplyPermissionMode(_ context.Context, session Session) error {
// The app-server protocol has no live "set mode" call; the permission mode
// maps to approvalPolicy/sandboxPolicy overrides applied on every
// turn/start. Record the mode so session state reflects it immediately.
a.mu.Lock()
defer a.mu.Unlock()
appSession := a.sessions[strings.TrimSpace(session.AgentSessionID)]
if appSession == nil {
return nil
}
if modeID := codexACPEffectiveModeID(session); modeID != "" {
appSession.currentMode = modeID
}
return nil
}
func (a *CodexAppServerAdapter) SessionState(session Session) SessionStateSnapshot {
snapshot := SessionStateSnapshot{
RoomID: session.RoomID,
AgentSessionID: session.AgentSessionID,
Provider: session.Provider,
ProviderSessionID: session.ProviderSessionID,
Status: session.Status,
PermissionModeID: session.PermissionModeID,
RuntimeContext: map[string]any{
"cwd": session.CWD,
"title": session.Title,
"permissionModeId": session.PermissionModeID,
},
UpdatedAtUnixMS: session.UpdatedAtUnixMS,
}
state, ok := a.snapshotSessionState(session.AgentSessionID)
if !ok {
return snapshot
}
for key, value := range state.resumeRuntimeContext {
snapshot.RuntimeContext[key] = value
}
snapshot.RuntimeContext["cwd"] = session.CWD
snapshot.RuntimeContext["title"] = session.Title
snapshot.RuntimeContext["permissionModeId"] = session.PermissionModeID
if len(state.serverInfo) > 0 {
snapshot.RuntimeContext["agent"] = state.serverInfo
}
if len(state.account) > 0 {
snapshot.RuntimeContext["account"] = state.account
}
if len(state.rateLimits) > 0 {
snapshot.RuntimeContext["rateLimits"] = state.rateLimits
}
startup := map[string]any{
"models": codexAppServerStartupStatus(state.startupModelsReady),
}
if a.config.rateLimits {
startup["rateLimits"] = codexAppServerStartupStatus(state.startupRateLimitsReady)
}
snapshot.RuntimeContext["appServerStartup"] = startup
if len(state.goal) > 0 {
snapshot.RuntimeContext["goal"] = state.goal
}
if state.authState != "" {
snapshot.AuthState = state.authState
}
if state.authMessage != "" {
snapshot.RuntimeContext["authMessage"] = state.authMessage
}
if state.currentMode != "" {
snapshot.RuntimeContext["mode"] = state.currentMode
}
if checkpoint := codexAppServerProtocolCheckpoint(
state.planModeMask,
state.defaultModeMask,
state.defaultModel,
); checkpoint != nil {
snapshot.RuntimeContext[canonical.ProviderResumeCheckpointRuntimeContextKey] =
checkpoint
}
if len(state.availableCommands) > 0 {
snapshot.RuntimeContext["commands"] = agentSessionCommandNames(state.availableCommands)
}
if len(state.configOptions) > 0 {
snapshot.RuntimeContext["config"] = state.configOptions
}
if len(state.configOptionDescriptors) > 0 {
snapshot.RuntimeContext["configOptions"] = state.configOptionDescriptors
}
if providerConfig := providerRuntimeConfig(session, session.Provider); len(providerConfig) > 0 {
snapshot.RuntimeContext["providerConfig"] = providerConfig
}
if usage := acpUsageRuntimeContext(state.usage); len(usage) > 0 {
snapshot.RuntimeContext["usage"] = usage
}
codexCapabilities := codexAppServerCapabilities(state.planModeSupported)
if !a.config.rateLimits {
codexCapabilities = slices.DeleteFunc(codexCapabilities, func(capability string) bool {
return capability == CapabilityRateLimits
})
}
codexCapabilities = appendBrowserUseCapability(codexCapabilities, session.Env)
codexCapabilities = appendComputerUseCapability(codexCapabilities, session.Env)
snapshot.Capabilities = canonical.NewCapabilitySnapshot(codexCapabilities)
snapshot.Settings = codexAppServerSessionSettingsWithConfig(
session.Settings,
session.Provider,
session.PermissionModeID,
state.configOptions,
)
if snapshot.Settings != nil {
snapshot.RuntimeContext["model"] = snapshot.Settings.Model
snapshot.RuntimeContext["reasoningEffort"] = snapshot.Settings.ReasoningEffort
snapshot.RuntimeContext["speed"] = snapshot.Settings.Speed
snapshot.RuntimeContext["planMode"] = snapshot.Settings.PlanMode
}
if state.pendingPrompt != nil {
snapshot.PendingInteractive = state.pendingPrompt
}
return snapshot
}
func codexAppServerStartupStatus(ready bool) string {
if ready {
return "ready"
}
return "loading"
}
type codexAppServerSessionStateSnapshot struct {
serverInfo map[string]any
account map[string]any
rateLimits map[string]any
startupModelsReady bool
startupRateLimitsReady bool
goal map[string]any
authState string
authMessage string
planModeSupported bool
planModeMask map[string]any
defaultModeMask map[string]any
defaultModel string
resumeRuntimeContext map[string]any
acpLiveStateSnapshot
pendingPrompt *SessionInteractivePrompt
}
func (a *CodexAppServerAdapter) snapshotSessionState(agentSessionID string) (codexAppServerSessionStateSnapshot, bool) {
if a == nil {
return codexAppServerSessionStateSnapshot{}, false
}
a.mu.Lock()
defer a.mu.Unlock()
appSession := a.sessions[strings.TrimSpace(agentSessionID)]
if appSession == nil {
return codexAppServerSessionStateSnapshot{}, false
}
var prompt *SessionInteractivePrompt
for _, pending := range appSession.pendingRequests {
if candidate := pending.snapshotPrompt(); candidate != nil {
prompt = candidate
break
}
}
return codexAppServerSessionStateSnapshot{
serverInfo: clonePayload(appSession.serverInfo),
account: clonePayload(appSession.account),
rateLimits: clonePayload(appSession.rateLimits),
startupModelsReady: appSession.startupModelsReady,
startupRateLimitsReady: appSession.startupRateLimitsReady,
goal: clonePayload(appSession.goal),
authState: strings.TrimSpace(appSession.authState),
authMessage: strings.TrimSpace(appSession.authMessage),
planModeSupported: appSession.planModeMask != nil,
planModeMask: clonePayload(appSession.planModeMask),
defaultModeMask: clonePayload(appSession.defaultModeMask),
defaultModel: strings.TrimSpace(appSession.defaultModel),
resumeRuntimeContext: clonePayload(appSession.resumeRuntimeContext),
acpLiveStateSnapshot: snapshotACPLiveState(appSession.acpLiveState),
pendingPrompt: prompt,
}, true
}
func codexAppServerProtocolCheckpoint(
planModeMask map[string]any,
defaultModeMask map[string]any,
defaultModel string,
) map[string]any {
checkpoint := map[string]any{}
if len(planModeMask) > 0 {
checkpoint["planModeMask"] = clonePayload(planModeMask)
}
if len(defaultModeMask) > 0 {
checkpoint["defaultModeMask"] = clonePayload(defaultModeMask)
}
if defaultModel = strings.TrimSpace(defaultModel); defaultModel != "" {
checkpoint["defaultModel"] = defaultModel
}
if len(checkpoint) == 0 {
return nil
}
return checkpoint
}
func codexAppServerProtocolCheckpointFromRuntimeContext(
runtimeContext map[string]any,
) (map[string]any, map[string]any, string, bool) {
checkpoint, ok := runtimeContext[canonical.ProviderResumeCheckpointRuntimeContextKey].(map[string]any)
if !ok || len(checkpoint) == 0 {
return nil, nil, "", false
}
planModeMask, _ := checkpoint["planModeMask"].(map[string]any)
defaultModeMask, _ := checkpoint["defaultModeMask"].(map[string]any)
return clonePayload(planModeMask),
clonePayload(defaultModeMask),
strings.TrimSpace(asString(checkpoint["defaultModel"])),
true
}
func (a *CodexAppServerAdapter) SessionCommandSnapshot(session Session) (AgentSessionCommandSnapshot, bool) {
if a == nil {
return AgentSessionCommandSnapshot{}, false
}
a.mu.Lock()
appSession := a.sessions[strings.TrimSpace(session.AgentSessionID)]
if appSession == nil {
a.mu.Unlock()
return AgentSessionCommandSnapshot{}, false
}
snapshot, ok := commandSnapshotFromACPLiveState(session.AgentSessionID, appSession.acpLiveState)
a.mu.Unlock()
return snapshot, ok
}
func (a *CodexAppServerAdapter) SubmitInteractive(ctx context.Context, session Session, input SubmitInteractiveInput) (SubmitInteractiveResult, error) {
turnID := strings.TrimSpace(input.TurnID)
if turnID == "" {
return SubmitInteractiveResult{}, fmt.Errorf("%w: interactive turn id is required", ErrInteractiveResponseInvalid)
}
requestID := strings.TrimSpace(input.RequestID)
if requestID == "" {
return SubmitInteractiveResult{}, fmt.Errorf("%w: interactive request id is required", ErrInteractiveResponseInvalid)
}
targetAgentSessionID := firstNonEmpty(strings.TrimSpace(input.AgentSessionID), strings.TrimSpace(session.AgentSessionID))
pending := a.getPendingRequest(targetAgentSessionID, turnID, requestID)
if pending == nil {
return SubmitInteractiveResult{}, fmt.Errorf("%w: %q", ErrInteractiveRequestNotLive, requestID)
}
if pending.callType == "approval" {
optionID := interactiveApprovalOptionID(input)
if optionID == "" {
return SubmitInteractiveResult{}, fmt.Errorf("%w: interactive option id is required", ErrInteractiveResponseInvalid)
}
resolvedOptionID, ok := pending.resolvePermissionOptionID(optionID)
if !ok {
return SubmitInteractiveResult{}, fmt.Errorf(
"%w: permission option %q is not available for request %q",
ErrInteractiveResponseInvalid,
optionID,
requestID,
)
}
if _, err := pending.dispatchResponse(ctx, pendingInteractiveResponse{
optionID: resolvedOptionID,
payload: clonePayload(input.Payload),
}); err != nil {
return SubmitInteractiveResult{}, err
}
if state, err := pending.waitForDisposition(ctx); err != nil {
return SubmitInteractiveResult{}, err
} else if state != pendingInteractiveRequestStateAnswered {
return SubmitInteractiveResult{}, interactiveDispositionError(requestID, state)
}
return SubmitInteractiveResult{AgentSessionID: targetAgentSessionID, RequestID: requestID, Accepted: true, OptionID: resolvedOptionID, Disposition: InteractiveDispositionAnswered}, nil
}
optionID := strings.TrimSpace(input.OptionID)
action := strings.TrimSpace(input.Action)
payload := clonePayload(input.Payload)
if _, err := pending.dispatchResponse(ctx, pendingInteractiveResponse{
optionID: optionID,
action: action,
payload: payload,
}); err != nil {
return SubmitInteractiveResult{}, err
}
if state, err := pending.waitForDisposition(ctx); err != nil {
return SubmitInteractiveResult{}, err
} else if state != pendingInteractiveRequestStateAnswered {
return SubmitInteractiveResult{}, interactiveDispositionError(requestID, state)
}
return SubmitInteractiveResult{
AgentSessionID: targetAgentSessionID,
RequestID: requestID,
Accepted: true,
Disposition: InteractiveDispositionAnswered,
}, nil
}
func (a *CodexAppServerAdapter) InteractiveDisposition(session Session, turnID string, requestID string) InteractiveDisposition {
return a.InteractiveDispositionForTarget(session, session.AgentSessionID, turnID, requestID)
}
func (a *CodexAppServerAdapter) InteractiveDispositionForTarget(_ Session, agentSessionID string, turnID string, requestID string) InteractiveDisposition {
if pending := a.getPendingRequest(agentSessionID, turnID, requestID); pending != nil {
return runtimeInteractiveDisposition(pending)
}
return a.terminalInteractiveDisposition(agentSessionID, turnID, requestID)
}
// lockSessionLifecycle serializes lifecycle operations (Start, Resume, Close,
// ReleaseLiveSession) for one agent session: any interleaving of these calls
// could otherwise spawn a second app-server process while the first is still
// live, or close the wrong process. The lock entry is refcounted so the map
// does not grow with retired session IDs.