feat: add agnet artifact content proxy

This commit is contained in:
gongzhiyong
2026-05-30 12:55:04 +08:00
parent 70663f47ee
commit edf7aeaf8a
13 changed files with 877 additions and 13 deletions
+50
View File
@@ -1,11 +1,13 @@
package controller
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
"os"
"sort"
"strconv"
@@ -768,6 +770,54 @@ func AgnetListUserDeploymentArtifacts(c *gin.Context) {
common.ApiSuccess(c, gin.H{"deployment_id": record.DeploymentID, "artifacts": items, "items": items, "total": len(items)})
}
func AgnetGetUserDeploymentArtifactContent(c *gin.Context) {
record, ok := requireAuthenticatedUserAgnetDeployment(c)
if !ok {
return
}
artifactID := strings.TrimSpace(c.Param("artifact_id"))
if artifactID == "" {
agnetError(c, "ARTIFACT_ID_REQUIRED", "artifact_id is required")
return
}
artifact, found, err := model.GetAgnetArtifactByDeployment(record.DeploymentID, artifactID)
if err != nil {
common.SysLog("AgnetGetUserDeploymentArtifactContent: " + err.Error())
agnetError(c, "ARTIFACT_QUERY_FAILED", "failed to query artifact")
return
}
if !found {
agnetError(c, "ARTIFACT_NOT_FOUND", "artifact not found")
return
}
cfg := agnetRuntimeClientConfigForMode(agnetRuntimeModeForRecord(record))
if !cfg.Enabled || strings.TrimSpace(cfg.BaseURL) == "" {
agnetError(c, "RUNTIME_NOT_CONFIGURED", "runtime is not configured")
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), cfg.Timeout)
defer cancel()
resp, err := callAgnetRuntimeArtifactContent(ctx, cfg, record, artifact.ArtifactID)
if err != nil {
common.SysLog("AgnetGetUserDeploymentArtifactContent: " + err.Error())
agnetError(c, "ARTIFACT_CONTENT_FETCH_FAILED", "failed to fetch artifact content")
return
}
defer resp.Body.Close()
headers := map[string]string{}
for _, key := range []string{"Content-Disposition", "ETag", "Last-Modified", "Cache-Control"} {
if value := strings.TrimSpace(resp.Header.Get(key)); value != "" {
headers[key] = value
}
}
contentType := strings.TrimSpace(resp.Header.Get("Content-Type"))
if contentType == "" {
contentType = "application/octet-stream"
}
c.DataFromReader(http.StatusOK, resp.ContentLength, contentType, resp.Body, headers)
}
func callbackPayloadMap(row model.AgnetCallbackEvent) map[string]any {
var payload agnetCallbackEnvelope
if err := common.UnmarshalJsonStr(row.PayloadJSON, &payload); err != nil {
@@ -397,6 +397,135 @@ func TestAgnetRuntimeShadowCreateStoresRuntimeMapping(t *testing.T) {
require.Equal(t, "accepted", stored.RuntimeState)
}
func TestAgnetRuntimeDiagnosticsWarnsOnCompletedRuntimeWithFailedAgents(t *testing.T) {
setupAgnetControlPlaneTestDB(t)
resetAgnetControlPlaneState(t)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "Bearer service-token", r.Header.Get("Authorization"))
switch {
case r.Method == http.MethodPost && r.URL.Path == "/api/agnet/deployments":
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"success":true,"data":{"deployment_id":"runtime-dep-1","swarm_id":"swarm-1","status":"running"}}`))
case r.Method == http.MethodGet && r.URL.Path == "/api/swarms/swarm-1/status":
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{
"deployment_id":"swarm-1",
"swarm_id":"swarm-1",
"status":"completed",
"phase":"development",
"agents":[{"agent_id":"agi_backend_1","role":"backend","status":"failed","output":"Cannot connect to host agent svc"}],
"artifacts":[{"artifact_id":"art_summary","artifact_type":"document","title":"Runtime execution summary","summary":"Runtime completed without per-agent artifacts; review swarm logs for details.","uri":"runtime://swarm-1/artifacts/summary"}],
"metrics":{"tokens_used":0}
}`))
default:
http.NotFound(w, r)
}
}))
defer server.Close()
t.Setenv("AGNET_RUNTIME_ENABLED", "true")
t.Setenv("AGNET_RUNTIME_ASYNC", "false")
t.Setenv("AGNET_RUNTIME_BASE_URL", server.URL)
t.Setenv("AGNET_RUNTIME_SERVICE_TOKEN", "service-token")
plan := baseAgnetResourceGrantPlan()
plan.UserContext.UserID = "7"
for idx := range plan.ResourceGrants {
plan.ResourceGrants[idx].UserID = "7"
}
for agentIdx := range plan.Agents {
for grantIdx := range plan.Agents[agentIdx].ResourceGrants {
plan.Agents[agentIdx].ResourceGrants[grantIdx].UserID = "7"
}
}
createRecorder, envelope := postAgnetCreateUserDeployment(t, 7, plan)
require.True(t, envelope.Success, createRecorder.Body.String())
var createBody map[string]any
require.NoError(t, common.Unmarshal(createRecorder.Body.Bytes(), &createBody))
deploymentID := createBody["data"].(map[string]any)["deployment_id"].(string)
recorder := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(recorder)
ctx.Set("id", 7)
ctx.Params = gin.Params{{Key: "deployment_id", Value: deploymentID}}
ctx.Request = httptest.NewRequest(http.MethodGet, "/api/agnet/user/deployments/"+deploymentID+"/runtime-diagnostics", nil)
AgnetGetUserDeploymentRuntimeDiagnostics(ctx)
require.Equal(t, http.StatusOK, recorder.Code)
require.Contains(t, recorder.Body.String(), `"data_source":"runtime_status"`)
require.Contains(t, recorder.Body.String(), `"runtime_agent_failed"`)
require.Contains(t, recorder.Body.String(), `"runtime_completed_with_failed_agents"`)
require.Contains(t, recorder.Body.String(), `"runtime_summary_artifact_only"`)
require.Contains(t, recorder.Body.String(), `"runtime_zero_model_usage"`)
}
func TestAgnetArtifactContentProxiesRuntimeContent(t *testing.T) {
setupAgnetControlPlaneTestDB(t)
resetAgnetControlPlaneState(t)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "Bearer service-token", r.Header.Get("Authorization"))
switch {
case r.Method == http.MethodPost && r.URL.Path == "/api/agnet/deployments":
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"success":true,"data":{"deployment_id":"runtime-dep-1","swarm_id":"swarm-1","status":"running"}}`))
case r.Method == http.MethodGet && r.URL.Path == "/api/swarms/swarm-1/artifacts/art-1/content":
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("Content-Disposition", `attachment; filename="art-1.txt"`)
_, _ = w.Write([]byte("artifact body"))
default:
http.NotFound(w, r)
}
}))
defer server.Close()
t.Setenv("AGNET_RUNTIME_ENABLED", "true")
t.Setenv("AGNET_RUNTIME_ASYNC", "false")
t.Setenv("AGNET_RUNTIME_BASE_URL", server.URL)
t.Setenv("AGNET_RUNTIME_SERVICE_TOKEN", "service-token")
plan := baseAgnetResourceGrantPlan()
plan.UserContext.UserID = "7"
for idx := range plan.ResourceGrants {
plan.ResourceGrants[idx].UserID = "7"
}
for agentIdx := range plan.Agents {
for grantIdx := range plan.Agents[agentIdx].ResourceGrants {
plan.Agents[agentIdx].ResourceGrants[grantIdx].UserID = "7"
}
}
createRecorder, envelope := postAgnetCreateUserDeployment(t, 7, plan)
require.True(t, envelope.Success, createRecorder.Body.String())
var createBody map[string]any
require.NoError(t, common.Unmarshal(createRecorder.Body.Bytes(), &createBody))
deploymentID := createBody["data"].(map[string]any)["deployment_id"].(string)
require.NoError(t, model.UpsertAgnetArtifact(&model.AgnetArtifact{
ArtifactID: "art-1",
DeploymentID: deploymentID,
UserID: "7",
URI: "runtime://swarm-1/artifacts/art-1",
CreatedAtMs: time.Now().UnixMilli(),
}))
recorder := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(recorder)
ctx.Set("id", 7)
ctx.Params = gin.Params{
{Key: "deployment_id", Value: deploymentID},
{Key: "artifact_id", Value: "art-1"},
}
ctx.Request = httptest.NewRequest(http.MethodGet, "/api/agnet/user/deployments/"+deploymentID+"/artifacts/art-1/content", nil)
AgnetGetUserDeploymentArtifactContent(ctx)
require.Equal(t, http.StatusOK, recorder.Code)
require.Equal(t, "artifact body", recorder.Body.String())
require.Equal(t, "text/plain; charset=utf-8", recorder.Header().Get("Content-Type"))
require.Equal(t, `attachment; filename="art-1.txt"`, recorder.Header().Get("Content-Disposition"))
}
func TestAgnetRuntimeShadowCreateFailureDoesNotFailLocalDeployment(t *testing.T) {
db := setupAgnetControlPlaneTestDB(t)
resetAgnetControlPlaneState(t)
+266
View File
@@ -32,6 +32,8 @@ type agnetRuntimeConfig struct {
Token string
CreatePath string
HealthPath string
StatusPath string
ArtifactContentPath string
StopPath string
ApprovalDecisionPath string
Timeout time.Duration
@@ -44,6 +46,25 @@ type agnetRuntimeSyncResult struct {
RawStatusCode int
}
type agnetRuntimeDiagnostics struct {
DeploymentID string `json:"deployment_id"`
RuntimeMode string `json:"runtime_mode"`
SubMode string `json:"sub_mode"`
RuntimeDeploymentID string `json:"runtime_deployment_id,omitempty"`
RuntimeSwarmID string `json:"runtime_swarm_id,omitempty"`
DataSource string `json:"data_source"`
HTTPStatus int `json:"http_status,omitempty"`
Status string `json:"status,omitempty"`
Phase string `json:"phase,omitempty"`
Progress any `json:"progress,omitempty"`
ErrorMessage string `json:"error_message,omitempty"`
Agents []gin.H `json:"agents"`
Artifacts []gin.H `json:"artifacts"`
Metrics map[string]any `json:"metrics,omitempty"`
Warnings []string `json:"warnings"`
CheckedAt string `json:"checked_at"`
}
func normalizeAgnetRuntimeMode(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case agnetRuntimeModeSwarm:
@@ -101,6 +122,8 @@ func agnetRuntimeClientConfigForMode(mode string) agnetRuntimeConfig {
Token: strings.TrimSpace(common.GetEnvOrDefaultString(prefix+"SERVICE_TOKEN", "")),
CreatePath: common.GetEnvOrDefaultString(prefix+"CREATE_PATH", defaultCreatePath),
HealthPath: common.GetEnvOrDefaultString(prefix+"HEALTH_PATH", "/api/agnet/health"),
StatusPath: common.GetEnvOrDefaultString(prefix+"STATUS_PATH", "/api/swarms/{swarm_id}/status"),
ArtifactContentPath: common.GetEnvOrDefaultString(prefix+"ARTIFACT_CONTENT_PATH", "/api/swarms/{swarm_id}/artifacts/{artifact_id}/content"),
StopPath: common.GetEnvOrDefaultString(prefix+"STOP_PATH", defaultStopPath),
ApprovalDecisionPath: common.GetEnvOrDefaultString(prefix+"APPROVAL_DECISION_PATH", defaultApprovalPath),
Timeout: time.Duration(timeoutSec) * time.Second,
@@ -505,6 +528,249 @@ func agnetRuntimeStopPathForRecord(cfg agnetRuntimeConfig, record agnetDeploymen
return replacer.Replace(path)
}
func agnetRuntimeStatusPathForRecord(cfg agnetRuntimeConfig, record agnetDeploymentRecord) string {
path := strings.TrimSpace(cfg.StatusPath)
if path == "" {
path = "/api/swarms/{swarm_id}/status"
}
replacer := strings.NewReplacer(
"{swarm_id}", url.PathEscape(strings.TrimSpace(record.RuntimeSwarmID)),
"{runtime_swarm_id}", url.PathEscape(strings.TrimSpace(record.RuntimeSwarmID)),
"{deployment_id}", url.PathEscape(strings.TrimSpace(record.RuntimeDeploymentID)),
"{runtime_deployment_id}", url.PathEscape(strings.TrimSpace(record.RuntimeDeploymentID)),
"{manager_deployment_id}", url.PathEscape(strings.TrimSpace(record.DeploymentID)),
)
return replacer.Replace(path)
}
func agnetRuntimeArtifactContentPathForRecord(cfg agnetRuntimeConfig, record agnetDeploymentRecord, artifactID string) string {
path := strings.TrimSpace(cfg.ArtifactContentPath)
if path == "" {
path = "/api/swarms/{swarm_id}/artifacts/{artifact_id}/content"
}
replacer := strings.NewReplacer(
"{swarm_id}", url.PathEscape(strings.TrimSpace(record.RuntimeSwarmID)),
"{runtime_swarm_id}", url.PathEscape(strings.TrimSpace(record.RuntimeSwarmID)),
"{deployment_id}", url.PathEscape(strings.TrimSpace(record.RuntimeDeploymentID)),
"{runtime_deployment_id}", url.PathEscape(strings.TrimSpace(record.RuntimeDeploymentID)),
"{manager_deployment_id}", url.PathEscape(strings.TrimSpace(record.DeploymentID)),
"{artifact_id}", url.PathEscape(strings.TrimSpace(artifactID)),
)
return replacer.Replace(path)
}
func callAgnetRuntimeStatus(ctx context.Context, cfg agnetRuntimeConfig, record agnetDeploymentRecord) (map[string]any, int, error) {
if strings.TrimSpace(record.RuntimeSwarmID) == "" && strings.TrimSpace(record.RuntimeDeploymentID) == "" {
return nil, 0, errors.New("runtime identifiers missing")
}
endpoint, err := agnetRuntimeURL(cfg.BaseURL, agnetRuntimeStatusPathForRecord(cfg, record))
if err != nil {
return nil, 0, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, 0, err
}
agnetRuntimeHeaders(req, cfg, record)
client := &http.Client{Timeout: cfg.Timeout}
resp, err := client.Do(req)
if err != nil {
return nil, 0, err
}
defer resp.Body.Close()
body, readErr := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if readErr != nil {
return nil, resp.StatusCode, readErr
}
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
return nil, resp.StatusCode, fmt.Errorf("runtime status returned HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
}
var envelope map[string]any
if len(body) > 0 {
if err := common.Unmarshal(body, &envelope); err != nil {
return nil, resp.StatusCode, err
}
}
if message := agnetRuntimeEnvelopeError(envelope); message != "" {
return nil, resp.StatusCode, errors.New(message)
}
return extractAgnetRuntimeData(envelope), resp.StatusCode, nil
}
func callAgnetRuntimeArtifactContent(ctx context.Context, cfg agnetRuntimeConfig, record agnetDeploymentRecord, artifactID string) (*http.Response, error) {
if strings.TrimSpace(artifactID) == "" {
return nil, errors.New("artifact_id is required")
}
if strings.TrimSpace(record.RuntimeSwarmID) == "" && strings.TrimSpace(record.RuntimeDeploymentID) == "" {
return nil, errors.New("runtime identifiers missing")
}
endpoint, err := agnetRuntimeURL(cfg.BaseURL, agnetRuntimeArtifactContentPathForRecord(cfg, record, artifactID))
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
agnetRuntimeHeaders(req, cfg, record)
req.Header.Set("Accept", "*/*")
client := &http.Client{Timeout: cfg.Timeout}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices {
return resp, nil
}
defer resp.Body.Close()
body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
return nil, fmt.Errorf("runtime artifact content returned HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
}
func mapSliceFromAny(value any) []gin.H {
raw, ok := value.([]any)
if !ok {
return nil
}
items := make([]gin.H, 0, len(raw))
for _, entry := range raw {
if item, ok := entry.(map[string]any); ok {
items = append(items, gin.H(item))
}
}
return items
}
func mapFromAny(value any) map[string]any {
if item, ok := value.(map[string]any); ok {
return item
}
return nil
}
func runtimeAgentHasFailed(agents []gin.H) bool {
for _, agent := range agents {
status := strings.ToLower(strings.TrimSpace(fmt.Sprint(agent["status"])))
if strings.Contains(status, "fail") ||
strings.Contains(status, "crash") ||
strings.Contains(status, "error") {
return true
}
}
return false
}
func runtimeArtifactsAreSummaryOnly(artifacts []gin.H) bool {
if len(artifacts) == 0 {
return false
}
for _, artifact := range artifacts {
title := strings.ToLower(strings.TrimSpace(fmt.Sprint(artifact["title"])))
summary := strings.ToLower(strings.TrimSpace(fmt.Sprint(artifact["summary"])))
uri := strings.ToLower(strings.TrimSpace(fmt.Sprint(artifact["uri"])))
if strings.Contains(summary, "without per-agent artifacts") ||
strings.Contains(title, "runtime execution summary") ||
strings.Contains(uri, "/artifacts/summary") {
continue
}
return false
}
return true
}
func buildAgnetRuntimeDiagnostics(record agnetDeploymentRecord, data map[string]any, httpStatus int, source string) agnetRuntimeDiagnostics {
agents := mapSliceFromAny(data["agents"])
artifacts := mapSliceFromAny(data["artifacts"])
status := stringFromMap(data, "runtime_status", "status")
metrics := mapFromAny(data["metrics"])
warnings := make([]string, 0, 4)
agentFailed := runtimeAgentHasFailed(agents)
if agentFailed {
warnings = append(warnings, "runtime_agent_failed")
}
if strings.Contains(strings.ToLower(status), "completed") && agentFailed {
warnings = append(warnings, "runtime_completed_with_failed_agents")
}
if runtimeArtifactsAreSummaryOnly(artifacts) {
warnings = append(warnings, "runtime_summary_artifact_only")
}
if metrics != nil {
tokens := fmt.Sprint(metrics["tokens_used"])
if tokens == "0" || tokens == "0.0" {
warnings = append(warnings, "runtime_zero_model_usage")
}
}
return agnetRuntimeDiagnostics{
DeploymentID: record.DeploymentID,
RuntimeMode: agnetRuntimeModeForRecord(record),
SubMode: record.SubMode,
RuntimeDeploymentID: record.RuntimeDeploymentID,
RuntimeSwarmID: record.RuntimeSwarmID,
DataSource: source,
HTTPStatus: httpStatus,
Status: status,
Phase: stringFromMap(data, "phase", "stage"),
Progress: data["progress"],
ErrorMessage: stringFromMap(data, "error_message", "failure_reason", "error"),
Agents: agents,
Artifacts: artifacts,
Metrics: metrics,
Warnings: warnings,
CheckedAt: agnetNow(),
}
}
func agnetRuntimeDiagnosticsForRecord(ctx context.Context, record agnetDeploymentRecord) agnetRuntimeDiagnostics {
mode := agnetRuntimeModeForRecord(record)
cfg := agnetRuntimeClientConfigForMode(mode)
if !cfg.Enabled || strings.TrimSpace(cfg.BaseURL) == "" {
return agnetRuntimeDiagnostics{
DeploymentID: record.DeploymentID,
RuntimeMode: mode,
SubMode: record.SubMode,
DataSource: "not_configured",
Warnings: []string{"runtime_not_configured"},
CheckedAt: agnetNow(),
}
}
if strings.TrimSpace(record.RuntimeSwarmID) == "" && strings.TrimSpace(record.RuntimeDeploymentID) == "" {
return agnetRuntimeDiagnostics{
DeploymentID: record.DeploymentID,
RuntimeMode: mode,
SubMode: record.SubMode,
DataSource: "missing_runtime_id",
Warnings: []string{"runtime_identifiers_missing"},
CheckedAt: agnetNow(),
}
}
data, status, err := callAgnetRuntimeStatus(ctx, cfg, record)
if err != nil {
return agnetRuntimeDiagnostics{
DeploymentID: record.DeploymentID,
RuntimeMode: mode,
SubMode: record.SubMode,
RuntimeDeploymentID: record.RuntimeDeploymentID,
RuntimeSwarmID: record.RuntimeSwarmID,
DataSource: "runtime_status_error",
HTTPStatus: status,
ErrorMessage: truncateAgnetFailureReason(err.Error()),
Warnings: []string{"runtime_status_query_failed"},
CheckedAt: agnetNow(),
}
}
return buildAgnetRuntimeDiagnostics(record, data, status, "runtime_status")
}
func AgnetGetUserDeploymentRuntimeDiagnostics(c *gin.Context) {
record, ok := requireAuthenticatedUserAgnetDeployment(c)
if !ok {
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), agnetRuntimeClientConfigForMode(agnetRuntimeModeForRecord(record)).Timeout)
defer cancel()
common.ApiSuccess(c, agnetRuntimeDiagnosticsForRecord(ctx, record))
}
func agnetRuntimeApprovalDecisionPath(cfg agnetRuntimeConfig, record agnetDeploymentRecord, approvalID string) string {
path := strings.TrimSpace(cfg.ApprovalDecisionPath)
if path == "" {
+27
View File
@@ -1,5 +1,12 @@
package model
import (
"errors"
"strings"
"gorm.io/gorm"
)
type AgnetArtifact struct {
Id int `gorm:"primaryKey" json:"id"`
ArtifactID string `gorm:"type:varchar(128);uniqueIndex" json:"artifact_id"`
@@ -64,3 +71,23 @@ func ListAgnetArtifacts(f ListAgnetArtifactsFilter) ([]AgnetArtifact, error) {
err := q.Order("created_at_ms asc, id asc").Limit(limit).Find(&items).Error
return items, err
}
func GetAgnetArtifactByDeployment(deploymentID string, artifactID string) (AgnetArtifact, bool, error) {
var row AgnetArtifact
if DB == nil {
return row, false, nil
}
deploymentID = strings.TrimSpace(deploymentID)
artifactID = strings.TrimSpace(artifactID)
if deploymentID == "" || artifactID == "" {
return row, false, nil
}
err := DB.Where("deployment_id = ? AND artifact_id = ?", deploymentID, artifactID).First(&row).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return row, false, nil
}
if err != nil {
return row, false, err
}
return row, true, nil
}
+2
View File
@@ -509,9 +509,11 @@ func SetApiRouter(router *gin.Engine) {
agnetApprovalRoute.POST("/user/deployments/:deployment_id/stop", controller.AgnetStopUserDeployment)
agnetApprovalRoute.GET("/user/deployments/:deployment_id/logs", controller.AgnetListUserDeploymentLogs)
agnetApprovalRoute.GET("/user/deployments/:deployment_id/metrics", controller.AgnetGetUserDeploymentMetrics)
agnetApprovalRoute.GET("/user/deployments/:deployment_id/runtime-diagnostics", controller.AgnetGetUserDeploymentRuntimeDiagnostics)
agnetApprovalRoute.GET("/user/deployments/:deployment_id/events", controller.AgnetListUserDeploymentEvents)
agnetApprovalRoute.POST("/user/deployments/:deployment_id/simulate-events", controller.AgnetSimulateUserDeploymentEvents)
agnetApprovalRoute.GET("/user/deployments/:deployment_id/artifacts", controller.AgnetListUserDeploymentArtifacts)
agnetApprovalRoute.GET("/user/deployments/:deployment_id/artifacts/:artifact_id/content", controller.AgnetGetUserDeploymentArtifactContent)
agnetApprovalRoute.GET("/user/deployments/:deployment_id/sk-snapshots", controller.AgnetListUserSKSnapshots)
agnetApprovalRoute.GET("/user/deployments/:deployment_id/timeline", controller.AgnetGetUserDeploymentTimeline)
agnetApprovalRoute.POST("/user/tasks/:task_id/deployment-draft", controller.AgnetCreateTaskDeploymentDraft)
+21 -3
View File
@@ -1,9 +1,27 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Heicode Manager placeholder</title>
</head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/heicode-logo.svg?v=h-glass-2" />
<link rel="icon" type="image/png" sizes="32x32" href="/logo.png?v=h-glass-2" />
<link rel="shortcut icon" href="/favicon.ico?v=h-glass-2" />
<link rel="apple-touch-icon" href="/logo.png?v=h-glass-2" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Primary Meta Tags -->
<title>Heicode Manager</title>
<meta name="title" content="Heicode Manager" />
<meta
name="description"
content="Heicode Manager — multi-tenant control plane for Agnet deployments, events and audit."
/>
<meta property="og:title" content="Heicode Manager" />
<meta property="og:image" content="/logo.png?v=h-glass-2" />
<meta property="og:type" content="website" />
<meta name="theme-color" content="#7B6BE3" />
<link rel="icon" href="/favicon.ico"><script defer src="/static/js/vendor-radix.8fa3e0a349.js"></script><script defer src="/static/js/vendor-tanstack.632dbe8908.js"></script><script defer src="/static/js/lib-react.5c8909c28c.js"></script><script defer src="/static/js/9238.45c9c35ccf.js"></script><script defer src="/static/js/index.c2989bbeef.js"></script><link href="/static/css/index.fd51d44fe8.css" rel="stylesheet"></head>
<body>
<div id="root"></div>
</body>
+33
View File
@@ -179,6 +179,25 @@ export type AgnetDeployment = {
}
}
export type AgnetRuntimeDiagnostics = {
deployment_id: string
runtime_mode?: 'agnet' | 'swarm' | string
sub_mode?: AgnetSubMode
runtime_deployment_id?: string
runtime_swarm_id?: string
data_source?: string
http_status?: number
status?: string
phase?: string
progress?: unknown
error_message?: string
agents?: Array<Record<string, unknown>>
artifacts?: Array<Record<string, unknown>>
metrics?: Record<string, unknown>
warnings?: string[]
checked_at?: string
}
export type AgnetApprovalRequest = {
approval_id: string
user_id: number
@@ -360,6 +379,20 @@ export async function getAgnetDeploymentTimeline(deploymentId: string) {
)
}
export async function getAgnetRuntimeDiagnostics(
deploymentId: string
): Promise<AgnetRuntimeDiagnostics | null> {
const res = await api.get<ApiEnvelope<AgnetRuntimeDiagnostics>>(
`/api/agnet/user/deployments/${deploymentId}/runtime-diagnostics`,
{
skipBusinessError: true,
skipErrorHandler: true,
} as Record<string, unknown>
)
if (!res.data?.success) return null
return res.data?.data ?? null
}
export async function simulateAgnetDeploymentEvents(
deploymentId: string,
events?: string[]
+182
View File
@@ -63,6 +63,7 @@ import { QueryState } from '@/components/query-state'
import {
approveAgnetApproval,
getAgnetDeploymentEvents,
getAgnetRuntimeDiagnostics,
getAgnetDeploymentTimeline,
listAgnetApprovals,
listAgnetCredentialLeases,
@@ -73,6 +74,7 @@ import {
type AgnetApprovalRequest,
type AgnetCredentialLease,
type AgnetDeployment,
type AgnetRuntimeDiagnostics,
type AgnetRuntimeExecution,
type AgnetSKAccessPolicy,
} from './api'
@@ -354,6 +356,17 @@ function artifactTypeToneClass(value: unknown): string {
return 'bg-muted/40 text-muted-foreground ring-border/60'
}
function isFallbackRuntimeArtifact(item: Record<string, unknown>): boolean {
const title = String(item.title || '').toLowerCase()
const summary = String(item.summary || '').toLowerCase()
const uri = String(item.uri || '').toLowerCase()
return (
title.includes('runtime execution summary') ||
summary.includes('without per-agent artifacts') ||
uri.includes('/artifacts/summary')
)
}
function isTaskFlowEvent(eventType: unknown): boolean {
const event = String(eventType || '').toLowerCase()
return event.startsWith('task.') || event.startsWith('handoff.')
@@ -530,6 +543,145 @@ function formatGrantStatus(
return status || t('Active')
}
function runtimeWarningLabel(value: string, t: (key: string) => string): string {
switch (value) {
case 'runtime_agent_failed':
return t('Runtime agent failed')
case 'runtime_completed_with_failed_agents':
return t('Runtime completed with failed agents')
case 'runtime_summary_artifact_only':
return t('Only fallback summary artifact returned')
case 'runtime_zero_model_usage':
return t('Runtime reported zero model usage')
case 'runtime_status_query_failed':
return t('Runtime status query failed')
case 'runtime_not_configured':
return t('Runtime not configured')
case 'runtime_identifiers_missing':
return t('Runtime identifiers missing')
default:
return value
}
}
function runtimeModeLabel(
diagnostics: AgnetRuntimeDiagnostics | null | undefined,
t: (key: string) => string
): string {
const mode = String(diagnostics?.runtime_mode || '').toLowerCase()
if (mode === 'swarm') return t('Swarm mode')
if (mode === 'agnet') return t('Ordinary sub mode')
return mode || '—'
}
function runtimeAgentRows(
diagnostics: AgnetRuntimeDiagnostics | null | undefined
) {
return (diagnostics?.agents ?? []).slice(0, 4).map((agent, idx) => ({
id: String(agent.agent_id || agent.instance_id || idx),
role: String(agent.role || '—'),
status: String(agent.status || agent.runtime_state || '—'),
output: String(agent.output || agent.failure_reason || ''),
}))
}
function RuntimeDiagnosticsPanel({
diagnostics,
isLoading,
}: {
diagnostics?: AgnetRuntimeDiagnostics | null
isLoading: boolean
}) {
const { t } = useTranslation()
const warnings = diagnostics?.warnings ?? []
const agents = runtimeAgentRows(diagnostics)
const hasWarning = warnings.length > 0
return (
<div
className={cn(
'mt-3 rounded-xl border p-3',
hasWarning
? 'border-amber-500/35 bg-amber-500/10'
: 'border-dashed border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-background/35'
)}
>
<div className='flex items-start justify-between gap-3'>
<div>
<p className='text-foreground flex items-center gap-2 text-xs font-semibold'>
<AlertOctagon
className={cn(
'h-3.5 w-3.5',
hasWarning ? 'text-amber-400' : 'text-muted-foreground'
)}
/>
{t('Runtime diagnostics')}
</p>
<p className='text-muted-foreground mt-1 text-xs'>
{t(
'Manager checks Runtime status separately from callback data, without mixing ordinary sub and swarm modes.'
)}
</p>
</div>
{isLoading ? (
<CircleDashed className='text-muted-foreground h-4 w-4 animate-spin' />
) : null}
</div>
<div className='mt-3 grid gap-2 sm:grid-cols-3'>
<MetaPill
icon={Rocket}
label={t('runtime mode')}
value={runtimeModeLabel(diagnostics, t)}
/>
<MetaPill
icon={Activity}
label={t('runtime status')}
value={formatStatusLabel(diagnostics?.status || '—', t)}
/>
<MetaPill
icon={FileSearch}
label={t('artifact source')}
value={String(diagnostics?.data_source || '—')}
/>
</div>
{hasWarning && (
<div className='mt-3 flex flex-wrap gap-1.5'>
{warnings.map((warning) => (
<span
key={warning}
className='rounded-full bg-amber-500/15 px-2 py-0.5 text-[10px] font-medium text-amber-200 ring-1 ring-amber-500/30 ring-inset'
>
{runtimeWarningLabel(warning, t)}
</span>
))}
</div>
)}
{agents.length > 0 && (
<div className='mt-3 space-y-1.5'>
{agents.map((agent) => (
<div
key={agent.id}
className='bg-background/45 rounded-lg px-2 py-1.5 text-[11px]'
>
<div className='flex items-center justify-between gap-2'>
<span className='font-mono'>{agent.role}</span>
<StatusBadge phase={agent.status} />
</div>
{agent.output && (
<p className='text-muted-foreground mt-1 line-clamp-2'>
{agent.output}
</p>
)}
</div>
))}
</div>
)}
</div>
)
}
function RunDetailPanel({ dep }: { dep: AgnetDeployment }) {
const { t } = useTranslation()
const queryClient = useQueryClient()
@@ -538,6 +690,13 @@ function RunDetailPanel({ dep }: { dep: AgnetDeployment }) {
const phase = dep.phase || dep.status
const risk = describeRiskLevel(dep)
const grants = collectResourceGrants(dep)
const runtimeDiagnosticsQuery = useQuery({
queryKey: ['agnet', 'runtime-diagnostics', dep.deployment_id],
queryFn: () => getAgnetRuntimeDiagnostics(dep.deployment_id),
enabled: Boolean(dep.deployment_id),
refetchInterval: 30_000,
})
const runtimeDiagnostics = runtimeDiagnosticsQuery.data
const simulateMutation = useMutation({
mutationFn: () => simulateAgnetDeploymentEvents(dep.deployment_id),
onSuccess: () => {
@@ -650,6 +809,11 @@ function RunDetailPanel({ dep }: { dep: AgnetDeployment }) {
/>
</div>
<RuntimeDiagnosticsPanel
diagnostics={runtimeDiagnostics}
isLoading={runtimeDiagnosticsQuery.isLoading}
/>
<div className='mt-4 grid gap-3 md:grid-cols-3'>
<div className='bg-background/45 rounded-xl border border-dashed border-[color-mix(in_oklch,var(--primary)_22%,var(--border))] p-3 md:col-span-3'>
<div className='flex items-center justify-between gap-3'>
@@ -1309,11 +1473,29 @@ function RunRelatedRecordsPanel({ deploymentId }: { deploymentId: string }) {
<p className='text-muted-foreground mt-1 line-clamp-2 text-[11px]'>
{String(item.summary || item.uri || '—')}
</p>
{isFallbackRuntimeArtifact(item) && (
<p className='mt-1 rounded-md bg-amber-500/10 px-2 py-1 text-[10px] text-amber-200 ring-1 ring-amber-500/25 ring-inset'>
{t(
'Fallback summary only; not a final business deliverable.'
)}
</p>
)}
{Boolean(item.uri) && (
<p className='text-muted-foreground mt-1 truncate font-mono text-[10px]'>
{String(item.uri)}
</p>
)}
{Boolean(item.artifact_id) && (
<a
href={`/api/agnet/user/deployments/${encodeURIComponent(deploymentId)}/artifacts/${encodeURIComponent(String(item.artifact_id))}/content`}
target='_blank'
rel='noreferrer'
className='text-primary mt-2 inline-flex items-center gap-1 text-[11px] font-medium hover:underline'
>
<ArrowUpRight className='h-3 w-3' />
{t('Download artifact')}
</a>
)}
</li>
))}
</ul>
+18
View File
@@ -3478,6 +3478,24 @@
"Task not found. It may have been removed or was never created.": "Task not found. It may have been removed or was never created.",
"Task overview": "Task overview",
"Tasks appear here after you confirm the recommendation in the desktop client and launch Agnet.": "Tasks appear here after you confirm the recommendation in the desktop client and launch Agnet.",
"Runtime diagnostics": "Runtime diagnostics",
"Manager checks Runtime status separately from callback data, without mixing ordinary sub and swarm modes.": "Manager checks Runtime status separately from callback data, without mixing ordinary sub and swarm modes.",
"runtime mode": "runtime mode",
"runtime status": "runtime status",
"artifact source": "artifact source",
"Runtime agent failed": "Runtime agent failed",
"Runtime completed with failed agents": "Runtime completed with failed agents",
"Only fallback summary artifact returned": "Only fallback summary artifact returned",
"Runtime reported zero model usage": "Runtime reported zero model usage",
"Runtime status query failed": "Runtime status query failed",
"Runtime not configured": "Runtime not configured",
"Runtime identifiers missing": "Runtime identifiers missing",
"Swarm mode": "Swarm mode",
"Ordinary sub mode": "Ordinary sub mode",
"Artifacts": "Artifacts",
"No artifacts yet": "No artifacts yet",
"Fallback summary only; not a final business deliverable.": "Fallback summary only; not a final business deliverable.",
"Download artifact": "Download artifact",
"Team Collaboration": "Team Collaboration",
"Technical Support": "Technical Support",
"Telegram": "Telegram",
+16
View File
@@ -3482,6 +3482,20 @@
"Task not found. It may have been removed or was never created.": "任务不存在,可能已被删除或从未创建。",
"Task overview": "任务总览",
"Tasks appear here after you confirm the recommendation in the desktop client and launch Agnet.": "在客户端确认推荐摘要并启动 Agnet 后,任务会出现在这里。",
"Runtime diagnostics": "运行时诊断",
"Manager checks Runtime status separately from callback data, without mixing ordinary sub and swarm modes.": "Manager 会独立检查运行时状态,并明确区分普通 sub 与蜂群模式,不混用回调数据。",
"runtime mode": "运行模式",
"runtime status": "运行状态",
"artifact source": "产物来源",
"Runtime agent failed": "运行时智能体失败",
"Runtime completed with failed agents": "运行时已结束但存在失败智能体",
"Only fallback summary artifact returned": "只返回了兜底摘要产物",
"Runtime reported zero model usage": "运行时模型用量为 0",
"Runtime status query failed": "运行时状态查询失败",
"Runtime not configured": "运行时未配置",
"Runtime identifiers missing": "缺少运行时标识",
"Swarm mode": "蜂群模式",
"Ordinary sub mode": "普通 sub 模式",
"Team Collaboration": "团队协作",
"Technical Support": "技术支持",
"Telegram": "Telegram",
@@ -4148,6 +4162,8 @@
"Agent role": "智能体角色",
"Artifacts": "产物",
"No artifacts yet": "暂无产物",
"Fallback summary only; not a final business deliverable.": "仅为运行时兜底摘要,不是最终业务交付物。",
"Download artifact": "下载产物",
"SK snapshots": "SK 快照",
"No SK snapshots yet": "暂无 SK 快照",
"Merged timeline": "合并时间线",