Files
teexiii 88a5793030 fix(agents): sync IDENTITY.md Name field on agent rename (#582)
## Summary

- Agent rename now updates Name field in IDENTITY.md (agent-level + per-user copies for open agents) via both HTTP and WS paths
- Fixes lossy identity rebuild: previous logic reconstructed IDENTITY.md from only Name/Emoji/Avatar, silently dropping LLM-written fields (Creature, Purpose, Vibe, etc.)
- Uses targeted field replacement (bootstrap.UpdateIdentityField) preserving original formatting
- Adds ListUserContextFilesByName to AgentContextStore (PG + SQLite)
- Fixes LastIndex bug in UpdateIdentityField where values containing colons (e.g. Avatar URLs) would break field replacement
- Removes ~335 lines of dead config.json fallback code (agentStore is always set)
- Adds 9 unit tests for UpdateIdentityField covering plain/markdown formats, URLs, edge cases
2026-03-30 20:56:49 +07:00

107 lines
2.7 KiB
Go

package methods
import (
"context"
"encoding/json"
"strings"
"github.com/nextlevelbuilder/goclaw/internal/gateway"
"github.com/nextlevelbuilder/goclaw/pkg/protocol"
)
// --- agent.identity.get ---
// Matching TS src/gateway/server-methods/agent.ts:601-643
func (m *AgentsMethods) handleIdentityGet(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
var params struct {
AgentID string `json:"agentId"`
SessionKey string `json:"sessionKey"`
}
if req.Params != nil {
json.Unmarshal(req.Params, &params)
}
if params.AgentID == "" {
// Try to extract from sessionKey: "agent:{agentId}:..."
if params.SessionKey != "" {
parts := strings.SplitN(params.SessionKey, ":", 3)
if len(parts) >= 2 {
params.AgentID = parts[1]
}
}
if params.AgentID == "" {
params.AgentID = "default"
}
}
result := map[string]any{
"agentId": params.AgentID,
}
if m.agentStore != nil {
// --- DB-backed: read identity from store ---
ag, err := m.agentStore.GetByKey(ctx, params.AgentID)
if err == nil {
result["name"] = ag.DisplayName
// Parse IDENTITY.md from DB bootstrap
dbFiles, _ := m.agentStore.GetAgentContextFiles(ctx, ag.ID)
for _, f := range dbFiles {
if f.FileName == "IDENTITY.md" {
if identity := parseIdentityContent(f.Content); identity != nil {
if identity["Name"] != "" {
result["name"] = identity["Name"]
}
if identity["Emoji"] != "" {
result["emoji"] = identity["Emoji"]
}
if identity["Avatar"] != "" {
result["avatar"] = identity["Avatar"]
}
if identity["Description"] != "" {
result["description"] = identity["Description"]
}
}
break
}
}
}
}
client.SendResponse(protocol.NewOKResponse(req.ID, result))
}
// parseIdentityContent parses IDENTITY.md content string and extracts Key: Value fields.
func parseIdentityContent(content string) map[string]string {
result := make(map[string]string)
for line := range strings.SplitSeq(content, "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "#") || line == "" {
continue
}
if idx := strings.Index(line, ":"); idx > 0 {
key := strings.TrimSpace(line[:idx])
val := strings.TrimSpace(line[idx+1:])
if val != "" {
result[key] = val
}
}
}
return result
}
// buildIdentityContent creates the content for IDENTITY.md from fields.
func buildIdentityContent(name, emoji, avatar string) string {
var lines []string
lines = append(lines, "# Identity")
if name != "" {
lines = append(lines, "Name: "+name)
}
if emoji != "" {
lines = append(lines, "Emoji: "+emoji)
}
if avatar != "" {
lines = append(lines, "Avatar: "+avatar)
}
return strings.Join(lines, "\n") + "\n"
}