From a96b350b284c9a0b7d52e63e4ed46c4b60d2f9d9 Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Sat, 12 Jul 2025 15:32:03 -0700 Subject: [PATCH] Simplified API --- mcp.go | 14 ++- taskcp.go | 231 +++++++++++++++++++++++++------------------------ taskcp_test.go | 109 ++++++++++------------- 3 files changed, 175 insertions(+), 179 deletions(-) diff --git a/mcp.go b/mcp.go index d0b4eac..f144f1e 100644 --- a/mcp.go +++ b/mcp.go @@ -113,7 +113,12 @@ func handleSetTaskSuccess(s *Service, ctx context.Context, args setTaskSuccessAr return nil, fmt.Errorf("failed to get project: %w", err) } - nextTask, err := project.SetTaskSuccess(args.TaskID, args.Result, args.Notes) + task, err := project.GetRunningTask(args.TaskID) + if err != nil { + return nil, fmt.Errorf("failed to get task: %w", err) + } + + nextTask, err := task.SetSuccess(args.Result, args.Notes) if err != nil { return nil, fmt.Errorf("completion callback error: %w", err) } @@ -131,7 +136,12 @@ func handleSetTaskFailure(s *Service, ctx context.Context, args setTaskFailureAr return nil, fmt.Errorf("failed to get project: %w", err) } - nextTask, err := project.SetTaskFailure(args.TaskID, args.Error, args.Notes) + task, err := project.GetRunningTask(args.TaskID) + if err != nil { + return nil, fmt.Errorf("failed to get task: %w", err) + } + + nextTask, err := task.SetFailure(args.Error, args.Notes) if err != nil { return nil, fmt.Errorf("completion callback error: %w", err) } diff --git a/taskcp.go b/taskcp.go index a4be1ea..979b458 100644 --- a/taskcp.go +++ b/taskcp.go @@ -3,7 +3,6 @@ package taskcp import ( "encoding/json" "fmt" - "iter" "strings" ) @@ -13,24 +12,17 @@ type Service struct { } type Project struct { - ID int - Tasks []*Task - nextTaskID int - mcpService string + ID int + PendingTasks []*Task + RunningTasks []*Task + SuccessTasks []*Task + FailureTasks []*Task + mcpService string + nextTaskID int } -type TaskState string - -const ( - TaskStatePending TaskState = "pending" - TaskStateRunning TaskState = "running" - TaskStateSuccess TaskState = "success" - TaskStateFailure TaskState = "failure" -) - type Task struct { ID int `json:"id"` - State TaskState `json:"-"` Title string `json:"title"` Instructions string `json:"instructions"` Data map[string]any `json:"data,omitempty"` @@ -38,21 +30,21 @@ type Task struct { Error string `json:"-"` Notes string `json:"-"` - NextTaskID int `json:"-"` - + completionCallback func(task *Task) error project *Project - completionCallback func(project *Project, task *Task) error } type TaskSummary struct { - Title string `json:"title"` - State TaskState `json:"state"` - Error string `json:"error,omitempty"` - Notes string `json:"notes,omitempty"` + Title string `json:"title"` + Error string `json:"error,omitempty"` + Notes string `json:"notes,omitempty"` } type ProjectSummary struct { - Tasks []TaskSummary `json:"tasks"` + PendingTasks []*TaskSummary `json:"pending_tasks"` + RunningTasks []*TaskSummary `json:"running_tasks"` + SuccessTasks []*TaskSummary `json:"success_tasks"` + FailureTasks []*TaskSummary `json:"failure_tasks"` } func New(mcpService string) *Service { @@ -63,10 +55,13 @@ func New(mcpService string) *Service { func (s *Service) AddProject() *Project { project := &Project{ - ID: len(s.projects), - Tasks: []*Task{}, - nextTaskID: -1, - mcpService: s.mcpService, + ID: len(s.projects), + PendingTasks: []*Task{}, + RunningTasks: []*Task{}, + SuccessTasks: []*Task{}, + FailureTasks: []*Task{}, + mcpService: s.mcpService, + nextTaskID: 0, } s.projects = append(s.projects, project) return project @@ -80,106 +75,75 @@ func (s *Service) GetProject(id int) (*Project, error) { return s.projects[id], nil } -func (p *Project) InsertTaskBefore(beforeID int) *Task { - newTask := p.newTask(beforeID) - - if p.nextTaskID == -1 && beforeID == -1 { - p.nextTaskID = newTask.ID - } else { - for t := range p.tasks() { - if t.NextTaskID == beforeID { - t.NextTaskID = newTask.ID - break - } - } - } - - return newTask +func (p *Project) AddNextTask() *Task { + t := p.newTask() + p.PendingTasks = append([]*Task{t}, p.PendingTasks...) + return t } -func (p *Project) GetNextTask() *Task { - if p.nextTaskID == -1 { - return nil - } - - task := p.Tasks[p.nextTaskID] - task.State = TaskStateRunning - return task +func (p *Project) AddLastTask() *Task { + t := p.newTask() + p.PendingTasks = append(p.PendingTasks, t) + return t } -func (p *Project) SetTaskSuccess(id int, result string, notes string) (*Task, error) { - task := p.Tasks[id] - task.State = TaskStateSuccess - task.Result = result - task.Notes = notes - - if task.completionCallback != nil { - err := task.completionCallback(task.project, task) - if err != nil { - return nil, err - } - } - - p.nextTaskID = task.NextTaskID - - return p.GetNextTask(), nil -} - -func (p *Project) SetTaskFailure(id int, error string, notes string) (*Task, error) { - task := p.Tasks[id] - task.State = TaskStateFailure - task.Error = error - task.Notes = notes - - if task.completionCallback != nil { - err := task.completionCallback(task.project, task) - if err != nil { - return nil, err - } - } - - p.nextTaskID = task.NextTaskID - - return p.GetNextTask(), nil -} - -func (p *Project) newTask(nextTaskID int) *Task { +func (p *Project) newTask() *Task { task := &Task{ - ID: len(p.Tasks), - State: TaskStatePending, - NextTaskID: nextTaskID, - Data: map[string]any{}, - project: p, + ID: p.nextTaskID, + Data: map[string]any{}, + project: p, } - p.Tasks = append(p.Tasks, task) + p.nextTaskID++ + return task } -func (p *Project) tasks() iter.Seq[*Task] { - return func(yield func(*Task) bool) { - for tid := p.nextTaskID; tid != -1; tid = p.Tasks[tid].NextTaskID { - t := p.Tasks[tid] - if !yield(t) { - return - } - } +func (p *Project) PopNextTask() (*Task, error) { + if len(p.PendingTasks) == 0 { + return nil, nil } + + task := p.PendingTasks[0] + p.PendingTasks = p.PendingTasks[1:] + return task, nil } -func (p *Project) Summary() ProjectSummary { - var tasks []TaskSummary - for _, task := range p.Tasks { - if task.State != TaskStatePending { - tasks = append(tasks, TaskSummary{ - Title: task.Title, - State: task.State, - Error: task.Error, - Notes: task.Notes, - }) +func (p *Project) GetRunningTask(id int) (*Task, error) { + for _, task := range p.RunningTasks { + if task.ID == id { + return task, nil } } - return ProjectSummary{Tasks: tasks} + + return nil, fmt.Errorf("task not found: %d", id) +} + +func (p *Project) Summary() *ProjectSummary { + s := &ProjectSummary{ + PendingTasks: []*TaskSummary{}, + RunningTasks: []*TaskSummary{}, + SuccessTasks: []*TaskSummary{}, + FailureTasks: []*TaskSummary{}, + } + + for _, task := range p.PendingTasks { + s.PendingTasks = append(s.PendingTasks, task.AsSummary()) + } + + for _, task := range p.RunningTasks { + s.RunningTasks = append(s.RunningTasks, task.AsSummary()) + } + + for _, task := range p.SuccessTasks { + s.SuccessTasks = append(s.SuccessTasks, task.AsSummary()) + } + + for _, task := range p.FailureTasks { + s.FailureTasks = append(s.FailureTasks, task.AsSummary()) + } + + return s } func (t *Task) WithTitle(title string) *Task { @@ -199,9 +163,40 @@ func (t *Task) WithData(key string, value any) *Task { return t } -func (t *Task) Then(completionCallback func(project *Project, task *Task) error) *Task { +func (t *Task) Then(completionCallback func(task *Task) error) { t.completionCallback = completionCallback - return t +} + +func (t *Task) SetSuccess(result string, notes string) (*Task, error) { + t.Result = result + t.Notes = notes + + t.project.SuccessTasks = append(t.project.SuccessTasks, t) + + if t.completionCallback != nil { + err := t.completionCallback(t) + if err != nil { + return nil, err + } + } + + return t.project.PopNextTask() +} + +func (t *Task) SetFailure(error string, notes string) (*Task, error) { + t.Error = error + t.Notes = notes + + t.project.FailureTasks = append(t.project.FailureTasks, t) + + if t.completionCallback != nil { + err := t.completionCallback(t) + if err != nil { + return nil, err + } + } + + return t.project.PopNextTask() } func (t *Task) SuccessPrompt() string { @@ -225,6 +220,14 @@ func (t *Task) String() string { return string(json) } +func (t *Task) AsSummary() *TaskSummary { + return &TaskSummary{ + Title: t.Title, + Error: t.Error, + Notes: t.Notes, + } +} + func (ps ProjectSummary) String() string { json, err := json.MarshalIndent(ps, "", " ") if err != nil { diff --git a/taskcp_test.go b/taskcp_test.go index c6d3a24..54cf259 100644 --- a/taskcp_test.go +++ b/taskcp_test.go @@ -8,113 +8,96 @@ import ( "github.com/stretchr/testify/require" ) -func TestTaskPrompts(t *testing.T) { - service := taskcp.New("my_service") - project := service.AddProject() - - task := project.InsertTaskBefore(-1). - WithTitle("Write unit tests"). - WithInstructions("This is a test task."). - Then(func(project *taskcp.Project, task *taskcp.Task) error { - return nil - }) - - successPrompt := task.SuccessPrompt() - require.Contains(t, successPrompt, "my_service.set_task_success") - require.Contains(t, successPrompt, fmt.Sprintf(`project_id=%d`, project.ID)) - require.Contains(t, successPrompt, fmt.Sprintf(`task_id=%d`, task.ID)) - - failurePrompt := task.FailurePrompt() - require.Contains(t, failurePrompt, "my_service.set_task_failure") - require.Contains(t, failurePrompt, fmt.Sprintf(`project_id=%d`, project.ID)) - require.Contains(t, failurePrompt, fmt.Sprintf(`task_id=%d`, task.ID)) -} - func TestPlaceholderExpansion(t *testing.T) { service := taskcp.New("my_service") - project := service.AddProject() + p := service.AddProject() - task1 := project.InsertTaskBefore(-1). + p.AddLastTask(). WithTitle("Please complete this task."). WithInstructions("{SUCCESS_PROMPT}"). - Then(func(project *taskcp.Project, task *taskcp.Task) error { + Then(func(task *taskcp.Task) error { return nil }) - require.Contains(t, task1.Instructions, "my_service.set_task_success") - require.NotContains(t, task1.Instructions, "{SUCCESS_PROMPT}") - task2 := project.InsertTaskBefore(-1). + task, err := p.PopNextTask() + require.NoError(t, err) + require.NotNil(t, task) + require.Contains(t, task.Instructions, "my_service.set_task_success") + require.NotContains(t, task.Instructions, "{SUCCESS_PROMPT}") + + p.AddLastTask(). WithTitle("Try this risky operation."). WithInstructions("{FAILURE_PROMPT}"). - Then(func(project *taskcp.Project, task *taskcp.Task) error { + Then(func(task *taskcp.Task) error { return nil }) - require.Contains(t, task2.Instructions, "my_service.set_task_failure") - require.NotContains(t, task2.Instructions, "{FAILURE_PROMPT}") + + task, err = p.PopNextTask() + require.NoError(t, err) + require.NotNil(t, task) + require.Contains(t, task.Instructions, "my_service.set_task_failure") + require.NotContains(t, task.Instructions, "{FAILURE_PROMPT}") } func TestTaskFlow(t *testing.T) { service := taskcp.New("test_service") - project := service.AddProject() + p := service.AddProject() - var completed []int + var completed []string - task1 := project.InsertTaskBefore(-1). + p.AddLastTask(). WithTitle("First task"). - Then(func(project *taskcp.Project, task *taskcp.Task) error { - completed = append(completed, task.ID) + Then(func(task *taskcp.Task) error { + completed = append(completed, task.Title) return nil }) - task2 := project.InsertTaskBefore(-1). + p.AddLastTask(). WithTitle("Second task"). - Then(func(project *taskcp.Project, task *taskcp.Task) error { - completed = append(completed, task.ID) + Then(func(task *taskcp.Task) error { + completed = append(completed, task.Title) return nil }) - current := project.GetNextTask() - require.NotNil(t, current) - require.Equal(t, task1.ID, current.ID) - - next, err := project.SetTaskSuccess(current.ID, "Task 1 done", "") + task1, err := p.PopNextTask() require.NoError(t, err) - require.NotNil(t, next) - require.Equal(t, task2.ID, next.ID) - require.Equal(t, taskcp.TaskStateRunning, next.State) + require.NotNil(t, task1) + require.Equal(t, "First task", task1.Title) - next2, err := project.SetTaskFailure(next.ID, "Task 2 failed", "Error details") + task2, err := task1.SetSuccess("Task 1 done", "") require.NoError(t, err) - require.Nil(t, next2) + require.NotNil(t, task2) + require.Equal(t, "Second task", task2.Title) - require.Equal(t, []int{task1.ID, task2.ID}, completed) - require.Equal(t, taskcp.TaskStateSuccess, project.Tasks[task1.ID].State) - require.Equal(t, taskcp.TaskStateFailure, project.Tasks[task2.ID].State) + task3, err := task2.SetFailure("Task 2 failed", "Error details") + require.NoError(t, err) + require.Nil(t, task3) + + require.Equal(t, []string{"First task", "Second task"}, completed) + require.Equal(t, "Task 1 done", task1.Result) + require.Equal(t, "Task 2 failed", task2.Error) } func TestCallbackError(t *testing.T) { service := taskcp.New("test_service") - project := service.AddProject() + p := service.AddProject() expectedErr := fmt.Errorf("callback error") - task := project.InsertTaskBefore(-1). + p.AddLastTask(). WithTitle("Task with error callback"). WithInstructions("This is a test task."). WithData("key", "value"). - Then(func(project *taskcp.Project, task *taskcp.Task) error { + Then(func(task *taskcp.Task) error { return expectedErr }) - current := project.GetNextTask() - require.NotNil(t, current) - require.Equal(t, task.ID, current.ID) + task, err := p.PopNextTask() + require.NoError(t, err) + require.NotNil(t, task) + require.Equal(t, "Task with error callback", task.Title) - _, err := project.SetTaskSuccess(current.ID, "Result", "") - require.Error(t, err) - require.Equal(t, expectedErr, err) - - _, err = project.SetTaskFailure(current.ID, "Task failed", "") + _, err = task.SetSuccess("Result", "") require.Error(t, err) require.Equal(t, expectedErr, err) }