Files
kandev/apps/backend/internal/agent/runtime/runtime.go
T

193 lines
7.9 KiB
Go

// Package runtime is the single seam between higher-level coordinators
// (the workflow engine, cron-driven trigger handlers, the orchestrator
// for user-initiated kanban launches) and the agent execution layer
// (lifecycle manager, executor backends, agentctl client).
//
// The runtime knows nothing about tasks, workflows, or office stages.
// It launches agents, resumes them with prompts, stops them, and exposes
// execution state. Anything task-shaped lives a layer above.
//
// Phase 1 of task-model-unification (ADR 0004) introduces this package.
// Sub-packages — `lifecycle` and `agentctl` — are the moved implementations.
// The `Runtime` interface here is the only surface external callers should
// reach for once migration completes; existing callers continue to use the
// sub-packages directly until later phases route them through this seam.
package runtime
import (
"context"
"errors"
"time"
"github.com/kandev/kandev/internal/agent/runtime/lifecycle"
"github.com/kandev/kandev/internal/agentctl/types/streams"
v1 "github.com/kandev/kandev/pkg/api/v1"
)
// These aliases keep higher-level callers on the runtime seam while the
// lifecycle package remains the implementation owner of the wire contracts.
type ExecutionOwnerKind = lifecycle.ExecutionOwnerKind
type ExecutionOwner = lifecycle.ExecutionOwner
type OwnerAdmission = lifecycle.OwnerAdmission
type LaunchRequest = lifecycle.LaunchRequest
type RouteOverride = lifecycle.RouteOverride
type AgentStreamEventPayload = lifecycle.AgentStreamEventPayload
const (
ExecutionOwnerTask = lifecycle.ExecutionOwnerTask
ExecutionOwnerRun = lifecycle.ExecutionOwnerRun
)
// Runtime is the public surface for launching, resuming, stopping, and
// observing agent executions.
//
// Implementations must be safe for concurrent use.
type Runtime interface {
// Launch starts a new agent execution against the supplied spec and
// returns a reference to the execution.
Launch(ctx context.Context, spec LaunchSpec) (ExecutionRef, error)
// Start prepares the process for a newly launched execution and delivers
// the initial prompt exactly once through the lifecycle startup seam.
Start(ctx context.Context, spec LaunchSpec) (ExecutionRef, error)
// StartExecution starts a previously registered execution. Callers that
// need to persist the execution identity before process startup use this
// split launch/start boundary.
StartExecution(ctx context.Context, executionID string) error
// Resume sends a follow-up prompt to an existing execution.
Resume(ctx context.Context, executionID string, prompt string) error
// Stop terminates an execution and records the supplied reason.
Stop(ctx context.Context, executionID string, reason string) error
// GetExecution returns a snapshot view of an execution by ID.
GetExecution(ctx context.Context, executionID string) (*Execution, error)
// SubscribeEvents returns a channel of stream events for an
// execution. The channel closes when the supplied context is cancelled
// or the execution finishes.
SubscribeEvents(ctx context.Context, executionID string) (<-chan Event, error)
// SetMcpMode swaps the MCP tool mode for a running execution.
SetMcpMode(ctx context.Context, executionID string, mode string) error
}
// LaunchSpec carries everything the runtime needs to start an agent.
//
// The minimum useful surface: profile, executor, workspace, prompt, prior
// ACP session id (for resume), MCP mode, plus a freeform metadata map.
// Richer launch parameters (worktree configuration, multi-repo specs,
// attachments, executor config) flow through `Metadata["launch_request"]`
// as a `*lifecycle.LaunchRequest` until later phases canonicalise them
// onto LaunchSpec directly.
type LaunchSpec struct {
// Owner and OwnerAdmission are optional for legacy task launches. Run-owned
// Office launches must provide both so admission remains strict.
Owner lifecycle.ExecutionOwner
OwnerAdmission OwnerAdmission
// AgentProfileID identifies the agent profile to run (e.g. "claude-acp-default").
AgentProfileID string
// ExecutorID identifies the executor backend to dispatch on
// (e.g. "local_pc", "local_docker", "sprites").
ExecutorID string
// Workspace describes the workspace the agent operates in.
Workspace WorkspaceRef
// Prompt is the initial prompt sent on session start.
Prompt string
// PriorACPSession is the ACP session id to resume, if any.
PriorACPSession string
// McpMode selects the MCP tool surface ("task", "config", or "office").
McpMode string
// Metadata carries integration-specific fields. Phase 1 supports
// the key "launch_request" with value `*lifecycle.LaunchRequest` to
// pass through fields not yet on LaunchSpec.
Metadata map[string]any
}
// WorkspaceRef identifies the workspace and (optionally) the worktree
// the agent should operate in.
type WorkspaceRef struct {
// Path is the host path to the workspace (or worktree root).
Path string
// RepositoryID, when non-empty, identifies the repository this workspace belongs to.
RepositoryID string
// IsEphemeral marks workspaces that may receive a fallback directory
// when no repository is configured.
IsEphemeral bool
}
// ExecutionRef is a lightweight handle to a launched execution.
type ExecutionRef struct {
// ID is the runtime-assigned execution id.
ID string
// SessionID is the task session this execution belongs to (if any).
SessionID string
// AgentctlURL is the host:port the agentctl HTTP server is reachable on.
AgentctlURL string
// StartedAt is when the runtime registered the execution.
StartedAt time.Time
}
// Execution is a snapshot view of a running execution.
type Execution struct {
ID string
SessionID string
TaskID string
AgentProfileID string
WorkspacePath string
AgentctlURL string // base URL of the agentctl HTTP server (empty when not yet wired)
Status v1.AgentStatus
StartedAt time.Time
FinishedAt *time.Time
ExitCode *int
ErrorMessage string
ACPSessionID string
Owner lifecycle.ExecutionOwner
Metadata map[string]interface{}
}
// Event is the protocol-agnostic event shape produced by an agent
// process. Re-exported from the agentctl streams types for ergonomics:
// callers don't need to import a sibling package for a tiny struct.
type Event = streams.AgentEvent
// ErrNotFound reports that an execution lookup or stop found no live runtime.
var ErrNotFound = errors.New("runtime: execution not found")
// ErrUnsupported is returned by methods that have not yet been wired up
// in the current implementation. Phase 1 leaves SubscribeEvents
// best-effort because the lifecycle manager publishes events via the
// global event bus rather than per-execution channels; callers that
// want raw streams should still use the agentctl client today.
var ErrUnsupported = errors.New("runtime: operation not supported")
// Backend is the lower-level dependency the default Runtime delegates to.
// Defining it as an interface lets tests substitute a fake without
// constructing a full lifecycle Manager.
type Backend interface {
Launch(ctx context.Context, req *lifecycle.LaunchRequest) (*lifecycle.AgentExecution, error)
StartAgentProcess(ctx context.Context, executionID string) error
PromptAgent(ctx context.Context, executionID string, prompt string, attachments []v1.MessageAttachment, dispatchOnly bool) (*lifecycle.PromptResult, error)
StopAgentWithReason(ctx context.Context, executionID string, reason string, force bool) error
GetExecution(executionID string) (*lifecycle.AgentExecution, bool)
SetMcpMode(ctx context.Context, executionID string, mode string) error
}
// Compile-time check: the lifecycle Manager satisfies Backend.
var _ Backend = (*lifecycle.Manager)(nil)
// RunOwnerRecovery reconciles durable run inventory before Office retries work.
// Implemented by the shared lifecycle backend; uncertain liveness is an error.
type RunOwnerRecovery interface {
StopRunOwnerForRecovery(context.Context, ExecutionOwner) error
}