189 lines
6.8 KiB
Go
189 lines
6.8 KiB
Go
// Package kiro implements the Kiro (AWS) agent adapter: launching new headless
|
|
// sessions, resuming hook-tracked sessions, installing workspace-local hooks,
|
|
// and reading hook-derived session info.
|
|
//
|
|
// Kiro is AWS's agentic coding assistant. Its terminal CLI ships as the
|
|
// `kiro-cli` binary. AO launches Kiro with a workspace-local custom agent so
|
|
// both worker and orchestrator sessions can use Kiro's normal interactive
|
|
// approval flow. See https://kiro.dev/docs/cli/headless/ and
|
|
// https://kiro.dev/docs/cli/reference/cli-commands/.
|
|
//
|
|
// Launch delivers the initial prompt as a positional argument after `--` so a
|
|
// leading "-" is not parsed as a flag. Permission/approval modes map onto
|
|
// Kiro's tool-trust flags (`--trust-all-tools`, `--trust-tools=<categories>`).
|
|
// Restore uses `kiro-cli chat --resume-id <UUID>` with the native session id
|
|
// captured from a Kiro hook payload.
|
|
//
|
|
// AO-managed sessions derive native session identity and display metadata from
|
|
// Kiro's native hooks (see hooks.go) rather than transcript scans.
|
|
package kiro
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/aoagents/agent-orchestrator/backend/internal/adapters"
|
|
"github.com/aoagents/agent-orchestrator/backend/internal/adapters/agent/agentbase"
|
|
"github.com/aoagents/agent-orchestrator/backend/internal/adapters/agent/binaryutil"
|
|
"github.com/aoagents/agent-orchestrator/backend/internal/domain"
|
|
"github.com/aoagents/agent-orchestrator/backend/internal/ports"
|
|
)
|
|
|
|
// Plugin is the Kiro agent adapter. It is safe for concurrent use; the binary
|
|
// path is resolved once and cached under binaryMu.
|
|
type Plugin struct {
|
|
agentbase.Base
|
|
binaryMu sync.Mutex
|
|
resolvedBinary string
|
|
}
|
|
|
|
// New returns a ready-to-register Kiro adapter.
|
|
func New() *Plugin {
|
|
return &Plugin{}
|
|
}
|
|
|
|
var _ adapters.Adapter = (*Plugin)(nil)
|
|
var _ ports.Agent = (*Plugin)(nil)
|
|
|
|
// Manifest returns the adapter's static self-description.
|
|
func (p *Plugin) Manifest() adapters.Manifest {
|
|
return adapters.Manifest{
|
|
ID: "kiro",
|
|
Name: "Kiro",
|
|
Description: "Run Kiro (AWS) worker sessions.",
|
|
Version: "0.0.1",
|
|
Capabilities: []adapters.Capability{
|
|
adapters.CapabilityAgent,
|
|
},
|
|
}
|
|
}
|
|
|
|
// GetLaunchCommand builds the argv to start a new Kiro session:
|
|
// `kiro-cli chat --agent ao [trust flags] [-- <prompt>]`.
|
|
//
|
|
// The prompt is passed as a positional argument after `--` so a leading "-" is
|
|
// not read as a flag. Kiro runs interactively for both workers and orchestrators;
|
|
// standing instructions come from the generated custom agent.
|
|
func (p *Plugin) GetLaunchCommand(ctx context.Context, cfg ports.LaunchConfig) (cmd []string, err error) {
|
|
binary, err := p.kiroBinary(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cmd = []string{binary, "chat", "--agent", kiroAgentName}
|
|
appendApprovalFlags(&cmd, cfg.Permissions)
|
|
|
|
prompt := cfg.Prompt
|
|
if prompt != "" {
|
|
cmd = append(cmd, "--", prompt)
|
|
}
|
|
|
|
return cmd, nil
|
|
}
|
|
|
|
// GetPromptDeliveryStrategy reports how Kiro receives the initial task prompt.
|
|
// Orchestrator standing instructions are delivered through the generated
|
|
// custom-agent prompt, so no command or post-start prompt injection is needed
|
|
// there.
|
|
func (p *Plugin) GetPromptDeliveryStrategy(ctx context.Context, cfg ports.LaunchConfig) (ports.PromptDeliveryStrategy, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return "", err
|
|
}
|
|
if cfg.Prompt != "" {
|
|
return ports.PromptDeliveryInCommand, nil
|
|
}
|
|
if cfg.Kind == domain.KindOrchestrator {
|
|
return ports.PromptDeliveryCustomAgent, nil
|
|
}
|
|
return ports.PromptDeliveryInCommand, nil
|
|
}
|
|
|
|
// GetRestoreCommand rebuilds the argv that continues an existing Kiro session.
|
|
// ok is false when the hook-derived native session id has not landed yet, so
|
|
// callers can fall back to fresh launch behavior.
|
|
func (p *Plugin) GetRestoreCommand(ctx context.Context, cfg ports.RestoreConfig) (cmd []string, ok bool, err error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, false, err
|
|
}
|
|
agentSessionID := strings.TrimSpace(cfg.Session.Metadata[ports.MetadataKeyAgentSessionID])
|
|
if agentSessionID == "" {
|
|
return nil, false, nil
|
|
}
|
|
|
|
binary, err := p.kiroBinary(ctx)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
cmd = []string{binary, "chat", "--agent", kiroAgentName, "--resume-id", agentSessionID}
|
|
appendApprovalFlags(&cmd, cfg.Permissions)
|
|
return cmd, true, nil
|
|
}
|
|
|
|
// SessionInfo surfaces Kiro hook-derived metadata. Metadata is intentionally
|
|
// nil for Kiro: callers get the normalized fields directly.
|
|
func (p *Plugin) SessionInfo(ctx context.Context, session ports.SessionRef) (ports.SessionInfo, bool, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return ports.SessionInfo{}, false, err
|
|
}
|
|
info, ok := agentbase.StandardSessionInfo(session)
|
|
return info, ok, nil
|
|
}
|
|
|
|
var kiroBinarySpec = binaryutil.BinarySpec{
|
|
Label: "kiro",
|
|
Names: []string{"kiro-cli"},
|
|
WinNames: []string{"kiro-cli.cmd", "kiro-cli.exe", "kiro-cli"},
|
|
UnixPaths: []string{"/usr/local/bin/kiro-cli", "/opt/homebrew/bin/kiro-cli"},
|
|
UnixHomePaths: [][]string{{".kiro", "bin", "kiro-cli"}, {".local", "bin", "kiro-cli"}},
|
|
// The native Kiro installer location is probed before the npm shim, matching
|
|
// the pre-refactor order so a native install still wins when both are present.
|
|
WinPaths: []binaryutil.WinPath{
|
|
{Base: binaryutil.WinLocalAppData, Parts: []string{"Programs", "kiro", "kiro-cli.exe"}},
|
|
{Base: binaryutil.WinAppData, Parts: []string{"npm", "kiro-cli.cmd"}},
|
|
{Base: binaryutil.WinAppData, Parts: []string{"npm", "kiro-cli.exe"}},
|
|
{Base: binaryutil.WinHome, Parts: []string{".kiro", "bin", "kiro-cli.exe"}},
|
|
},
|
|
}
|
|
|
|
// ResolveKiroBinary returns the path to the kiro-cli binary on this machine,
|
|
// searching PATH then a handful of well-known install locations. It returns a
|
|
// wrapped ports.ErrAgentBinaryNotFound when kiro-cli is absent.
|
|
func ResolveKiroBinary(ctx context.Context) (string, error) {
|
|
return binaryutil.ResolveBinary(ctx, kiroBinarySpec)
|
|
}
|
|
|
|
func (p *Plugin) kiroBinary(ctx context.Context) (string, error) {
|
|
p.binaryMu.Lock()
|
|
defer p.binaryMu.Unlock()
|
|
|
|
if p.resolvedBinary != "" {
|
|
return p.resolvedBinary, nil
|
|
}
|
|
|
|
binary, err := ResolveKiroBinary(ctx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
p.resolvedBinary = binary
|
|
return binary, nil
|
|
}
|
|
|
|
// appendApprovalFlags maps AO's permission modes onto Kiro's tool-trust flags.
|
|
// Default emits no flag so Kiro uses its normal interactive approval flow.
|
|
// accept-edits grants the write-capable built-in tools; auto/bypass grant all
|
|
// tools.
|
|
func appendApprovalFlags(cmd *[]string, permissions ports.PermissionMode) {
|
|
switch ports.NormalizePermissionMode(permissions) {
|
|
case ports.PermissionModeDefault:
|
|
// No flag: defer to Kiro's normal interactive approval flow.
|
|
case ports.PermissionModeAcceptEdits:
|
|
*cmd = append(*cmd, "--trust-tools=fs_read,fs_write")
|
|
case ports.PermissionModeAuto:
|
|
*cmd = append(*cmd, "--trust-all-tools")
|
|
case ports.PermissionModeBypassPermissions:
|
|
*cmd = append(*cmd, "--trust-all-tools")
|
|
}
|
|
}
|