Files
tutti/services/tuttid/issue_run_agent_adapters.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

257 lines
8.7 KiB
Go

package main
import (
"context"
"errors"
"strings"
agenthost "github.com/tutti-os/tutti/packages/agent/host"
agentstoresqlite "github.com/tutti-os/tutti/packages/agent/store-sqlite"
workspaceissues "github.com/tutti-os/tutti/packages/workspace/issues"
agentservice "github.com/tutti-os/tutti/services/tuttid/service/agent"
workspaceservice "github.com/tutti-os/tutti/services/tuttid/service/workspace"
)
type issueRunAgentSessionCreator interface {
CreateWithResult(context.Context, string, agentservice.CreateSessionInput) (agentservice.CreateSessionResult, error)
}
type issueRunCanonicalTurnFinder interface {
FindTurnByClientSubmitID(context.Context, agenthost.SessionRef, string) (string, bool, error)
}
type issueRunSessionCanceller struct {
Host issueRunSettlementHost
Sessions *agentservice.Service
}
type issueRunSettlementHost interface {
issueRunCanonicalTurnFinder
GetTurn(context.Context, agenthost.SessionRef, string) (agentstoresqlite.Turn, bool, error)
}
type issueRunSettlementReader struct {
Host issueRunSettlementHost
}
func (c issueRunSessionCanceller) CancelAutomationTurn(
ctx context.Context,
workspaceID string,
agentSessionID string,
turnID string,
) error {
if c.Sessions == nil {
return errors.New("automation Turn canceller is unavailable")
}
_, err := c.Sessions.CancelTurn(ctx, workspaceID, agentSessionID, turnID)
return err
}
func (r issueRunSettlementReader) ReadRunSettlement(ctx context.Context, workspaceID string, agentSessionID string, clientSubmitID string) (workspaceservice.IssueRunSettlement, bool, error) {
if r.Host == nil {
return workspaceservice.IssueRunSettlement{}, false, nil
}
ref := agenthost.SessionRef{WorkspaceID: workspaceID, AgentSessionID: agentSessionID}
turnID, found, err := r.Host.FindTurnByClientSubmitID(ctx, ref, clientSubmitID)
if err != nil || !found {
return workspaceservice.IssueRunSettlement{}, false, err
}
turn, found, err := r.Host.GetTurn(ctx, ref, turnID)
if err != nil || !found || strings.TrimSpace(turn.Phase) != "settled" {
return workspaceservice.IssueRunSettlement{}, false, err
}
status := workspaceissues.StatusFailed
switch strings.TrimSpace(turn.Outcome) {
case "completed":
status = workspaceissues.StatusCompleted
case "canceled":
status = workspaceissues.StatusCanceled
case "":
return workspaceservice.IssueRunSettlement{}, false, nil
}
return workspaceservice.IssueRunSettlement{
WorkspaceID: workspaceID,
AgentSessionID: agentSessionID,
TurnID: turnID,
Status: status,
ErrorMessage: strings.TrimSpace(turn.ErrorMessage),
}, true, nil
}
func (c issueRunSessionCanceller) RequestRunCancellation(ctx context.Context, request workspaceservice.IssueRunCancellationRequest) (workspaceservice.IssueRunCancelResult, error) {
if c.Host == nil || c.Sessions == nil {
return workspaceservice.IssueRunCancelResult{}, errors.New("issue run session canceller is unavailable")
}
clientSubmitID := strings.TrimSpace(request.ClientSubmitID)
if clientSubmitID == "" {
return workspaceservice.IssueRunCancelResult{}, errors.New("issue run client submit identity is required")
}
ref := agenthost.SessionRef{WorkspaceID: request.WorkspaceID, AgentSessionID: request.AgentSessionID}
turnID, found, err := c.Host.FindTurnByClientSubmitID(ctx, ref, clientSubmitID)
if err != nil {
return workspaceservice.IssueRunCancelResult{}, err
}
if !found {
return workspaceservice.IssueRunCancelResult{State: workspaceservice.IssueRunCancelNotFound}, nil
}
result, err := c.Sessions.CancelTurn(ctx, request.WorkspaceID, request.AgentSessionID, turnID)
if err != nil {
return workspaceservice.IssueRunCancelResult{}, err
}
reader := issueRunSettlementReader{Host: c.Host}
settlement, settled, readErr := reader.ReadRunSettlement(
ctx, request.WorkspaceID, request.AgentSessionID, clientSubmitID,
)
if readErr != nil {
return workspaceservice.IssueRunCancelResult{}, readErr
}
switch result.Reason {
case agentservice.CancelTurnReasonTurnCanceled:
if !settled {
settlement = workspaceservice.IssueRunSettlement{
WorkspaceID: request.WorkspaceID,
AgentSessionID: request.AgentSessionID,
TurnID: turnID,
Status: workspaceissues.StatusCanceled,
}
}
return workspaceservice.IssueRunCancelResult{
State: workspaceservice.IssueRunCancelCanceled,
Settlement: &settlement,
}, nil
case agentservice.CancelTurnReasonAlreadySettled:
if !settled {
return workspaceservice.IssueRunCancelResult{}, errors.New("settled Agent turn is unavailable")
}
return workspaceservice.IssueRunCancelResult{
State: workspaceservice.IssueRunCancelSettled,
Settlement: &settlement,
}, nil
case agentservice.CancelTurnReasonNotFound:
return workspaceservice.IssueRunCancelResult{State: workspaceservice.IssueRunCancelNotFound}, nil
default:
return workspaceservice.IssueRunCancelResult{State: workspaceservice.IssueRunCancelAccepted}, nil
}
}
type issueRunAgentLauncher struct {
Sessions issueRunAgentSessionCreator
Host issueRunCanonicalTurnFinder
}
func (l issueRunAgentLauncher) Launch(ctx context.Context, launch workspaceservice.IssueRunLaunch) error {
if l.Sessions == nil {
return errors.New("issue run agent launcher is unavailable")
}
clientSubmitID := strings.TrimSpace(launch.ClientSubmitID)
if clientSubmitID == "" {
return errors.New("issue run client submit id is required")
}
title := launch.Title
reasoningIntensity := launch.ReasoningIntensity
permissionModeID := optionalString(launch.PermissionModeID)
initialContent := []agentservice.PromptContentBlock{{Type: "text", Text: launch.Prompt}}
for _, attachment := range launch.Attachments {
initialContent = append(initialContent, agentservice.PromptContentBlock{
Type: "image",
MimeType: attachment.MimeType,
Name: attachment.Name,
Path: attachment.Path,
})
}
result, err := l.Sessions.CreateWithResult(ctx, launch.WorkspaceID, agentservice.CreateSessionInput{
AgentSessionID: launch.AgentSessionID,
AgentTargetID: launch.AgentTargetID,
ReasoningIntensity: &reasoningIntensity,
ReasoningEffort: optionalString(launch.ReasoningEffort),
PermissionModeID: permissionModeID,
StrictPermissionMode: permissionModeID != nil,
InitialContent: initialContent,
ClientSubmitID: clientSubmitID,
Title: &title,
Cwd: optionalString(launch.ExecutionDirectory),
Model: optionalString(launch.Model),
ModelPlanID: optionalString(launch.ModelPlanID),
Visible: boolPointer(!launch.HideSession),
RailPlacement: agentRailPlacementFromIssueRun(launch.RailPlacement),
})
if err == nil ||
strings.TrimSpace(result.TurnID) != "" ||
errors.Is(err, agentservice.ErrSubmitDeliveryUnknown) {
return err
}
if l.Host == nil {
return err
}
_, found, lookupErr := l.Host.FindTurnByClientSubmitID(
ctx,
agenthost.SessionRef{
WorkspaceID: launch.WorkspaceID,
AgentSessionID: launch.AgentSessionID,
},
clientSubmitID,
)
if lookupErr != nil || found {
return err
}
return workspaceservice.NewIssueRunLaunchNotStartedError(err)
}
func agentRailPlacementFromIssueRun(
placement *workspaceservice.IssueRunRailPlacement,
) *agenthost.RailPlacement {
if placement == nil {
return nil
}
return &agenthost.RailPlacement{
Version: 1,
Kind: agenthost.RailPlacementKind(strings.TrimSpace(placement.Kind)),
ProjectPath: strings.TrimSpace(placement.ProjectPath),
SectionKey: strings.TrimSpace(placement.SectionKey),
}
}
type issueSourceSessionContextResolver struct {
Sessions agentservice.SessionReader
}
func (r issueSourceSessionContextResolver) ResolveSourceSessionContext(
workspaceID string,
agentSessionID string,
) (workspaceservice.IssueSourceSessionContext, bool) {
if r.Sessions == nil {
return workspaceservice.IssueSourceSessionContext{}, false
}
session, ok := r.Sessions.GetSession(workspaceID, agentSessionID)
if !ok {
return workspaceservice.IssueSourceSessionContext{}, false
}
context := workspaceservice.IssueSourceSessionContext{
WorkingDirectory: strings.TrimSpace(session.Cwd),
}
if strings.TrimSpace(session.RailSectionKey) != "" {
context.RailPlacement = &workspaceservice.IssueRunRailPlacement{
Kind: strings.TrimSpace(session.RailSectionKind),
ProjectPath: strings.TrimSpace(session.RailProjectPath),
SectionKey: strings.TrimSpace(session.RailSectionKey),
}
if context.WorkingDirectory == "" &&
context.RailPlacement.Kind == "project" {
context.WorkingDirectory = context.RailPlacement.ProjectPath
}
}
return context, true
}
func optionalString(value string) *string {
value = strings.TrimSpace(value)
if value == "" {
return nil
}
return &value
}
func boolPointer(value bool) *bool {
return &value
}