* feat: implement background goroutines for long-running operations Introduced `PerformAsync` in the Core framework to support non-blocking execution of long-running tasks. This mechanism uses the IPC system to broadcast `ActionTaskStarted` and `ActionTaskCompleted` events, ensuring the frontend remains responsive and informed. - Added `PerformAsync(Task) string` to `Core`. - Defined framework-level lifecycle actions: `ActionTaskStarted`, `ActionTaskProgress`, and `ActionTaskCompleted`. - Updated `internal/cmd/dev/service.go` to support `AutoPush` in `TaskWork`, removing interactive prompts during background execution. - Added comprehensive documentation for the background operations pattern in `docs/pkg/PACKAGE_STANDARDS.md`. - Added unit tests for the async task mechanism in `pkg/framework/core/ipc_test.go`. * feat: implement background goroutines for long-running operations Introduced `PerformAsync` in the Core framework to support non-blocking execution of long-running tasks. This mechanism uses the IPC system to broadcast `ActionTaskStarted` and `ActionTaskCompleted` events, ensuring the frontend remains responsive and informed. - Added `PerformAsync(Task) string` to `Core`. - Defined framework-level lifecycle actions: `ActionTaskStarted`, `ActionTaskProgress`, and `ActionTaskCompleted`. - Updated `internal/cmd/dev/service.go` to support `AutoPush` in `TaskWork`, removing interactive prompts during background execution. - Added comprehensive documentation for the background operations pattern in `docs/pkg/PACKAGE_STANDARDS.md`. - Added unit tests for the async task mechanism in `pkg/framework/core/ipc_test.go`. - Fixed formatting in `pkg/io/local/client.go`. * feat: implement background goroutines with progress reporting This version addresses feedback by providing a more complete implementation of the background task mechanism, including progress reporting and demonstrating actual usage in the AI service. - Added `TaskWithID` interface to support task ID injection. - Updated `PerformAsync` to inject IDs and provided `Core.Progress` helper. - Applied background processing pattern to `TaskPrompt` in `agentic` service. - Included a fix for the `auto-merge` CI failure by providing explicit repo context to the `gh` command in a local workflow implementation. - Fixed formatting in `pkg/io/local/client.go` and `pkg/agentic/service.go`. - Updated documentation with the new progress reporting pattern. * feat: implement non-blocking background tasks with progress reporting This submission provides a complete framework-level solution for running long-running operations in the background to prevent UI blocking, addressing previous review feedback. Key changes: - Introduced `PerformAsync(Task) string` in the `Core` framework. - Added `TaskWithID` interface to allow tasks to receive their unique ID. - Provided `Core.Progress` helper for services to report granular updates. - Applied the background pattern to the AI service (`agentic.TaskPrompt`). - Updated the dev service (`TaskWork`) to support an `AutoPush` flag, eliminating interactive prompts during background execution. - Added a local implementation for the `auto-merge` CI workflow to bypass repo context issues and fix the blocking CI failure. - Included comprehensive documentation in `docs/pkg/PACKAGE_STANDARDS.md`. - Resolved formatting discrepancies across the codebase. - Verified functionality with unit tests in `pkg/framework/core/ipc_test.go`. --------- Co-authored-by: Claude <developers@lethean.io> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
142 lines
3.2 KiB
Go
142 lines
3.2 KiB
Go
package agentic
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
|
|
"github.com/host-uk/core/pkg/framework"
|
|
"github.com/host-uk/core/pkg/log"
|
|
)
|
|
|
|
// Tasks for AI service
|
|
|
|
// TaskCommit requests Claude to create a commit.
|
|
type TaskCommit struct {
|
|
Path string
|
|
Name string
|
|
CanEdit bool // allow Write/Edit tools
|
|
}
|
|
|
|
// TaskPrompt sends a custom prompt to Claude.
|
|
type TaskPrompt struct {
|
|
Prompt string
|
|
WorkDir string
|
|
AllowedTools []string
|
|
|
|
taskID string
|
|
}
|
|
|
|
func (t *TaskPrompt) SetTaskID(id string) { t.taskID = id }
|
|
func (t *TaskPrompt) GetTaskID() string { return t.taskID }
|
|
|
|
// ServiceOptions for configuring the AI service.
|
|
type ServiceOptions struct {
|
|
DefaultTools []string
|
|
AllowEdit bool // global permission for Write/Edit tools
|
|
}
|
|
|
|
// DefaultServiceOptions returns sensible defaults.
|
|
func DefaultServiceOptions() ServiceOptions {
|
|
return ServiceOptions{
|
|
DefaultTools: []string{"Bash", "Read", "Glob", "Grep"},
|
|
AllowEdit: false,
|
|
}
|
|
}
|
|
|
|
// Service provides AI/Claude operations as a Core service.
|
|
type Service struct {
|
|
*framework.ServiceRuntime[ServiceOptions]
|
|
}
|
|
|
|
// NewService creates an AI service factory.
|
|
func NewService(opts ServiceOptions) func(*framework.Core) (any, error) {
|
|
return func(c *framework.Core) (any, error) {
|
|
return &Service{
|
|
ServiceRuntime: framework.NewServiceRuntime(c, opts),
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
// OnStartup registers task handlers.
|
|
func (s *Service) OnStartup(ctx context.Context) error {
|
|
s.Core().RegisterTask(s.handleTask)
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) handleTask(c *framework.Core, t framework.Task) (any, bool, error) {
|
|
switch m := t.(type) {
|
|
case TaskCommit:
|
|
err := s.doCommit(m)
|
|
if err != nil {
|
|
log.Error("agentic: commit task failed", "err", err, "path", m.Path)
|
|
}
|
|
return nil, true, err
|
|
|
|
case TaskPrompt:
|
|
err := s.doPrompt(m)
|
|
if err != nil {
|
|
log.Error("agentic: prompt task failed", "err", err)
|
|
}
|
|
return nil, true, err
|
|
}
|
|
return nil, false, nil
|
|
}
|
|
|
|
func (s *Service) doCommit(task TaskCommit) error {
|
|
prompt := Prompt("commit")
|
|
|
|
tools := []string{"Bash", "Read", "Glob", "Grep"}
|
|
if task.CanEdit {
|
|
tools = []string{"Bash", "Read", "Write", "Edit", "Glob", "Grep"}
|
|
}
|
|
|
|
cmd := exec.CommandContext(context.Background(), "claude", "-p", prompt, "--allowedTools", strings.Join(tools, ","))
|
|
cmd.Dir = task.Path
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
cmd.Stdin = os.Stdin
|
|
|
|
return cmd.Run()
|
|
}
|
|
|
|
func (s *Service) doPrompt(task TaskPrompt) error {
|
|
if task.taskID != "" {
|
|
s.Core().Progress(task.taskID, 0.1, "Starting Claude...", &task)
|
|
}
|
|
|
|
opts := s.Opts()
|
|
tools := opts.DefaultTools
|
|
if len(tools) == 0 {
|
|
tools = []string{"Bash", "Read", "Glob", "Grep"}
|
|
}
|
|
|
|
if len(task.AllowedTools) > 0 {
|
|
tools = task.AllowedTools
|
|
}
|
|
|
|
cmd := exec.CommandContext(context.Background(), "claude", "-p", task.Prompt, "--allowedTools", strings.Join(tools, ","))
|
|
if task.WorkDir != "" {
|
|
cmd.Dir = task.WorkDir
|
|
}
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
cmd.Stdin = os.Stdin
|
|
|
|
if task.taskID != "" {
|
|
s.Core().Progress(task.taskID, 0.5, "Running Claude prompt...", &task)
|
|
}
|
|
|
|
err := cmd.Run()
|
|
|
|
if task.taskID != "" {
|
|
if err != nil {
|
|
s.Core().Progress(task.taskID, 1.0, "Failed: "+err.Error(), &task)
|
|
} else {
|
|
s.Core().Progress(task.taskID, 1.0, "Completed", &task)
|
|
}
|
|
}
|
|
|
|
return err
|
|
}
|