fix(agentic): add task aliases and session model normalization

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-02 07:59:58 +00:00
parent 425008f855
commit 9f9e42768d
6 changed files with 119 additions and 14 deletions

View file

@ -123,16 +123,16 @@ func (s *PrepSubsystem) cmdSessionList(options core.Options) core.Result {
return core.Result{Value: output, OK: true}
}
// core-agent session start ax-follow-up --agent-type=codex
// core-agent session start ax-follow-up --agent-type=claude:opus
func (s *PrepSubsystem) cmdSessionStart(options core.Options) core.Result {
planSlug := optionStringValue(options, "plan_slug", "plan", "_arg")
agentType := optionStringValue(options, "agent_type", "agent")
if planSlug == "" {
core.Print(nil, "usage: core-agent session start <plan-slug> --agent-type=codex [--context='{\"repo\":\"go-io\"}']")
core.Print(nil, "usage: core-agent session start <plan-slug> --agent-type=claude:opus [--context='{\"repo\":\"go-io\"}']")
return core.Result{Value: core.E("agentic.cmdSessionStart", "plan_slug is required", nil), OK: false}
}
if agentType == "" {
core.Print(nil, "usage: core-agent session start <plan-slug> --agent-type=codex [--context='{\"repo\":\"go-io\"}']")
core.Print(nil, "usage: core-agent session start <plan-slug> --agent-type=claude:opus [--context='{\"repo\":\"go-io\"}']")
return core.Result{Value: core.E("agentic.cmdSessionStart", "agent_type is required", nil), OK: false}
}

View file

@ -123,6 +123,37 @@ func TestCommandsSession_CmdSessionStart_Good(t *testing.T) {
assert.Equal(t, "opus", output.Session.AgentType)
}
func TestCommandsSession_CmdSessionStart_Good_CanonicalAlias(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/v1/sessions", r.URL.Path)
require.Equal(t, http.MethodPost, r.Method)
bodyResult := core.ReadAll(r.Body)
require.True(t, bodyResult.OK)
var payload map[string]any
parseResult := core.JSONUnmarshalString(bodyResult.Value.(string), &payload)
require.True(t, parseResult.OK)
assert.Equal(t, "opus", payload["agent_type"])
_, _ = w.Write([]byte(`{"data":{"session_id":"ses-start","plan_slug":"ax-follow-up","agent_type":"opus","status":"active"}}`))
}))
defer server.Close()
subsystem := testPrepWithPlatformServer(t, server, "secret-token")
result := subsystem.cmdSessionStart(core.NewOptions(
core.Option{Key: "_arg", Value: "ax-follow-up"},
core.Option{Key: "agent_type", Value: "claude:opus"},
))
require.True(t, result.OK)
output, ok := result.Value.(SessionOutput)
require.True(t, ok)
assert.Equal(t, "ses-start", output.Session.SessionID)
assert.Equal(t, "ax-follow-up", output.Session.PlanSlug)
assert.Equal(t, "opus", output.Session.AgentType)
}
func TestCommandsSession_CmdSessionStart_Bad_MissingPlanSlug(t *testing.T) {
subsystem := testPrepWithPlatformServer(t, nil, "secret-token")
@ -143,7 +174,7 @@ func TestCommandsSession_CmdSessionStart_Bad_InvalidAgentType(t *testing.T) {
assert.False(t, result.OK)
require.Error(t, result.Value.(error))
assert.Contains(t, result.Value.(error).Error(), "opus, sonnet, or haiku")
assert.Contains(t, result.Value.(error).Error(), "claude:opus")
}
func TestCommandsSession_CmdSessionStart_Ugly_InvalidResponse(t *testing.T) {

View file

@ -11,12 +11,15 @@ func (s *PrepSubsystem) registerTaskCommands() {
c.Command("task", core.Command{Description: "Manage plan tasks", Action: s.cmdTask})
c.Command("agentic:task", core.Command{Description: "Manage plan tasks", Action: s.cmdTask})
c.Command("task/create", core.Command{Description: "Create a task in a plan phase", Action: s.cmdTaskCreate})
c.Command("agentic:task/create", core.Command{Description: "Create a task in a plan phase", Action: s.cmdTaskCreate})
c.Command("task/update", core.Command{Description: "Update a plan task status, notes, priority, or category", Action: s.cmdTaskUpdate})
c.Command("agentic:task/update", core.Command{Description: "Update a plan task status, notes, priority, or category", Action: s.cmdTaskUpdate})
c.Command("task/toggle", core.Command{Description: "Toggle a plan task between pending and completed", Action: s.cmdTaskToggle})
c.Command("agentic:task/toggle", core.Command{Description: "Toggle a plan task between pending and completed", Action: s.cmdTaskToggle})
}
func (s *PrepSubsystem) cmdTask(options core.Options) core.Result {
action := optionStringValue(options, "action")
action := optionStringValue(options, "action", "_arg")
switch action {
case "create":
return s.cmdTaskCreate(options)

View file

@ -58,6 +58,9 @@ func TestCommands_TaskCommand_Good_SpecAliasRegistered(t *testing.T) {
s.registerTaskCommands()
assert.Contains(t, c.Commands(), "agentic:task")
assert.Contains(t, c.Commands(), "agentic:task/create")
assert.Contains(t, c.Commands(), "agentic:task/update")
assert.Contains(t, c.Commands(), "agentic:task/toggle")
}
func TestCommands_TaskCommand_Good_Create(t *testing.T) {

View file

@ -408,12 +408,13 @@ func (s *PrepSubsystem) sessionStart(ctx context.Context, _ *mcp.CallToolRequest
if input.AgentType == "" {
return nil, SessionOutput{}, core.E("sessionStart", "agent_type is required", nil)
}
if !validSessionAgentType(input.AgentType) {
return nil, SessionOutput{}, core.E("sessionStart", "agent_type must be opus, sonnet, or haiku", nil)
normalisedAgentType, ok := normaliseSessionAgentType(input.AgentType)
if !ok {
return nil, SessionOutput{}, core.E("sessionStart", "agent_type must be opus, sonnet, haiku, or claude:opus|claude:sonnet|claude:haiku", nil)
}
body := map[string]any{
"agent_type": input.AgentType,
"agent_type": normalisedAgentType,
}
if input.PlanSlug != "" {
body["plan_slug"] = input.PlanSlug
@ -453,7 +454,11 @@ func (s *PrepSubsystem) sessionGet(ctx context.Context, _ *mcp.CallToolRequest,
func (s *PrepSubsystem) sessionList(ctx context.Context, _ *mcp.CallToolRequest, input SessionListInput) (*mcp.CallToolResult, SessionListOutput, error) {
path := "/v1/sessions"
path = appendQueryParam(path, "plan_slug", input.PlanSlug)
path = appendQueryParam(path, "agent_type", input.AgentType)
if agentType, ok := normaliseSessionAgentType(input.AgentType); ok {
path = appendQueryParam(path, "agent_type", agentType)
} else {
path = appendQueryParam(path, "agent_type", input.AgentType)
}
path = appendQueryParam(path, "status", input.Status)
if input.Limit > 0 {
path = appendQueryParam(path, "limit", core.Sprint(input.Limit))
@ -478,7 +483,9 @@ func (s *PrepSubsystem) sessionContinue(ctx context.Context, _ *mcp.CallToolRequ
}
body := map[string]any{}
if input.AgentType != "" {
if agentType, ok := normaliseSessionAgentType(input.AgentType); ok {
body["agent_type"] = agentType
} else if input.AgentType != "" {
body["agent_type"] = input.AgentType
}
if len(input.WorkLog) > 0 {
@ -1186,10 +1193,38 @@ func resultErrorValue(action string, result core.Result) error {
}
func validSessionAgentType(agentType string) bool {
switch core.Lower(core.Trim(agentType)) {
_, ok := normaliseSessionAgentType(agentType)
return ok
}
func normaliseSessionAgentType(agentType string) (string, bool) {
trimmed := core.Lower(core.Trim(agentType))
if trimmed == "" {
return "", false
}
switch trimmed {
case "claude":
return "opus", true
case "opus", "sonnet", "haiku":
return true
return trimmed, true
}
parts := core.SplitN(trimmed, ":", 2)
if len(parts) != 2 {
return "", false
}
if parts[0] != "claude" {
return "", false
}
switch core.Lower(core.Trim(agentType)) {
case "claude:opus":
return "opus", true
case "claude:sonnet":
return "sonnet", true
case "claude:haiku":
return "haiku", true
default:
return false
return "", false
}
}

View file

@ -47,6 +47,39 @@ func TestSession_HandleSessionStart_Good(t *testing.T) {
assert.Equal(t, "opus", output.Session.AgentType)
}
func TestSession_HandleSessionStart_Good_CanonicalAlias(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/v1/sessions", r.URL.Path)
require.Equal(t, http.MethodPost, r.Method)
bodyResult := core.ReadAll(r.Body)
require.True(t, bodyResult.OK)
var payload map[string]any
parseResult := core.JSONUnmarshalString(bodyResult.Value.(string), &payload)
require.True(t, parseResult.OK)
require.Equal(t, "opus", payload["agent_type"])
require.Equal(t, "ax-follow-up", payload["plan_slug"])
_, _ = w.Write([]byte(`{"data":{"id":1,"session_id":"ses_abc123","plan_slug":"ax-follow-up","agent_type":"opus","status":"active","context_summary":{"repo":"core/go"}}}`))
}))
defer server.Close()
subsystem := testPrepWithPlatformServer(t, server, "secret-token")
result := subsystem.handleSessionStart(context.Background(), core.NewOptions(
core.Option{Key: "agent_type", Value: "claude:opus"},
core.Option{Key: "plan_slug", Value: "ax-follow-up"},
core.Option{Key: "context", Value: `{"repo":"core/go"}`},
))
require.True(t, result.OK)
output, ok := result.Value.(SessionOutput)
require.True(t, ok)
assert.Equal(t, "ses_abc123", output.Session.SessionID)
assert.Equal(t, "active", output.Session.Status)
assert.Equal(t, "opus", output.Session.AgentType)
}
func TestSession_HandleSessionStart_Bad(t *testing.T) {
subsystem := testPrepWithPlatformServer(t, nil, "secret-token")
@ -61,7 +94,7 @@ func TestSession_HandleSessionStart_Bad_InvalidAgentType(t *testing.T) {
core.Option{Key: "agent_type", Value: "codex"},
))
assert.False(t, result.OK)
require.Contains(t, result.Value.(error).Error(), "opus, sonnet, or haiku")
require.Contains(t, result.Value.(error).Error(), "claude:opus")
}
func TestSession_HandleSessionStart_Ugly(t *testing.T) {