feat(agentic): add state delete action
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
c99a8d4d7e
commit
dc9a81e101
4 changed files with 132 additions and 0 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue