源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
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
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
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
package agentruntime
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
agentsessionstore "github.com/tutti-os/tutti/packages/agent/daemon/activity"
|
||||
activityshared "github.com/tutti-os/tutti/packages/agent/daemon/activity/events"
|
||||
"github.com/tutti-os/tutti/packages/agent/daemon/providerregistry"
|
||||
"github.com/tutti-os/tutti/packages/agent/store-sqlite/canonical"
|
||||
)
|
||||
|
||||
func (c *Controller) SubmitInteractive(ctx context.Context, input SubmitInteractiveInput) (SubmitInteractiveResult, error) {
|
||||
rootAgentSessionID := strings.TrimSpace(input.RootAgentSessionID)
|
||||
if rootAgentSessionID == "" {
|
||||
return SubmitInteractiveResult{}, fmt.Errorf("%w: root agent session id is required", ErrInteractiveResponseInvalid)
|
||||
}
|
||||
input.AgentSessionID = strings.TrimSpace(input.AgentSessionID)
|
||||
if input.AgentSessionID == "" {
|
||||
return SubmitInteractiveResult{}, fmt.Errorf("%w: target agent session id is required", ErrInteractiveResponseInvalid)
|
||||
}
|
||||
session, adapter, err := c.sessionAndAdapter(input.RoomID, rootAgentSessionID)
|
||||
if err != nil {
|
||||
return SubmitInteractiveResult{}, err
|
||||
}
|
||||
if interactiveAdapter, ok := adapter.(InteractiveAdapter); ok {
|
||||
result, err := interactiveAdapter.SubmitInteractive(ctx, session, input)
|
||||
if result.Disposition == "" {
|
||||
if dispositionAdapter, ok := adapter.(InteractiveDispositionAdapter); ok {
|
||||
result.Disposition = dispositionAdapter.InteractiveDisposition(session, input.TurnID, input.RequestID)
|
||||
}
|
||||
}
|
||||
if isTerminalInteractiveDisposition(result.Disposition) {
|
||||
c.recordTerminalInteractiveDisposition(input.AgentSessionID, input.TurnID, input.RequestID, result.Disposition)
|
||||
}
|
||||
if err == nil {
|
||||
c.syncInteractiveSelectionState(adapter, session, result.OptionID)
|
||||
if adapterShouldReceiveInteractiveDenyFollowUp(adapter) {
|
||||
result.FollowUpPrompt = interactiveDenyFollowUpPrompt(input)
|
||||
}
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
return SubmitInteractiveResult{}, fmt.Errorf("agent provider %q does not support interactive submission", session.Provider)
|
||||
}
|
||||
|
||||
func (c *Controller) InteractiveDisposition(roomID string, rootAgentSessionID string, agentSessionID string, turnID string, requestID string) InteractiveDisposition {
|
||||
if disposition := c.terminalInteractiveDisposition(agentSessionID, turnID, requestID); disposition != InteractiveDispositionUnknown {
|
||||
return disposition
|
||||
}
|
||||
session, adapter, err := c.sessionAndAdapter(roomID, rootAgentSessionID)
|
||||
if err != nil {
|
||||
return InteractiveDispositionUnknown
|
||||
}
|
||||
interactiveAdapter, ok := adapter.(InteractiveDispositionAdapter)
|
||||
if targeted, targetedOK := adapter.(TargetedInteractiveDispositionAdapter); targetedOK {
|
||||
return targeted.InteractiveDispositionForTarget(session, agentSessionID, turnID, requestID)
|
||||
}
|
||||
if !ok || strings.TrimSpace(agentSessionID) != strings.TrimSpace(rootAgentSessionID) {
|
||||
return InteractiveDispositionUnknown
|
||||
}
|
||||
return interactiveAdapter.InteractiveDisposition(session, turnID, requestID)
|
||||
}
|
||||
|
||||
func isTerminalInteractiveDisposition(disposition InteractiveDisposition) bool {
|
||||
return disposition == InteractiveDispositionAnswered ||
|
||||
disposition == InteractiveDispositionSuperseded ||
|
||||
disposition == InteractiveDispositionInterrupted
|
||||
}
|
||||
|
||||
func (c *Controller) recordTerminalInteractiveDisposition(agentSessionID string, turnID string, requestID string, disposition InteractiveDisposition) {
|
||||
if c == nil || !isTerminalInteractiveDisposition(disposition) {
|
||||
return
|
||||
}
|
||||
c.mu.Lock()
|
||||
c.terminalInteractions.put(newInteractiveRequestKey(agentSessionID, turnID, requestID), disposition)
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *Controller) terminalInteractiveDisposition(agentSessionID string, turnID string, requestID string) InteractiveDisposition {
|
||||
if c == nil {
|
||||
return InteractiveDispositionUnknown
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.terminalInteractions.get(newInteractiveRequestKey(agentSessionID, turnID, requestID))
|
||||
}
|
||||
|
||||
// syncInteractiveSelectionState asks the adapter to interpret its protocol
|
||||
// option id, then keeps the controller as the single session-state writer.
|
||||
func (c *Controller) syncInteractiveSelectionState(adapter Adapter, session Session, optionID string) {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
stateAdapter, ok := adapter.(InteractiveSelectionStateAdapter)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
state, ok := stateAdapter.StateAfterInteractiveSelection(session, optionID)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
current, found := c.Session(session.RoomID, session.AgentSessionID)
|
||||
if !found {
|
||||
return
|
||||
}
|
||||
c.applyInteractiveSelectionState(current, state)
|
||||
}
|
||||
|
||||
func (c *Controller) applyInteractiveSelectionState(current Session, state InteractiveSelectionState) {
|
||||
currentSettings := normalizeSessionSettings(current.Settings, current.Provider, current.PermissionModeID)
|
||||
nextPermission := strings.TrimSpace(state.PermissionMode)
|
||||
if nextPermission == "" {
|
||||
nextPermission = strings.TrimSpace(currentSettings.PermissionModeID)
|
||||
}
|
||||
if currentSettings.PlanMode == state.PlanMode &&
|
||||
strings.TrimSpace(currentSettings.PermissionModeID) == nextPermission &&
|
||||
strings.TrimSpace(current.PermissionModeID) == nextPermission {
|
||||
return
|
||||
}
|
||||
nextSession := current
|
||||
nextSession.PermissionModeID = nextPermission
|
||||
settings := normalizeSessionSettings(nextSession.Settings, nextSession.Provider, nextSession.PermissionModeID)
|
||||
settings.PlanMode = state.PlanMode
|
||||
settings.PermissionModeID = nextPermission
|
||||
nextSession.Settings = cloneSessionSettings(settings)
|
||||
nextSession.UpdatedAtUnixMS = unixMS(now())
|
||||
c.store(nextSession)
|
||||
patch := permissionModeStatePatch(nextSession)
|
||||
c.publishSessionStatePatch(nextSession, patch)
|
||||
c.enqueueSessionStatePatchReport(context.Background(), nextSession, patch)
|
||||
}
|
||||
|
||||
// applySessionPlanModeOnly updates the orthogonal plan-mode flag without
|
||||
// touching permission tiers or model selection.
|
||||
func (c *Controller) applySessionPlanModeOnly(current Session, planMode bool) {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
c.mu.Lock()
|
||||
key := sessionKey(current.RoomID, current.AgentSessionID)
|
||||
latest, found := c.sessions[key]
|
||||
if !found {
|
||||
c.mu.Unlock()
|
||||
return
|
||||
}
|
||||
current = latest
|
||||
currentSettings := normalizeSessionSettings(current.Settings, current.Provider, current.PermissionModeID)
|
||||
if currentSettings.PlanMode == planMode {
|
||||
c.mu.Unlock()
|
||||
return
|
||||
}
|
||||
settings := normalizeSessionSettings(current.Settings, current.Provider, current.PermissionModeID)
|
||||
settings.PlanMode = planMode
|
||||
nextSession := current
|
||||
nextSession.Settings = cloneSessionSettings(settings)
|
||||
nextSession.UpdatedAtUnixMS = unixMS(now())
|
||||
c.sessions[key] = nextSession
|
||||
c.mu.Unlock()
|
||||
patch := permissionModeStatePatch(nextSession)
|
||||
c.publishSessionStatePatch(nextSession, patch)
|
||||
c.enqueueSessionStatePatchReport(context.Background(), nextSession, patch)
|
||||
}
|
||||
|
||||
func (c *Controller) syncCursorPlanModeFromACPUpdate(session Session, modeID string) {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
descriptor, found := providerregistry.Find(session.Provider)
|
||||
if !found || !descriptor.Runtime.StandardACP.ProjectCurrentMode {
|
||||
return
|
||||
}
|
||||
planMode, ok := projectCurrentPlanModeFromACPModeID(descriptor.Runtime.StandardACP, modeID)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
current, found := c.Session(session.RoomID, session.AgentSessionID)
|
||||
if !found || strings.TrimSpace(current.Provider) != descriptor.Identity.ID {
|
||||
return
|
||||
}
|
||||
c.applySessionPlanModeOnly(current, planMode)
|
||||
}
|
||||
|
||||
func projectCurrentPlanModeFromACPModeID(descriptor providerregistry.StandardACPRuntimeDescriptor, modeID string) (bool, bool) {
|
||||
modeID = strings.TrimSpace(modeID)
|
||||
if modeID == "" {
|
||||
return false, false
|
||||
}
|
||||
for _, mode := range descriptor.PermissionModes {
|
||||
if strings.TrimSpace(mode.RuntimeID) == modeID {
|
||||
return modeID == strings.TrimSpace(descriptor.PlanModeRuntimeID), true
|
||||
}
|
||||
}
|
||||
return false, false
|
||||
}
|
||||
|
||||
func (c *Controller) syncCursorPlanModeFromEvents(session Session, events []activityshared.Event) {
|
||||
for _, event := range events {
|
||||
if event.Type != activityshared.EventSessionUpdated {
|
||||
continue
|
||||
}
|
||||
if normalizedSessionUpdateKind(event.Payload.Metadata) != "current_mode_update" {
|
||||
continue
|
||||
}
|
||||
c.syncCursorPlanModeFromACPUpdate(session, asString(event.Payload.Metadata["acpModeId"]))
|
||||
}
|
||||
}
|
||||
|
||||
func permissionModeStatePatch(session Session) agentsessionstore.WorkspaceAgentStatePatch {
|
||||
settings := normalizeSessionSettings(session.Settings, session.Provider, session.PermissionModeID)
|
||||
runtimeContext := map[string]any{
|
||||
"permissionModeId": strings.TrimSpace(settings.PermissionModeID),
|
||||
"planMode": settings.PlanMode,
|
||||
}
|
||||
if strings.TrimSpace(session.CWD) != "" {
|
||||
runtimeContext["cwd"] = strings.TrimSpace(session.CWD)
|
||||
}
|
||||
if strings.TrimSpace(session.Title) != "" {
|
||||
runtimeContext["title"] = strings.TrimSpace(session.Title)
|
||||
}
|
||||
return agentsessionstore.WorkspaceAgentStatePatch{
|
||||
AgentSessionID: strings.TrimSpace(session.AgentSessionID),
|
||||
Provider: strings.TrimSpace(session.Provider),
|
||||
ProviderSessionID: strings.TrimSpace(session.ProviderSessionID),
|
||||
PermissionModeID: strings.TrimSpace(settings.PermissionModeID),
|
||||
Settings: sessionSettingsPayload(&settings),
|
||||
RuntimeContextPatch: &canonical.RuntimeContextPatch{Set: runtimeContext},
|
||||
OccurredAtUnixMS: session.UpdatedAtUnixMS,
|
||||
}
|
||||
}
|
||||
|
||||
func adapterShouldReceiveInteractiveDenyFollowUp(adapter Adapter) bool {
|
||||
if policy, ok := adapter.(InteractiveDenyFollowUpPolicyAdapter); ok {
|
||||
return policy.ControllerSendsInteractiveDenyFollowUp()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func interactiveDenyFollowUpPrompt(input SubmitInteractiveInput) string {
|
||||
if input.Payload == nil || !isInteractiveDenySelection(input) {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(asString(input.Payload["denyMessage"]))
|
||||
}
|
||||
|
||||
func isInteractiveDenySelection(input SubmitInteractiveInput) bool {
|
||||
for _, value := range []string{
|
||||
input.Action,
|
||||
input.OptionID,
|
||||
asString(input.Payload["optionId"]),
|
||||
} {
|
||||
if isDenyInteractiveSelectionValue(value) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isDenyInteractiveSelectionValue(value string) bool {
|
||||
token := normalizePermissionOptionToken(value)
|
||||
if token == "" {
|
||||
return false
|
||||
}
|
||||
if permissionOptionDecision(token) == "denied" {
|
||||
return true
|
||||
}
|
||||
switch token {
|
||||
case "abort", "aborted":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user