feat: add agnet artifact content proxy
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Vendored
+21
-3
@@ -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>
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -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
@@ -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
@@ -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": "合并时间线",
|
||||
|
||||
Reference in New Issue
Block a user