Files
goclaw/internal/store/pg/agents_export_team_standalone.go
Luan VuandLuvu182 0e2282be8f fix: use errors.Is() for sentinel comparisons + remove unused @xyflow/react (#727)
Replace direct sentinel error comparisons (==, !=) with errors.Is()
across the codebase. Direct comparison breaks when errors are wrapped
with fmt.Errorf("...: %w", err), which can cause missed matches for
sql.ErrNoRows, io.EOF, context.DeadlineExceeded, and context.Canceled.

Affected packages:
- store/pg: sql.ErrNoRows in secure_cli, agents_export_team_*, mcp_export_queries
- store/sqlitestore: sql.ErrNoRows in schema migration
- tools: context.DeadlineExceeded in shell, credentialed_exec
- mcp: context.DeadlineExceeded in bridge_tool
- providers: context.Canceled in acp_provider, anthropic_stream_test
- updater: io.EOF in tar extraction

Also removes unused @xyflow/react dependency from web UI — it was
replaced by react-force-graph-2d but never cleaned up from package.json.

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
2026-04-07 12:16:45 +07:00

302 lines
9.4 KiB
Go

package pg
import (
"context"
"database/sql"
"encoding/json"
"errors"
"log/slog"
"github.com/google/uuid"
)
// ExportTeamByID fetches team metadata by team_id directly (not via lead_agent_id).
func ExportTeamByID(ctx context.Context, db *sql.DB, teamID uuid.UUID) (*TeamExport, error) {
tc, tcArgs, _, err := scopeClause(ctx, 2)
if err != nil {
return nil, err
}
var t TeamExport
err = db.QueryRowContext(ctx,
"SELECT name, COALESCE(description,''), status, COALESCE(settings,'{}')"+
" FROM agent_teams WHERE id = $1"+tc,
append([]any{teamID}, tcArgs...)...,
).Scan(&t.Name, &t.Description, &t.Status, &t.Settings)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, err
}
return &t, nil
}
// GetTeamMemberAgents returns all agents (id + agent_key) in a team.
func GetTeamMemberAgents(ctx context.Context, db *sql.DB, teamID uuid.UUID) ([]struct {
ID uuid.UUID
AgentKey string
}, error) {
tc, tcArgs, _, err := scopeClauseAlias(ctx, 2, "m")
if err != nil {
return nil, err
}
rows, err := db.QueryContext(ctx,
"SELECT a.id, a.agent_key"+
" FROM agent_team_members m"+
" JOIN agents a ON a.id = m.agent_id"+
" WHERE m.team_id = $1"+tc,
append([]any{teamID}, tcArgs...)...,
)
if err != nil {
return nil, err
}
defer rows.Close()
var out []struct {
ID uuid.UUID
AgentKey string
}
for rows.Next() {
var item struct {
ID uuid.UUID
AgentKey string
}
if err := rows.Scan(&item.ID, &item.AgentKey); err != nil {
slog.Warn("export.team.member_agents.scan", "error", err)
continue
}
out = append(out, item)
}
return out, rows.Err()
}
// ExportTeamLinksForTeam returns agent_links where both source and target are members of the team.
func ExportTeamLinksForTeam(ctx context.Context, db *sql.DB, teamID uuid.UUID) ([]AgentLinkExport, error) {
tc, tcArgs, _, err := scopeClauseAlias(ctx, 2, "l")
if err != nil {
return nil, err
}
rows, err := db.QueryContext(ctx,
"SELECT sa.agent_key, ta.agent_key, l.direction, COALESCE(l.description,'')"+
" FROM agent_links l"+
" JOIN agents sa ON sa.id = l.source_agent_id"+
" JOIN agents ta ON ta.id = l.target_agent_id"+
" WHERE l.source_agent_id IN (SELECT agent_id FROM agent_team_members WHERE team_id = $1)"+
" AND l.target_agent_id IN (SELECT agent_id FROM agent_team_members WHERE team_id = $1)"+tc,
append([]any{teamID}, tcArgs...)...,
)
if err != nil {
return nil, err
}
defer rows.Close()
var out []AgentLinkExport
for rows.Next() {
var l AgentLinkExport
if err := rows.Scan(&l.SourceAgentKey, &l.TargetAgentKey, &l.Direction, &l.Description); err != nil {
slog.Warn("export.team.links.scan", "error", err)
continue
}
out = append(out, l)
}
return out, rows.Err()
}
// ExportTeamPreviewCountsByID returns task/member/link counts for a team by team_id.
func ExportTeamPreviewCountsByID(ctx context.Context, db *sql.DB, teamID uuid.UUID) (tasks, members, links int, err error) {
tc, tcArgs, _, err := scopeClause(ctx, 2)
if err != nil {
return
}
args := append([]any{teamID}, tcArgs...)
_ = db.QueryRowContext(ctx,
"SELECT"+
" (SELECT COUNT(*) FROM team_tasks WHERE team_id = $1"+tc+"),"+
" (SELECT COUNT(*) FROM agent_team_members WHERE team_id = $1"+tc+"),"+
" (SELECT COUNT(*) FROM agent_links"+
" WHERE source_agent_id IN (SELECT agent_id FROM agent_team_members WHERE team_id = $1"+tc+")"+
" AND target_agent_id IN (SELECT agent_id FROM agent_team_members WHERE team_id = $1"+tc+")"+tc+")",
args...,
).Scan(&tasks, &members, &links)
return
}
// ExportTeamMembersNonLead returns all non-lead members by team_id (no lead exclusion needed when doing standalone team export).
func ExportTeamMembersNonLead(ctx context.Context, db *sql.DB, teamID uuid.UUID, leadAgentID uuid.UUID) ([]TeamMemberExport, error) {
tc, tcArgs, _, err := scopeClauseAlias(ctx, 3, "m")
if err != nil {
return nil, err
}
rows, err := db.QueryContext(ctx,
"SELECT a.agent_key, m.role"+
" FROM agent_team_members m"+
" JOIN agents a ON a.id = m.agent_id"+
" WHERE m.team_id = $1 AND m.agent_id != $2"+tc,
append([]any{teamID, leadAgentID}, tcArgs...)...,
)
if err != nil {
return nil, err
}
defer rows.Close()
var out []TeamMemberExport
for rows.Next() {
var m TeamMemberExport
if err := rows.Scan(&m.AgentKey, &m.Role); err != nil {
slog.Warn("export.team.members_non_lead.scan", "error", err)
continue
}
out = append(out, m)
}
return out, rows.Err()
}
// ExportTeamLeadAgentID returns the lead agent_id for a team, or uuid.Nil if not found.
func ExportTeamLeadAgentID(ctx context.Context, db *sql.DB, teamID uuid.UUID) (uuid.UUID, error) {
tc, tcArgs, _, err := scopeClause(ctx, 2)
if err != nil {
return uuid.Nil, err
}
var leadID uuid.UUID
err = db.QueryRowContext(ctx,
"SELECT lead_agent_id FROM agent_teams WHERE id = $1"+tc,
append([]any{teamID}, tcArgs...)...,
).Scan(&leadID)
if errors.Is(err, sql.ErrNoRows) {
return uuid.Nil, nil
}
return leadID, err
}
// ExportTeamAgentConfig returns agent metadata (id, agent_key, display_name, provider, model, etc.)
// as a raw JSON-serializable map using marshalAgentConfigExport helper. Returned as json.RawMessage.
func ExportTeamAgentBasicInfo(ctx context.Context, db *sql.DB, agentID uuid.UUID) (agentKey string, err error) {
tc, tcArgs, _, err := scopeClause(ctx, 2)
if err != nil {
return "", err
}
err = db.QueryRowContext(ctx,
"SELECT agent_key FROM agents WHERE id = $1"+tc,
append([]any{agentID}, tcArgs...)...,
).Scan(&agentKey)
if errors.Is(err, sql.ErrNoRows) {
return "", nil
}
return agentKey, err
}
// ExportTeamMembersAll returns ALL members of a team (including lead) with agent_key resolution.
func ExportTeamMembersAll(ctx context.Context, db *sql.DB, teamID uuid.UUID) ([]TeamMemberExport, error) {
tc, tcArgs, _, err := scopeClauseAlias(ctx, 2, "m")
if err != nil {
return nil, err
}
rows, err := db.QueryContext(ctx,
"SELECT a.agent_key, m.role"+
" FROM agent_team_members m"+
" JOIN agents a ON a.id = m.agent_id"+
" WHERE m.team_id = $1"+tc,
append([]any{teamID}, tcArgs...)...,
)
if err != nil {
return nil, err
}
defer rows.Close()
var out []TeamMemberExport
for rows.Next() {
var m TeamMemberExport
if err := rows.Scan(&m.AgentKey, &m.Role); err != nil {
slog.Warn("export.team.all_members.scan", "error", err)
continue
}
out = append(out, m)
}
return out, rows.Err()
}
// exportTeamAgentFullData returns a fully-serializable agent config for team export.
func ExportTeamAgentJSON(ctx context.Context, db *sql.DB, agentID uuid.UUID) (json.RawMessage, error) {
tc, tcArgs, _, err := scopeClause(ctx, 2)
if err != nil {
return nil, err
}
var (
agentKey string
displayName string
frontmatter string
provider string
model string
contextWindow int
maxToolIterations int
agentType string
status string
toolsConfig []byte
sandboxConfig []byte
subagentsConfig []byte
memoryConfig []byte
compactionConfig []byte
contextPruning []byte
otherConfig []byte
)
err = db.QueryRowContext(ctx,
"SELECT agent_key, COALESCE(display_name,''), COALESCE(frontmatter,''),"+
" COALESCE(provider,''), COALESCE(model,''),"+
" context_window, max_tool_iterations, COALESCE(agent_type,'open'), COALESCE(status,'active'),"+
" tools_config, sandbox_config, subagents_config, memory_config, compaction_config,"+
" context_pruning, other_config"+
" FROM agents WHERE id = $1"+tc,
append([]any{agentID}, tcArgs...)...,
).Scan(
&agentKey, &displayName, &frontmatter,
&provider, &model,
&contextWindow, &maxToolIterations, &agentType, &status,
&toolsConfig, &sandboxConfig, &subagentsConfig, &memoryConfig, &compactionConfig,
&contextPruning, &otherConfig,
)
if err != nil {
return nil, err
}
type exportableAgent struct {
AgentKey string `json:"agent_key"`
DisplayName string `json:"display_name,omitempty"`
Frontmatter string `json:"frontmatter,omitempty"`
Provider string `json:"provider"`
Model string `json:"model"`
ContextWindow int `json:"context_window"`
MaxToolIterations int `json:"max_tool_iterations"`
AgentType string `json:"agent_type"`
Status string `json:"status"`
ToolsConfig json.RawMessage `json:"tools_config,omitempty"`
SandboxConfig json.RawMessage `json:"sandbox_config,omitempty"`
SubagentsConfig json.RawMessage `json:"subagents_config,omitempty"`
MemoryConfig json.RawMessage `json:"memory_config,omitempty"`
CompactionConfig json.RawMessage `json:"compaction_config,omitempty"`
ContextPruning json.RawMessage `json:"context_pruning,omitempty"`
OtherConfig json.RawMessage `json:"other_config,omitempty"`
}
raw, err := json.Marshal(exportableAgent{
AgentKey: agentKey,
DisplayName: displayName,
Frontmatter: frontmatter,
Provider: provider,
Model: model,
ContextWindow: contextWindow,
MaxToolIterations: maxToolIterations,
AgentType: agentType,
Status: status,
ToolsConfig: toolsConfig,
SandboxConfig: sandboxConfig,
SubagentsConfig: subagentsConfig,
MemoryConfig: memoryConfig,
CompactionConfig: compactionConfig,
ContextPruning: contextPruning,
OtherConfig: otherConfig,
})
return raw, err
}