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
690 lines
25 KiB
Go
690 lines
25 KiB
Go
package agentruntime
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"runtime"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
activityshared "github.com/tutti-os/tutti/packages/agent/daemon/activity/events"
|
|
)
|
|
|
|
func (a *standardACPAdapter) startupCallTimeout() time.Duration {
|
|
if a != nil && a.config.startupTimeout > 0 {
|
|
return a.config.startupTimeout
|
|
}
|
|
return acpStartCallTimeout
|
|
}
|
|
|
|
func (a *standardACPAdapter) Start(ctx context.Context, session Session) ([]activityshared.Event, error) {
|
|
unlockLifecycle := a.lockSessionLifecycle(session.AgentSessionID)
|
|
defer unlockLifecycle()
|
|
if err := a.admitReplacementLocked(session.AgentSessionID); err != nil {
|
|
return nil, err
|
|
}
|
|
previousSession := a.getSession(session.AgentSessionID)
|
|
a.logStandardACPStartupDiagnostics("start.enter", map[string]any{
|
|
"room_id": session.RoomID,
|
|
"agent_session_id": session.AgentSessionID,
|
|
"cwd": session.CWD,
|
|
"permission_mode_id": session.PermissionModeID,
|
|
"has_settings": session.Settings != nil,
|
|
})
|
|
client, initializeResult, err := a.startInitializedClient(ctx, session)
|
|
if err != nil {
|
|
a.logStandardACPStartupDiagnostics("start.initialized_client_failed", map[string]any{
|
|
"room_id": session.RoomID,
|
|
"agent_session_id": session.AgentSessionID,
|
|
"error": err.Error(),
|
|
})
|
|
return nil, err
|
|
}
|
|
mcpServers := acpMCPServers(session.MCPServers)
|
|
if len(mcpServers) > 0 && !standardACPHTTPMCPSupported(initializeResult) {
|
|
a.logStandardACPStartupDiagnostics("mcp_http.unsupported_fallback", map[string]any{
|
|
"room_id": session.RoomID,
|
|
"agent_session_id": session.AgentSessionID,
|
|
"binding_count": len(mcpServers),
|
|
})
|
|
mcpServers = nil
|
|
}
|
|
started := false
|
|
keepSession := false
|
|
var acpSession *standardACPSession
|
|
defer func() {
|
|
if !started && acpSession != nil {
|
|
a.closeOrRetainSession(session, acpSession)
|
|
}
|
|
if !keepSession {
|
|
if previousSession != nil {
|
|
a.storeSession(session.AgentSessionID, previousSession)
|
|
} else {
|
|
a.removeSession(session.AgentSessionID)
|
|
}
|
|
}
|
|
}()
|
|
initialPromptContext, err := a.resolveInitialPromptContext(session)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
acpSession = &standardACPSession{
|
|
client: client,
|
|
agentInfo: acpAgentInfo(initializeResult),
|
|
promptImage: standardACPProviderPromptImageSupported(a.config.provider, initializeResult),
|
|
sessionClose: standardACPSessionCloseSupported(initializeResult),
|
|
resumeMethod: acpResumeMethod(initializeResult),
|
|
acpLiveState: standardACPInitialLiveState(),
|
|
pendingApprovals: make(map[string]*pendingACPApproval),
|
|
permissionModeID: strings.TrimSpace(session.PermissionModeID),
|
|
planMode: session.SettingsValue().PlanMode,
|
|
lifecycleSeq: session.LifecycleSeq,
|
|
initialPromptContext: initialPromptContext,
|
|
}
|
|
a.storeSession(session.AgentSessionID, acpSession)
|
|
if a.config.localToolBridge != nil && standardACPHTTPMCPSupported(initializeResult) {
|
|
binding, release, bindErr := a.config.localToolBridge.Bind(ctx, session)
|
|
if bindErr != nil {
|
|
return nil, fmt.Errorf("bind local ACP tool bridge: %w", bindErr)
|
|
}
|
|
acpSession.localToolRelease = release
|
|
mcpServers = append(mcpServers, acpMCPServers([]MCPServerBinding{binding})...)
|
|
}
|
|
|
|
newSessionParams := map[string]any{
|
|
"cwd": standardACPProtocolCWD(session.CWD),
|
|
"mcpServers": mcpServers,
|
|
}
|
|
if err := a.applyProviderSessionMeta(newSessionParams, session); err != nil {
|
|
return nil, err
|
|
}
|
|
newSessionStartedAt := time.Now()
|
|
a.logStandardACPStartupDiagnostics("session_new.start", map[string]any{
|
|
"room_id": session.RoomID,
|
|
"agent_session_id": session.AgentSessionID,
|
|
"cwd": standardACPProtocolCWD(session.CWD),
|
|
"timeout_ms": a.startupCallTimeout().Milliseconds(),
|
|
})
|
|
newSessionResult, err := a.callSessionNewWithRetry(ctx, client, session, newSessionParams, func(ctx context.Context, message acpMessage) error {
|
|
_, err := a.handleACPMessage(ctx, client, session, "", message, nil, nil, nil)
|
|
return err
|
|
})
|
|
if err != nil {
|
|
a.logStandardACPStartupDiagnostics("session_new.failed", map[string]any{
|
|
"room_id": session.RoomID,
|
|
"agent_session_id": session.AgentSessionID,
|
|
"elapsed_ms": time.Since(newSessionStartedAt).Milliseconds(),
|
|
"error": err.Error(),
|
|
})
|
|
var callErr *acpCallError
|
|
if errors.As(err, &callErr) && callErr.AuthRequired() {
|
|
return nil, fmt.Errorf("%s: %w", a.config.authRequiredMessage, err)
|
|
}
|
|
return nil, &AppError{
|
|
Code: AppErrorProviderSessionCreateFailed,
|
|
Message: err.Error(),
|
|
DebugMessage: "provider session/new failed: " + err.Error(),
|
|
Cause: err,
|
|
}
|
|
}
|
|
providerSessionID, err := acpSessionID(newSessionResult)
|
|
if err != nil {
|
|
a.logStandardACPStartupDiagnostics("session_new.invalid_result", map[string]any{
|
|
"room_id": session.RoomID,
|
|
"agent_session_id": session.AgentSessionID,
|
|
"elapsed_ms": time.Since(newSessionStartedAt).Milliseconds(),
|
|
"error": err.Error(),
|
|
})
|
|
return nil, err
|
|
}
|
|
a.logStandardACPStartupDiagnostics("session_new.succeeded", map[string]any{
|
|
"room_id": session.RoomID,
|
|
"agent_session_id": session.AgentSessionID,
|
|
"provider_session_id": providerSessionID,
|
|
"elapsed_ms": time.Since(newSessionStartedAt).Milliseconds(),
|
|
"config_option_ids": acpConfigOptionIDList(newSessionResult),
|
|
})
|
|
session.ProviderSessionID = providerSessionID
|
|
acpSession.providerSessionID = providerSessionID
|
|
applyACPConfigOptionsResult(
|
|
&acpSession.acpLiveState,
|
|
newSessionResult,
|
|
a.config.modelConfigOptionID,
|
|
a.config.modelDescriptionFormat,
|
|
)
|
|
applyACPModelsResult(&acpSession.acpLiveState, newSessionResult, a.config.modelDescriptionFormat)
|
|
applyACPModesResult(&acpSession.acpLiveState, newSessionResult)
|
|
if a.config.validateNewSessionResult != nil {
|
|
if err := a.config.validateNewSessionResult(newSessionResult); err != nil {
|
|
a.logStandardACPStartupDiagnostics("session_new.validation_failed", map[string]any{
|
|
"room_id": session.RoomID,
|
|
"agent_session_id": session.AgentSessionID,
|
|
"provider_session_id": session.ProviderSessionID,
|
|
"error": err.Error(),
|
|
})
|
|
return nil, err
|
|
}
|
|
}
|
|
if err := a.applySessionConfigOptions(ctx, client, session, newSessionResult); err != nil {
|
|
a.logStandardACPStartupDiagnostics("config_options.failed", map[string]any{
|
|
"room_id": session.RoomID,
|
|
"agent_session_id": session.AgentSessionID,
|
|
"provider_session_id": session.ProviderSessionID,
|
|
"error": err.Error(),
|
|
})
|
|
return nil, err
|
|
}
|
|
if err := a.applyACPMode(ctx, client, session, a.startupModeID(session)); err != nil {
|
|
a.logStandardACPStartupDiagnostics("session_mode.failed", map[string]any{
|
|
"room_id": session.RoomID,
|
|
"agent_session_id": session.AgentSessionID,
|
|
"provider_session_id": session.ProviderSessionID,
|
|
"permission_mode_id": session.PermissionModeID,
|
|
"error": err.Error(),
|
|
})
|
|
return nil, err
|
|
}
|
|
|
|
started = true
|
|
keepSession = true
|
|
a.closeReplacedSession(session.AgentSessionID, previousSession, client)
|
|
a.logStandardACPStartupDiagnostics("start.succeeded", map[string]any{
|
|
"room_id": session.RoomID,
|
|
"agent_session_id": session.AgentSessionID,
|
|
"provider_session_id": session.ProviderSessionID,
|
|
})
|
|
return []activityshared.Event{newSessionActivityEvent(session, EventSessionStarted, SessionStatusReady, map[string]any{
|
|
"adapter": a.config.adapterName,
|
|
"command": strings.Join(a.config.command, " "),
|
|
"agent": acpAgentInfo(initializeResult),
|
|
"permissionModeId": session.PermissionModeID,
|
|
})}, nil
|
|
}
|
|
|
|
func (a *standardACPAdapter) callSessionNewWithRetry(
|
|
ctx context.Context,
|
|
client *acpClient,
|
|
session Session,
|
|
params map[string]any,
|
|
handler acpMessageHandler,
|
|
) (json.RawMessage, error) {
|
|
limit := 0
|
|
if a != nil && a.config.retrySessionNewError != nil && a.config.sessionNewRetryLimit > 0 {
|
|
limit = a.config.sessionNewRetryLimit
|
|
}
|
|
for attempt := 0; ; attempt++ {
|
|
result, err := client.CallWithTimeout(ctx, a.startupCallTimeout(), acpMethodNewSession, params, handler)
|
|
if err == nil || attempt >= limit || a.config.retrySessionNewError == nil || !a.config.retrySessionNewError(err) {
|
|
return result, err
|
|
}
|
|
a.logStandardACPStartupDiagnostics("session_new.retry", map[string]any{
|
|
"room_id": session.RoomID,
|
|
"agent_session_id": session.AgentSessionID,
|
|
"attempt": attempt + 1,
|
|
"max_retries": limit,
|
|
"error": err.Error(),
|
|
})
|
|
}
|
|
}
|
|
|
|
// standardACPProtocolCWD keeps the provider protocol's working directory
|
|
// aligned with the process working directory when the caller did not supply
|
|
// one. A POSIX root is not a valid Windows workspace fallback: sending "/"
|
|
// makes a Windows provider resolve searches against a path that cannot exist.
|
|
func standardACPProtocolCWD(cwd string) string {
|
|
if strings.TrimSpace(cwd) != "" {
|
|
return cwd
|
|
}
|
|
if runtime.GOOS == "windows" {
|
|
if processCWD, err := os.Getwd(); err == nil && strings.TrimSpace(processCWD) != "" {
|
|
return processCWD
|
|
}
|
|
// Keep the provider anchored to the child process's native cwd even if
|
|
// the host cannot materialize an absolute spelling for it.
|
|
return "."
|
|
}
|
|
return "/"
|
|
}
|
|
|
|
func (a *standardACPAdapter) Resume(ctx context.Context, session Session) error {
|
|
if strings.TrimSpace(session.ProviderSessionID) == "" {
|
|
return missingProviderSessionResumeError(session)
|
|
}
|
|
unlockLifecycle := a.lockSessionLifecycle(session.AgentSessionID)
|
|
defer unlockLifecycle()
|
|
if err := a.admitReplacementLocked(session.AgentSessionID); err != nil {
|
|
return err
|
|
}
|
|
return a.resumeLocked(ctx, session)
|
|
}
|
|
|
|
// resumeLocked reconnects a provider process while the caller owns the
|
|
// per-session lifecycle lock. ApplySessionSettings uses it when a launch-time
|
|
// Plan setting must replace an already usable process without re-entering the
|
|
// same lock.
|
|
func (a *standardACPAdapter) resumeLocked(ctx context.Context, session Session) error {
|
|
previousSession := a.getSession(session.AgentSessionID)
|
|
client, initializeResult, attachedCheckpoint, err := a.startClient(ctx, session, true, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
mcpServers := acpMCPServers(session.MCPServers)
|
|
if !attachedCheckpoint && len(mcpServers) > 0 && !standardACPHTTPMCPSupported(initializeResult) {
|
|
a.logStandardACPStartupDiagnostics("mcp_http.unsupported_fallback", map[string]any{
|
|
"room_id": session.RoomID,
|
|
"agent_session_id": session.AgentSessionID,
|
|
"binding_count": len(mcpServers),
|
|
})
|
|
mcpServers = nil
|
|
}
|
|
started := false
|
|
keepSession := false
|
|
var acpSession *standardACPSession
|
|
defer func() {
|
|
if !started && acpSession != nil {
|
|
a.closeOrRetainSession(session, acpSession)
|
|
}
|
|
if !keepSession {
|
|
if previousSession != nil {
|
|
a.storeSession(session.AgentSessionID, previousSession)
|
|
} else {
|
|
a.removeSession(session.AgentSessionID)
|
|
}
|
|
}
|
|
}()
|
|
initialPromptContext, err := a.resolveInitialPromptContext(session)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if attachedCheckpoint {
|
|
liveState := standardACPInitialLiveState()
|
|
liveState.currentMode = firstNonEmpty(
|
|
asString(session.RuntimeContext["mode"]),
|
|
a.startupModeID(session),
|
|
)
|
|
agentInfo, _ := session.RuntimeContext["agent"].(map[string]any)
|
|
acpSession = &standardACPSession{
|
|
client: client,
|
|
providerSessionID: session.ProviderSessionID,
|
|
resumeRuntimeContext: clonePayload(session.RuntimeContext),
|
|
agentInfo: clonePayload(agentInfo),
|
|
acpLiveState: liveState,
|
|
pendingApprovals: make(map[string]*pendingACPApproval),
|
|
permissionModeID: strings.TrimSpace(session.PermissionModeID),
|
|
planMode: session.SettingsValue().PlanMode,
|
|
lifecycleSeq: session.LifecycleSeq,
|
|
initialPromptContext: initialPromptContext,
|
|
}
|
|
started = true
|
|
keepSession = true
|
|
a.storeSession(session.AgentSessionID, acpSession)
|
|
a.closeReplacedSession(session.AgentSessionID, previousSession, client)
|
|
return nil
|
|
}
|
|
acpSession = &standardACPSession{
|
|
client: client,
|
|
providerSessionID: session.ProviderSessionID,
|
|
agentInfo: acpAgentInfo(initializeResult),
|
|
promptImage: standardACPProviderPromptImageSupported(a.config.provider, initializeResult),
|
|
sessionClose: standardACPSessionCloseSupported(initializeResult),
|
|
resumeMethod: acpResumeMethod(initializeResult),
|
|
acpLiveState: standardACPInitialLiveState(),
|
|
pendingApprovals: make(map[string]*pendingACPApproval),
|
|
permissionModeID: strings.TrimSpace(session.PermissionModeID),
|
|
planMode: session.SettingsValue().PlanMode,
|
|
lifecycleSeq: session.LifecycleSeq,
|
|
initialPromptContext: initialPromptContext,
|
|
}
|
|
if previousSession != nil {
|
|
acpSession.acpLiveState = cloneACPLiveState(previousSession.acpLiveState)
|
|
}
|
|
a.storeSession(session.AgentSessionID, acpSession)
|
|
if a.config.localToolBridge != nil && standardACPHTTPMCPSupported(initializeResult) {
|
|
binding, release, bindErr := a.config.localToolBridge.Bind(ctx, session)
|
|
if bindErr != nil {
|
|
return fmt.Errorf("bind local ACP tool bridge: %w", bindErr)
|
|
}
|
|
acpSession.localToolRelease = release
|
|
mcpServers = append(mcpServers, acpMCPServers([]MCPServerBinding{binding})...)
|
|
}
|
|
|
|
method := acpSession.resumeMethod
|
|
if method == "" {
|
|
return unsupportedACPResumeError(session)
|
|
}
|
|
resumeParams := map[string]any{
|
|
"sessionId": session.ProviderSessionID,
|
|
"cwd": standardACPProtocolCWD(session.CWD),
|
|
"mcpServers": mcpServers,
|
|
}
|
|
if err := a.applyProviderSessionMeta(resumeParams, session); err != nil {
|
|
return err
|
|
}
|
|
loadSessionResult, err := client.CallWithTimeout(ctx, acpStartCallTimeout, method, resumeParams, func(ctx context.Context, message acpMessage) error {
|
|
_, err := a.handleACPMessage(ctx, client, session, "", message, nil, nil, nil)
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return classifyACPResumeError(session, method, err)
|
|
}
|
|
applyACPConfigOptionsResult(
|
|
&acpSession.acpLiveState,
|
|
loadSessionResult,
|
|
a.config.modelConfigOptionID,
|
|
a.config.modelDescriptionFormat,
|
|
)
|
|
applyACPModelsResult(&acpSession.acpLiveState, loadSessionResult, a.config.modelDescriptionFormat)
|
|
applyACPModesResult(&acpSession.acpLiveState, loadSessionResult)
|
|
if err := a.applySessionConfigOptions(ctx, client, session, loadSessionResult); err != nil {
|
|
return err
|
|
}
|
|
targetModeID := a.startupModeID(session)
|
|
if standardACPResumeModeMatchesPersistedSelection(session, targetModeID) {
|
|
a.setSessionCurrentMode(session.AgentSessionID, targetModeID)
|
|
}
|
|
if err := a.applyACPMode(ctx, client, session, targetModeID); err != nil {
|
|
return err
|
|
}
|
|
started = true
|
|
keepSession = true
|
|
a.closeReplacedSession(session.AgentSessionID, previousSession, client)
|
|
return nil
|
|
}
|
|
|
|
func acpMCPServers(bindings []MCPServerBinding) []any {
|
|
servers := make([]any, 0, len(bindings))
|
|
for _, binding := range bindings {
|
|
if strings.TrimSpace(binding.Name) == "" || strings.TrimSpace(binding.URL) == "" || strings.TrimSpace(binding.Type) != "http" {
|
|
continue
|
|
}
|
|
headerNames := make([]string, 0, len(binding.Headers))
|
|
for name := range binding.Headers {
|
|
headerNames = append(headerNames, name)
|
|
}
|
|
sort.Strings(headerNames)
|
|
headers := make([]any, 0, len(headerNames))
|
|
for _, name := range headerNames {
|
|
headers = append(headers, map[string]any{"name": name, "value": binding.Headers[name]})
|
|
}
|
|
servers = append(servers, map[string]any{
|
|
"name": binding.Name, "type": "http", "url": binding.URL, "headers": headers,
|
|
})
|
|
}
|
|
return servers
|
|
}
|
|
|
|
func standardACPHTTPMCPSupported(raw json.RawMessage) bool {
|
|
var result struct {
|
|
AgentCapabilities struct {
|
|
MCPCapabilities struct {
|
|
HTTP bool `json:"http"`
|
|
} `json:"mcpCapabilities"`
|
|
} `json:"agentCapabilities"`
|
|
}
|
|
return json.Unmarshal(raw, &result) == nil && result.AgentCapabilities.MCPCapabilities.HTTP
|
|
}
|
|
|
|
func (*standardACPAdapter) CanResume(session Session) bool {
|
|
return strings.TrimSpace(session.ProviderSessionID) != ""
|
|
}
|
|
|
|
func (a *standardACPAdapter) HasLiveSession(session Session) bool {
|
|
acpSession := a.getUsableSession(session.AgentSessionID)
|
|
if acpSession == nil || acpSession.client == nil {
|
|
return false
|
|
}
|
|
select {
|
|
case <-acpSession.client.Done():
|
|
return false
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
|
|
func (*standardACPAdapter) waitForACPClientDone(client *acpClient, timeout time.Duration) {
|
|
if client == nil {
|
|
return
|
|
}
|
|
timer := time.NewTimer(timeout)
|
|
defer timer.Stop()
|
|
select {
|
|
case <-client.Done():
|
|
case <-timer.C:
|
|
}
|
|
}
|
|
|
|
func (a *standardACPAdapter) logACPCloseDiagnostics(stage string, session Session, acpSession *standardACPSession, err error) {
|
|
if a == nil || acpSession == nil || acpSession.client == nil {
|
|
return
|
|
}
|
|
diag := acpSession.client.Diagnostics()
|
|
args := []any{
|
|
"event", "agent_session.acp.close",
|
|
"provider", a.config.provider,
|
|
"stage", stage,
|
|
"room_id", session.RoomID,
|
|
"agent_session_id", session.AgentSessionID,
|
|
"provider_session_id", firstNonEmptyString(acpSession.providerSessionID, session.ProviderSessionID),
|
|
"stdout_tail", truncateACPLogValue(diag.StdoutTail, 1200),
|
|
"stderr_tail", truncateACPLogValue(diag.StderrTail, 1200),
|
|
}
|
|
if diag.ExitCode != nil {
|
|
args = append(args, "exit_code", *diag.ExitCode)
|
|
}
|
|
if err != nil {
|
|
args = append(args, "error", err.Error())
|
|
slog.Warn("agent session ACP close diagnostic", args...)
|
|
return
|
|
}
|
|
slog.Info("agent session ACP close diagnostic", args...)
|
|
}
|
|
|
|
func (a *standardACPAdapter) startInitializedClient(
|
|
ctx context.Context,
|
|
session Session,
|
|
) (*acpClient, json.RawMessage, error) {
|
|
client, initializeResult, _, err := a.startClient(ctx, session, false, true)
|
|
return client, initializeResult, err
|
|
}
|
|
|
|
func (a *standardACPAdapter) ConnectorCapabilities(
|
|
ctx context.Context,
|
|
session Session,
|
|
) (ConnectorCapabilities, error) {
|
|
client, initializeResult, _, err := a.startClient(ctx, session, false, false)
|
|
if err != nil {
|
|
return ConnectorCapabilities{}, err
|
|
}
|
|
if err := client.Close(); err != nil {
|
|
slog.WarnContext(ctx, "close ACP Connector capability probe", "provider", a.Provider(), "error", err)
|
|
}
|
|
return ConnectorCapabilities{HTTPMCP: standardACPHTTPMCPSupported(initializeResult)}, nil
|
|
}
|
|
|
|
func (a *standardACPAdapter) startClient(
|
|
ctx context.Context,
|
|
session Session,
|
|
allowAttachedCheckpoint bool,
|
|
runBeforeNewSession bool,
|
|
) (*acpClient, json.RawMessage, bool, error) {
|
|
if a == nil || a.transport == nil {
|
|
return nil, nil, false, errors.New("ACP process transport is unavailable")
|
|
}
|
|
command := append([]string(nil), a.config.command...)
|
|
env := append(a.config.env(session), session.Env...)
|
|
if a.config.commandResolver != nil {
|
|
resolved, err := a.config.commandResolver(ctx, a.config.provider)
|
|
if err != nil {
|
|
return nil, nil, false, err
|
|
}
|
|
if len(resolved.Command) > 0 {
|
|
command = append([]string(nil), resolved.Command...)
|
|
}
|
|
env = append(env, resolved.Env...)
|
|
}
|
|
if a.config.commandWithSettings != nil {
|
|
command = a.config.commandWithSettings(command, session)
|
|
}
|
|
var err error
|
|
if a.config.planModeUsesLaunchPermission && session.SettingsValue().PlanMode {
|
|
command, err = applyStandardACPLaunchPermissionValue(command, a.config.launchPermission, a.config.planModeRuntimeID)
|
|
} else {
|
|
command, err = applyStandardACPLaunchPermission(command, a.config.launchPermission, session.PermissionModeID)
|
|
}
|
|
if err != nil {
|
|
return nil, nil, false, err
|
|
}
|
|
spec, cleanup, err := prepareProviderLaunch(ctx, a.preparer, session, ProcessSpec{
|
|
Provider: a.config.provider,
|
|
AgentSessionID: session.AgentSessionID,
|
|
RootAgentSessionID: session.RootAgentSessionID,
|
|
RoomID: session.RoomID,
|
|
CWD: session.CWD,
|
|
ProtocolCWD: standardACPProtocolCWD(session.CWD),
|
|
Command: command,
|
|
Env: env,
|
|
DirectStart: false,
|
|
ExecutableIdentity: cloneExecutableIdentity(a.config.executableIdentity),
|
|
})
|
|
if err != nil {
|
|
a.logStandardACPStartupDiagnostics("process_prepare.failed", map[string]any{
|
|
"room_id": session.RoomID,
|
|
"agent_session_id": session.AgentSessionID,
|
|
"error": err.Error(),
|
|
})
|
|
return nil, nil, false, err
|
|
}
|
|
if a.config.finalizeEnv != nil {
|
|
spec.Env, err = a.config.finalizeEnv(spec.Env, session)
|
|
if err != nil {
|
|
cleanupPreparedLaunch(cleanup)
|
|
return nil, nil, false, err
|
|
}
|
|
}
|
|
processStartedAt := time.Now()
|
|
a.logStandardACPStartupDiagnostics("process_start.start", map[string]any{
|
|
"room_id": session.RoomID,
|
|
"agent_session_id": session.AgentSessionID,
|
|
"cwd": spec.CWD,
|
|
"protocol_cwd": spec.ProtocolCWD,
|
|
"command": spec.Command,
|
|
"direct_start": spec.DirectStart,
|
|
})
|
|
conn, err := a.transport.Start(ctx, spec)
|
|
if err != nil {
|
|
cleanupPreparedLaunch(cleanup)
|
|
a.logStandardACPStartupDiagnostics("process_start.failed", map[string]any{
|
|
"room_id": session.RoomID,
|
|
"agent_session_id": session.AgentSessionID,
|
|
"elapsed_ms": time.Since(processStartedAt).Milliseconds(),
|
|
"error": err.Error(),
|
|
})
|
|
return nil, nil, false, err
|
|
}
|
|
conn = wrapProviderLaunchCleanup(conn, cleanup)
|
|
a.logStandardACPStartupDiagnostics("process_start.succeeded", map[string]any{
|
|
"room_id": session.RoomID,
|
|
"agent_session_id": session.AgentSessionID,
|
|
"elapsed_ms": time.Since(processStartedAt).Milliseconds(),
|
|
})
|
|
client := newACPClientWithStderrMessageMapper(conn, a.config.stderrMessageMapper)
|
|
client.SetMessageHandler(func(ctx context.Context, message acpMessage) error {
|
|
if !a.isUsableCurrentClient(session.AgentSessionID, client) {
|
|
return nil
|
|
}
|
|
endInputUnit := a.inputUnits.begin(ctx, session.AgentSessionID)
|
|
defer endInputUnit()
|
|
turnSession := session
|
|
turnID := a.sessionRecentTurnID(session.AgentSessionID)
|
|
if acpSession := a.getSession(session.AgentSessionID); acpSession != nil {
|
|
turnSession.ProviderSessionID = firstNonEmptyString(acpSession.providerSessionID, turnSession.ProviderSessionID)
|
|
}
|
|
_, err := a.handleACPMessage(ctx, client, turnSession, turnID, message, nil, nil, nil)
|
|
return err
|
|
})
|
|
started := false
|
|
failedSession := &standardACPSession{
|
|
client: client,
|
|
pendingApprovals: make(map[string]*pendingACPApproval),
|
|
}
|
|
defer func() {
|
|
if !started {
|
|
a.closeOrRetainSession(session, failedSession)
|
|
}
|
|
}()
|
|
captureOrigin := processCassetteCaptureOrigin(conn)
|
|
if captureOrigin == ProcessCassetteCaptureOriginAttachedLiveConnection {
|
|
if !allowAttachedCheckpoint {
|
|
return nil, nil, false, errors.New(
|
|
"attached live provider checkpoint cannot start a new ACP session",
|
|
)
|
|
}
|
|
started = true
|
|
return client, nil, true, nil
|
|
}
|
|
|
|
initializeParams := defaultACPInitializeParams(a.host)
|
|
if a.config.initializeParams != nil {
|
|
initializeParams = a.config.initializeParams()
|
|
}
|
|
initializeStartedAt := time.Now()
|
|
a.logStandardACPStartupDiagnostics("initialize.start", map[string]any{
|
|
"room_id": session.RoomID,
|
|
"agent_session_id": session.AgentSessionID,
|
|
"timeout_ms": a.startupCallTimeout().Milliseconds(),
|
|
})
|
|
initializeResult, err := client.CallWithTimeout(ctx, a.startupCallTimeout(), acpMethodInitialize, initializeParams, func(ctx context.Context, message acpMessage) error {
|
|
_, err := a.handleACPMessage(ctx, client, session, "", message, nil, nil, nil)
|
|
return err
|
|
})
|
|
if err != nil {
|
|
a.logStandardACPStartupDiagnostics("initialize.failed", map[string]any{
|
|
"room_id": session.RoomID,
|
|
"agent_session_id": session.AgentSessionID,
|
|
"elapsed_ms": time.Since(initializeStartedAt).Milliseconds(),
|
|
"error": err.Error(),
|
|
})
|
|
return nil, nil, false, err
|
|
}
|
|
a.logStandardACPStartupDiagnostics("initialize.succeeded", map[string]any{
|
|
"room_id": session.RoomID,
|
|
"agent_session_id": session.AgentSessionID,
|
|
"elapsed_ms": time.Since(initializeStartedAt).Milliseconds(),
|
|
"agent_info": acpAgentInfo(initializeResult),
|
|
})
|
|
|
|
if runBeforeNewSession && a.config.beforeNewSession != nil {
|
|
beforeNewSessionStartedAt := time.Now()
|
|
a.logStandardACPStartupDiagnostics("before_new_session.start", map[string]any{
|
|
"room_id": session.RoomID,
|
|
"agent_session_id": session.AgentSessionID,
|
|
})
|
|
if err := a.config.beforeNewSession(ctx, client, session, initializeResult); err != nil {
|
|
a.logStandardACPStartupDiagnostics("before_new_session.failed", map[string]any{
|
|
"room_id": session.RoomID,
|
|
"agent_session_id": session.AgentSessionID,
|
|
"elapsed_ms": time.Since(beforeNewSessionStartedAt).Milliseconds(),
|
|
"error": err.Error(),
|
|
})
|
|
var callErr *acpCallError
|
|
if errors.As(err, &callErr) && callErr.AuthRequired() {
|
|
return nil, nil, false, fmt.Errorf("%s: %w", a.config.authRequiredMessage, err)
|
|
}
|
|
return nil, nil, false, err
|
|
}
|
|
a.logStandardACPStartupDiagnostics("before_new_session.succeeded", map[string]any{
|
|
"room_id": session.RoomID,
|
|
"agent_session_id": session.AgentSessionID,
|
|
"elapsed_ms": time.Since(beforeNewSessionStartedAt).Milliseconds(),
|
|
})
|
|
}
|
|
|
|
started = true
|
|
return client, initializeResult, false, nil
|
|
}
|