f367c9b125
Co-authored-by: nova28 <17953305+nova28@users.noreply.github.com> Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
593 lines
17 KiB
Go
593 lines
17 KiB
Go
package runtime
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"go.uber.org/zap"
|
|
|
|
commonlogger "github.com/kandev/kandev/internal/common/logger"
|
|
"github.com/kandev/kandev/internal/office/agents"
|
|
"github.com/kandev/kandev/internal/office/models"
|
|
"github.com/kandev/kandev/internal/office/shared"
|
|
)
|
|
|
|
const runtimeInternalErrorMessage = "internal runtime error"
|
|
|
|
// Handler exposes run-scoped runtime actions to agent processes.
|
|
type Handler struct {
|
|
agentSvc *agents.AgentService
|
|
actions *Actions
|
|
skillLister SkillLister
|
|
runEvents RunEventAppender
|
|
decisions DecisionRecorder
|
|
logger *commonlogger.Logger
|
|
}
|
|
|
|
// RunEventAppender records runtime behavior against a run.
|
|
type RunEventAppender interface {
|
|
AppendRunEvent(ctx context.Context, runID, eventType, level string, payload map[string]interface{})
|
|
}
|
|
|
|
// NewHandler creates a runtime handler.
|
|
func NewHandler(
|
|
agentSvc *agents.AgentService,
|
|
actions *Actions,
|
|
skillLister SkillLister,
|
|
runEvents RunEventAppender,
|
|
decisions DecisionRecorder,
|
|
log *commonlogger.Logger,
|
|
) *Handler {
|
|
if log == nil {
|
|
log = commonlogger.Default()
|
|
}
|
|
return &Handler{
|
|
agentSvc: agentSvc,
|
|
actions: actions,
|
|
skillLister: skillLister,
|
|
runEvents: runEvents,
|
|
decisions: decisions,
|
|
logger: log,
|
|
}
|
|
}
|
|
|
|
// RegisterRoutes mounts runtime syscall routes.
|
|
func RegisterRoutes(group *gin.RouterGroup, h *Handler) {
|
|
group.POST("/runtime/comments", h.postComment)
|
|
group.POST("/runtime/task/decision", h.recordAgentDecision)
|
|
group.POST("/runtime/tasks/:id/status", h.updateTaskStatus)
|
|
group.POST("/runtime/tasks/:id/subtasks", h.createSubtask)
|
|
group.POST("/runtime/tasks", h.createTask)
|
|
group.POST("/runtime/agents", h.createAgent)
|
|
group.GET("/runtime/projects", h.listProjects)
|
|
group.POST("/runtime/projects", h.createProject)
|
|
group.PATCH("/runtime/agents/:id", h.modifyAgent)
|
|
group.POST("/runtime/agents/:id/runs", h.spawnAgentRun)
|
|
group.POST("/runtime/approvals", h.requestApproval)
|
|
group.GET("/runtime/memory/*path", h.getMemory)
|
|
group.PUT("/runtime/memory/*path", h.putMemory)
|
|
group.GET("/runtime/skills", h.listSkills)
|
|
group.DELETE("/runtime/skills/:id", h.deleteSkill)
|
|
}
|
|
|
|
type recordAgentDecisionRequest struct {
|
|
Decision string `json:"decision"`
|
|
Reason string `json:"reason"`
|
|
}
|
|
|
|
func (h *Handler) recordAgentDecision(c *gin.Context) {
|
|
runCtx, _, ok := h.contextFromRequest(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
if runCtx.TaskID == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "decision requires a task-bound runtime"})
|
|
return
|
|
}
|
|
var req recordAgentDecisionRequest
|
|
if !bindClosedJSON(c, &req) {
|
|
return
|
|
}
|
|
if h.decisions == nil {
|
|
h.respondRuntimeError(c, runCtx, "record_agent_decision", "task", runCtx.TaskID, errDecisionRecorderMissing)
|
|
return
|
|
}
|
|
result, err := h.decisions.RecordAgentDecision(c.Request.Context(), RecordAgentDecisionInput{
|
|
TaskID: runCtx.TaskID,
|
|
AgentProfileID: runCtx.AgentID,
|
|
SessionID: runCtx.SessionID,
|
|
Decision: req.Decision,
|
|
Reason: req.Reason,
|
|
})
|
|
if err != nil {
|
|
h.respondRuntimeError(c, runCtx, "record_agent_decision", "task", runCtx.TaskID, err)
|
|
return
|
|
}
|
|
h.appendActionRunEvent(c.Request.Context(), runCtx, "record_agent_decision", "task", runCtx.TaskID)
|
|
c.JSON(http.StatusOK, result)
|
|
}
|
|
|
|
func (h *Handler) createTask(c *gin.Context) {
|
|
runCtx, _, ok := h.contextFromRequest(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
var req CreateTaskInput
|
|
if !bindJSON(c, &req) {
|
|
return
|
|
}
|
|
if field := req.unsupportedField(); field != "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": field + " is not supported by Office runtime task create"})
|
|
return
|
|
}
|
|
taskID, err := h.actions.CreateTask(c.Request.Context(), runCtx, req)
|
|
if err != nil {
|
|
h.respondRuntimeError(c, runCtx, "create_task", "task", req.ParentTaskID, err)
|
|
return
|
|
}
|
|
h.appendActionRunEvent(c.Request.Context(), runCtx, "create_task", "task", taskID)
|
|
c.JSON(http.StatusCreated, gin.H{"task_id": taskID})
|
|
}
|
|
|
|
func (h *Handler) listProjects(c *gin.Context) {
|
|
runCtx, _, ok := h.contextFromRequest(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
projects, err := h.actions.ListProjects(c.Request.Context(), runCtx)
|
|
if err != nil {
|
|
h.respondRuntimeError(c, runCtx, "list_projects", "projects", runCtx.WorkspaceID, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"projects": projects})
|
|
}
|
|
|
|
func (h *Handler) createProject(c *gin.Context) {
|
|
runCtx, _, ok := h.contextFromRequest(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
var req CreateProjectInput
|
|
if !bindJSON(c, &req) {
|
|
return
|
|
}
|
|
project, err := h.actions.CreateProject(c.Request.Context(), runCtx, req)
|
|
if err != nil {
|
|
h.respondRuntimeError(c, runCtx, "create_project", "project", req.LeadAgentProfileID, err)
|
|
return
|
|
}
|
|
h.appendActionRunEvent(c.Request.Context(), runCtx, "create_project", "project", project.ID)
|
|
c.JSON(http.StatusCreated, gin.H{"project": project})
|
|
}
|
|
|
|
func (h *Handler) postComment(c *gin.Context) {
|
|
runCtx, _, ok := h.contextFromRequest(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
var req struct {
|
|
TaskID string `json:"task_id"`
|
|
Body string `json:"body"`
|
|
}
|
|
if !bindJSON(c, &req) {
|
|
return
|
|
}
|
|
taskID := firstNonEmpty(req.TaskID, runCtx.TaskID)
|
|
if err := h.actions.PostComment(c.Request.Context(), runCtx, taskID, req.Body); err != nil {
|
|
h.respondRuntimeError(c, runCtx, "post_comment", "task", taskID, err)
|
|
return
|
|
}
|
|
h.appendActionRunEvent(c.Request.Context(), runCtx, "post_comment", "task", taskID)
|
|
c.JSON(http.StatusCreated, gin.H{"ok": true})
|
|
}
|
|
|
|
func (h *Handler) updateTaskStatus(c *gin.Context) {
|
|
runCtx, _, ok := h.contextFromRequest(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
var req struct {
|
|
Status string `json:"status"`
|
|
Comment string `json:"comment"`
|
|
}
|
|
if !bindJSON(c, &req) {
|
|
return
|
|
}
|
|
if err := h.actions.UpdateTaskStatus(
|
|
c.Request.Context(),
|
|
runCtx,
|
|
c.Param("id"),
|
|
req.Status,
|
|
req.Comment,
|
|
); err != nil {
|
|
h.respondTaskStatusError(c, runCtx, c.Param("id"), err)
|
|
return
|
|
}
|
|
h.appendActionRunEvent(c.Request.Context(), runCtx, "update_task_status", "task", c.Param("id"))
|
|
c.JSON(http.StatusOK, gin.H{"ok": true})
|
|
}
|
|
|
|
func (h *Handler) createSubtask(c *gin.Context) {
|
|
runCtx, _, ok := h.contextFromRequest(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
var req CreateSubtaskInput
|
|
if !bindJSON(c, &req) {
|
|
return
|
|
}
|
|
taskID, err := h.actions.CreateSubtask(c.Request.Context(), runCtx, req)
|
|
if err != nil {
|
|
h.respondRuntimeError(c, runCtx, "create_subtask", "task", runCtx.TaskID, err)
|
|
return
|
|
}
|
|
h.appendActionRunEvent(c.Request.Context(), runCtx, "create_subtask", "task", taskID)
|
|
c.JSON(http.StatusCreated, gin.H{"task_id": taskID})
|
|
}
|
|
|
|
func (h *Handler) createAgent(c *gin.Context) {
|
|
runCtx, caller, ok := h.contextFromRequest(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
var req CreateAgentInput
|
|
if !bindJSON(c, &req) {
|
|
return
|
|
}
|
|
agent, err := h.actions.CreateAgent(c.Request.Context(), runCtx, caller, req)
|
|
if err != nil {
|
|
h.respondRuntimeError(c, runCtx, "create_agent", "agent", runCtx.AgentID, err)
|
|
return
|
|
}
|
|
h.appendActionRunEvent(c.Request.Context(), runCtx, "create_agent", "agent", agent.ID)
|
|
c.JSON(http.StatusCreated, gin.H{"agent": agent})
|
|
}
|
|
|
|
func (h *Handler) modifyAgent(c *gin.Context) {
|
|
runCtx, _, ok := h.contextFromRequest(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
var req ModifyAgentInput
|
|
if !bindJSON(c, &req) {
|
|
return
|
|
}
|
|
agent, err := h.actions.ModifyAgent(c.Request.Context(), runCtx, c.Param("id"), req)
|
|
if err != nil {
|
|
h.respondRuntimeError(c, runCtx, "modify_agents", "agent", c.Param("id"), err)
|
|
return
|
|
}
|
|
h.appendActionRunEvent(c.Request.Context(), runCtx, "modify_agents", "agent", agent.ID)
|
|
c.JSON(http.StatusOK, gin.H{"agent": agent})
|
|
}
|
|
|
|
func (h *Handler) spawnAgentRun(c *gin.Context) {
|
|
runCtx, _, ok := h.contextFromRequest(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
var req SpawnAgentRunInput
|
|
if !bindJSON(c, &req) {
|
|
return
|
|
}
|
|
req.AgentID = firstNonEmpty(req.AgentID, c.Param("id"))
|
|
if err := h.actions.SpawnAgentRun(c.Request.Context(), runCtx, req); err != nil {
|
|
h.respondRuntimeError(c, runCtx, "spawn_agent_run", "agent", req.AgentID, err)
|
|
return
|
|
}
|
|
h.appendActionRunEvent(c.Request.Context(), runCtx, "spawn_agent_run", "agent", req.AgentID)
|
|
c.JSON(http.StatusAccepted, gin.H{"ok": true})
|
|
}
|
|
|
|
func (h *Handler) requestApproval(c *gin.Context) {
|
|
runCtx, _, ok := h.contextFromRequest(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
var req RequestApprovalInput
|
|
if !bindJSON(c, &req) {
|
|
return
|
|
}
|
|
approval, err := h.actions.RequestApproval(c.Request.Context(), runCtx, req)
|
|
if err != nil {
|
|
h.respondRuntimeError(c, runCtx, "request_approval", "approval", req.TargetID, err)
|
|
return
|
|
}
|
|
h.appendActionRunEvent(c.Request.Context(), runCtx, "request_approval", "approval", approval.ID)
|
|
c.JSON(http.StatusCreated, gin.H{"approval": approval})
|
|
}
|
|
|
|
func (h *Handler) getMemory(c *gin.Context) {
|
|
runCtx, _, ok := h.contextFromRequest(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
ns, err := ParseMemoryNamespace(c.Param("path"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if !CanAccessMemory(runCtx, ns, false) {
|
|
h.respondRuntimeError(c, runCtx, "read_memory", "memory", c.Param("path"), ErrCapabilityDenied)
|
|
return
|
|
}
|
|
layer, key := memoryLayerAndKey(ns)
|
|
mem, err := h.agentSvc.GetMemory(c.Request.Context(), runCtx.AgentID, layer, key)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "memory not found"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"memory": mem})
|
|
}
|
|
|
|
func (h *Handler) putMemory(c *gin.Context) {
|
|
runCtx, _, ok := h.contextFromRequest(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
ns, err := ParseMemoryNamespace(c.Param("path"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if !CanAccessMemory(runCtx, ns, true) {
|
|
h.respondRuntimeError(c, runCtx, "write_memory", "memory", c.Param("path"), ErrCapabilityDenied)
|
|
return
|
|
}
|
|
var req struct {
|
|
Content string `json:"content"`
|
|
}
|
|
if !bindJSON(c, &req) {
|
|
return
|
|
}
|
|
layer, key := memoryLayerAndKey(ns)
|
|
if err := h.agentSvc.UpsertAgentMemory(c.Request.Context(), &models.AgentMemory{
|
|
AgentProfileID: runCtx.AgentID,
|
|
Layer: layer,
|
|
Key: key,
|
|
Content: req.Content,
|
|
}); err != nil {
|
|
h.respondRuntimeError(c, runCtx, "write_memory", "memory", c.Param("path"), err)
|
|
return
|
|
}
|
|
h.appendActionRunEvent(c.Request.Context(), runCtx, "write_memory", "memory", c.Param("path"))
|
|
c.JSON(http.StatusOK, gin.H{"ok": true})
|
|
}
|
|
|
|
func (h *Handler) listSkills(c *gin.Context) {
|
|
runCtx, _, ok := h.contextFromRequest(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
if !runCtx.Capabilities.Allows(CapabilityListSkills) {
|
|
h.respondRuntimeError(c, runCtx, "list_skills", "skills", runCtx.WorkspaceID, ErrCapabilityDenied)
|
|
return
|
|
}
|
|
if h.skillLister == nil {
|
|
h.respondRuntimeError(c, runCtx, "list_skills", "skills", runCtx.WorkspaceID, ErrRuntimeDependencyMissing)
|
|
return
|
|
}
|
|
skills, err := h.skillLister.ListSkillsFromConfig(c.Request.Context(), runCtx.WorkspaceID)
|
|
if err != nil {
|
|
h.respondRuntimeError(c, runCtx, "list_skills", "skills", runCtx.WorkspaceID, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"skills": skills})
|
|
}
|
|
|
|
func (h *Handler) deleteSkill(c *gin.Context) {
|
|
runCtx, _, ok := h.contextFromRequest(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
if err := h.actions.DeleteSkill(c.Request.Context(), runCtx, c.Param("id")); err != nil {
|
|
h.respondRuntimeError(c, runCtx, "delete_skills", "skill", c.Param("id"), err)
|
|
return
|
|
}
|
|
h.appendActionRunEvent(c.Request.Context(), runCtx, "delete_skills", "skill", c.Param("id"))
|
|
c.JSON(http.StatusOK, gin.H{"ok": true})
|
|
}
|
|
|
|
func memoryLayerAndKey(ns MemoryNamespace) (string, string) {
|
|
parts := strings.SplitN(ns.Key, "/", 2)
|
|
if ns.Kind == MemoryKindAgent {
|
|
if len(parts) == 2 {
|
|
return parts[0], parts[1]
|
|
}
|
|
return "knowledge", ns.Key
|
|
}
|
|
layer := ns.Kind + ":" + ns.ID
|
|
return layer, ns.Key
|
|
}
|
|
|
|
func (h *Handler) contextFromRequest(c *gin.Context) (RunContext, *models.AgentInstance, bool) {
|
|
token := bearerToken(c.GetHeader("Authorization"))
|
|
if token == "" {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing runtime token"})
|
|
return RunContext{}, nil, false
|
|
}
|
|
claims, err := h.agentSvc.ValidateAgentJWT(token)
|
|
if err != nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid runtime token"})
|
|
return RunContext{}, nil, false
|
|
}
|
|
agent, err := h.agentSvc.GetAgentInstance(c.Request.Context(), claims.AgentProfileID)
|
|
if err != nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "agent not found"})
|
|
return RunContext{}, nil, false
|
|
}
|
|
caps := FromAgent(agent)
|
|
if claims.Capabilities != "" {
|
|
_ = json.Unmarshal([]byte(claims.Capabilities), &caps)
|
|
}
|
|
runCtx := RunContext{
|
|
WorkspaceID: claims.WorkspaceID,
|
|
AgentID: claims.AgentProfileID,
|
|
TaskID: claims.TaskID,
|
|
RunID: claims.RunID,
|
|
SessionID: claims.SessionID,
|
|
Capabilities: caps,
|
|
}
|
|
return runCtx, agent, true
|
|
}
|
|
|
|
func bindJSON(c *gin.Context, target any) bool {
|
|
if err := c.ShouldBindJSON(target); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func bindClosedJSON(c *gin.Context, target any) bool {
|
|
decoder := json.NewDecoder(c.Request.Body)
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(target); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return false
|
|
}
|
|
var extra any
|
|
if err := decoder.Decode(&extra); err != io.EOF {
|
|
if err == nil {
|
|
err = errors.New("request body must contain one JSON value")
|
|
}
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func bearerToken(header string) string {
|
|
const prefix = "Bearer "
|
|
if !strings.HasPrefix(header, prefix) {
|
|
return ""
|
|
}
|
|
return strings.TrimPrefix(header, prefix)
|
|
}
|
|
|
|
func (h *Handler) respondRuntimeError(
|
|
c *gin.Context,
|
|
runCtx RunContext,
|
|
action string,
|
|
targetType string,
|
|
targetID string,
|
|
err error,
|
|
) {
|
|
if errors.Is(err, errTaskTitleRequired) || errors.Is(err, ErrProjectRequired) {
|
|
h.appendDeniedRunEvent(c.Request.Context(), runCtx, action, targetType, targetID, err)
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
var decisionValidation *DecisionValidationError
|
|
if errors.As(err, &decisionValidation) {
|
|
h.appendDeniedRunEvent(c.Request.Context(), runCtx, action, targetType, targetID, err)
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if errors.Is(err, shared.ErrForbidden) {
|
|
h.appendDeniedRunEvent(c.Request.Context(), runCtx, action, targetType, targetID, err)
|
|
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
h.logger.Error("office runtime action failed",
|
|
zap.String("action", action),
|
|
zap.String("target_type", targetType),
|
|
zap.String("target_id", targetID),
|
|
zap.String("run_id", runCtx.RunID),
|
|
zap.String("agent_id", runCtx.AgentID),
|
|
zap.String("session_id", runCtx.SessionID),
|
|
zap.Error(err),
|
|
)
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": runtimeInternalErrorMessage})
|
|
}
|
|
|
|
func (h *Handler) respondTaskStatusError(c *gin.Context, runCtx RunContext, taskID string, err error) {
|
|
if errors.Is(err, shared.ErrForbidden) {
|
|
h.respondRuntimeError(c, runCtx, "update_task_status", "task", taskID, err)
|
|
return
|
|
}
|
|
var pending PendingApprovalsError
|
|
if errors.As(err, &pending) {
|
|
h.appendActionRunEvent(c.Request.Context(), runCtx, "update_task_status", "task", taskID)
|
|
c.JSON(http.StatusConflict, gin.H{
|
|
"error": err.Error(),
|
|
"pending_approvers": h.resolvePendingApprovers(c.Request.Context(), pending.PendingApproverIDs()),
|
|
"status": "in_review",
|
|
})
|
|
return
|
|
}
|
|
var validation StatusValidationError
|
|
if errors.As(err, &validation) {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
h.respondRuntimeError(c, runCtx, "update_task_status", "task", taskID, err)
|
|
}
|
|
|
|
type pendingApproverResponse struct {
|
|
AgentProfileID string `json:"agent_profile_id"`
|
|
Name string `json:"name,omitempty"`
|
|
}
|
|
|
|
func (h *Handler) resolvePendingApprovers(ctx context.Context, ids []string) []pendingApproverResponse {
|
|
approvers := make([]pendingApproverResponse, len(ids))
|
|
for i, id := range ids {
|
|
approvers[i] = pendingApproverResponse{AgentProfileID: id, Name: id}
|
|
if h.agentSvc == nil || id == "" {
|
|
continue
|
|
}
|
|
agent, err := h.agentSvc.GetAgentInstance(ctx, id)
|
|
if err == nil && agent != nil && agent.ID == id {
|
|
if name := strings.TrimSpace(agent.Name); name != "" {
|
|
approvers[i].Name = name
|
|
}
|
|
}
|
|
}
|
|
return approvers
|
|
}
|
|
|
|
func (h *Handler) appendDeniedRunEvent(
|
|
ctx context.Context,
|
|
runCtx RunContext,
|
|
action string,
|
|
targetType string,
|
|
targetID string,
|
|
err error,
|
|
) {
|
|
if h.runEvents == nil || runCtx.RunID == "" {
|
|
return
|
|
}
|
|
h.runEvents.AppendRunEvent(ctx, runCtx.RunID, "runtime.denied", "warn", map[string]interface{}{
|
|
"action": action,
|
|
"target_type": targetType,
|
|
"target_id": targetID,
|
|
"agent_id": runCtx.AgentID,
|
|
"session_id": runCtx.SessionID,
|
|
"error": err.Error(),
|
|
})
|
|
}
|
|
|
|
func (h *Handler) appendActionRunEvent(
|
|
ctx context.Context,
|
|
runCtx RunContext,
|
|
action string,
|
|
targetType string,
|
|
targetID string,
|
|
) {
|
|
if h.runEvents == nil || runCtx.RunID == "" {
|
|
return
|
|
}
|
|
h.runEvents.AppendRunEvent(ctx, runCtx.RunID, "runtime.action", "info", map[string]interface{}{
|
|
"action": action,
|
|
"target_type": targetType,
|
|
"target_id": targetID,
|
|
"agent_id": runCtx.AgentID,
|
|
"session_id": runCtx.SessionID,
|
|
})
|
|
}
|