feat(agentic): add state command surface

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-02 00:42:55 +00:00
parent dce59978aa
commit f07ea569ba
4 changed files with 331 additions and 0 deletions

View file

@ -72,6 +72,7 @@ func (s *PrepSubsystem) registerCommands(ctx context.Context) {
s.registerPlanCommands()
s.registerSessionCommands()
s.registerTaskCommands()
s.registerStateCommands()
s.registerLanguageCommands()
s.registerSetupCommands()
}

View file

@ -0,0 +1,174 @@
// SPDX-License-Identifier: EUPL-1.2
package agentic
import (
core "dappco.re/go/core"
)
func (s *PrepSubsystem) registerStateCommands() {
c := s.Core()
c.Command("state", core.Command{Description: "Manage shared plan state", Action: s.cmdState})
c.Command("agentic:state", core.Command{Description: "Manage shared plan state", Action: s.cmdState})
c.Command("state/set", core.Command{Description: "Store shared plan state", Action: s.cmdStateSet})
c.Command("agentic:state/set", core.Command{Description: "Store shared plan state", Action: s.cmdStateSet})
c.Command("state/get", core.Command{Description: "Read shared plan state by key", Action: s.cmdStateGet})
c.Command("agentic:state/get", core.Command{Description: "Read shared plan state by key", Action: s.cmdStateGet})
c.Command("state/list", core.Command{Description: "List shared plan state for a plan", Action: s.cmdStateList})
c.Command("agentic:state/list", core.Command{Description: "List shared plan state for a plan", Action: s.cmdStateList})
c.Command("state/delete", core.Command{Description: "Delete shared plan state by key", Action: s.cmdStateDelete})
c.Command("agentic:state/delete", core.Command{Description: "Delete shared plan state by key", Action: s.cmdStateDelete})
}
func (s *PrepSubsystem) cmdState(options core.Options) core.Result {
switch action := optionStringValue(options, "action"); action {
case "set":
return s.cmdStateSet(options)
case "get":
return s.cmdStateGet(options)
case "list":
return s.cmdStateList(options)
case "delete":
return s.cmdStateDelete(options)
case "":
core.Print(nil, "usage: core-agent state set <plan> --key=pattern --value=observer [--type=general] [--description=\"Shared across sessions\"]")
core.Print(nil, " core-agent state get <plan> --key=pattern")
core.Print(nil, " core-agent state list <plan> [--type=general] [--category=general]")
core.Print(nil, " core-agent state delete <plan> --key=pattern")
return core.Result{OK: true}
default:
core.Print(nil, "usage: core-agent state set <plan> --key=pattern --value=observer [--type=general] [--description=\"Shared across sessions\"]")
core.Print(nil, " core-agent state get <plan> --key=pattern")
core.Print(nil, " core-agent state list <plan> [--type=general] [--category=general]")
core.Print(nil, " core-agent state delete <plan> --key=pattern")
return core.Result{Value: core.E("agentic.cmdState", core.Concat("unknown state command: ", action), nil), OK: false}
}
}
func (s *PrepSubsystem) cmdStateSet(options core.Options) core.Result {
result := s.handleStateSet(s.commandContext(), core.NewOptions(
core.Option{Key: "plan_slug", Value: optionStringValue(options, "plan_slug", "plan", "slug", "_arg")},
core.Option{Key: "key", Value: optionStringValue(options, "key")},
core.Option{Key: "value", Value: optionAnyValue(options, "value")},
core.Option{Key: "type", Value: optionStringValue(options, "type")},
core.Option{Key: "description", Value: optionStringValue(options, "description")},
core.Option{Key: "category", Value: optionStringValue(options, "category")},
))
if !result.OK {
err := commandResultError("agentic.cmdStateSet", result)
core.Print(nil, "error: %v", err)
return core.Result{Value: err, OK: false}
}
output, ok := result.Value.(StateOutput)
if !ok {
err := core.E("agentic.cmdStateSet", "invalid state set output", nil)
core.Print(nil, "error: %v", err)
return core.Result{Value: err, OK: false}
}
core.Print(nil, "key: %s", output.State.Key)
core.Print(nil, "type: %s", output.State.Type)
if output.State.Description != "" {
core.Print(nil, "desc: %s", output.State.Description)
}
core.Print(nil, "value: %s", stateValueString(output.State.Value))
return core.Result{Value: output, OK: true}
}
func (s *PrepSubsystem) cmdStateGet(options core.Options) core.Result {
result := s.handleStateGet(s.commandContext(), core.NewOptions(
core.Option{Key: "plan_slug", Value: optionStringValue(options, "plan_slug", "plan", "slug", "_arg")},
core.Option{Key: "key", Value: optionStringValue(options, "key")},
))
if !result.OK {
err := commandResultError("agentic.cmdStateGet", result)
core.Print(nil, "error: %v", err)
return core.Result{Value: err, OK: false}
}
output, ok := result.Value.(StateOutput)
if !ok {
err := core.E("agentic.cmdStateGet", "invalid state get output", nil)
core.Print(nil, "error: %v", err)
return core.Result{Value: err, OK: false}
}
core.Print(nil, "key: %s", output.State.Key)
core.Print(nil, "type: %s", output.State.Type)
if output.State.Description != "" {
core.Print(nil, "desc: %s", output.State.Description)
}
if output.State.UpdatedAt != "" {
core.Print(nil, "updated: %s", output.State.UpdatedAt)
}
core.Print(nil, "value: %s", stateValueString(output.State.Value))
return core.Result{Value: output, OK: true}
}
func (s *PrepSubsystem) cmdStateList(options core.Options) core.Result {
result := s.handleStateList(s.commandContext(), core.NewOptions(
core.Option{Key: "plan_slug", Value: optionStringValue(options, "plan_slug", "plan", "slug", "_arg")},
core.Option{Key: "type", Value: optionStringValue(options, "type")},
core.Option{Key: "category", Value: optionStringValue(options, "category")},
))
if !result.OK {
err := commandResultError("agentic.cmdStateList", result)
core.Print(nil, "error: %v", err)
return core.Result{Value: err, OK: false}
}
output, ok := result.Value.(StateListOutput)
if !ok {
err := core.E("agentic.cmdStateList", "invalid state list output", nil)
core.Print(nil, "error: %v", err)
return core.Result{Value: err, OK: false}
}
if output.Total == 0 {
core.Print(nil, "no states")
return core.Result{Value: output, OK: true}
}
for _, state := range output.States {
core.Print(nil, " %-20s %-12s %s", state.Key, state.Type, stateValueString(state.Value))
}
core.Print(nil, "%d state(s)", output.Total)
return core.Result{Value: output, OK: true}
}
func (s *PrepSubsystem) cmdStateDelete(options core.Options) core.Result {
result := s.handleStateDelete(s.commandContext(), core.NewOptions(
core.Option{Key: "plan_slug", Value: optionStringValue(options, "plan_slug", "plan", "slug", "_arg")},
core.Option{Key: "key", Value: optionStringValue(options, "key")},
))
if !result.OK {
err := commandResultError("agentic.cmdStateDelete", result)
core.Print(nil, "error: %v", err)
return core.Result{Value: err, OK: false}
}
output, ok := result.Value.(StateDeleteOutput)
if !ok {
err := core.E("agentic.cmdStateDelete", "invalid state delete output", nil)
core.Print(nil, "error: %v", err)
return core.Result{Value: err, OK: false}
}
core.Print(nil, "deleted: %s", output.Deleted.Key)
core.Print(nil, "type: %s", output.Deleted.Type)
return core.Result{Value: output, OK: true}
}
func stateValueString(value any) string {
if text, ok := value.(string); ok {
return text
}
jsonValue := core.JSONMarshalString(value)
if jsonValue != "" {
return jsonValue
}
return core.Sprint(value)
}

View file

@ -0,0 +1,151 @@
// SPDX-License-Identifier: EUPL-1.2
package agentic
import (
"testing"
core "dappco.re/go/core"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCommandsState_RegisterStateCommands_Good(t *testing.T) {
s, c := testPrepWithCore(t, nil)
s.registerStateCommands()
assert.Contains(t, c.Commands(), "state")
assert.Contains(t, c.Commands(), "agentic:state")
assert.Contains(t, c.Commands(), "state/set")
assert.Contains(t, c.Commands(), "state/get")
assert.Contains(t, c.Commands(), "state/list")
assert.Contains(t, c.Commands(), "state/delete")
}
func TestCommandsState_CmdStateSet_Good(t *testing.T) {
s, _ := testPrepWithCore(t, nil)
result := s.cmdStateSet(core.NewOptions(
core.Option{Key: "_arg", Value: "ax-follow-up"},
core.Option{Key: "key", Value: "pattern"},
core.Option{Key: "value", Value: "observer"},
core.Option{Key: "type", Value: "general"},
core.Option{Key: "description", Value: "Shared across sessions"},
))
require.True(t, result.OK)
output, ok := result.Value.(StateOutput)
require.True(t, ok)
assert.Equal(t, "pattern", output.State.Key)
assert.Equal(t, "general", output.State.Type)
assert.Equal(t, "observer", output.State.Value)
assert.Equal(t, "Shared across sessions", output.State.Description)
}
func TestCommandsState_CmdStateSet_Bad_MissingValue(t *testing.T) {
s, _ := testPrepWithCore(t, nil)
result := s.cmdStateSet(core.NewOptions(
core.Option{Key: "_arg", Value: "ax-follow-up"},
core.Option{Key: "key", Value: "pattern"},
))
assert.False(t, result.OK)
}
func TestCommandsState_CmdStateGet_Good(t *testing.T) {
s, _ := testPrepWithCore(t, nil)
_, output, err := s.stateSet(s.commandContext(), nil, StateSetInput{
PlanSlug: "ax-follow-up",
Key: "pattern",
Value: "observer",
Type: "general",
})
require.NoError(t, err)
require.Equal(t, "pattern", output.State.Key)
result := s.cmdStateGet(core.NewOptions(
core.Option{Key: "_arg", Value: "ax-follow-up"},
core.Option{Key: "key", Value: "pattern"},
))
require.True(t, result.OK)
stateOutput, ok := result.Value.(StateOutput)
require.True(t, ok)
assert.Equal(t, "pattern", stateOutput.State.Key)
assert.Equal(t, "observer", stateOutput.State.Value)
}
func TestCommandsState_CmdStateGet_Bad_MissingKey(t *testing.T) {
s, _ := testPrepWithCore(t, nil)
result := s.cmdStateGet(core.NewOptions(core.Option{Key: "_arg", Value: "ax-follow-up"}))
assert.False(t, result.OK)
}
func TestCommandsState_CmdStateList_Good(t *testing.T) {
s, _ := testPrepWithCore(t, nil)
_, _, err := s.stateSet(s.commandContext(), nil, StateSetInput{
PlanSlug: "ax-follow-up",
Key: "pattern",
Value: "observer",
Type: "general",
})
require.NoError(t, err)
result := s.cmdStateList(core.NewOptions(core.Option{Key: "_arg", Value: "ax-follow-up"}))
require.True(t, result.OK)
listOutput, ok := result.Value.(StateListOutput)
require.True(t, ok)
assert.Equal(t, 1, listOutput.Total)
assert.Len(t, listOutput.States, 1)
}
func TestCommandsState_CmdStateList_Ugly_EmptyPlan(t *testing.T) {
s, _ := testPrepWithCore(t, nil)
result := s.cmdStateList(core.NewOptions(core.Option{Key: "_arg", Value: "ax-follow-up"}))
require.True(t, result.OK)
listOutput, ok := result.Value.(StateListOutput)
require.True(t, ok)
assert.Zero(t, listOutput.Total)
assert.Empty(t, listOutput.States)
}
func TestCommandsState_CmdStateDelete_Good(t *testing.T) {
s, _ := testPrepWithCore(t, nil)
_, _, err := s.stateSet(s.commandContext(), nil, StateSetInput{
PlanSlug: "ax-follow-up",
Key: "pattern",
Value: "observer",
Type: "general",
})
require.NoError(t, err)
result := s.cmdStateDelete(core.NewOptions(
core.Option{Key: "_arg", Value: "ax-follow-up"},
core.Option{Key: "key", Value: "pattern"},
))
require.True(t, result.OK)
deleteOutput, ok := result.Value.(StateDeleteOutput)
require.True(t, ok)
assert.Equal(t, "pattern", deleteOutput.Deleted.Key)
assert.False(t, fs.Exists(statePath("ax-follow-up")))
}
func TestCommandsState_CmdStateDelete_Bad_MissingKey(t *testing.T) {
s, _ := testPrepWithCore(t, nil)
result := s.cmdStateDelete(core.NewOptions(core.Option{Key: "_arg", Value: "ax-follow-up"}))
assert.False(t, result.OK)
}

View file

@ -711,6 +711,11 @@ func TestPrep_OnStartup_Good_RegistersGenerateCommand(t *testing.T) {
assert.Contains(t, c.Commands(), "task/create")
assert.Contains(t, c.Commands(), "task/update")
assert.Contains(t, c.Commands(), "task/toggle")
assert.Contains(t, c.Commands(), "state")
assert.Contains(t, c.Commands(), "state/set")
assert.Contains(t, c.Commands(), "state/get")
assert.Contains(t, c.Commands(), "state/list")
assert.Contains(t, c.Commands(), "state/delete")
}
func TestPrep_OnStartup_Bad(t *testing.T) {