You've already forked agentic-coding-workflow
feat(acp): handler refactor + sessions REST endpoints (list/create/get/delete/events)
This commit is contained in:
@@ -46,3 +46,69 @@ type UpdateAgentKindReq struct {
|
|||||||
Env map[string]string `json:"env,omitempty"`
|
Env map[string]string `json:"env,omitempty"`
|
||||||
Enabled *bool `json:"enabled,omitempty"`
|
Enabled *bool `json:"enabled,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateSessionReq is the request body for POST /api/v1/acp/sessions.
|
||||||
|
type CreateSessionReq struct {
|
||||||
|
WorkspaceID string `json:"workspace_id"`
|
||||||
|
AgentKindID string `json:"agent_kind_id"`
|
||||||
|
Branch *string `json:"branch,omitempty"`
|
||||||
|
IssueNumber *int `json:"issue_number,omitempty"`
|
||||||
|
RequirementNumber *int `json:"requirement_number,omitempty"`
|
||||||
|
InitialPrompt *string `json:"initial_prompt,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionDTO is the response shape for sessions.
|
||||||
|
type SessionDTO struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
WorkspaceID string `json:"workspace_id"`
|
||||||
|
ProjectID string `json:"project_id"`
|
||||||
|
AgentKindID string `json:"agent_kind_id"`
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
IssueID *string `json:"issue_id"`
|
||||||
|
RequirementID *string `json:"requirement_id"`
|
||||||
|
AgentSessionID *string `json:"agent_session_id"`
|
||||||
|
Branch string `json:"branch"`
|
||||||
|
CwdPath string `json:"cwd_path"`
|
||||||
|
IsMainWorktree bool `json:"is_main_worktree"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
PID *int32 `json:"pid"`
|
||||||
|
ExitCode *int32 `json:"exit_code"`
|
||||||
|
LastError *string `json:"last_error"`
|
||||||
|
StartedAt string `json:"started_at"`
|
||||||
|
EndedAt *string `json:"ended_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func sessionToDTO(s *Session) SessionDTO {
|
||||||
|
var issueID, reqID, ended *string
|
||||||
|
if s.IssueID != nil {
|
||||||
|
v := s.IssueID.String()
|
||||||
|
issueID = &v
|
||||||
|
}
|
||||||
|
if s.RequirementID != nil {
|
||||||
|
v := s.RequirementID.String()
|
||||||
|
reqID = &v
|
||||||
|
}
|
||||||
|
if s.EndedAt != nil {
|
||||||
|
v := s.EndedAt.UTC().Format("2006-01-02T15:04:05Z")
|
||||||
|
ended = &v
|
||||||
|
}
|
||||||
|
return SessionDTO{
|
||||||
|
ID: s.ID.String(),
|
||||||
|
WorkspaceID: s.WorkspaceID.String(),
|
||||||
|
ProjectID: s.ProjectID.String(),
|
||||||
|
AgentKindID: s.AgentKindID.String(),
|
||||||
|
UserID: s.UserID.String(),
|
||||||
|
IssueID: issueID,
|
||||||
|
RequirementID: reqID,
|
||||||
|
AgentSessionID: s.AgentSessionID,
|
||||||
|
Branch: s.Branch,
|
||||||
|
CwdPath: s.CwdPath,
|
||||||
|
IsMainWorktree: s.IsMainWorktree,
|
||||||
|
Status: string(s.Status),
|
||||||
|
PID: s.PID,
|
||||||
|
ExitCode: s.ExitCode,
|
||||||
|
LastError: s.LastError,
|
||||||
|
StartedAt: s.StartedAt.UTC().Format("2006-01-02T15:04:05Z"),
|
||||||
|
EndedAt: ended,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+162
-5
@@ -1,5 +1,6 @@
|
|||||||
// Package acp handler.go exposes the ACP module's HTTP / WS endpoints.
|
// Package acp handler.go exposes the ACP module's HTTP / WS endpoints.
|
||||||
// Part 1: agent_kinds routes only. Part 2 will add sessions REST + WS upgrader.
|
// Part 1: agent_kinds routes. Part 2 (G1/G2): sessions REST endpoints.
|
||||||
|
// Part 3 (G3): WebSocket upgrader.
|
||||||
package acp
|
package acp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -7,6 +8,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
@@ -23,17 +25,24 @@ type AdminLookup interface {
|
|||||||
IsAdmin(ctx context.Context, userID uuid.UUID) (bool, error)
|
IsAdmin(ctx context.Context, userID uuid.UUID) (bool, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handler exposes ACP HTTP endpoints. Part 2 will extend with SessionService and Supervisor.
|
// Handler exposes ACP HTTP endpoints.
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
akSvc AgentKindService
|
akSvc AgentKindService
|
||||||
|
sessSvc SessionService
|
||||||
|
sup *Supervisor
|
||||||
|
repo Repository
|
||||||
resolver middleware.SessionResolver
|
resolver middleware.SessionResolver
|
||||||
adminLookup AdminLookup
|
adminLookup AdminLookup
|
||||||
enc *crypto.Encryptor
|
enc *crypto.Encryptor
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler constructs Handler (Part 1: agent_kinds only).
|
// NewHandler constructs Handler with full dependencies.
|
||||||
func NewHandler(ak AgentKindService, resolver middleware.SessionResolver, al AdminLookup, enc *crypto.Encryptor) *Handler {
|
func NewHandler(ak AgentKindService, ss SessionService, sup *Supervisor, repo Repository,
|
||||||
return &Handler{akSvc: ak, resolver: resolver, adminLookup: al, enc: enc}
|
resolver middleware.SessionResolver, al AdminLookup, enc *crypto.Encryptor) *Handler {
|
||||||
|
return &Handler{
|
||||||
|
akSvc: ak, sessSvc: ss, sup: sup, repo: repo,
|
||||||
|
resolver: resolver, adminLookup: al, enc: enc,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mount registers /api/v1/acp/* routes.
|
// Mount registers /api/v1/acp/* routes.
|
||||||
@@ -46,6 +55,15 @@ func (h *Handler) Mount(r chi.Router) {
|
|||||||
r.Patch("/{id}", h.updateAgentKind)
|
r.Patch("/{id}", h.updateAgentKind)
|
||||||
r.Delete("/{id}", h.deleteAgentKind)
|
r.Delete("/{id}", h.deleteAgentKind)
|
||||||
})
|
})
|
||||||
|
r.Route("/api/v1/acp/sessions", func(r chi.Router) {
|
||||||
|
r.Use(middleware.Auth(h.resolver, middleware.AuthOptions{}))
|
||||||
|
r.Get("/", h.listSessions)
|
||||||
|
r.Post("/", h.createSession)
|
||||||
|
r.Get("/{id}", h.getSession)
|
||||||
|
r.Delete("/{id}", h.terminateSession)
|
||||||
|
r.Get("/{id}/events", h.getEvents)
|
||||||
|
// /{id}/ws — added in G3 (WebSocket)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) caller(r *http.Request) (Caller, error) {
|
func (h *Handler) caller(r *http.Request) (Caller, error) {
|
||||||
@@ -191,6 +209,145 @@ func (h *Handler) deleteAgentKind(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.WriteHeader(http.StatusNoContent)
|
w.WriteHeader(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== Session endpoints =====
|
||||||
|
|
||||||
|
func (h *Handler) listSessions(w http.ResponseWriter, r *http.Request) {
|
||||||
|
c, err := h.caller(r)
|
||||||
|
if err != nil {
|
||||||
|
writeErr(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
all := r.URL.Query().Get("all") == "true"
|
||||||
|
list, err := h.sessSvc.List(r.Context(), c, all)
|
||||||
|
if err != nil {
|
||||||
|
writeErr(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out := make([]SessionDTO, 0, len(list))
|
||||||
|
for _, s := range list {
|
||||||
|
out = append(out, sessionToDTO(s))
|
||||||
|
}
|
||||||
|
httpx.WriteJSON(w, http.StatusOK, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) createSession(w http.ResponseWriter, r *http.Request) {
|
||||||
|
c, err := h.caller(r)
|
||||||
|
if err != nil {
|
||||||
|
writeErr(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var req CreateSessionReq
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
writeErr(w, r, errs.New(errs.CodeInvalidInput, "bad json"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
wsID, perr := uuid.Parse(req.WorkspaceID)
|
||||||
|
if perr != nil {
|
||||||
|
writeErr(w, r, errs.New(errs.CodeInvalidInput, "bad workspace_id"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
akID, perr := uuid.Parse(req.AgentKindID)
|
||||||
|
if perr != nil {
|
||||||
|
writeErr(w, r, errs.New(errs.CodeInvalidInput, "bad agent_kind_id"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
in := CreateSessionInput{
|
||||||
|
WorkspaceID: wsID,
|
||||||
|
AgentKindID: akID,
|
||||||
|
Branch: req.Branch,
|
||||||
|
IssueNumber: req.IssueNumber,
|
||||||
|
RequirementNumber: req.RequirementNumber,
|
||||||
|
InitialPrompt: req.InitialPrompt,
|
||||||
|
}
|
||||||
|
out, err := h.sessSvc.Create(r.Context(), c, in)
|
||||||
|
if err != nil {
|
||||||
|
writeErr(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
httpx.WriteJSON(w, http.StatusCreated, sessionToDTO(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) getSession(w http.ResponseWriter, r *http.Request) {
|
||||||
|
c, err := h.caller(r)
|
||||||
|
if err != nil {
|
||||||
|
writeErr(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, perr := uuid.Parse(chi.URLParam(r, "id"))
|
||||||
|
if perr != nil {
|
||||||
|
writeErr(w, r, errs.New(errs.CodeInvalidInput, "bad id"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s, err := h.sessSvc.Get(r.Context(), c, id)
|
||||||
|
if err != nil {
|
||||||
|
writeErr(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
httpx.WriteJSON(w, http.StatusOK, sessionToDTO(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) terminateSession(w http.ResponseWriter, r *http.Request) {
|
||||||
|
c, err := h.caller(r)
|
||||||
|
if err != nil {
|
||||||
|
writeErr(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, perr := uuid.Parse(chi.URLParam(r, "id"))
|
||||||
|
if perr != nil {
|
||||||
|
writeErr(w, r, errs.New(errs.CodeInvalidInput, "bad id"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.sessSvc.Terminate(r.Context(), c, id); err != nil {
|
||||||
|
writeErr(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) getEvents(w http.ResponseWriter, r *http.Request) {
|
||||||
|
c, err := h.caller(r)
|
||||||
|
if err != nil {
|
||||||
|
writeErr(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, perr := uuid.Parse(chi.URLParam(r, "id"))
|
||||||
|
if perr != nil {
|
||||||
|
writeErr(w, r, errs.New(errs.CodeInvalidInput, "bad id"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := h.sessSvc.Get(r.Context(), c, id); err != nil {
|
||||||
|
writeErr(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var since int64
|
||||||
|
if v := r.URL.Query().Get("since"); v != "" {
|
||||||
|
if n, err := strconv.ParseInt(v, 10, 64); err == nil {
|
||||||
|
since = n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
evs, err := h.repo.ListEventsSince(r.Context(), id, since, 1000)
|
||||||
|
if err != nil {
|
||||||
|
writeErr(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out := make([]EventEnvelope, 0, len(evs))
|
||||||
|
for _, e := range evs {
|
||||||
|
method := ""
|
||||||
|
if e.Method != nil {
|
||||||
|
method = *e.Method
|
||||||
|
}
|
||||||
|
out = append(out, EventEnvelope{
|
||||||
|
ID: e.ID,
|
||||||
|
Direction: string(e.Direction),
|
||||||
|
Kind: string(e.RPCKind),
|
||||||
|
Method: method,
|
||||||
|
Payload: e.Payload,
|
||||||
|
Truncated: e.Truncated,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
httpx.WriteJSON(w, http.StatusOK, out)
|
||||||
|
}
|
||||||
|
|
||||||
// ===== DTO converters =====
|
// ===== DTO converters =====
|
||||||
|
|
||||||
// agentKindToAdminDTO is a Handler method because env-key extraction needs the encryptor.
|
// agentKindToAdminDTO is a Handler method because env-key extraction needs the encryptor.
|
||||||
|
|||||||
@@ -46,6 +46,9 @@ func mountHandlerWithRepo(t *testing.T, callerUID uuid.UUID, isAdmin bool, repo
|
|||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
h := acp.NewHandler(
|
h := acp.NewHandler(
|
||||||
svc,
|
svc,
|
||||||
|
nil, // sessSvc — not exercised by agent-kinds tests
|
||||||
|
nil, // sup — not exercised by agent-kinds tests
|
||||||
|
nil, // repo — not exercised by agent-kinds tests
|
||||||
fakeResolver{uid: callerUID},
|
fakeResolver{uid: callerUID},
|
||||||
fakeAdminLookup{admin: map[uuid.UUID]bool{callerUID: isAdmin}},
|
fakeAdminLookup{admin: map[uuid.UUID]bool{callerUID: isAdmin}},
|
||||||
enc,
|
enc,
|
||||||
|
|||||||
+1
-4
@@ -327,10 +327,7 @@ func New(ctx context.Context, cfg *config.Config, dist embed.FS) (*App, error) {
|
|||||||
MaxActivePerUser: cfg.Acp.MaxActivePerUser,
|
MaxActivePerUser: cfg.Acp.MaxActivePerUser,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
_ = sessSvc // wired in G1 (handler refactor)
|
acp.NewHandler(akSvc, sessSvc, acpSup, acpRepo, userSvc, userAdminAdapter{svc: userSvc}, enc).Mount(r)
|
||||||
|
|
||||||
// Handler: G1 will refactor to take sessSvc + acpSup. For now use Part 1 signature.
|
|
||||||
acp.NewHandler(akSvc, userSvc, userAdminAdapter{svc: userSvc}, enc).Mount(r)
|
|
||||||
|
|
||||||
// ===== jobs module wiring =====
|
// ===== jobs module wiring =====
|
||||||
jobsRepo := jobs.NewPostgresRepository(pool)
|
jobsRepo := jobs.NewPostgresRepository(pool)
|
||||||
|
|||||||
Reference in New Issue
Block a user