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
655 lines
23 KiB
Go
655 lines
23 KiB
Go
package agentcontext
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/google/uuid"
|
|
agenthost "github.com/tutti-os/tutti/packages/agent/host"
|
|
agentactivitybiz "github.com/tutti-os/tutti/packages/agent/store-sqlite"
|
|
"github.com/tutti-os/tutti/services/tuttid/biz/agentgui"
|
|
agenttargetbiz "github.com/tutti-os/tutti/services/tuttid/biz/agenttarget"
|
|
agentservice "github.com/tutti-os/tutti/services/tuttid/service/agent"
|
|
cliservice "github.com/tutti-os/tutti/services/tuttid/service/cli"
|
|
"github.com/tutti-os/tutti/services/tuttid/service/cli/framework"
|
|
)
|
|
|
|
var sessionActionColumns = []cliservice.TableColumn{
|
|
{Key: "id", Label: "ID"},
|
|
{Key: "provider", Label: "Provider"},
|
|
{Key: "activeTurnId", Label: "Active Turn"},
|
|
{Key: "launchRequested", Label: "Launch Requested"},
|
|
}
|
|
|
|
type startInput struct {
|
|
AgentID string `cli:"agent-id" advertise-required:"true" hint:"Use agent list --json to discover available agents."`
|
|
Cwd string `cli:"cwd"`
|
|
DisplayPrompt string `cli:"display-prompt"`
|
|
Hidden bool `cli:"hidden"`
|
|
Images []string `cli:"image" description:"Image file to attach to the initial prompt. May be passed multiple times."`
|
|
Isolation string `cli:"isolation" enum:"worktree" description:"Run the new session in a dedicated git worktree."`
|
|
Model string `cli:"model"`
|
|
PermissionMode string `cli:"permission-mode"`
|
|
Prompt string `cli:"prompt" validate:"required"`
|
|
Provider string `cli:"provider" hidden:"true"`
|
|
ReasoningEffort string `cli:"reasoning-effort"`
|
|
Show bool `cli:"show"`
|
|
Speed string `cli:"speed"`
|
|
Title string `cli:"title"`
|
|
}
|
|
|
|
type sessionIDInput struct {
|
|
SessionID string `cli:"session-id" validate:"required"`
|
|
}
|
|
|
|
type cancelTurnInput struct {
|
|
SessionID string `cli:"session-id" validate:"required" description:"Agent session id containing the turn to cancel."`
|
|
TurnID string `cli:"turn-id" validate:"required" description:"Exact turn id to cancel."`
|
|
}
|
|
|
|
type sendInput struct {
|
|
SessionID string `cli:"session-id" validate:"required"`
|
|
Guidance bool `cli:"guidance" description:"Capture and guide the exact active turn instead of starting a new turn."`
|
|
Images []string `cli:"image" description:"Image file to attach to this prompt. May be passed multiple times."`
|
|
Prompt string `cli:"prompt" validate:"required"`
|
|
}
|
|
|
|
type respondInput struct {
|
|
SessionID string `cli:"session-id" validate:"required" description:"Agent session id containing the pending interaction."`
|
|
TurnID string `cli:"turn-id" validate:"required" description:"Exact turn id containing the pending interaction."`
|
|
RequestID string `cli:"request-id" validate:"required" description:"Pending interaction request id."`
|
|
Action string `cli:"action" description:"Provider action id to submit."`
|
|
Option string `cli:"option" description:"Provider option id to submit."`
|
|
Payload string `cli:"payload" description:"JSON object payload to submit."`
|
|
Semantic string `cli:"semantic" description:"Resolve one uniquely matching action semantic from the pending interaction."`
|
|
}
|
|
|
|
type sessionActionResult struct {
|
|
Session agentservice.Session
|
|
WorkspaceID string
|
|
TurnID string
|
|
LaunchRequested bool
|
|
WaitAfterVersion *uint64
|
|
Warnings []cliservice.CommandWarning
|
|
}
|
|
|
|
type cancelTurnCommandResult struct {
|
|
AgentSessionID string
|
|
TurnID string
|
|
Result agentservice.CancelTurnResult
|
|
}
|
|
|
|
func (p Provider) newStartCommand() cliservice.Command {
|
|
return framework.Register(framework.CommandSpec[startInput]{
|
|
ID: appID + ".agent.start",
|
|
Path: []string{"agent", "start"},
|
|
Summary: "Start an agent session",
|
|
Description: "Start an agent session by agent id. Use agent list --json to discover the currently available agents.",
|
|
Kind: framework.KindAction,
|
|
Workspace: framework.WorkspaceRequired,
|
|
Workspaces: p.workspaces,
|
|
Inputs: framework.FromStruct[startInput](),
|
|
Output: sessionActionOutputSpec(),
|
|
Run: func(ctx context.Context, invoke framework.InvokeContext, input startInput) (any, error) {
|
|
target, _, err := p.resolveAgentSelector(ctx, input.AgentID, input.Provider)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return p.runStart(ctx, invoke, target, startFields{
|
|
Cwd: input.Cwd,
|
|
DisplayPrompt: input.DisplayPrompt,
|
|
Hidden: input.Hidden,
|
|
Images: input.Images,
|
|
Isolation: input.Isolation,
|
|
Model: input.Model,
|
|
PermissionMode: input.PermissionMode,
|
|
Prompt: input.Prompt,
|
|
ReasoningEffort: input.ReasoningEffort,
|
|
Show: input.Show,
|
|
Speed: input.Speed,
|
|
Title: input.Title,
|
|
})
|
|
},
|
|
})
|
|
}
|
|
|
|
type startFields struct {
|
|
Cwd string
|
|
DisplayPrompt string
|
|
Hidden bool
|
|
Images []string
|
|
Isolation string
|
|
Model string
|
|
PermissionMode string
|
|
Prompt string
|
|
ReasoningEffort string
|
|
Show bool
|
|
Speed string
|
|
Title string
|
|
}
|
|
|
|
func (p Provider) runStart(ctx context.Context, invoke framework.InvokeContext, target agenttargetbiz.Target, input startFields) (any, error) {
|
|
if err := p.requireSessions(); err != nil {
|
|
return nil, err
|
|
}
|
|
agentTargetID := strings.TrimSpace(target.ID)
|
|
if agentTargetID == "" {
|
|
return nil, fmt.Errorf("%w: agent target id is required", cliservice.ErrInvalidInput)
|
|
}
|
|
provider := strings.TrimSpace(target.Provider)
|
|
launchContext, err := resolveStartLaunchContext(input.Cwd, invoke.Request.Context)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
initialContent, err := promptContentFromCLIInput(input.Prompt, input.Images)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
created, err := p.sessions.CreateWithResult(ctx, invoke.WorkspaceID, agentservice.CreateSessionInput{
|
|
Provider: provider,
|
|
AgentTargetID: agentTargetID,
|
|
Cwd: optionalStringPointer(launchContext.cwd),
|
|
InitialContent: initialContent,
|
|
InitialDisplayPrompt: input.DisplayPrompt,
|
|
Model: optionalStringPointer(input.Model),
|
|
PermissionModeID: optionalStringPointer(input.PermissionMode),
|
|
ReasoningEffort: optionalStringPointer(input.ReasoningEffort),
|
|
Speed: optionalStringPointer(input.Speed),
|
|
Title: optionalStringPointer(input.Title),
|
|
Visible: hiddenVisibleOverride(input.Hidden),
|
|
ConversationDetailMode: p.composerConversationDetailMode(ctx),
|
|
Isolation: input.Isolation,
|
|
RailPlacement: launchContext.railPlacement,
|
|
ClientSubmitID: uuid.NewString(),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
session := created.Session
|
|
launchRequested := false
|
|
if input.Show {
|
|
if err := p.publishLaunchRequested(ctx, invoke.WorkspaceID, session, "start_show", invoke.Request.Context.Source); err != nil {
|
|
return nil, err
|
|
}
|
|
launchRequested = true
|
|
}
|
|
warnings := make([]cliservice.CommandWarning, 0, len(session.Warnings))
|
|
for _, warning := range session.Warnings {
|
|
warnings = append(warnings, cliservice.CommandWarning{Code: warning.Code, Message: warning.Message})
|
|
}
|
|
return sessionActionResult{
|
|
Session: session, TurnID: strings.TrimSpace(created.TurnID),
|
|
LaunchRequested: launchRequested, Warnings: warnings, WorkspaceID: invoke.WorkspaceID,
|
|
}, nil
|
|
}
|
|
|
|
func (p Provider) composerConversationDetailMode(ctx context.Context) string {
|
|
if p.preferences == nil {
|
|
return ""
|
|
}
|
|
preferences, err := p.preferences.Get(ctx)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return preferences.AgentConversationDetailMode
|
|
}
|
|
|
|
func hiddenVisibleOverride(hidden bool) *bool {
|
|
if !hidden {
|
|
return nil
|
|
}
|
|
visible := false
|
|
return &visible
|
|
}
|
|
|
|
type startLaunchContext struct {
|
|
cwd string
|
|
railPlacement *agenthost.RailPlacement
|
|
}
|
|
|
|
func resolveStartLaunchContext(
|
|
explicit string,
|
|
invokeContext cliservice.InvokeContext,
|
|
) (startLaunchContext, error) {
|
|
if cwd := strings.TrimSpace(explicit); cwd != "" {
|
|
return startLaunchContext{cwd: cwd}, nil
|
|
}
|
|
cwd := strings.TrimSpace(invokeContext.AgentCWD)
|
|
encodedPlacement := strings.TrimSpace(invokeContext.AgentRailPlacementJSON)
|
|
if cwd == "" && encodedPlacement == "" {
|
|
return startLaunchContext{}, nil
|
|
}
|
|
if cwd == "" || encodedPlacement == "" {
|
|
return startLaunchContext{}, fmt.Errorf(
|
|
"%w: inherited Agent cwd and rail placement must be provided together",
|
|
cliservice.ErrInvalidInput,
|
|
)
|
|
}
|
|
placement, err := agenthost.ParseAgentRailPlacementEnvironment(encodedPlacement)
|
|
if err != nil {
|
|
return startLaunchContext{}, fmt.Errorf(
|
|
"%w: invalid inherited Agent rail placement: %v",
|
|
cliservice.ErrInvalidInput,
|
|
err,
|
|
)
|
|
}
|
|
return startLaunchContext{
|
|
cwd: cwd,
|
|
railPlacement: placement,
|
|
}, nil
|
|
}
|
|
|
|
func (p Provider) newOpenCommand() cliservice.Command {
|
|
return framework.Register(framework.CommandSpec[sessionIDInput]{
|
|
ID: appID + ".agent.open",
|
|
Path: []string{"agent", "open"},
|
|
Summary: "Request AgentGUI open",
|
|
Description: "Request desktop AgentGUI activation for an existing agent session.",
|
|
Kind: framework.KindAction,
|
|
Workspace: framework.WorkspaceRequired,
|
|
Workspaces: p.workspaces,
|
|
Inputs: framework.FromStruct[sessionIDInput](),
|
|
Output: sessionActionOutputSpec(),
|
|
Run: p.runOpen,
|
|
})
|
|
}
|
|
|
|
func (p Provider) runOpen(ctx context.Context, invoke framework.InvokeContext, input sessionIDInput) (any, error) {
|
|
if err := p.requireSessions(); err != nil {
|
|
return nil, err
|
|
}
|
|
session, err := p.sessions.Get(ctx, invoke.WorkspaceID, input.SessionID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := p.publishLaunchRequested(ctx, invoke.WorkspaceID, session, "open", invoke.Request.Context.Source); err != nil {
|
|
return nil, err
|
|
}
|
|
return sessionActionResult{Session: session, LaunchRequested: true, WorkspaceID: invoke.WorkspaceID}, nil
|
|
}
|
|
|
|
func (p Provider) newGetCommand() cliservice.Command {
|
|
return framework.Register(framework.CommandSpec[getSessionInput]{
|
|
ID: appID + ".agent.get",
|
|
Path: []string{"agent", "get"},
|
|
Summary: "Get an agent session",
|
|
Description: "Get recent turn conversation by default, session metadata only, or a detailed trace for one turn.",
|
|
Kind: framework.KindGet,
|
|
Workspace: framework.WorkspaceRequired,
|
|
Workspaces: p.workspaces,
|
|
Inputs: framework.FromStruct[getSessionInput](),
|
|
Output: framework.OutputSpec{
|
|
DefaultMode: cliservice.OutputModeJSON,
|
|
DefaultView: framework.ViewDetail,
|
|
JSON: true,
|
|
JSONViews: map[framework.OutputView]func(any) map[string]any{framework.ViewDetail: sessionGetJSONValue},
|
|
},
|
|
Run: p.runGet,
|
|
})
|
|
}
|
|
|
|
func (p Provider) runGet(ctx context.Context, invoke framework.InvokeContext, input getSessionInput) (any, error) {
|
|
return p.getSessionContext(ctx, invoke.WorkspaceID, input)
|
|
}
|
|
|
|
func (p Provider) newSendCommand() cliservice.Command {
|
|
return framework.Register(framework.CommandSpec[sendInput]{
|
|
ID: appID + ".agent.send",
|
|
Path: []string{"agent", "send"},
|
|
Summary: "Send input to an agent session",
|
|
Description: "Send user input to an existing agent session. Use --guidance to guide the currently active turn without attaching to output.",
|
|
Kind: framework.KindAction,
|
|
Workspace: framework.WorkspaceRequired,
|
|
Workspaces: p.workspaces,
|
|
Inputs: framework.FromStruct[sendInput](),
|
|
Output: sessionActionOutputSpec(),
|
|
Run: p.runSend,
|
|
})
|
|
}
|
|
|
|
func (p Provider) runSend(ctx context.Context, invoke framework.InvokeContext, input sendInput) (any, error) {
|
|
if err := p.requireSessions(); err != nil {
|
|
return nil, err
|
|
}
|
|
content, err := promptContentFromCLIInput(input.Prompt, input.Images)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
turnID := ""
|
|
if input.Guidance {
|
|
session, err := p.sessions.Get(ctx, invoke.WorkspaceID, input.SessionID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
turnID = strings.TrimSpace(session.ActiveTurnID)
|
|
if turnID == "" {
|
|
return nil, agentservice.ErrActiveTurnTargetRequired
|
|
}
|
|
}
|
|
messagePage, err := p.sessions.ListMessages(ctx, invoke.WorkspaceID, input.SessionID, agentservice.ListMessagesInput{
|
|
Limit: 1,
|
|
Order: agentactivitybiz.MessageOrderDesc,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
waitAfterVersion := messagePage.LatestVersion
|
|
result, err := p.sessions.SendInput(ctx, invoke.WorkspaceID, input.SessionID, agentservice.SendInput{
|
|
Content: content,
|
|
Guidance: input.Guidance,
|
|
TurnID: turnID,
|
|
ClientSubmitID: uuid.NewString(),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
session := result.Session
|
|
return sessionActionResult{
|
|
Session: session, TurnID: strings.TrimSpace(result.TurnID), WaitAfterVersion: &waitAfterVersion,
|
|
WorkspaceID: invoke.WorkspaceID,
|
|
}, nil
|
|
}
|
|
|
|
func (p Provider) newRespondCommand() cliservice.Command {
|
|
return framework.Register(framework.CommandSpec[respondInput]{
|
|
ID: appID + ".agent.respond",
|
|
Path: []string{"agent", "respond"},
|
|
Summary: "Respond to a pending agent interaction",
|
|
Description: "Answer a pending agent approval or input request by action, option, payload, or self-described semantic.",
|
|
Kind: framework.KindAction,
|
|
Workspace: framework.WorkspaceRequired,
|
|
Workspaces: p.workspaces,
|
|
Inputs: framework.FromStruct[respondInput](),
|
|
Output: framework.OutputSpec{
|
|
DefaultMode: cliservice.OutputModeJSON,
|
|
DefaultView: framework.ViewSummary,
|
|
JSON: true,
|
|
JSONViews: map[framework.OutputView]func(any) map[string]any{
|
|
framework.ViewSummary: func(result any) map[string]any {
|
|
responded := result.(agentservice.RespondResult)
|
|
return map[string]any{
|
|
"requestId": responded.RequestID, "turnId": responded.TurnID,
|
|
"disposition": string(responded.Disposition),
|
|
}
|
|
},
|
|
},
|
|
},
|
|
Run: p.runRespond,
|
|
})
|
|
}
|
|
|
|
func (p Provider) runRespond(ctx context.Context, invoke framework.InvokeContext, input respondInput) (any, error) {
|
|
if err := p.requireSessions(); err != nil {
|
|
return nil, err
|
|
}
|
|
if strings.TrimSpace(input.Action) != "" && strings.TrimSpace(input.Semantic) != "" {
|
|
return nil, fmt.Errorf("%w: action and semantic are mutually exclusive", cliservice.ErrInvalidInput)
|
|
}
|
|
var payload map[string]any
|
|
payloadProvided := strings.TrimSpace(input.Payload) != ""
|
|
if payloadProvided {
|
|
if err := json.Unmarshal([]byte(input.Payload), &payload); err != nil || payload == nil {
|
|
return nil, fmt.Errorf("%w: payload must be a JSON object", cliservice.ErrInvalidInput)
|
|
}
|
|
}
|
|
if strings.TrimSpace(input.Action) == "" && strings.TrimSpace(input.Option) == "" &&
|
|
strings.TrimSpace(input.Semantic) == "" && !payloadProvided {
|
|
return nil, fmt.Errorf("%w: provide action, option, payload, or semantic", cliservice.ErrInvalidInput)
|
|
}
|
|
result, err := p.sessions.Respond(ctx, agentservice.RespondInput{
|
|
WorkspaceID: invoke.WorkspaceID, AgentSessionID: input.SessionID, TurnID: input.TurnID, RequestID: input.RequestID,
|
|
Action: optionalStringPointer(input.Action), OptionID: optionalStringPointer(input.Option),
|
|
Payload: payload, Semantic: input.Semantic,
|
|
})
|
|
if err != nil {
|
|
if isRespondInputError(err) {
|
|
return nil, fmt.Errorf("%w: %v", cliservice.ErrInvalidInput, err)
|
|
}
|
|
return nil, err
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func isRespondInputError(err error) bool {
|
|
return errors.Is(err, agentservice.ErrInvalidArgument) ||
|
|
errors.Is(err, agentservice.ErrInteractionRequestNotFound) ||
|
|
errors.Is(err, agentservice.ErrInteractionSemanticNotFound) ||
|
|
errors.Is(err, agentservice.ErrInteractionSemanticAmbiguous)
|
|
}
|
|
|
|
func promptContentFromCLIInput(prompt string, imagePaths []string) ([]agentservice.PromptContentBlock, error) {
|
|
content := agentservice.TextPromptContent(prompt)
|
|
for _, imagePath := range normalizeCLIImagePaths(imagePaths) {
|
|
block, err := promptImageContentBlockFromFile(imagePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
content = append(content, block)
|
|
}
|
|
return content, nil
|
|
}
|
|
|
|
func normalizeCLIImagePaths(values []string) []string {
|
|
result := make([]string, 0, len(values))
|
|
for _, value := range values {
|
|
if trimmed := strings.TrimSpace(value); trimmed != "" {
|
|
result = append(result, trimmed)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func promptImageContentBlockFromFile(path string) (agentservice.PromptContentBlock, error) {
|
|
mimeType := promptImageMimeTypeFromPath(path)
|
|
if mimeType == "" {
|
|
return agentservice.PromptContentBlock{}, fmt.Errorf("%w: invalid input %q, expected a PNG, JPEG, or WebP image", cliservice.ErrInvalidInput, "image")
|
|
}
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return agentservice.PromptContentBlock{}, fmt.Errorf("%w: read image %q: %v", cliservice.ErrInvalidInput, path, err)
|
|
}
|
|
return agentservice.PromptContentBlock{
|
|
Type: "image",
|
|
MimeType: mimeType,
|
|
Data: base64.StdEncoding.EncodeToString(data),
|
|
Name: filepath.Base(path),
|
|
}, nil
|
|
}
|
|
|
|
func promptImageMimeTypeFromPath(path string) string {
|
|
switch strings.ToLower(strings.TrimSpace(filepath.Ext(path))) {
|
|
case ".png":
|
|
return "image/png"
|
|
case ".jpg", ".jpeg":
|
|
return "image/jpeg"
|
|
case ".webp":
|
|
return "image/webp"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func (p Provider) newCancelTurnCommand() cliservice.Command {
|
|
return framework.Register(framework.CommandSpec[cancelTurnInput]{
|
|
ID: appID + ".agent.cancel-turn",
|
|
Path: []string{"agent", "cancel-turn"},
|
|
Summary: "Cancel an agent turn",
|
|
Description: "Cancel one exact turn while preserving its agent session for later input.",
|
|
Kind: framework.KindAction,
|
|
Workspace: framework.WorkspaceRequired,
|
|
Workspaces: p.workspaces,
|
|
Inputs: framework.FromStruct[cancelTurnInput](),
|
|
Output: cancelTurnOutputSpec(),
|
|
Run: p.runCancelTurn,
|
|
})
|
|
}
|
|
|
|
func (p Provider) runCancelTurn(ctx context.Context, invoke framework.InvokeContext, input cancelTurnInput) (any, error) {
|
|
if err := p.requireSessions(); err != nil {
|
|
return nil, err
|
|
}
|
|
result, err := p.sessions.CancelTurn(ctx, invoke.WorkspaceID, input.SessionID, input.TurnID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return cancelTurnCommandResult{
|
|
AgentSessionID: strings.TrimSpace(input.SessionID),
|
|
TurnID: strings.TrimSpace(input.TurnID),
|
|
Result: result,
|
|
}, nil
|
|
}
|
|
|
|
func cancelTurnOutputSpec() framework.OutputSpec {
|
|
columns := []cliservice.TableColumn{
|
|
{Key: "id", Label: "Session"},
|
|
{Key: "turnId", Label: "Turn"},
|
|
{Key: "canceled", Label: "Canceled"},
|
|
{Key: "reason", Label: "Reason"},
|
|
}
|
|
jsonValue := func(result any) map[string]any {
|
|
canceled := result.(cancelTurnCommandResult)
|
|
return map[string]any{
|
|
"agentSessionId": canceled.AgentSessionID,
|
|
"turnId": canceled.TurnID,
|
|
"canceled": canceled.Result.Canceled,
|
|
"reason": string(canceled.Result.Reason),
|
|
}
|
|
}
|
|
return framework.OutputSpec{
|
|
DefaultMode: cliservice.OutputModeTable,
|
|
DefaultView: framework.ViewSummary,
|
|
JSON: true,
|
|
Table: &framework.TableOutputSpec{
|
|
Columns: columns,
|
|
Rows: func(result any) []map[string]any {
|
|
canceled := result.(cancelTurnCommandResult)
|
|
return []map[string]any{{
|
|
"id": canceled.AgentSessionID,
|
|
"turnId": canceled.TurnID,
|
|
"canceled": canceled.Result.Canceled,
|
|
"reason": string(canceled.Result.Reason),
|
|
}}
|
|
},
|
|
},
|
|
JSONViews: map[framework.OutputView]func(any) map[string]any{
|
|
framework.ViewSummary: jsonValue,
|
|
},
|
|
}
|
|
}
|
|
|
|
func (p Provider) newLegacyCancelCommand() cliservice.Command {
|
|
return framework.Register(framework.CommandSpec[sessionIDInput]{
|
|
ID: appID + ".agent.cancel",
|
|
Path: []string{"agent", "cancel"},
|
|
Summary: "Cancel the active agent turn (deprecated)",
|
|
Description: "Deprecated compatibility alias. Use agent cancel-turn with an exact session id and turn id.",
|
|
Kind: framework.KindAction,
|
|
Visibility: cliservice.CapabilityVisibilityIntegration,
|
|
Workspace: framework.WorkspaceRequired,
|
|
Workspaces: p.workspaces,
|
|
Inputs: framework.FromStruct[sessionIDInput](),
|
|
Output: sessionActionOutputSpec(),
|
|
Run: p.runLegacyCancel,
|
|
})
|
|
}
|
|
|
|
func (p Provider) runLegacyCancel(ctx context.Context, invoke framework.InvokeContext, input sessionIDInput) (any, error) {
|
|
if err := p.requireSessions(); err != nil {
|
|
return nil, err
|
|
}
|
|
session, err := p.sessions.Get(ctx, invoke.WorkspaceID, input.SessionID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if turnID := strings.TrimSpace(session.ActiveTurnID); turnID != "" {
|
|
result, cancelErr := p.sessions.CancelTurn(ctx, invoke.WorkspaceID, input.SessionID, turnID)
|
|
if cancelErr != nil {
|
|
return nil, cancelErr
|
|
}
|
|
if strings.TrimSpace(result.Session.ID) != "" {
|
|
session = result.Session
|
|
}
|
|
}
|
|
return sessionActionResult{
|
|
Session: session, WorkspaceID: invoke.WorkspaceID,
|
|
Warnings: []cliservice.CommandWarning{{
|
|
Code: "deprecated_agent_cancel",
|
|
Message: "agent cancel is deprecated; use agent cancel-turn --session-id <id> --turn-id <id>",
|
|
}},
|
|
}, nil
|
|
}
|
|
|
|
func sessionActionOutputSpec() framework.OutputSpec {
|
|
return framework.OutputSpec{
|
|
DefaultMode: cliservice.OutputModeTable,
|
|
DefaultView: framework.ViewSummary,
|
|
JSON: true,
|
|
Table: &framework.TableOutputSpec{
|
|
Columns: sessionActionColumns,
|
|
Rows: func(result any) []map[string]any {
|
|
action := result.(sessionActionResult)
|
|
return []map[string]any{{
|
|
"id": action.Session.ID,
|
|
"provider": action.Session.Provider,
|
|
"activeTurnId": action.Session.ActiveTurnID,
|
|
"launchRequested": action.LaunchRequested,
|
|
}}
|
|
},
|
|
},
|
|
JSONViews: map[framework.OutputView]func(any) map[string]any{
|
|
framework.ViewSummary: func(result any) map[string]any {
|
|
action := result.(sessionActionResult)
|
|
value := map[string]any{
|
|
"launchRequested": action.LaunchRequested,
|
|
"session": sessionActionValue(action.WorkspaceID, action.Session),
|
|
}
|
|
addAgentSessionReference(value, action.WorkspaceID, action.Session.ID)
|
|
if turnID := strings.TrimSpace(action.TurnID); turnID != "" {
|
|
value["turnId"] = turnID
|
|
}
|
|
if action.WaitAfterVersion != nil {
|
|
value["waitAfterVersion"] = *action.WaitAfterVersion
|
|
}
|
|
return value
|
|
},
|
|
},
|
|
Warnings: func(result any) []cliservice.CommandWarning {
|
|
return append([]cliservice.CommandWarning(nil), result.(sessionActionResult).Warnings...)
|
|
},
|
|
}
|
|
}
|
|
|
|
func (p Provider) publishLaunchRequested(
|
|
ctx context.Context,
|
|
workspaceID string,
|
|
session agentservice.Session,
|
|
reason string,
|
|
source string,
|
|
) error {
|
|
if p.launchPublisher == nil {
|
|
return nil
|
|
}
|
|
return p.launchPublisher.PublishAgentGUILaunchRequested(ctx, agentgui.NormalizeLaunchRequest(agentgui.LaunchRequest{
|
|
WorkspaceID: workspaceID,
|
|
AgentSessionID: session.ID,
|
|
AgentTargetID: session.AgentTargetID,
|
|
Provider: session.Provider,
|
|
Source: source,
|
|
Reason: reason,
|
|
}))
|
|
}
|
|
|
|
func optionalStringPointer(value string) *string {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" {
|
|
return nil
|
|
}
|
|
return &value
|
|
}
|