agent/pkg/agentic/phase.go
Virgil 6bc24d5213 docs(ax): replace alias descriptions with usage examples
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-02 09:07:12 +00:00

209 lines
6.7 KiB
Go

// SPDX-License-Identifier: EUPL-1.2
package agentic
import (
"context"
"time"
core "dappco.re/go/core"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
// input := agentic.PhaseGetInput{PlanSlug: "my-plan-abc123", PhaseOrder: 1}
type PhaseGetInput struct {
PlanSlug string `json:"plan_slug"`
PhaseOrder int `json:"phase_order"`
}
// input := agentic.PhaseStatusInput{PlanSlug: "my-plan-abc123", PhaseOrder: 1, Status: "completed"}
type PhaseStatusInput struct {
PlanSlug string `json:"plan_slug"`
PhaseOrder int `json:"phase_order"`
Status string `json:"status"`
Reason string `json:"reason,omitempty"`
}
// input := agentic.PhaseCheckpointInput{PlanSlug: "my-plan-abc123", PhaseOrder: 1, Note: "Build passes"}
type PhaseCheckpointInput struct {
PlanSlug string `json:"plan_slug"`
PhaseOrder int `json:"phase_order"`
Note string `json:"note"`
Context map[string]any `json:"context,omitempty"`
}
// out := agentic.PhaseOutput{Success: true, Phase: agentic.Phase{Number: 1, Name: "Setup"}}
type PhaseOutput struct {
Success bool `json:"success"`
Phase Phase `json:"phase"`
}
// phase := agentic.AgentPhase{Number: 1, Name: "Build", Status: "in_progress"}
type AgentPhase = Phase
// result := c.Action("phase.get").Run(ctx, core.NewOptions(core.Option{Key: "plan_slug", Value: "my-plan-abc123"}))
func (s *PrepSubsystem) handlePhaseGet(ctx context.Context, options core.Options) core.Result {
_, output, err := s.phaseGet(ctx, nil, PhaseGetInput{
PlanSlug: optionStringValue(options, "plan_slug", "plan", "slug"),
PhaseOrder: optionIntValue(options, "phase_order", "phase"),
})
if err != nil {
return core.Result{Value: err, OK: false}
}
return core.Result{Value: output, OK: true}
}
// result := c.Action("phase.update_status").Run(ctx, core.NewOptions(core.Option{Key: "status", Value: "completed"}))
func (s *PrepSubsystem) handlePhaseUpdateStatus(ctx context.Context, options core.Options) core.Result {
_, output, err := s.phaseUpdateStatus(ctx, nil, PhaseStatusInput{
PlanSlug: optionStringValue(options, "plan_slug", "plan", "slug"),
PhaseOrder: optionIntValue(options, "phase_order", "phase"),
Status: optionStringValue(options, "status"),
Reason: optionStringValue(options, "reason"),
})
if err != nil {
return core.Result{Value: err, OK: false}
}
return core.Result{Value: output, OK: true}
}
// result := c.Action("phase.add_checkpoint").Run(ctx, core.NewOptions(core.Option{Key: "note", Value: "Build passes"}))
func (s *PrepSubsystem) handlePhaseAddCheckpoint(ctx context.Context, options core.Options) core.Result {
_, output, err := s.phaseAddCheckpoint(ctx, nil, PhaseCheckpointInput{
PlanSlug: optionStringValue(options, "plan_slug", "plan", "slug"),
PhaseOrder: optionIntValue(options, "phase_order", "phase"),
Note: optionStringValue(options, "note"),
Context: optionAnyMapValue(options, "context"),
})
if err != nil {
return core.Result{Value: err, OK: false}
}
return core.Result{Value: output, OK: true}
}
func (s *PrepSubsystem) registerPhaseTools(server *mcp.Server) {
mcp.AddTool(server, &mcp.Tool{
Name: "phase_get",
Description: "Get a phase by plan slug and phase order.",
}, s.phaseGet)
mcp.AddTool(server, &mcp.Tool{
Name: "phase_update_status",
Description: "Update a phase status by plan slug and phase order.",
}, s.phaseUpdateStatus)
mcp.AddTool(server, &mcp.Tool{
Name: "phase_add_checkpoint",
Description: "Append a checkpoint note to a phase.",
}, s.phaseAddCheckpoint)
}
func (s *PrepSubsystem) phaseGet(_ context.Context, _ *mcp.CallToolRequest, input PhaseGetInput) (*mcp.CallToolResult, PhaseOutput, error) {
plan, phaseIndex, err := planPhaseByOrder(PlansRoot(), input.PlanSlug, input.PhaseOrder)
if err != nil {
return nil, PhaseOutput{}, err
}
return nil, PhaseOutput{
Success: true,
Phase: plan.Phases[phaseIndex],
}, nil
}
func (s *PrepSubsystem) phaseUpdateStatus(_ context.Context, _ *mcp.CallToolRequest, input PhaseStatusInput) (*mcp.CallToolResult, PhaseOutput, error) {
if !validPhaseStatus(input.Status) {
return nil, PhaseOutput{}, core.E("phaseUpdateStatus", core.Concat("invalid status: ", input.Status), nil)
}
plan, phaseIndex, err := planPhaseByOrder(PlansRoot(), input.PlanSlug, input.PhaseOrder)
if err != nil {
return nil, PhaseOutput{}, err
}
plan.Phases[phaseIndex].Status = input.Status
if reason := core.Trim(input.Reason); reason != "" {
plan.Phases[phaseIndex].Notes = appendPlanNote(plan.Phases[phaseIndex].Notes, reason)
}
plan.UpdatedAt = time.Now()
if result := writePlanResult(PlansRoot(), plan); !result.OK {
err, _ := result.Value.(error)
if err == nil {
err = core.E("phaseUpdateStatus", "failed to write plan", nil)
}
return nil, PhaseOutput{}, err
}
return nil, PhaseOutput{
Success: true,
Phase: plan.Phases[phaseIndex],
}, nil
}
func (s *PrepSubsystem) phaseAddCheckpoint(_ context.Context, _ *mcp.CallToolRequest, input PhaseCheckpointInput) (*mcp.CallToolResult, PhaseOutput, error) {
if core.Trim(input.Note) == "" {
return nil, PhaseOutput{}, core.E("phaseAddCheckpoint", "note is required", nil)
}
plan, phaseIndex, err := planPhaseByOrder(PlansRoot(), input.PlanSlug, input.PhaseOrder)
if err != nil {
return nil, PhaseOutput{}, err
}
plan.Phases[phaseIndex].Checkpoints = append(plan.Phases[phaseIndex].Checkpoints, PhaseCheckpoint{
Note: input.Note,
Context: input.Context,
CreatedAt: time.Now().Format(time.RFC3339),
})
plan.UpdatedAt = time.Now()
if result := writePlanResult(PlansRoot(), plan); !result.OK {
err, _ := result.Value.(error)
if err == nil {
err = core.E("phaseAddCheckpoint", "failed to write plan", nil)
}
return nil, PhaseOutput{}, err
}
return nil, PhaseOutput{
Success: true,
Phase: plan.Phases[phaseIndex],
}, nil
}
func planPhaseByOrder(dir, planSlug string, phaseOrder int) (*Plan, int, error) {
if core.Trim(planSlug) == "" {
return nil, 0, core.E("planPhaseByOrder", "plan_slug is required", nil)
}
if phaseOrder <= 0 {
return nil, 0, core.E("planPhaseByOrder", "phase_order is required", nil)
}
plan, err := readPlan(dir, planSlug)
if err != nil {
return nil, 0, err
}
for index := range plan.Phases {
if plan.Phases[index].Number == phaseOrder {
return plan, index, nil
}
}
return nil, 0, core.E("planPhaseByOrder", core.Concat("phase not found: ", core.Sprint(phaseOrder)), nil)
}
func appendPlanNote(existing, note string) string {
if existing == "" {
return note
}
return core.Concat(existing, "\n", note)
}
func validPhaseStatus(status string) bool {
switch status {
case "pending", "in_progress", "completed", "blocked", "skipped":
return true
}
return false
}