feat(agentic): add state delete action

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-01 13:58:16 +00:00
parent c99a8d4d7e
commit dc9a81e101
4 changed files with 132 additions and 0 deletions

View file

@ -206,6 +206,7 @@ func (s *PrepSubsystem) OnStartup(ctx context.Context) core.Result {
c.Action("state.set", s.handleStateSet).Description = "Store shared plan state for later sessions"
c.Action("state.get", s.handleStateGet).Description = "Read shared plan state by key"
c.Action("state.list", s.handleStateList).Description = "List shared plan state for a plan"
c.Action("state.delete", s.handleStateDelete).Description = "Delete shared plan state by key"
c.Action("template.list", s.handleTemplateList).Description = "List available YAML plan templates"
c.Action("template.preview", s.handleTemplatePreview).Description = "Preview a YAML plan template with variable substitution"
c.Action("template.create_plan", s.handleTemplateCreatePlan).Description = "Create a stored plan from a YAML template"

View file

@ -478,6 +478,7 @@ func TestPrep_OnStartup_Good_RegistersSessionActions(t *testing.T) {
assert.True(t, c.Action("state.set").Exists())
assert.True(t, c.Action("state.get").Exists())
assert.True(t, c.Action("state.list").Exists())
assert.True(t, c.Action("state.delete").Exists())
assert.True(t, c.Action("issue.create").Exists())
assert.True(t, c.Action("issue.get").Exists())
assert.True(t, c.Action("issue.list").Exists())

View file

@ -46,6 +46,12 @@ type StateListInput struct {
Category string `json:"category,omitempty"`
}
// input := agentic.StateDeleteInput{PlanSlug: "ax-follow-up", Key: "pattern"}
type StateDeleteInput struct {
PlanSlug string `json:"plan_slug"`
Key string `json:"key"`
}
// out := agentic.StateOutput{Success: true, State: agentic.WorkspaceState{Key: "pattern"}}
type StateOutput struct {
Success bool `json:"success"`
@ -59,6 +65,12 @@ type StateListOutput struct {
States []WorkspaceState `json:"states"`
}
// out := agentic.StateDeleteOutput{Success: true, Deleted: agentic.WorkspaceState{Key: "pattern"}}
type StateDeleteOutput struct {
Success bool `json:"success"`
Deleted WorkspaceState `json:"deleted"`
}
// result := c.Action("state.set").Run(ctx, core.NewOptions(
//
// core.Option{Key: "plan_slug", Value: "ax-follow-up"},
@ -111,6 +123,23 @@ func (s *PrepSubsystem) handleStateList(ctx context.Context, options core.Option
return core.Result{Value: output, OK: true}
}
// result := c.Action("state.delete").Run(ctx, core.NewOptions(
//
// core.Option{Key: "plan_slug", Value: "ax-follow-up"},
// core.Option{Key: "key", Value: "pattern"},
//
// ))
func (s *PrepSubsystem) handleStateDelete(ctx context.Context, options core.Options) core.Result {
_, output, err := s.stateDelete(ctx, nil, StateDeleteInput{
PlanSlug: optionStringValue(options, "plan_slug", "plan"),
Key: optionStringValue(options, "key"),
})
if err != nil {
return core.Result{Value: err, OK: false}
}
return core.Result{Value: output, OK: true}
}
func (s *PrepSubsystem) registerStateTools(server *mcp.Server) {
mcp.AddTool(server, &mcp.Tool{
Name: "state_set",
@ -126,6 +155,11 @@ func (s *PrepSubsystem) registerStateTools(server *mcp.Server) {
Name: "state_list",
Description: "List all stored workspace state values for a plan, with optional type or category filtering.",
}, s.stateList)
mcp.AddTool(server, &mcp.Tool{
Name: "state_delete",
Description: "Delete a stored workspace state value for a plan by key.",
}, s.stateDelete)
}
func (s *PrepSubsystem) stateSet(_ context.Context, _ *mcp.CallToolRequest, input StateSetInput) (*mcp.CallToolResult, StateOutput, error) {
@ -238,6 +272,51 @@ func (s *PrepSubsystem) stateList(_ context.Context, _ *mcp.CallToolRequest, inp
}, nil
}
func (s *PrepSubsystem) stateDelete(_ context.Context, _ *mcp.CallToolRequest, input StateDeleteInput) (*mcp.CallToolResult, StateDeleteOutput, error) {
if input.PlanSlug == "" {
return nil, StateDeleteOutput{}, core.E("stateDelete", "plan_slug is required", nil)
}
if input.Key == "" {
return nil, StateDeleteOutput{}, core.E("stateDelete", "key is required", nil)
}
states, err := readPlanStates(input.PlanSlug)
if err != nil {
return nil, StateDeleteOutput{}, err
}
filtered := make([]WorkspaceState, 0, len(states))
deleted := WorkspaceState{}
found := false
for _, state := range states {
if state.Key == input.Key {
deleted = normaliseWorkspaceState(state)
found = true
continue
}
filtered = append(filtered, state)
}
if !found {
return nil, StateDeleteOutput{}, core.E("stateDelete", core.Concat("state not found: ", input.Key), nil)
}
path := statePath(input.PlanSlug)
if len(filtered) == 0 {
if deleteResult := fs.Delete(path); !deleteResult.OK {
err, _ := deleteResult.Value.(error)
return nil, StateDeleteOutput{}, core.E("stateDelete", "failed to delete empty state file", err)
}
} else if err := writePlanStates(input.PlanSlug, filtered); err != nil {
return nil, StateDeleteOutput{}, err
}
return nil, StateDeleteOutput{
Success: true,
Deleted: deleted,
}, nil
}
func stateRoot() string {
return core.JoinPath(CoreRoot(), "state")
}

View file

@ -145,3 +145,54 @@ func TestState_HandleStateList_Ugly_CorruptStateFile(t *testing.T) {
))
assert.False(t, result.OK)
}
func TestState_HandleStateDelete_Good(t *testing.T) {
subsystem := testPrepWithPlatformServer(t, nil, "")
require.NoError(t, writePlanStates("ax-follow-up", []WorkspaceState{
{Key: "pattern", Value: "observer", Type: "general", Description: "Shared across sessions"},
{Key: "risk", Value: "auth", Type: "security"},
}))
result := subsystem.handleStateDelete(context.Background(), core.NewOptions(
core.Option{Key: "plan_slug", Value: "ax-follow-up"},
core.Option{Key: "key", Value: "pattern"},
))
require.True(t, result.OK)
output, ok := result.Value.(StateDeleteOutput)
require.True(t, ok)
assert.Equal(t, "pattern", output.Deleted.Key)
assert.Equal(t, "general", output.Deleted.Type)
assert.Equal(t, "Shared across sessions", output.Deleted.Description)
states, err := readPlanStates("ax-follow-up")
require.NoError(t, err)
require.Len(t, states, 1)
assert.Equal(t, "risk", states[0].Key)
require.True(t, subsystem.handleStateDelete(context.Background(), core.NewOptions(
core.Option{Key: "plan_slug", Value: "ax-follow-up"},
core.Option{Key: "key", Value: "risk"},
)).OK)
assert.False(t, fs.Exists(statePath("ax-follow-up")))
}
func TestState_HandleStateDelete_Bad(t *testing.T) {
subsystem := testPrepWithPlatformServer(t, nil, "")
result := subsystem.handleStateDelete(context.Background(), core.NewOptions(
core.Option{Key: "plan_slug", Value: "ax-follow-up"},
))
assert.False(t, result.OK)
}
func TestState_HandleStateDelete_Ugly_CorruptStateFile(t *testing.T) {
subsystem := testPrepWithPlatformServer(t, nil, "")
require.True(t, fs.EnsureDir(stateRoot()).OK)
require.True(t, fs.Write(statePath("ax-follow-up"), `{broken`).OK)
result := subsystem.handleStateDelete(context.Background(), core.NewOptions(
core.Option{Key: "plan_slug", Value: "ax-follow-up"},
core.Option{Key: "key", Value: "pattern"},
))
assert.False(t, result.OK)
}