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

187 lines
5.4 KiB
Go

package computer
import (
"context"
"strings"
"sync"
"time"
agentruntime "github.com/tutti-os/tutti/packages/agent/daemon/runtime"
)
// defaultIdleTTL shuts a workspace's computer session (and cua-driver) down
// after a period with no tool calls.
const defaultIdleTTL = 5 * time.Minute
// Service drives a supported desktop for agents via the `tutti computer` CLI.
// It owns one cua-driver subprocess per workspace, reused across CLI calls and
// torn down on idle or daemon shutdown.
type Service struct {
transport agentruntime.ProcessTransport
idleTTL time.Duration
daemon computerDaemon
mu sync.Mutex
sessions map[string]*computerSession
}
// NewService constructs a computer Service using a local process transport.
func NewService() *Service {
return &Service{
transport: agentruntime.NewLocalProcessTransport(),
idleTTL: defaultIdleTTL,
daemon: newComputerDaemon(),
sessions: make(map[string]*computerSession),
}
}
// CheckReady performs a bounded, side-effect-free cua-driver readiness probe.
// On Windows it does not start the driver daemon; callers that are about to
// open an MCP session use ensureReady instead.
func (*Service) CheckReady(ctx context.Context) error {
return checkReady(ctx)
}
func (s *Service) ensureReady(ctx context.Context) error {
if ctx == nil {
ctx = context.Background()
}
if _, hasDeadline := ctx.Deadline(); !hasDeadline {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, computerPermissionCheckTimeout)
defer cancel()
}
if err := checkReady(ctx); err != nil {
return err
}
if s == nil || s.daemon == nil {
return nil
}
return s.daemon.Ensure(ctx, resolveComputerMCPCommand(ctx))
}
// CallTool invokes a cua-driver MCP tool against the workspace's computer
// session, lazily starting it on first use.
func (s *Service) CallTool(ctx context.Context, workspaceID, cwd, tool string, args map[string]any) (ToolResult, error) {
return withComputerSession(s, ctx, workspaceID, cwd, func(session *computerSession) (ToolResult, error) {
return session.adaptToolCall(ctx, tool, args)
})
}
// CallNativeTool forwards one live-catalog tool allowed by Tutti's capability
// policy without applying stable CLI aliases or automatic target selection.
func (s *Service) CallNativeTool(ctx context.Context, workspaceID, cwd, tool string, args map[string]any) (ToolResult, error) {
return withComputerSession(s, ctx, workspaceID, cwd, func(session *computerSession) (ToolResult, error) {
catalog, err := session.listTools(ctx)
if err != nil {
return ToolResult{}, err
}
definition, err := requireAllowedNativeTool(catalog, tool)
if err != nil {
return ToolResult{}, err
}
return session.callNativeTool(ctx, definition.Name, args)
})
}
// ListTools returns the complete versioned cua-driver tool catalog annotated
// with Tutti-owned authorization decisions.
func (s *Service) ListTools(ctx context.Context, workspaceID, cwd string) (ToolCatalog, error) {
return withComputerSession(s, ctx, workspaceID, cwd, func(session *computerSession) (ToolCatalog, error) {
catalog, err := session.listTools(ctx)
if err != nil {
return ToolCatalog{}, err
}
return annotateNativeToolCatalog(catalog)
})
}
func withComputerSession[T any](s *Service, ctx context.Context, workspaceID, cwd string, call func(*computerSession) (T, error)) (T, error) {
var zero T
workspaceID = strings.TrimSpace(workspaceID)
if err := s.ensureReady(ctx); err != nil {
return zero, err
}
session := s.getOrCreate(workspaceID)
session.beginCall()
defer session.endCall(func() { s.resetIdle(workspaceID, session) })
if err := session.start(ctx, cwd); err != nil {
// A failed start should not be cached; drop the session so the next
// call retries (e.g. transient failure).
s.Shutdown(workspaceID)
return zero, err
}
result, err := call(session)
if err != nil && session.client != nil && session.client.isClosed() {
s.Shutdown(workspaceID)
}
return result, err
}
func (s *Service) getOrCreate(workspaceID string) *computerSession {
s.mu.Lock()
defer s.mu.Unlock()
if session, ok := s.sessions[workspaceID]; ok {
return session
}
session := &computerSession{
transport: s.transport,
command: resolveComputerMCPCommand,
}
s.sessions[workspaceID] = session
return session
}
func (s *Service) resetIdle(workspaceID string, session *computerSession) {
if session.inFlightCount() != 0 {
return
}
session.idleMu.Lock()
defer session.idleMu.Unlock()
if session.inFlightCount() != 0 {
return
}
if session.idle != nil {
session.idle.Stop()
}
session.idle = time.AfterFunc(s.idleTTL, func() { s.Shutdown(workspaceID) })
}
// Shutdown tears down a single workspace's computer session.
func (s *Service) Shutdown(workspaceID string) {
s.mu.Lock()
session := s.sessions[strings.TrimSpace(workspaceID)]
delete(s.sessions, strings.TrimSpace(workspaceID))
s.mu.Unlock()
if session == nil {
return
}
session.idleMu.Lock()
if session.idle != nil {
session.idle.Stop()
}
session.idleMu.Unlock()
session.close()
}
// Close tears down all computer sessions (daemon shutdown).
func (s *Service) Close() {
s.mu.Lock()
sessions := s.sessions
s.sessions = make(map[string]*computerSession)
s.mu.Unlock()
for _, session := range sessions {
session.idleMu.Lock()
if session.idle != nil {
session.idle.Stop()
}
session.idleMu.Unlock()
session.close()
}
if s.daemon != nil {
_ = s.daemon.Close()
}
}