package store import ( "context" "fmt" "strings" "time" "github.com/google/uuid" ext "git.warky.dev/wdevs/amcs/internal/types" ) // ────────────────────────────────────────────── // Helpers // ────────────────────────────────────────────── func nilToEmptyStrings(s []string) []string { if s == nil { return []string{} } return s } // ────────────────────────────────────────────── // Personas // ────────────────────────────────────────────── func (db *DB) CreatePersona(ctx context.Context, p ext.Persona) (ext.Persona, error) { if p.Tags == nil { p.Tags = []string{} } row := db.pool.QueryRow(ctx, ` insert into agent_personas (name, description, summary, detail, tags) values ($1, $2, $3, $4, $5) returning id, guid, compiled_summary, compiled_detail, compiled_at, created_at, updated_at `, p.Name, p.Description, p.Summary, p.Detail, p.Tags) created := p var id int64 var guid uuid.UUID var cs, cd string var compiledAt *time.Time var createdAt, updatedAt time.Time if err := row.Scan(&id, &guid, &cs, &cd, &compiledAt, &createdAt, &updatedAt); err != nil { return ext.Persona{}, fmt.Errorf("create persona: %w", err) } created.ID = id created.GUID = guid created.CompiledSummary = cs created.CompiledDetail = cd created.CompiledAt = compiledAt created.CreatedAt = createdAt created.UpdatedAt = updatedAt return created, nil } func (db *DB) UpdatePersona(ctx context.Context, name string, updates map[string]any) (ext.Persona, error) { if len(updates) == 0 { return db.GetPersonaByName(ctx, name) } allowed := map[string]bool{"name": true, "description": true, "summary": true, "detail": true, "tags": true} setClauses := []string{} args := []any{} for col, val := range updates { if !allowed[col] { return ext.Persona{}, fmt.Errorf("unknown field: %s", col) } args = append(args, val) setClauses = append(setClauses, fmt.Sprintf("%s = $%d", col, len(args))) } args = append(args, name) q := fmt.Sprintf(` update agent_personas set %s, updated_at = now() where name = $%d returning id, guid, name, description, summary, detail, compiled_summary, compiled_detail, compiled_at, tags::text[], created_at, updated_at `, strings.Join(setClauses, ", "), len(args)) row := db.pool.QueryRow(ctx, q, args...) return scanPersona(row) } func (db *DB) DeletePersona(ctx context.Context, name string) error { tag, err := db.pool.Exec(ctx, `delete from agent_personas where name = $1`, name) if err != nil { return fmt.Errorf("delete persona: %w", err) } if tag.RowsAffected() == 0 { return fmt.Errorf("persona not found: %s", name) } return nil } func (db *DB) ListPersonas(ctx context.Context, tag string) ([]ext.Persona, error) { q := `select id, guid, name, description, summary, detail, compiled_summary, compiled_detail, compiled_at, tags::text[], created_at, updated_at from agent_personas` args := []any{} if t := strings.TrimSpace(tag); t != "" { args = append(args, t) q += fmt.Sprintf(" where $%d = any(tags)", len(args)) } q += " order by name" rows, err := db.pool.Query(ctx, q, args...) if err != nil { return nil, fmt.Errorf("list personas: %w", err) } defer rows.Close() var personas []ext.Persona for rows.Next() { p, err := scanPersona(rows) if err != nil { return nil, fmt.Errorf("scan persona: %w", err) } personas = append(personas, p) } return personas, rows.Err() } func (db *DB) GetPersonaByName(ctx context.Context, name string) (ext.Persona, error) { row := db.pool.QueryRow(ctx, ` select id, guid, name, description, summary, detail, compiled_summary, compiled_detail, compiled_at, tags::text[], created_at, updated_at from agent_personas where name = $1 `, name) p, err := scanPersona(row) if err != nil { return ext.Persona{}, fmt.Errorf("get persona: %w", err) } return p, nil } // GetPersona assembles the full persona with parts, skills, guardrails, traits, // and arc state. Assembly priority per part_type: overrides > arc stage > persona base. func (db *DB) GetPersona(ctx context.Context, name string, detail bool, overrides map[string]string) (ext.PersonaFull, error) { // 1. Persona metadata persona, err := db.GetPersonaByName(ctx, name) if err != nil { return ext.PersonaFull{}, err } // 2. Base persona parts (ordered) baseParts, err := db.listPersonaParts(ctx, persona.ID) if err != nil { return ext.PersonaFull{}, err } // 3. Arc state + stage parts arcState, stageParts, err := db.getArcStateAndParts(ctx, persona.ID) if err != nil { return ext.PersonaFull{}, err } // 4. Assemble: start with base, override with arc stage parts per type assembled := assembleParts(baseParts, stageParts, "arc_stage") // 5. Apply runtime overrides (fetch by name, replace per type) if len(overrides) > 0 { overrideParts, err := db.fetchPartsByNames(ctx, overrideValues(overrides)) if err != nil { return ext.PersonaFull{}, err } // Build name→part lookup byName := make(map[string]rawPart, len(overrideParts)) for _, p := range overrideParts { byName[p.Name] = p } for partType, partName := range overrides { op, ok := byName[partName] if !ok { return ext.PersonaFull{}, fmt.Errorf("override part not found: %s", partName) } if op.PartType != partType { return ext.PersonaFull{}, fmt.Errorf("override part %q has type %q, expected %q", partName, op.PartType, partType) } // Remove all parts of this type, then append the override without := assembled[:0] for _, ap := range assembled { if ap.PartType != partType { without = append(without, ap) } } op.Source = "override" assembled = append(without, op) } } // 6. Skills, guardrails, traits skills, err := db.listPersonaSkills(ctx, persona.ID) if err != nil { return ext.PersonaFull{}, err } guardrails, err := db.listPersonaGuardrails(ctx, persona.ID) if err != nil { return ext.PersonaFull{}, err } traits, err := db.listPersonaTraits(ctx, persona.ID) if err != nil { return ext.PersonaFull{}, err } // 7. Build output body := persona.Summary if detail && persona.Detail != "" { body = persona.Detail } out := ext.PersonaFull{ Name: persona.Name, Description: persona.Description, Body: body, CompiledSummary: persona.CompiledSummary, Tags: persona.Tags, Arc: arcState, Detail: detail, } for _, rp := range assembled { partBody := rp.Summary if detail && rp.Content != "" { partBody = rp.Content } out.Parts = append(out.Parts, ext.AssembledPart{ Name: rp.Name, PartType: rp.PartType, Description: rp.Description, Body: partBody, Tags: rp.Tags, Source: rp.Source, }) } for _, s := range skills { entry := ext.PersonaSkillEntry{ ID: s.ID, Name: s.Name, Description: s.Description, Tags: s.Tags, } if detail { entry.Content = s.Content } out.Skills = append(out.Skills, entry) } for _, g := range guardrails { entry := ext.PersonaGuardrailEntry{ ID: g.ID, Name: g.Name, Description: g.Description, Tags: g.Tags, } if detail { entry.Content = g.Content entry.Severity = g.Severity } out.Guardrails = append(out.Guardrails, entry) } for _, t := range traits { entry := ext.PersonaTraitEntry{ ID: t.ID, Name: t.Name, TraitType: t.TraitType, Description: t.Description, Tags: t.Tags, } if detail { entry.Instruction = t.Instruction } out.Traits = append(out.Traits, entry) } if out.Parts == nil { out.Parts = []ext.AssembledPart{} } if out.Skills == nil { out.Skills = []ext.PersonaSkillEntry{} } if out.Guardrails == nil { out.Guardrails = []ext.PersonaGuardrailEntry{} } if out.Traits == nil { out.Traits = []ext.PersonaTraitEntry{} } return out, nil } // GetPersonaManifest returns a lightweight structure-only view of the persona. func (db *DB) GetPersonaManifest(ctx context.Context, name string) (ext.PersonaManifest, error) { persona, err := db.GetPersonaByName(ctx, name) if err != nil { return ext.PersonaManifest{}, err } baseParts, err := db.listPersonaParts(ctx, persona.ID) if err != nil { return ext.PersonaManifest{}, err } arcState, stageParts, err := db.getArcStateAndParts(ctx, persona.ID) if err != nil { return ext.PersonaManifest{}, err } assembled := assembleParts(baseParts, stageParts, "arc_stage") skills, err := db.listPersonaSkills(ctx, persona.ID) if err != nil { return ext.PersonaManifest{}, err } guardrails, err := db.listPersonaGuardrails(ctx, persona.ID) if err != nil { return ext.PersonaManifest{}, err } traits, err := db.listPersonaTraits(ctx, persona.ID) if err != nil { return ext.PersonaManifest{}, err } manifest := ext.PersonaManifest{ Name: persona.Name, Description: persona.Description, Arc: arcState, OnDemandTools: []string{ "get_agent_part(name) — load full content of a specific part", "get_agent_trait(name) — load instruction for a specific trait", "get_skill(name) — load skill content", "get_guardrail(name) — load guardrail content", "compile_persona(name) — load pre-merged compiled_summary if full context needed", "get_agent_persona(name, detail=true) — load full persona with all content", }, } for _, rp := range assembled { manifest.Parts = append(manifest.Parts, ext.ManifestPart{ Name: rp.Name, PartType: rp.PartType, Description: rp.Description, }) } for _, s := range skills { manifest.Skills = append(manifest.Skills, ext.ManifestSkill{ ID: s.ID, Name: s.Name, Description: s.Description, }) } for _, g := range guardrails { manifest.Guardrails = append(manifest.Guardrails, ext.ManifestGuardrail{ ID: g.ID, Name: g.Name, Description: g.Description, Severity: g.Severity, }) } for _, t := range traits { manifest.Traits = append(manifest.Traits, ext.ManifestTrait{ Name: t.Name, TraitType: t.TraitType, Description: t.Description, }) } if manifest.Parts == nil { manifest.Parts = []ext.ManifestPart{} } if manifest.Skills == nil { manifest.Skills = []ext.ManifestSkill{} } if manifest.Guardrails == nil { manifest.Guardrails = []ext.ManifestGuardrail{} } if manifest.Traits == nil { manifest.Traits = []ext.ManifestTrait{} } return manifest, nil } // CompilePersona regenerates compiled_summary and compiled_detail from current parts and arc stage. func (db *DB) CompilePersona(ctx context.Context, name string) (ext.Persona, error) { persona, err := db.GetPersonaByName(ctx, name) if err != nil { return ext.Persona{}, err } baseParts, err := db.listPersonaParts(ctx, persona.ID) if err != nil { return ext.Persona{}, err } _, stageParts, err := db.getArcStateAndParts(ctx, persona.ID) if err != nil { return ext.Persona{}, err } assembled := assembleParts(baseParts, stageParts, "arc_stage") summaryParts := []string{persona.Summary} detailParts := []string{persona.Detail} for _, rp := range assembled { if rp.Summary != "" { summaryParts = append(summaryParts, rp.Summary) } if rp.Content != "" { detailParts = append(detailParts, rp.Content) } } compiledSummary := strings.Join(summaryParts, "\n\n") compiledDetail := strings.Join(detailParts, "\n\n") row := db.pool.QueryRow(ctx, ` update agent_personas set compiled_summary = $1, compiled_detail = $2, compiled_at = now(), updated_at = now() where id = $3 returning id, guid, name, description, summary, detail, compiled_summary, compiled_detail, compiled_at, tags::text[], created_at, updated_at `, compiledSummary, compiledDetail, persona.ID) return scanPersona(row) } // ────────────────────────────────────────────── // Parts // ────────────────────────────────────────────── func (db *DB) CreatePart(ctx context.Context, p ext.Part) (ext.Part, error) { if p.Tags == nil { p.Tags = []string{} } row := db.pool.QueryRow(ctx, ` insert into agent_parts (name, part_type, description, summary, content, tags) values ($1, $2, $3, $4, $5, $6) returning id, guid, created_at, updated_at `, p.Name, p.PartType, p.Description, p.Summary, p.Content, p.Tags) created := p var id int64 var guid uuid.UUID var createdAt, updatedAt time.Time if err := row.Scan(&id, &guid, &createdAt, &updatedAt); err != nil { return ext.Part{}, fmt.Errorf("create part: %w", err) } created.ID = id created.GUID = guid created.CreatedAt = createdAt created.UpdatedAt = updatedAt return created, nil } func (db *DB) UpdatePart(ctx context.Context, name string, updates map[string]any) (ext.Part, error) { if len(updates) == 0 { return db.GetPartByName(ctx, name) } allowed := map[string]bool{"name": true, "part_type": true, "description": true, "summary": true, "content": true, "tags": true} setClauses := []string{} args := []any{} for col, val := range updates { if !allowed[col] { return ext.Part{}, fmt.Errorf("unknown field: %s", col) } args = append(args, val) setClauses = append(setClauses, fmt.Sprintf("%s = $%d", col, len(args))) } args = append(args, name) q := fmt.Sprintf(` update agent_parts set %s, updated_at = now() where name = $%d returning id, guid, name, part_type, description, summary, content, tags::text[], created_at, updated_at `, strings.Join(setClauses, ", "), len(args)) row := db.pool.QueryRow(ctx, q, args...) return scanPart(row) } func (db *DB) DeletePart(ctx context.Context, name string) error { tag, err := db.pool.Exec(ctx, `delete from agent_parts where name = $1`, name) if err != nil { return fmt.Errorf("delete part: %w", err) } if tag.RowsAffected() == 0 { return fmt.Errorf("part not found: %s", name) } return nil } func (db *DB) ListParts(ctx context.Context, partType, tag string) ([]ext.Part, error) { args := []any{} conditions := []string{} if t := strings.TrimSpace(partType); t != "" { args = append(args, t) conditions = append(conditions, fmt.Sprintf("part_type = $%d", len(args))) } if t := strings.TrimSpace(tag); t != "" { args = append(args, t) conditions = append(conditions, fmt.Sprintf("$%d = any(tags)", len(args))) } q := `select id, guid, name, part_type, description, summary, content, tags::text[], created_at, updated_at from agent_parts` if len(conditions) > 0 { q += " where " + strings.Join(conditions, " and ") } q += " order by part_type, name" rows, err := db.pool.Query(ctx, q, args...) if err != nil { return nil, fmt.Errorf("list parts: %w", err) } defer rows.Close() var parts []ext.Part for rows.Next() { p, err := scanPart(rows) if err != nil { return nil, fmt.Errorf("scan part: %w", err) } parts = append(parts, p) } return parts, rows.Err() } func (db *DB) GetPartByName(ctx context.Context, name string) (ext.Part, error) { row := db.pool.QueryRow(ctx, ` select id, guid, name, part_type, description, summary, content, tags::text[], created_at, updated_at from agent_parts where name = $1 `, name) p, err := scanPart(row) if err != nil { return ext.Part{}, fmt.Errorf("get part: %w", err) } return p, nil } // ────────────────────────────────────────────── // Persona-Part links // ────────────────────────────────────────────── func (db *DB) AddPersonaPart(ctx context.Context, personaName, partName string, order, priority int) error { persona, err := db.GetPersonaByName(ctx, personaName) if err != nil { return err } part, err := db.GetPartByName(ctx, partName) if err != nil { return err } _, err = db.pool.Exec(ctx, ` insert into agent_persona_parts (persona_id, part_id, part_order, priority) values ($1, $2, $3, $4) on conflict (persona_id, part_id) do update set part_order = excluded.part_order, priority = excluded.priority `, persona.ID, part.ID, order, priority) if err != nil { return fmt.Errorf("add persona part: %w", err) } return nil } func (db *DB) RemovePersonaPart(ctx context.Context, personaName, partName string) error { tag, err := db.pool.Exec(ctx, ` delete from agent_persona_parts where persona_id = (select id from agent_personas where name = $1) and part_id = (select id from agent_parts where name = $2) `, personaName, partName) if err != nil { return fmt.Errorf("remove persona part: %w", err) } if tag.RowsAffected() == 0 { return fmt.Errorf("persona-part link not found") } return nil } // ────────────────────────────────────────────── // Persona-Skill links // ────────────────────────────────────────────── func (db *DB) AddPersonaSkill(ctx context.Context, personaID, skillID int64) error { _, err := db.pool.Exec(ctx, ` insert into agent_persona_skills (persona_id, skill_id) values ($1, $2) on conflict do nothing `, personaID, skillID) if err != nil { return fmt.Errorf("add persona skill: %w", err) } return nil } func (db *DB) RemovePersonaSkill(ctx context.Context, personaID, skillID int64) error { tag, err := db.pool.Exec(ctx, ` delete from agent_persona_skills where persona_id = $1 and skill_id = $2 `, personaID, skillID) if err != nil { return fmt.Errorf("remove persona skill: %w", err) } if tag.RowsAffected() == 0 { return fmt.Errorf("persona-skill link not found") } return nil } // ────────────────────────────────────────────── // Persona-Guardrail links // ────────────────────────────────────────────── func (db *DB) AddPersonaGuardrail(ctx context.Context, personaID, guardrailID int64) error { _, err := db.pool.Exec(ctx, ` insert into agent_persona_guardrails (persona_id, guardrail_id) values ($1, $2) on conflict do nothing `, personaID, guardrailID) if err != nil { return fmt.Errorf("add persona guardrail: %w", err) } return nil } func (db *DB) RemovePersonaGuardrail(ctx context.Context, personaID, guardrailID int64) error { tag, err := db.pool.Exec(ctx, ` delete from agent_persona_guardrails where persona_id = $1 and guardrail_id = $2 `, personaID, guardrailID) if err != nil { return fmt.Errorf("remove persona guardrail: %w", err) } if tag.RowsAffected() == 0 { return fmt.Errorf("persona-guardrail link not found") } return nil } // ────────────────────────────────────────────── // Traits // ────────────────────────────────────────────── func (db *DB) CreateTrait(ctx context.Context, t ext.Trait) (ext.Trait, error) { if t.Tags == nil { t.Tags = []string{} } row := db.pool.QueryRow(ctx, ` insert into agent_traits (name, trait_type, description, instruction, tags) values ($1, $2, $3, $4, $5) returning id, guid, created_at, updated_at `, t.Name, t.TraitType, t.Description, t.Instruction, t.Tags) created := t var id int64 var guid uuid.UUID var createdAt, updatedAt time.Time if err := row.Scan(&id, &guid, &createdAt, &updatedAt); err != nil { return ext.Trait{}, fmt.Errorf("create trait: %w", err) } created.ID = id created.GUID = guid created.CreatedAt = createdAt created.UpdatedAt = updatedAt return created, nil } func (db *DB) UpdateTrait(ctx context.Context, name string, updates map[string]any) (ext.Trait, error) { if len(updates) == 0 { return db.GetTraitByName(ctx, name) } allowed := map[string]bool{"name": true, "trait_type": true, "description": true, "instruction": true, "tags": true} setClauses := []string{} args := []any{} for col, val := range updates { if !allowed[col] { return ext.Trait{}, fmt.Errorf("unknown field: %s", col) } args = append(args, val) setClauses = append(setClauses, fmt.Sprintf("%s = $%d", col, len(args))) } args = append(args, name) q := fmt.Sprintf(` update agent_traits set %s, updated_at = now() where name = $%d returning id, guid, name, trait_type, description, instruction, tags::text[], created_at, updated_at `, strings.Join(setClauses, ", "), len(args)) row := db.pool.QueryRow(ctx, q, args...) return scanTrait(row) } func (db *DB) DeleteTrait(ctx context.Context, name string) error { tag, err := db.pool.Exec(ctx, `delete from agent_traits where name = $1`, name) if err != nil { return fmt.Errorf("delete trait: %w", err) } if tag.RowsAffected() == 0 { return fmt.Errorf("trait not found: %s", name) } return nil } func (db *DB) ListTraits(ctx context.Context, traitType, tag string) ([]ext.Trait, error) { args := []any{} conditions := []string{} if t := strings.TrimSpace(traitType); t != "" { args = append(args, t) conditions = append(conditions, fmt.Sprintf("trait_type = $%d", len(args))) } if t := strings.TrimSpace(tag); t != "" { args = append(args, t) conditions = append(conditions, fmt.Sprintf("$%d = any(tags)", len(args))) } q := `select id, guid, name, trait_type, description, instruction, tags::text[], created_at, updated_at from agent_traits` if len(conditions) > 0 { q += " where " + strings.Join(conditions, " and ") } q += " order by trait_type, name" rows, err := db.pool.Query(ctx, q, args...) if err != nil { return nil, fmt.Errorf("list traits: %w", err) } defer rows.Close() var traits []ext.Trait for rows.Next() { t, err := scanTrait(rows) if err != nil { return nil, fmt.Errorf("scan trait: %w", err) } traits = append(traits, t) } return traits, rows.Err() } func (db *DB) GetTraitByName(ctx context.Context, name string) (ext.Trait, error) { row := db.pool.QueryRow(ctx, ` select id, guid, name, trait_type, description, instruction, tags::text[], created_at, updated_at from agent_traits where name = $1 `, name) t, err := scanTrait(row) if err != nil { return ext.Trait{}, fmt.Errorf("get trait: %w", err) } return t, nil } func (db *DB) AddPersonaTrait(ctx context.Context, personaID, traitID int64) error { _, err := db.pool.Exec(ctx, ` insert into agent_persona_traits (persona_id, trait_id) values ($1, $2) on conflict do nothing `, personaID, traitID) if err != nil { return fmt.Errorf("add persona trait: %w", err) } return nil } func (db *DB) RemovePersonaTrait(ctx context.Context, personaID, traitID int64) error { tag, err := db.pool.Exec(ctx, ` delete from agent_persona_traits where persona_id = $1 and trait_id = $2 `, personaID, traitID) if err != nil { return fmt.Errorf("remove persona trait: %w", err) } if tag.RowsAffected() == 0 { return fmt.Errorf("persona-trait link not found") } return nil } // ────────────────────────────────────────────── // Character Arcs // ────────────────────────────────────────────── func (db *DB) CreateCharacterArc(ctx context.Context, arc ext.CharacterArc) (ext.CharacterArc, error) { row := db.pool.QueryRow(ctx, ` insert into character_arcs (name, description, summary) values ($1, $2, $3) returning id, created_at, updated_at `, arc.Name, arc.Description, arc.Summary) created := arc var id int64 var createdAt, updatedAt time.Time if err := row.Scan(&id, &createdAt, &updatedAt); err != nil { return ext.CharacterArc{}, fmt.Errorf("create character arc: %w", err) } created.ID = id created.CreatedAt = createdAt created.UpdatedAt = updatedAt return created, nil } func (db *DB) ListCharacterArcs(ctx context.Context) ([]ext.CharacterArc, error) { rows, err := db.pool.Query(ctx, ` select id, name, description, summary, created_at, updated_at from character_arcs order by name `) if err != nil { return nil, fmt.Errorf("list character arcs: %w", err) } defer rows.Close() var arcs []ext.CharacterArc for rows.Next() { var a ext.CharacterArc if err := rows.Scan(&a.ID, &a.Name, &a.Description, &a.Summary, &a.CreatedAt, &a.UpdatedAt); err != nil { return nil, fmt.Errorf("scan character arc: %w", err) } arcs = append(arcs, a) } return arcs, rows.Err() } func (db *DB) AddArcStage(ctx context.Context, arcName string, stage ext.ArcStage) (ext.ArcStage, error) { row := db.pool.QueryRow(ctx, ` insert into arc_stages (arc_id, name, stage_order, description, condition) values ((select id from character_arcs where name = $1), $2, $3, $4, $5) returning id, arc_id, created_at `, arcName, stage.Name, stage.StageOrder, stage.Description, stage.Condition) created := stage if err := row.Scan(&created.ID, &created.ArcID, &created.CreatedAt); err != nil { return ext.ArcStage{}, fmt.Errorf("add arc stage: %w", err) } return created, nil } func (db *DB) AddStagePart(ctx context.Context, stageID int64, partName string) error { _, err := db.pool.Exec(ctx, ` insert into arc_stage_parts (stage_id, part_id) values ($1, (select id from agent_parts where name = $2)) on conflict do nothing `, stageID, partName) if err != nil { return fmt.Errorf("add stage part: %w", err) } return nil } func (db *DB) RemoveStagePart(ctx context.Context, stageID int64, partName string) error { tag, err := db.pool.Exec(ctx, ` delete from arc_stage_parts where stage_id = $1 and part_id = (select id from agent_parts where name = $2) `, stageID, partName) if err != nil { return fmt.Errorf("remove stage part: %w", err) } if tag.RowsAffected() == 0 { return fmt.Errorf("stage-part link not found") } return nil } func (db *DB) AssignPersonaArc(ctx context.Context, personaName, arcName, startStageName string) error { _, err := db.pool.Exec(ctx, ` insert into persona_arc (persona_id, arc_id, current_stage_id) values ( (select id from agent_personas where name = $1), (select id from character_arcs where name = $2), (select id from arc_stages where arc_id = (select id from character_arcs where name = $2) and name = $3) ) on conflict (persona_id) do update set arc_id = excluded.arc_id, current_stage_id = excluded.current_stage_id, updated_at = now() `, personaName, arcName, startStageName) if err != nil { return fmt.Errorf("assign persona arc: %w", err) } return nil } func (db *DB) AdvancePersonaStage(ctx context.Context, personaName string) (ext.ArcStage, error) { row := db.pool.QueryRow(ctx, ` with current as ( select pa.persona_id, pa.arc_id, pa.current_stage_id, s.stage_order from persona_arc pa join arc_stages s on s.id = pa.current_stage_id where pa.persona_id = (select id from agent_personas where name = $1) ), next_stage as ( select s.id, s.arc_id, s.name, s.stage_order, s.description, s.condition, s.created_at from arc_stages s join current c on c.arc_id = s.arc_id where s.stage_order > c.stage_order order by s.stage_order limit 1 ) update persona_arc pa set current_stage_id = next_stage.id, updated_at = now() from next_stage, current where pa.persona_id = current.persona_id returning next_stage.id, next_stage.arc_id, next_stage.name, next_stage.stage_order, next_stage.description, next_stage.condition, next_stage.created_at `, personaName) var s ext.ArcStage if err := row.Scan(&s.ID, &s.ArcID, &s.Name, &s.StageOrder, &s.Description, &s.Condition, &s.CreatedAt); err != nil { return ext.ArcStage{}, fmt.Errorf("advance persona stage: %w", err) } return s, nil } func (db *DB) ResetPersonaStage(ctx context.Context, personaName string) (ext.ArcStage, error) { row := db.pool.QueryRow(ctx, ` with first_stage as ( select s.id, s.arc_id, s.name, s.stage_order, s.description, s.condition, s.created_at from arc_stages s join persona_arc pa on pa.arc_id = s.arc_id where pa.persona_id = (select id from agent_personas where name = $1) order by s.stage_order limit 1 ) update persona_arc pa set current_stage_id = first_stage.id, updated_at = now() from first_stage where pa.persona_id = (select id from agent_personas where name = $1) returning first_stage.id, first_stage.arc_id, first_stage.name, first_stage.stage_order, first_stage.description, first_stage.condition, first_stage.created_at `, personaName) var s ext.ArcStage if err := row.Scan(&s.ID, &s.ArcID, &s.Name, &s.StageOrder, &s.Description, &s.Condition, &s.CreatedAt); err != nil { return ext.ArcStage{}, fmt.Errorf("reset persona stage: %w", err) } return s, nil } // ────────────────────────────────────────────── // Internal helpers // ────────────────────────────────────────────── type rawPart struct { ID int64 Name string PartType string Description string Summary string Content string Tags []string Order int Priority int Source string } func (db *DB) listPersonaParts(ctx context.Context, personaID int64) ([]rawPart, error) { rows, err := db.pool.Query(ctx, ` select ap.id, ap.name, ap.part_type, ap.description, ap.summary, ap.content, ap.tags::text[], app.part_order, app.priority from agent_parts ap join agent_persona_parts app on app.part_id = ap.id where app.persona_id = $1 order by app.part_order, ap.part_type, ap.name `, personaID) if err != nil { return nil, fmt.Errorf("list persona parts: %w", err) } defer rows.Close() var parts []rawPart for rows.Next() { var rp rawPart var tags []string if err := rows.Scan(&rp.ID, &rp.Name, &rp.PartType, &rp.Description, &rp.Summary, &rp.Content, &tags, &rp.Order, &rp.Priority); err != nil { return nil, fmt.Errorf("scan persona part: %w", err) } rp.Tags = nilToEmptyStrings(tags) rp.Source = "persona" parts = append(parts, rp) } return parts, rows.Err() } func (db *DB) getArcStateAndParts(ctx context.Context, personaID int64) (*ext.PersonaArcState, []rawPart, error) { row := db.pool.QueryRow(ctx, ` select ca.name, s.id, s.name, s.stage_order, s.description, s.condition from persona_arc pa join character_arcs ca on ca.id = pa.arc_id join arc_stages s on s.id = pa.current_stage_id where pa.persona_id = $1 `, personaID) var arcName, stageName, stageDesc, stageCond string var stageID int64 var stageOrder int if err := row.Scan(&arcName, &stageID, &stageName, &stageOrder, &stageDesc, &stageCond); err != nil { // No arc assigned — not an error return nil, nil, nil } arcState := &ext.PersonaArcState{ ArcName: arcName, StageName: stageName, StageOrder: stageOrder, Description: stageDesc, Condition: stageCond, } rows, err := db.pool.Query(ctx, ` select ap.id, ap.name, ap.part_type, ap.description, ap.summary, ap.content, ap.tags::text[] from agent_parts ap join arc_stage_parts asp on asp.part_id = ap.id where asp.stage_id = $1 order by ap.part_type, ap.name `, stageID) if err != nil { return nil, nil, fmt.Errorf("list stage parts: %w", err) } defer rows.Close() var parts []rawPart for rows.Next() { var rp rawPart var tags []string if err := rows.Scan(&rp.ID, &rp.Name, &rp.PartType, &rp.Description, &rp.Summary, &rp.Content, &tags); err != nil { return nil, nil, fmt.Errorf("scan stage part: %w", err) } rp.Tags = nilToEmptyStrings(tags) rp.Source = "arc_stage" parts = append(parts, rp) } return arcState, parts, rows.Err() } // assembleParts merges base parts with override parts. Override parts replace all // base parts of the same part_type. func assembleParts(base, overrides []rawPart, overrideSource string) []rawPart { if len(overrides) == 0 { return append([]rawPart{}, base...) } overrideTypes := make(map[string]bool) for _, op := range overrides { overrideTypes[op.PartType] = true } result := make([]rawPart, 0, len(base)+len(overrides)) for _, bp := range base { if !overrideTypes[bp.PartType] { result = append(result, bp) } } for _, op := range overrides { op.Source = overrideSource result = append(result, op) } return result } func (db *DB) fetchPartsByNames(ctx context.Context, names []string) ([]rawPart, error) { if len(names) == 0 { return nil, nil } rows, err := db.pool.Query(ctx, ` select id, name, part_type, description, summary, content, tags::text[] from agent_parts where name = any($1) `, names) if err != nil { return nil, fmt.Errorf("fetch parts by names: %w", err) } defer rows.Close() var parts []rawPart for rows.Next() { var rp rawPart var tags []string if err := rows.Scan(&rp.ID, &rp.Name, &rp.PartType, &rp.Description, &rp.Summary, &rp.Content, &tags); err != nil { return nil, fmt.Errorf("scan part by name: %w", err) } rp.Tags = nilToEmptyStrings(tags) parts = append(parts, rp) } return parts, rows.Err() } func (db *DB) listPersonaSkills(ctx context.Context, personaID int64) ([]AgentSkillRow, error) { rows, err := db.pool.Query(ctx, ` select s.id, s.name, s.description, s.content, s.tags::text[] from agent_skills s join agent_persona_skills aps on aps.skill_id = s.id where aps.persona_id = $1 order by s.name `, personaID) if err != nil { return nil, fmt.Errorf("list persona skills: %w", err) } defer rows.Close() var skills []AgentSkillRow for rows.Next() { var s AgentSkillRow var tags []string if err := rows.Scan(&s.ID, &s.Name, &s.Description, &s.Content, &tags); err != nil { return nil, fmt.Errorf("scan persona skill: %w", err) } s.Tags = nilToEmptyStrings(tags) skills = append(skills, s) } return skills, rows.Err() } func (db *DB) listPersonaGuardrails(ctx context.Context, personaID int64) ([]AgentGuardrailRow, error) { rows, err := db.pool.Query(ctx, ` select g.id, g.name, g.description, g.content, g.severity, g.tags::text[] from agent_guardrails g join agent_persona_guardrails apg on apg.guardrail_id = g.id where apg.persona_id = $1 order by g.name `, personaID) if err != nil { return nil, fmt.Errorf("list persona guardrails: %w", err) } defer rows.Close() var gs []AgentGuardrailRow for rows.Next() { var g AgentGuardrailRow var tags []string if err := rows.Scan(&g.ID, &g.Name, &g.Description, &g.Content, &g.Severity, &tags); err != nil { return nil, fmt.Errorf("scan persona guardrail: %w", err) } g.Tags = nilToEmptyStrings(tags) gs = append(gs, g) } return gs, rows.Err() } func (db *DB) listPersonaTraits(ctx context.Context, personaID int64) ([]ext.Trait, error) { rows, err := db.pool.Query(ctx, ` select t.id, t.guid, t.name, t.trait_type, t.description, t.instruction, t.tags::text[], t.created_at, t.updated_at from agent_traits t join agent_persona_traits apt on apt.trait_id = t.id where apt.persona_id = $1 order by t.trait_type, t.name `, personaID) if err != nil { return nil, fmt.Errorf("list persona traits: %w", err) } defer rows.Close() var traits []ext.Trait for rows.Next() { t, err := scanTrait(rows) if err != nil { return nil, fmt.Errorf("scan persona trait: %w", err) } traits = append(traits, t) } return traits, rows.Err() } // Minimal row types used internally (avoid importing full ext types for joins) type AgentSkillRow struct { ID int64 Name string Description string Content string Tags []string } type AgentGuardrailRow struct { ID int64 Name string Description string Content string Severity string Tags []string } func overrideValues(m map[string]string) []string { out := make([]string, 0, len(m)) for _, v := range m { out = append(out, v) } return out } // ────────────────────────────────────────────── // Scanners // ────────────────────────────────────────────── type personaScanner interface{ Scan(dest ...any) error } type partScanner interface{ Scan(dest ...any) error } type traitScanner interface{ Scan(dest ...any) error } func scanPersona(row personaScanner) (ext.Persona, error) { var p ext.Persona var tags []string if err := row.Scan(&p.ID, &p.GUID, &p.Name, &p.Description, &p.Summary, &p.Detail, &p.CompiledSummary, &p.CompiledDetail, &p.CompiledAt, &tags, &p.CreatedAt, &p.UpdatedAt); err != nil { return ext.Persona{}, err } p.Tags = nilToEmptyStrings(tags) return p, nil } func scanPart(row partScanner) (ext.Part, error) { var p ext.Part var tags []string if err := row.Scan(&p.ID, &p.GUID, &p.Name, &p.PartType, &p.Description, &p.Summary, &p.Content, &tags, &p.CreatedAt, &p.UpdatedAt); err != nil { return ext.Part{}, err } p.Tags = nilToEmptyStrings(tags) return p, nil } func scanTrait(row traitScanner) (ext.Trait, error) { var t ext.Trait var tags []string if err := row.Scan(&t.ID, &t.GUID, &t.Name, &t.TraitType, &t.Description, &t.Instruction, &tags, &t.CreatedAt, &t.UpdatedAt); err != nil { return ext.Trait{}, err } t.Tags = nilToEmptyStrings(tags) return t, nil }