145 lines
4.4 KiB
Go
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
|
|
}
|