Files
krow_backend/go-api/internal/runtime/executor.go
2026-08-28 12:21:44 +05:30

145 lines
4.4 KiB
Go

package runtime
import (
"context"
"fmt"
"github.com/krow/krow-backend/go-api/internal/authctx"
"github.com/krow/krow-backend/go-api/internal/repo"
)
// AgentExecutor is the boundary interface for executing an authored agent.
type AgentExecutor interface {
ExecuteAgent(ctx context.Context, agent *Agent, input ExecutionInput) (*ExecutionResult, error)
}
// SkillExecutor is the boundary interface for executing an authored skill.
type SkillExecutor interface {
ExecuteSkill(ctx context.Context, skill *Skill, input ExecutionInput) (*ExecutionResult, error)
}
// UnavailableExecutor is the Phase 4F default stub executor that explicitly refuses execution
// until real AI / Owliver / LangGraph executors are installed in Phase 5.
type UnavailableExecutor struct{}
// ExecuteAgent implements AgentExecutor by returning ErrExecutorUnavailable.
func (u *UnavailableExecutor) ExecuteAgent(_ context.Context, agent *Agent, _ ExecutionInput) (*ExecutionResult, error) {
skillIDs := make([]string, len(agent.ResolvedSkills))
for i, s := range agent.ResolvedSkills {
skillIDs[i] = s.ID
}
return &ExecutionResult{
Success: false,
AgentID: agent.ID,
AgentVersion: agent.Version,
ResolvedSkills: skillIDs,
Error: ErrExecutorUnavailable,
}, ErrExecutorUnavailable
}
// ExecuteSkill implements SkillExecutor by returning ErrExecutorUnavailable.
func (u *UnavailableExecutor) ExecuteSkill(_ context.Context, skill *Skill, _ ExecutionInput) (*ExecutionResult, error) {
return &ExecutionResult{
Success: false,
Error: ErrExecutorUnavailable,
}, ErrExecutorUnavailable
}
// Engine coordinates runtime loading, eligibility checks, dependency resolution and execution.
type Engine struct {
Loader *Loader
AgentExec AgentExecutor
SkillExec SkillExecutor
}
// Option configures the runtime engine.
type Option func(*Engine)
// WithAgentExecutor overrides the agent executor implementation.
func WithAgentExecutor(exec AgentExecutor) Option {
return func(e *Engine) {
if exec != nil {
e.AgentExec = exec
}
}
}
// WithSkillExecutor overrides the skill executor implementation.
func WithSkillExecutor(exec SkillExecutor) Option {
return func(e *Engine) {
if exec != nil {
e.SkillExec = exec
}
}
}
// NewEngine builds a runtime engine over a database querier.
func NewEngine(db repo.Querier, opts ...Option) *Engine {
e := &Engine{
Loader: NewLoader(db),
AgentExec: &UnavailableExecutor{},
SkillExec: &UnavailableExecutor{},
}
for _, opt := range opts {
opt(e)
}
return e
}
// RunAgent loads an executable agent with dependencies and dispatches to the executor boundary.
func (e *Engine) RunAgent(ctx context.Context, ident authctx.Identity, idOrDefID string, input ExecutionInput) (*ExecutionResult, error) {
// A pinned run loads the agent AS IT WAS. §3: running conversations pin the
// version they started with — which matters most on a resumed run, where a
// person approved a write while looking at one version and an edit may have
// landed since.
agent, unpinned, err := e.Loader.LoadAgentVersion(ctx, ident, idOrDefID, input.AgentVersion)
if err != nil {
return &ExecutionResult{
Success: false,
Error: err,
}, err
}
if unpinned {
// Asked for a version that has no snapshot — a definition published
// before versions were recorded. The run proceeds on the current
// definition rather than failing, and says so, because a silent
// substitution is the thing worth preventing.
input.Notes = append(input.Notes, fmt.Sprintf(
"version_unavailable: version %d of %s is not in the published history; "+
"this run used the current definition (v%d)",
input.AgentVersion, agent.ID, agent.Version))
}
res, err := e.AgentExec.ExecuteAgent(ctx, agent, input)
if res == nil {
res = &ExecutionResult{
Success: err == nil,
AgentID: agent.ID,
AgentVersion: agent.Version,
Error: err,
}
}
return res, err
}
// RunSkill loads an executable skill and dispatches to the executor boundary.
func (e *Engine) RunSkill(ctx context.Context, ident authctx.Identity, idOrDefID string, input ExecutionInput) (*ExecutionResult, error) {
skill, err := e.Loader.LoadExecutableSkill(ctx, ident, idOrDefID)
if err != nil {
return &ExecutionResult{
Success: false,
Error: err,
}, err
}
res, err := e.SkillExec.ExecuteSkill(ctx, skill, input)
if res == nil {
res = &ExecutionResult{
Success: err == nil,
Error: err,
}
}
return res, err
}