Files
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

196 lines
7.3 KiB
Go

package agenthost
import (
"context"
"sort"
"strings"
storesqlite "github.com/tutti-os/tutti/packages/agent/store-sqlite"
)
type runtimeControlRoute struct {
RootAgentSessionID string
TargetSession storesqlite.Session
}
func (h *Host) CancelTurn(ctx context.Context, input CancelTurnInput) (CancelTurnResult, error) {
input.WorkspaceID = strings.TrimSpace(input.WorkspaceID)
input.AgentSessionID = strings.TrimSpace(input.AgentSessionID)
input.TurnID = strings.TrimSpace(input.TurnID)
if h == nil || h.store == nil || h.runtime == nil || input.WorkspaceID == "" || input.AgentSessionID == "" || input.TurnID == "" {
return CancelTurnResult{}, ErrInvalidArgument
}
turn, found, err := h.store.GetTurn(ctx, input.WorkspaceID, input.AgentSessionID, input.TurnID)
if err != nil {
return CancelTurnResult{}, err
}
canonical, sessionFound, readErr := h.store.GetSession(ctx, input.WorkspaceID, input.AgentSessionID)
if readErr != nil {
return CancelTurnResult{}, readErr
}
if !sessionFound {
return CancelTurnResult{}, ErrSessionNotFound
}
if !found {
return CancelTurnResult{Canonical: canonical, State: CancelStateNotFound}, nil
}
if turn.Phase == storesqlite.TurnPhaseSettled {
return CancelTurnResult{Canonical: canonical, Turn: &turn, State: CancelStateAlreadySettled, Settled: true, Outcome: turn.Outcome}, nil
}
route, err := h.resolveRuntimeControlRouteWithPolicy(
ctx,
input.WorkspaceID,
input.AgentSessionID,
input.RequireLive,
)
if err != nil {
return CancelTurnResult{}, err
}
targets, err := h.cancelTargetsForTurn(ctx, input.WorkspaceID, route, input.TurnID)
if err != nil {
return CancelTurnResult{}, err
}
operation, err := h.prepareCancelRuntimeOperation(ctx, input, route.RootAgentSessionID, targets)
if err != nil {
return CancelTurnResult{}, err
}
result := CancelTurnResult{Canonical: canonical, Turn: &turn, Operation: operation, State: CancelStateRequested, IntentAccepted: true}
completed, err := h.processRuntimeOperation(ctx, operation, false)
result.Operation = completed
if confirmed, _ := completed.Payload["providerConfirmed"].(bool); confirmed {
result.ProviderConfirmed = true
}
if err != nil {
return result, err
}
canonical, sessionFound, err = h.store.GetSession(ctx, input.WorkspaceID, input.AgentSessionID)
if err != nil {
return result, err
}
if !sessionFound {
return result, ErrSessionNotFound
}
result.Canonical = canonical
if settled, ok, readErr := h.store.GetTurn(ctx, input.WorkspaceID, input.AgentSessionID, input.TurnID); readErr != nil {
return result, readErr
} else if ok {
result.Turn = &settled
result.Settled = settled.Phase == storesqlite.TurnPhaseSettled
result.Outcome = settled.Outcome
if result.Settled {
result.State = CancelStateSettled
}
}
return result, nil
}
func (h *Host) SubmitInteractive(ctx context.Context, ref InteractionRef, input SubmitInteractiveInput) (SubmitInteractiveResult, error) {
ref.WorkspaceID = strings.TrimSpace(ref.WorkspaceID)
ref.AgentSessionID = strings.TrimSpace(ref.AgentSessionID)
ref.TurnID = strings.TrimSpace(ref.TurnID)
ref.RequestID = strings.TrimSpace(ref.RequestID)
if h == nil || h.store == nil || h.runtime == nil || ref.WorkspaceID == "" || ref.AgentSessionID == "" || ref.TurnID == "" || ref.RequestID == "" {
return SubmitInteractiveResult{}, ErrInvalidArgument
}
route, err := h.resolveRuntimeControlRoute(ctx, ref.WorkspaceID, ref.AgentSessionID)
if err != nil {
return SubmitInteractiveResult{}, err
}
operation, claimDisposition, claimed, err := h.prepareInteractiveRuntimeOperation(ctx, ref, input, route.RootAgentSessionID)
if err != nil {
return SubmitInteractiveResult{}, err
}
if !claimed {
canonical, sessionFound, readErr := h.store.GetSession(ctx, ref.WorkspaceID, ref.AgentSessionID)
if readErr != nil {
return SubmitInteractiveResult{}, readErr
}
if !sessionFound {
return SubmitInteractiveResult{}, ErrSessionNotFound
}
return SubmitInteractiveResult{Canonical: canonical, Operation: operation, Disposition: claimDisposition}, nil
}
processed, err := h.processRuntimeOperation(ctx, operation, false)
result := SubmitInteractiveResult{Operation: processed, Disposition: claimDisposition}
switch processed.Result {
case storesqlite.RuntimeOperationResultAnswered:
result.Disposition = RuntimeInteractiveDispositionAnswered
case storesqlite.RuntimeOperationResultSuperseded:
result.Disposition = RuntimeInteractiveDispositionSuperseded
}
if err != nil {
return result, err
}
canonical, sessionFound, err := h.store.GetSession(ctx, ref.WorkspaceID, ref.AgentSessionID)
if err != nil {
return result, err
}
if !sessionFound {
return result, ErrSessionNotFound
}
result.Canonical = canonical
return result, nil
}
func (h *Host) resolveRuntimeControlRoute(ctx context.Context, workspaceID, agentSessionID string) (runtimeControlRoute, error) {
return h.resolveRuntimeControlRouteWithPolicy(ctx, workspaceID, agentSessionID, false)
}
func (h *Host) resolveRuntimeControlRouteWithPolicy(
ctx context.Context,
workspaceID string,
agentSessionID string,
requireLive bool,
) (runtimeControlRoute, error) {
target, found, err := h.store.GetSession(ctx, workspaceID, agentSessionID)
if err != nil {
return runtimeControlRoute{}, err
}
if !found {
return runtimeControlRoute{}, ErrSessionNotFound
}
rootAgentSessionID := strings.TrimSpace(target.RootAgentSessionID)
if strings.TrimSpace(target.Kind) != storesqlite.SessionKindChild {
rootAgentSessionID = strings.TrimSpace(target.ID)
}
if rootAgentSessionID == "" {
return runtimeControlRoute{}, ErrSessionNotFound
}
if requireLive {
if !h.runtimeSessionLive(workspaceID, rootAgentSessionID) {
return runtimeControlRoute{}, ErrRuntimeSessionDisconnected
}
return runtimeControlRoute{RootAgentSessionID: rootAgentSessionID, TargetSession: target}, nil
}
if _, err := h.EnsureRuntimeSession(ctx, SessionRef{WorkspaceID: workspaceID, AgentSessionID: rootAgentSessionID}); err != nil {
return runtimeControlRoute{}, err
}
return runtimeControlRoute{RootAgentSessionID: rootAgentSessionID, TargetSession: target}, nil
}
func (h *Host) cancelTargetsForTurn(ctx context.Context, workspaceID string, route runtimeControlRoute, turnID string) ([]RuntimeCancelTarget, error) {
targetSessionID := strings.TrimSpace(route.TargetSession.ID)
targets := []RuntimeCancelTarget{{AgentSessionID: targetSessionID, TurnID: strings.TrimSpace(turnID)}}
if strings.TrimSpace(route.TargetSession.Kind) == storesqlite.SessionKindChild {
return targets, nil
}
children, err := h.store.ListChildSessions(ctx, workspaceID, targetSessionID)
if err != nil {
return nil, err
}
childTargets := make([]RuntimeCancelTarget, 0, len(children))
for _, child := range children {
if strings.TrimSpace(child.RootTurnID) != strings.TrimSpace(turnID) || strings.TrimSpace(child.ActiveTurnID) == "" {
continue
}
childTargets = append(childTargets, RuntimeCancelTarget{AgentSessionID: strings.TrimSpace(child.ID), TurnID: strings.TrimSpace(child.ActiveTurnID)})
}
sort.Slice(childTargets, func(left, right int) bool {
if childTargets[left].AgentSessionID == childTargets[right].AgentSessionID {
return childTargets[left].TurnID < childTargets[right].TurnID
}
return childTargets[left].AgentSessionID < childTargets[right].AgentSessionID
})
return append(childTargets, targets...), nil
}