feat(db): add project personas and skills tables
CI / build-and-test (push) Failing after 1m52s

* Introduce project_personas table with foreign keys to projects and agent_personas
* Add project_skills table with foreign key to projects and agent_skills
* Include override boolean field in agent_persona_skills and project_skills
* Update schema and migration files to reflect new tables and fields
* Enhance CORS handling to reflect request origin
This commit is contained in:
2026-07-04 23:45:51 +02:00
parent 1adf50e3db
commit c179e014ad
69 changed files with 1329 additions and 184 deletions
+22 -20
View File
@@ -194,26 +194,28 @@ func routes(logger *slog.Logger, cfg *config.Config, info buildinfo.Info, db *st
adminActions := newAdminActions(backfillTool, enrichmentRetryer, logger)
toolSet := mcpserver.ToolSet{
Capture: tools.NewCaptureTool(db, embeddings, cfg.Capture, activeProjects, enrichmentRetryer, backfillTool),
Search: tools.NewSearchTool(db, embeddings, cfg.Search, activeProjects),
List: tools.NewListTool(db, cfg.Search, activeProjects),
Stats: tools.NewStatsTool(db),
Get: tools.NewGetTool(db),
Update: tools.NewUpdateTool(db, embeddings, metadata, cfg.Capture, logger),
Delete: tools.NewDeleteTool(db),
Archive: tools.NewArchiveTool(db),
Projects: tools.NewProjectsTool(db, activeProjects),
Version: tools.NewVersionTool(cfg.MCP.ServerName, info),
Learnings: tools.NewLearningsTool(db, activeProjects, cfg.Search),
Plans: tools.NewPlansTool(db, activeProjects, cfg.Search),
Context: tools.NewContextTool(db, embeddings, cfg.Search, activeProjects),
Recall: tools.NewRecallTool(db, embeddings, cfg.Search, activeProjects),
Summarize: tools.NewSummarizeTool(db, embeddings, metadata, cfg.Search, activeProjects),
Links: tools.NewLinksTool(db, embeddings, cfg.Search),
Files: filesTool,
Backfill: backfillTool,
Reparse: tools.NewReparseMetadataTool(db, bgMetadata, cfg.Capture, activeProjects, logger),
RetryMetadata: tools.NewRetryEnrichmentTool(enrichmentRetryer),
Capture: tools.NewCaptureTool(db, embeddings, cfg.Capture, activeProjects, enrichmentRetryer, backfillTool),
Search: tools.NewSearchTool(db, embeddings, cfg.Search, activeProjects),
List: tools.NewListTool(db, cfg.Search, activeProjects),
Stats: tools.NewStatsTool(db),
Get: tools.NewGetTool(db),
Update: tools.NewUpdateTool(db, embeddings, metadata, cfg.Capture, logger),
Delete: tools.NewDeleteTool(db),
Archive: tools.NewArchiveTool(db),
Projects: tools.NewProjectsTool(db, activeProjects),
Version: tools.NewVersionTool(cfg.MCP.ServerName, info),
Learnings: tools.NewLearningsTool(db, activeProjects, cfg.Search),
Plans: tools.NewPlansTool(db, activeProjects, cfg.Search),
ProjectPersonas: tools.NewProjectPersonasTool(db, activeProjects),
WorldModel: tools.NewWorldModelTool(db, activeProjects),
Context: tools.NewContextTool(db, embeddings, cfg.Search, activeProjects),
Recall: tools.NewRecallTool(db, embeddings, cfg.Search, activeProjects),
Summarize: tools.NewSummarizeTool(db, embeddings, metadata, cfg.Search, activeProjects),
Links: tools.NewLinksTool(db, embeddings, cfg.Search),
Files: filesTool,
Backfill: backfillTool,
Reparse: tools.NewReparseMetadataTool(db, bgMetadata, cfg.Capture, activeProjects, logger),
RetryMetadata: tools.NewRetryEnrichmentTool(enrichmentRetryer),
//Maintenance: tools.NewMaintenanceTool(db),
Skills: tools.NewSkillsTool(db, activeProjects),
Personas: tools.NewAgentPersonasTool(db),
@@ -28,6 +28,7 @@ func resolveSpecModels() []resolveSpecModel {
{schema: "public", entity: "plan_skills", model: generatedmodels.ModelPublicPlanSkills{}},
{schema: "public", entity: "plans", model: generatedmodels.ModelPublicPlans{}},
{schema: "public", entity: "project_guardrails", model: generatedmodels.ModelPublicProjectGuardrails{}},
{schema: "public", entity: "project_personas", model: generatedmodels.ModelPublicProjectPersonas{}},
{schema: "public", entity: "project_skills", model: generatedmodels.ModelPublicProjectSkills{}},
{schema: "public", entity: "projects", model: generatedmodels.ModelPublicProjects{}},
{schema: "public", entity: "stored_files", model: generatedmodels.ModelPublicStoredFiles{}},
@@ -16,7 +16,7 @@ type ModelPublicAgentGuardrails struct {
GUID resolvespec_common.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
Name resolvespec_common.SqlString `bun:"name,type:text,notnull," json:"name"`
Severity resolvespec_common.SqlString `bun:"severity,type:text,default:'medium',notnull," json:"severity"`
Tags resolvespec_common.SqlStringArray `bun:"tags,type:text,default:'{}',notnull," json:"tags"`
Tags resolvespec_common.SqlStringArray `bun:"tags,type:text[],default:'{}',notnull," json:"tags"`
UpdatedAt resolvespec_common.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
RelGuardrailIDPublicAgentPersonaGuardrails []*ModelPublicAgentPersonaGuardrails `bun:"rel:has-many,join:id=guardrail_id" json:"relguardrailidpublicagentpersonaguardrails,omitempty"` // Has many ModelPublicAgentPersonaGuardrails
RelGuardrailIDPublicPlanGuardrails []*ModelPublicPlanGuardrails `bun:"rel:has-many,join:id=guardrail_id" json:"relguardrailidpublicplanguardrails,omitempty"` // Has many ModelPublicPlanGuardrails
@@ -45,7 +45,7 @@ func (m ModelPublicAgentGuardrails) GetID() int64 {
// GetIDStr returns the primary key as a string
func (m ModelPublicAgentGuardrails) GetIDStr() string {
return fmt.Sprintf("%v", m.ID)
return m.ID.String()
}
// SetID sets the primary key value
@@ -17,7 +17,7 @@ type ModelPublicAgentParts struct {
Name resolvespec_common.SqlString `bun:"name,type:text,notnull," json:"name"`
PartType resolvespec_common.SqlString `bun:"part_type,type:text,notnull," json:"part_type"`
Summary resolvespec_common.SqlString `bun:"summary,type:text,notnull," json:"summary"`
Tags resolvespec_common.SqlStringArray `bun:"tags,type:text,default:'{}',notnull," json:"tags"`
Tags resolvespec_common.SqlStringArray `bun:"tags,type:text[],default:'{}',notnull," json:"tags"`
UpdatedAt resolvespec_common.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
RelPartIDPublicAgentPersonaParts []*ModelPublicAgentPersonaParts `bun:"rel:has-many,join:id=part_id" json:"relpartidpublicagentpersonaparts,omitempty"` // Has many ModelPublicAgentPersonaParts
RelPartIDPublicArcStageParts []*ModelPublicArcStageParts `bun:"rel:has-many,join:id=part_id" json:"relpartidpublicarcstageparts,omitempty"` // Has many ModelPublicArcStageParts
@@ -45,7 +45,7 @@ func (m ModelPublicAgentParts) GetID() int64 {
// GetIDStr returns the primary key as a string
func (m ModelPublicAgentParts) GetIDStr() string {
return fmt.Sprintf("%v", m.ID)
return m.ID.String()
}
// SetID sets the primary key value
@@ -38,7 +38,7 @@ func (m ModelPublicAgentPersonaGuardrails) GetID() int64 {
// GetIDStr returns the primary key as a string
func (m ModelPublicAgentPersonaGuardrails) GetIDStr() string {
return fmt.Sprintf("%v", m.ID)
return m.ID.String()
}
// SetID sets the primary key value
@@ -40,7 +40,7 @@ func (m ModelPublicAgentPersonaParts) GetID() int64 {
// GetIDStr returns the primary key as a string
func (m ModelPublicAgentPersonaParts) GetIDStr() string {
return fmt.Sprintf("%v", m.ID)
return m.ID.String()
}
// SetID sets the primary key value
@@ -10,6 +10,7 @@ import (
type ModelPublicAgentPersonaSkills struct {
bun.BaseModel `bun:"table:public.agent_persona_skills,alias:agent_persona_skills"`
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
Override bool `bun:"override,type:boolean,default:false,notnull," json:"override"`
PersonaID int64 `bun:"persona_id,type:bigint,notnull," json:"persona_id"`
SkillID int64 `bun:"skill_id,type:bigint,notnull," json:"skill_id"`
RelPersonaID *ModelPublicAgentPersonas `bun:"rel:has-one,join:persona_id=id" json:"relpersonaid,omitempty"` // Has one ModelPublicAgentPersonas
@@ -38,7 +39,7 @@ func (m ModelPublicAgentPersonaSkills) GetID() int64 {
// GetIDStr returns the primary key as a string
func (m ModelPublicAgentPersonaSkills) GetIDStr() string {
return fmt.Sprintf("%v", m.ID)
return m.ID.String()
}
// SetID sets the primary key value
@@ -38,7 +38,7 @@ func (m ModelPublicAgentPersonaTraits) GetID() int64 {
// GetIDStr returns the primary key as a string
func (m ModelPublicAgentPersonaTraits) GetIDStr() string {
return fmt.Sprintf("%v", m.ID)
return m.ID.String()
}
// SetID sets the primary key value
@@ -19,10 +19,11 @@ type ModelPublicAgentPersonas struct {
GUID resolvespec_common.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
Name resolvespec_common.SqlString `bun:"name,type:text,notnull," json:"name"`
Summary resolvespec_common.SqlString `bun:"summary,type:text,notnull," json:"summary"`
Tags resolvespec_common.SqlStringArray `bun:"tags,type:text,default:'{}',notnull," json:"tags"`
Tags resolvespec_common.SqlStringArray `bun:"tags,type:text[],default:'{}',notnull," json:"tags"`
UpdatedAt resolvespec_common.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
RelPersonaIDPublicAgentPersonaParts []*ModelPublicAgentPersonaParts `bun:"rel:has-many,join:id=persona_id" json:"relpersonaidpublicagentpersonaparts,omitempty"` // Has many ModelPublicAgentPersonaParts
RelPersonaIDPublicAgentPersonaSkills []*ModelPublicAgentPersonaSkills `bun:"rel:has-many,join:id=persona_id" json:"relpersonaidpublicagentpersonaskills,omitempty"` // Has many ModelPublicAgentPersonaSkills
RelPersonaIDPublicProjectPersonas []*ModelPublicProjectPersonas `bun:"rel:has-many,join:id=persona_id" json:"relpersonaidpublicprojectpersonas,omitempty"` // Has many ModelPublicProjectPersonas
RelPersonaIDPublicAgentPersonaGuardrails []*ModelPublicAgentPersonaGuardrails `bun:"rel:has-many,join:id=persona_id" json:"relpersonaidpublicagentpersonaguardrails,omitempty"` // Has many ModelPublicAgentPersonaGuardrails
RelPersonaIDPublicAgentPersonaTraits []*ModelPublicAgentPersonaTraits `bun:"rel:has-many,join:id=persona_id" json:"relpersonaidpublicagentpersonatraits,omitempty"` // Has many ModelPublicAgentPersonaTraits
RelPersonaIDPublicPersonaArcs []*ModelPublicPersonaArc `bun:"rel:has-many,join:id=persona_id" json:"relpersonaidpublicpersonaarcs,omitempty"` // Has many ModelPublicPersonaArc
@@ -50,7 +51,7 @@ func (m ModelPublicAgentPersonas) GetID() int64 {
// GetIDStr returns the primary key as a string
func (m ModelPublicAgentPersonas) GetIDStr() string {
return fmt.Sprintf("%v", m.ID)
return m.ID.String()
}
// SetID sets the primary key value
@@ -13,13 +13,13 @@ type ModelPublicAgentSkills struct {
Content resolvespec_common.SqlString `bun:"content,type:text,notnull," json:"content"`
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
Description resolvespec_common.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
DomainTags resolvespec_common.SqlStringArray `bun:"domain_tags,type:text,default:'{}',notnull," json:"domain_tags"`
FrameworkTags resolvespec_common.SqlStringArray `bun:"framework_tags,type:text,default:'{}',notnull," json:"framework_tags"`
DomainTags resolvespec_common.SqlStringArray `bun:"domain_tags,type:text[],default:'{}',notnull," json:"domain_tags"`
FrameworkTags resolvespec_common.SqlStringArray `bun:"framework_tags,type:text[],default:'{}',notnull," json:"framework_tags"`
GUID resolvespec_common.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
LanguageTags resolvespec_common.SqlStringArray `bun:"language_tags,type:text,default:'{}',notnull," json:"language_tags"`
LibraryTags resolvespec_common.SqlStringArray `bun:"library_tags,type:text,default:'{}',notnull," json:"library_tags"`
LanguageTags resolvespec_common.SqlStringArray `bun:"language_tags,type:text[],default:'{}',notnull," json:"language_tags"`
LibraryTags resolvespec_common.SqlStringArray `bun:"library_tags,type:text[],default:'{}',notnull," json:"library_tags"`
Name resolvespec_common.SqlString `bun:"name,type:text,notnull," json:"name"`
Tags resolvespec_common.SqlStringArray `bun:"tags,type:text,default:'{}',notnull," json:"tags"`
Tags resolvespec_common.SqlStringArray `bun:"tags,type:text[],default:'{}',notnull," json:"tags"`
UpdatedAt resolvespec_common.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
RelSkillIDPublicAgentPersonaSkills []*ModelPublicAgentPersonaSkills `bun:"rel:has-many,join:id=skill_id" json:"relskillidpublicagentpersonaskills,omitempty"` // Has many ModelPublicAgentPersonaSkills
RelRelatedSkillIDPublicLearnings []*ModelPublicLearnings `bun:"rel:has-many,join:id=related_skill_id" json:"relrelatedskillidpubliclearnings,omitempty"` // Has many ModelPublicLearnings
@@ -49,7 +49,7 @@ func (m ModelPublicAgentSkills) GetID() int64 {
// GetIDStr returns the primary key as a string
func (m ModelPublicAgentSkills) GetIDStr() string {
return fmt.Sprintf("%v", m.ID)
return m.ID.String()
}
// SetID sets the primary key value
@@ -15,7 +15,7 @@ type ModelPublicAgentTraits struct {
GUID resolvespec_common.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
Instruction resolvespec_common.SqlString `bun:"instruction,type:text,default:'',notnull," json:"instruction"`
Name resolvespec_common.SqlString `bun:"name,type:text,notnull," json:"name"`
Tags resolvespec_common.SqlStringArray `bun:"tags,type:text,default:'{}',notnull," json:"tags"`
Tags resolvespec_common.SqlStringArray `bun:"tags,type:text[],default:'{}',notnull," json:"tags"`
TraitType resolvespec_common.SqlString `bun:"trait_type,type:text,notnull," json:"trait_type"`
UpdatedAt resolvespec_common.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
RelTraitIDPublicAgentPersonaTraits []*ModelPublicAgentPersonaTraits `bun:"rel:has-many,join:id=trait_id" json:"reltraitidpublicagentpersonatraits,omitempty"` // Has many ModelPublicAgentPersonaTraits
@@ -43,7 +43,7 @@ func (m ModelPublicAgentTraits) GetID() int64 {
// GetIDStr returns the primary key as a string
func (m ModelPublicAgentTraits) GetIDStr() string {
return fmt.Sprintf("%v", m.ID)
return m.ID.String()
}
// SetID sets the primary key value
@@ -38,7 +38,7 @@ func (m ModelPublicArcStageParts) GetID() int64 {
// GetIDStr returns the primary key as a string
func (m ModelPublicArcStageParts) GetIDStr() string {
return fmt.Sprintf("%v", m.ID)
return m.ID.String()
}
// SetID sets the primary key value
@@ -43,7 +43,7 @@ func (m ModelPublicArcStages) GetID() int64 {
// GetIDStr returns the primary key as a string
func (m ModelPublicArcStages) GetIDStr() string {
return fmt.Sprintf("%v", m.ID)
return m.ID.String()
}
// SetID sets the primary key value
@@ -41,7 +41,7 @@ func (m ModelPublicCharacterArcs) GetID() int64 {
// GetIDStr returns the primary key as a string
func (m ModelPublicCharacterArcs) GetIDStr() string {
return fmt.Sprintf("%v", m.ID)
return m.ID.String()
}
// SetID sets the primary key value
@@ -14,7 +14,7 @@ type ModelPublicChatHistories struct {
Channel resolvespec_common.SqlString `bun:"channel,type:text,nullzero," json:"channel"`
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
GUID resolvespec_common.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
Messages resolvespec_common.SqlJSONB `bun:"messages,type:jsonb,default:'',notnull," json:"messages"`
Messages resolvespec_common.SqlJSONB `bun:"messages,type:jsonb,default:'[]',notnull," json:"messages"`
Metadata resolvespec_common.SqlJSONB `bun:"metadata,type:jsonb,default:'{}',notnull," json:"metadata"`
ProjectID resolvespec_common.SqlInt64 `bun:"project_id,type:bigint,nullzero," json:"project_id"`
SessionID resolvespec_common.SqlString `bun:"session_id,type:text,notnull," json:"session_id"`
@@ -46,7 +46,7 @@ func (m ModelPublicChatHistories) GetID() int64 {
// GetIDStr returns the primary key as a string
func (m ModelPublicChatHistories) GetIDStr() string {
return fmt.Sprintf("%v", m.ID)
return m.ID.String()
}
// SetID sets the primary key value
@@ -42,7 +42,7 @@ func (m ModelPublicEmbeddings) GetID() int64 {
// GetIDStr returns the primary key as a string
func (m ModelPublicEmbeddings) GetIDStr() string {
return fmt.Sprintf("%v", m.ID)
return m.ID.String()
}
// SetID sets the primary key value
@@ -29,7 +29,7 @@ type ModelPublicLearnings struct {
Status resolvespec_common.SqlString `bun:"status,type:text,default:'pending',notnull," json:"status"`
Summary resolvespec_common.SqlString `bun:"summary,type:text,notnull," json:"summary"`
SupersedesLearningID resolvespec_common.SqlInt64 `bun:"supersedes_learning_id,type:bigint,nullzero," json:"supersedes_learning_id"`
Tags resolvespec_common.SqlStringArray `bun:"tags,type:text,default:'{}',notnull," json:"tags"`
Tags resolvespec_common.SqlStringArray `bun:"tags,type:text[],default:'{}',notnull," json:"tags"`
UpdatedAt resolvespec_common.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
RelDuplicateOfLearningID *ModelPublicLearnings `bun:"rel:has-one,join:duplicate_of_learning_id=id" json:"relduplicateoflearningid,omitempty"` // Has one ModelPublicLearnings
RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects
@@ -60,7 +60,7 @@ func (m ModelPublicLearnings) GetID() int64 {
// GetIDStr returns the primary key as a string
func (m ModelPublicLearnings) GetIDStr() string {
return fmt.Sprintf("%v", m.ID)
return m.ID.String()
}
// SetID sets the primary key value
@@ -13,7 +13,7 @@ type ModelPublicOauthClients struct {
ClientID resolvespec_common.SqlString `bun:"client_id,type:text,notnull," json:"client_id"`
ClientName resolvespec_common.SqlString `bun:"client_name,type:text,default:'',notnull," json:"client_name"`
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
RedirectUris resolvespec_common.SqlStringArray `bun:"redirect_uris,type:text,default:'{}',notnull," json:"redirect_uris"`
RedirectUris resolvespec_common.SqlStringArray `bun:"redirect_uris,type:text[],default:'{}',notnull," json:"redirect_uris"`
}
// TableName returns the table name for ModelPublicOauthClients
@@ -38,7 +38,7 @@ func (m ModelPublicOauthClients) GetID() int64 {
// GetIDStr returns the primary key as a string
func (m ModelPublicOauthClients) GetIDStr() string {
return fmt.Sprintf("%v", m.ID)
return m.ID.String()
}
// SetID sets the primary key value
@@ -41,7 +41,7 @@ func (m ModelPublicPersonaArc) GetID() int64 {
// GetIDStr returns the primary key as a string
func (m ModelPublicPersonaArc) GetIDStr() string {
return fmt.Sprintf("%v", m.ID)
return m.ID.String()
}
// SetID sets the primary key value
@@ -39,7 +39,7 @@ func (m ModelPublicPlanDependencies) GetID() int64 {
// GetIDStr returns the primary key as a string
func (m ModelPublicPlanDependencies) GetIDStr() string {
return fmt.Sprintf("%v", m.ID)
return m.ID.String()
}
// SetID sets the primary key value
@@ -39,7 +39,7 @@ func (m ModelPublicPlanGuardrails) GetID() int64 {
// GetIDStr returns the primary key as a string
func (m ModelPublicPlanGuardrails) GetIDStr() string {
return fmt.Sprintf("%v", m.ID)
return m.ID.String()
}
// SetID sets the primary key value
@@ -39,7 +39,7 @@ func (m ModelPublicPlanRelatedPlans) GetID() int64 {
// GetIDStr returns the primary key as a string
func (m ModelPublicPlanRelatedPlans) GetIDStr() string {
return fmt.Sprintf("%v", m.ID)
return m.ID.String()
}
// SetID sets the primary key value
@@ -39,7 +39,7 @@ func (m ModelPublicPlanSkills) GetID() int64 {
// GetIDStr returns the primary key as a string
func (m ModelPublicPlanSkills) GetIDStr() string {
return fmt.Sprintf("%v", m.ID)
return m.ID.String()
}
// SetID sets the primary key value
+2 -2
View File
@@ -22,7 +22,7 @@ type ModelPublicPlans struct {
ReviewedBy resolvespec_common.SqlString `bun:"reviewed_by,type:text,nullzero," json:"reviewed_by"`
Status resolvespec_common.SqlString `bun:"status,type:text,default:'draft',notnull," json:"status"` // draft, active, blocked, completed, cancelled, superseded
SupersedesPlanID resolvespec_common.SqlInt64 `bun:"supersedes_plan_id,type:bigint,nullzero," json:"supersedes_plan_id"`
Tags resolvespec_common.SqlStringArray `bun:"tags,type:text,default:'{}',notnull," json:"tags"`
Tags resolvespec_common.SqlStringArray `bun:"tags,type:text[],default:'{}',notnull," json:"tags"`
Title resolvespec_common.SqlString `bun:"title,type:text,notnull," json:"title"`
UpdatedAt resolvespec_common.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects
@@ -57,7 +57,7 @@ func (m ModelPublicPlans) GetID() int64 {
// GetIDStr returns the primary key as a string
func (m ModelPublicPlans) GetIDStr() string {
return fmt.Sprintf("%v", m.ID)
return m.ID.String()
}
// SetID sets the primary key value
@@ -39,7 +39,7 @@ func (m ModelPublicProjectGuardrails) GetID() int64 {
// GetIDStr returns the primary key as a string
func (m ModelPublicProjectGuardrails) GetIDStr() string {
return fmt.Sprintf("%v", m.ID)
return m.ID.String()
}
// SetID sets the primary key value
@@ -0,0 +1,64 @@
// Code generated by relspecgo. DO NOT EDIT.
package generatedmodels
import (
"fmt"
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
"github.com/uptrace/bun"
)
type ModelPublicProjectPersonas struct {
bun.BaseModel `bun:"table:public.project_personas,alias:project_personas"`
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
IsDefault bool `bun:"is_default,type:boolean,default:false,notnull," json:"is_default"`
PersonaID int64 `bun:"persona_id,type:bigint,notnull," json:"persona_id"`
ProjectID int64 `bun:"project_id,type:bigint,notnull," json:"project_id"`
RelPersonaID *ModelPublicAgentPersonas `bun:"rel:has-one,join:persona_id=id" json:"relpersonaid,omitempty"` // Has one ModelPublicAgentPersonas
RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects
}
// TableName returns the table name for ModelPublicProjectPersonas
func (m ModelPublicProjectPersonas) TableName() string {
return "public.project_personas"
}
// TableNameOnly returns the table name without schema for ModelPublicProjectPersonas
func (m ModelPublicProjectPersonas) TableNameOnly() string {
return "project_personas"
}
// SchemaName returns the schema name for ModelPublicProjectPersonas
func (m ModelPublicProjectPersonas) SchemaName() string {
return "public"
}
// GetID returns the primary key value
func (m ModelPublicProjectPersonas) GetID() int64 {
return m.ID.Int64()
}
// GetIDStr returns the primary key as a string
func (m ModelPublicProjectPersonas) GetIDStr() string {
return m.ID.String()
}
// SetID sets the primary key value
func (m ModelPublicProjectPersonas) SetID(newid int64) {
m.UpdateID(newid)
}
// UpdateID updates the primary key value
func (m *ModelPublicProjectPersonas) UpdateID(newid int64) {
m.ID.FromString(fmt.Sprintf("%d", newid))
}
// GetIDName returns the name of the primary key column
func (m ModelPublicProjectPersonas) GetIDName() string {
return "id"
}
// GetPrefix returns the table prefix
func (m ModelPublicProjectPersonas) GetPrefix() string {
return "PPR"
}
@@ -11,6 +11,7 @@ type ModelPublicProjectSkills struct {
bun.BaseModel `bun:"table:public.project_skills,alias:project_skills"`
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
Override bool `bun:"override,type:boolean,default:false,notnull," json:"override"`
ProjectID int64 `bun:"project_id,type:bigint,notnull," json:"project_id"`
SkillID int64 `bun:"skill_id,type:bigint,notnull," json:"skill_id"`
RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects
@@ -39,7 +40,7 @@ func (m ModelPublicProjectSkills) GetID() int64 {
// GetIDStr returns the primary key as a string
func (m ModelPublicProjectSkills) GetIDStr() string {
return fmt.Sprintf("%v", m.ID)
return m.ID.String()
}
// SetID sets the primary key value
@@ -16,6 +16,7 @@ type ModelPublicProjects struct {
LastActiveAt resolvespec_common.SqlTimeStamp `bun:"last_active_at,type:timestamptz,default:now(),nullzero," json:"last_active_at"`
Name resolvespec_common.SqlString `bun:"name,type:text,notnull," json:"name"`
ThoughtCount resolvespec_common.SqlInt64 `bun:"thought_count,scanonly" json:"thought_count"`
RelProjectIDPublicProjectPersonas []*ModelPublicProjectPersonas `bun:"rel:has-many,join:id=project_id" json:"relprojectidpublicprojectpersonas,omitempty"` // Has many ModelPublicProjectPersonas
RelProjectIDPublicThoughts []*ModelPublicThoughts `bun:"rel:has-many,join:id=project_id" json:"relprojectidpublicthoughts,omitempty"` // Has many ModelPublicThoughts
RelProjectIDPublicStoredFiles []*ModelPublicStoredFiles `bun:"rel:has-many,join:id=project_id" json:"relprojectidpublicstoredfiles,omitempty"` // Has many ModelPublicStoredFiles
RelProjectIDPublicChatHistories []*ModelPublicChatHistories `bun:"rel:has-many,join:id=project_id" json:"relprojectidpublicchathistories,omitempty"` // Has many ModelPublicChatHistories
@@ -47,7 +48,7 @@ func (m ModelPublicProjects) GetID() int64 {
// GetIDStr returns the primary key as a string
func (m ModelPublicProjects) GetIDStr() string {
return fmt.Sprintf("%v", m.ID)
return m.ID.String()
}
// SetID sets the primary key value
@@ -48,7 +48,7 @@ func (m ModelPublicStoredFiles) GetID() int64 {
// GetIDStr returns the primary key as a string
func (m ModelPublicStoredFiles) GetIDStr() string {
return fmt.Sprintf("%v", m.ID)
return m.ID.String()
}
// SetID sets the primary key value
@@ -40,7 +40,7 @@ func (m ModelPublicThoughtLinks) GetID() int64 {
// GetIDStr returns the primary key as a string
func (m ModelPublicThoughtLinks) GetIDStr() string {
return fmt.Sprintf("%v", m.ID)
return m.ID.String()
}
// SetID sets the primary key value
@@ -47,7 +47,7 @@ func (m ModelPublicThoughts) GetID() int64 {
// GetIDStr returns the primary key as a string
func (m ModelPublicThoughts) GetIDStr() string {
return fmt.Sprintf("%v", m.ID)
return m.ID.String()
}
// SetID sets the primary key value
@@ -38,7 +38,7 @@ func (m ModelPublicToolAnnotations) GetID() int64 {
// GetIDStr returns the primary key as a string
func (m ModelPublicToolAnnotations) GetIDStr() string {
return fmt.Sprintf("%v", m.ID)
return m.ID.String()
}
// SetID sets the primary key value
+42 -6
View File
@@ -1,6 +1,7 @@
package mcpserver
import (
"context"
"log/slog"
"net/http"
"strings"
@@ -37,12 +38,14 @@ type ToolSet struct {
Reparse *tools.ReparseMetadataTool
RetryMetadata *tools.RetryEnrichmentTool
//Maintenance *tools.MaintenanceTool
Skills *tools.SkillsTool
Personas *tools.AgentPersonasTool
ChatHistory *tools.ChatHistoryTool
Describe *tools.DescribeTool
Learnings *tools.LearningsTool
Plans *tools.PlansTool
Skills *tools.SkillsTool
Personas *tools.AgentPersonasTool
ChatHistory *tools.ChatHistoryTool
Describe *tools.DescribeTool
Learnings *tools.LearningsTool
Plans *tools.PlansTool
ProjectPersonas *tools.ProjectPersonasTool
WorldModel *tools.WorldModelTool
}
// Handlers groups the HTTP handlers produced for an MCP server instance.
@@ -86,6 +89,7 @@ func NewHandlers(cfg config.MCPConfig, logger *slog.Logger, toolSet ToolSet, onS
registerSystemTools,
registerThoughtTools,
registerProjectTools,
registerWorldModelTools,
registerLearningTools,
registerPlanTools,
registerFileTools,
@@ -123,6 +127,33 @@ func NewHandlers(cfg config.MCPConfig, logger *slog.Logger, toolSet ToolSet, onS
return h, nil
}
func registerWorldModelTools(server *mcp.Server, logger *slog.Logger, toolSet ToolSet) error {
server.AddResource(&mcp.Resource{
Name: "world_model_intro",
URI: "amcs://world-model/intro",
MIMEType: "text/markdown",
Description: "Mandatory startup instructions for discovering and loading the AMCS world model.",
}, func(_ context.Context, req *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
return &mcp.ReadResourceResult{Contents: []*mcp.ResourceContents{{URI: req.Params.URI, MIMEType: "text/markdown", Text: string(amcsllm.WorldModelIntro)}}}, nil
})
if err := addTool(server, logger, &mcp.Tool{Name: "bootstrap_world_model", Description: "Validate and activate a project, then return its effective skills, guardrails, personas, and bounded recent context."}, toolSet.WorldModel.Bootstrap); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "add_project_persona", Description: "Link a persona to a project and optionally make it the default."}, toolSet.ProjectPersonas.Add); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "remove_project_persona", Description: "Unlink a persona from a project."}, toolSet.ProjectPersonas.Remove); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "set_default_project_persona", Description: "Set a linked persona as the project's default persona."}, toolSet.ProjectPersonas.SetDefault); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "list_project_personas", Description: "List personas linked to a project, with the default marked."}, toolSet.ProjectPersonas.List); err != nil {
return err
}
return nil
}
// buildServerIcons returns icon definitions referencing the server's own /images/icon.png endpoint.
// Returns nil when publicURL is empty so the icons field is omitted from the MCP identity.
func buildServerIcons(publicURL string) []mcp.Icon {
@@ -713,6 +744,11 @@ func BuildToolCatalog() []tools.ToolEntry {
{Name: "list_projects", Description: "List projects and their current thought counts.", Category: "projects"},
{Name: "set_active_project", Description: "Set the active project for the current MCP session. Requires a stateful MCP client that reuses the same session across calls. If your client does not preserve sessions, pass project explicitly to each tool instead.", Category: "projects"},
{Name: "get_active_project", Description: "Return the active project for the current MCP session. If your client does not preserve MCP sessions, pass project explicitly to project-scoped tools instead of relying on this.", Category: "projects"},
{Name: "bootstrap_world_model", Description: "Validate and activate a project, then return effective skills, guardrails, personas, and bounded recent context.", Category: "projects"},
{Name: "add_project_persona", Description: "Link a persona to a project and optionally make it the default.", Category: "personas"},
{Name: "remove_project_persona", Description: "Unlink a persona from a project.", Category: "personas"},
{Name: "set_default_project_persona", Description: "Set a linked persona as the project's default persona.", Category: "personas"},
{Name: "list_project_personas", Description: "List personas linked to a project, with the default marked.", Category: "personas"},
{Name: "get_project_context", Description: "Get recent and semantic context for a project. Uses the explicit project when provided, otherwise the active MCP session project. Falls back to full-text search when no embeddings exist.", Category: "projects"},
// learnings
+21
View File
@@ -5,6 +5,7 @@ import (
"net/http/httptest"
"reflect"
"sort"
"strings"
"testing"
"time"
@@ -60,6 +61,26 @@ func TestNewListsStoredFileResourceTemplate(t *testing.T) {
}
}
func TestNewExposesWorldModelIntroResource(t *testing.T) {
cs := newStreamableTestClient(t)
listed, err := cs.ListResources(context.Background(), nil)
if err != nil {
t.Fatalf("ListResources() error = %v", err)
}
if len(listed.Resources) != 1 || listed.Resources[0].URI != "amcs://world-model/intro" {
t.Fatalf("ListResources() = %#v, want world model intro", listed.Resources)
}
read, err := cs.ReadResource(context.Background(), &mcp.ReadResourceParams{URI: "amcs://world-model/intro"})
if err != nil {
t.Fatalf("ReadResource() error = %v", err)
}
if len(read.Contents) != 1 || !strings.Contains(read.Contents[0].Text, "bootstrap_world_model") {
t.Fatalf("ReadResource() did not return bootstrap instructions: %#v", read.Contents)
}
}
func newStreamableTestClient(t *testing.T) *mcp.ClientSession {
t.Helper()
@@ -189,28 +189,30 @@ func TestStreamableHTTPReturnsStructuredToolErrors(t *testing.T) {
func streamableTestToolSet() ToolSet {
return ToolSet{
Version: tools.NewVersionTool("test", buildinfo.Info{Version: "0.0.1", TagName: "v0.0.1", Commit: "test", BuildDate: "2026-03-31T00:00:00Z"}),
Capture: new(tools.CaptureTool),
Search: new(tools.SearchTool),
List: new(tools.ListTool),
Stats: new(tools.StatsTool),
Get: new(tools.GetTool),
Update: new(tools.UpdateTool),
Delete: new(tools.DeleteTool),
Archive: new(tools.ArchiveTool),
Projects: new(tools.ProjectsTool),
Context: new(tools.ContextTool),
Recall: new(tools.RecallTool),
Summarize: new(tools.SummarizeTool),
Links: new(tools.LinksTool),
Files: new(tools.FilesTool),
Backfill: new(tools.BackfillTool),
Reparse: new(tools.ReparseMetadataTool),
RetryMetadata: new(tools.RetryEnrichmentTool),
Skills: new(tools.SkillsTool),
ChatHistory: new(tools.ChatHistoryTool),
Describe: new(tools.DescribeTool),
Learnings: new(tools.LearningsTool),
Plans: new(tools.PlansTool),
Version: tools.NewVersionTool("test", buildinfo.Info{Version: "0.0.1", TagName: "v0.0.1", Commit: "test", BuildDate: "2026-03-31T00:00:00Z"}),
Capture: new(tools.CaptureTool),
Search: new(tools.SearchTool),
List: new(tools.ListTool),
Stats: new(tools.StatsTool),
Get: new(tools.GetTool),
Update: new(tools.UpdateTool),
Delete: new(tools.DeleteTool),
Archive: new(tools.ArchiveTool),
Projects: new(tools.ProjectsTool),
Context: new(tools.ContextTool),
Recall: new(tools.RecallTool),
Summarize: new(tools.SummarizeTool),
Links: new(tools.LinksTool),
Files: new(tools.FilesTool),
Backfill: new(tools.BackfillTool),
Reparse: new(tools.ReparseMetadataTool),
RetryMetadata: new(tools.RetryEnrichmentTool),
Skills: new(tools.SkillsTool),
ChatHistory: new(tools.ChatHistoryTool),
Describe: new(tools.DescribeTool),
Learnings: new(tools.LearningsTool),
Plans: new(tools.PlansTool),
ProjectPersonas: new(tools.ProjectPersonasTool),
WorldModel: new(tools.WorldModelTool),
}
}
+10 -6
View File
@@ -234,6 +234,7 @@ func (db *DB) GetPersona(ctx context.Context, name string, detail bool, override
Name: s.Name,
Description: s.Description,
Tags: s.Tags,
Override: s.Override,
}
if detail {
entry.Content = s.Content
@@ -341,6 +342,7 @@ func (db *DB) GetPersonaManifest(ctx context.Context, name string) (ext.PersonaM
ID: s.ID,
Name: s.Name,
Description: s.Description,
Override: s.Override,
})
}
for _, g := range guardrails {
@@ -567,11 +569,12 @@ func (db *DB) RemovePersonaPart(ctx context.Context, personaName, partName strin
// Persona-Skill links
// ──────────────────────────────────────────────
func (db *DB) AddPersonaSkill(ctx context.Context, personaID, skillID int64) error {
func (db *DB) AddPersonaSkill(ctx context.Context, personaID, skillID int64, override bool) error {
_, err := db.pool.Exec(ctx, `
insert into agent_persona_skills (persona_id, skill_id)
values ($1, $2) on conflict do nothing
`, personaID, skillID)
insert into agent_persona_skills (persona_id, skill_id, override)
values ($1, $2, $3)
on conflict (persona_id, skill_id) do update set override = excluded.override
`, personaID, skillID, override)
if err != nil {
return fmt.Errorf("add persona skill: %w", err)
}
@@ -1058,7 +1061,7 @@ func (db *DB) fetchPartsByNames(ctx context.Context, names []string) ([]rawPart,
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[]
select s.id, s.name, s.description, s.content, s.tags::text[], aps.override
from agent_skills s
join agent_persona_skills aps on aps.skill_id = s.id
where aps.persona_id = $1
@@ -1073,7 +1076,7 @@ func (db *DB) listPersonaSkills(ctx context.Context, personaID int64) ([]AgentSk
for rows.Next() {
var s AgentSkillRow
var tags []string
if err := rows.Scan(&s.ID, &s.Name, &s.Description, &s.Content, &tags); err != nil {
if err := rows.Scan(&s.ID, &s.Name, &s.Description, &s.Content, &tags, &s.Override); err != nil {
return nil, fmt.Errorf("scan persona skill: %w", err)
}
s.Tags = nilToEmptyStrings(tags)
@@ -1139,6 +1142,7 @@ type AgentSkillRow struct {
Description string
Content string
Tags []string
Override bool
}
type AgentGuardrailRow struct {
+99
View File
@@ -0,0 +1,99 @@
package store
import (
"context"
"fmt"
"github.com/jackc/pgx/v5"
ext "git.warky.dev/wdevs/amcs/internal/types"
)
func (db *DB) AddProjectPersona(ctx context.Context, projectID, personaID int64, isDefault bool) error {
tx, err := db.pool.Begin(ctx)
if err != nil {
return fmt.Errorf("begin add project persona: %w", err)
}
defer func() { _ = tx.Rollback(ctx) }()
if _, err := tx.Exec(ctx, `select pg_advisory_xact_lock($1)`, projectID); err != nil {
return fmt.Errorf("lock project persona defaults: %w", err)
}
if isDefault {
if _, err := tx.Exec(ctx, `update project_personas set is_default = false where project_id = $1`, projectID); err != nil {
return fmt.Errorf("clear default project persona: %w", err)
}
}
if _, err := tx.Exec(ctx, `
insert into project_personas (project_id, persona_id, is_default)
values ($1, $2, $3)
on conflict (project_id, persona_id) do update set is_default = excluded.is_default
`, projectID, personaID, isDefault); err != nil {
return fmt.Errorf("add project persona: %w", err)
}
if err := tx.Commit(ctx); err != nil {
return fmt.Errorf("commit add project persona: %w", err)
}
return nil
}
func (db *DB) RemoveProjectPersona(ctx context.Context, projectID, personaID int64) error {
tag, err := db.pool.Exec(ctx, `delete from project_personas where project_id = $1 and persona_id = $2`, projectID, personaID)
if err != nil {
return fmt.Errorf("remove project persona: %w", err)
}
if tag.RowsAffected() == 0 {
return fmt.Errorf("project persona link not found")
}
return nil
}
func (db *DB) ListProjectPersonas(ctx context.Context, projectID int64) ([]ext.ProjectPersona, error) {
rows, err := db.pool.Query(ctx, `
select p.id, p.guid, p.name, p.description, p.summary, p.detail,
p.compiled_summary, p.compiled_detail, p.compiled_at, p.tags::text[],
p.created_at, p.updated_at, pp.is_default
from agent_personas p
join project_personas pp on pp.persona_id = p.id
where pp.project_id = $1
order by pp.is_default desc, p.name
`, projectID)
if err != nil {
return nil, fmt.Errorf("list project personas: %w", err)
}
defer rows.Close()
var result []ext.ProjectPersona
for rows.Next() {
var item ext.ProjectPersona
var tags []string
if err := rows.Scan(&item.Persona.ID, &item.Persona.GUID, &item.Persona.Name,
&item.Persona.Description, &item.Persona.Summary, &item.Persona.Detail,
&item.Persona.CompiledSummary, &item.Persona.CompiledDetail, &item.Persona.CompiledAt,
&tags, &item.Persona.CreatedAt, &item.Persona.UpdatedAt, &item.IsDefault); err != nil {
return nil, fmt.Errorf("scan project persona: %w", err)
}
item.Persona.Tags = nilToEmptyStrings(tags)
result = append(result, item)
}
return result, rows.Err()
}
func (db *DB) GetDefaultProjectPersona(ctx context.Context, projectID int64) (*ext.Persona, error) {
row := db.pool.QueryRow(ctx, `
select p.id, p.guid, p.name, p.description, p.summary, p.detail,
p.compiled_summary, p.compiled_detail, p.compiled_at, p.tags::text[],
p.created_at, p.updated_at
from agent_personas p
join project_personas pp on pp.persona_id = p.id
where pp.project_id = $1 and pp.is_default
`, projectID)
persona, err := scanPersona(row)
if err == pgx.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("get default project persona: %w", err)
}
return &persona, nil
}
+31 -8
View File
@@ -116,6 +116,24 @@ func scanSkill(row skillScanner) (ext.AgentSkill, error) {
}, nil
}
func normalizeSkillSlices(skill *ext.AgentSkill) {
if skill.Tags == nil {
skill.Tags = []string{}
}
if skill.LanguageTags == nil {
skill.LanguageTags = []string{}
}
if skill.LibraryTags == nil {
skill.LibraryTags = []string{}
}
if skill.FrameworkTags == nil {
skill.FrameworkTags = []string{}
}
if skill.DomainTags == nil {
skill.DomainTags = []string{}
}
}
func (db *DB) GetSkill(ctx context.Context, id int64) (ext.AgentSkill, error) {
row := db.pool.QueryRow(ctx, `select `+skillSelectCols+` from agent_skills where id = $1`, id)
s, err := scanSkill(row)
@@ -278,12 +296,12 @@ func (db *DB) GetGuardrail(ctx context.Context, id int64) (ext.AgentGuardrail, e
// Project Skills
func (db *DB) AddProjectSkill(ctx context.Context, projectID, skillID int64) error {
func (db *DB) AddProjectSkill(ctx context.Context, projectID, skillID int64, override bool) error {
_, err := db.pool.Exec(ctx, `
insert into project_skills (project_id, skill_id)
values ($1, $2)
on conflict do nothing
`, projectID, skillID)
insert into project_skills (project_id, skill_id, override)
values ($1, $2, $3)
on conflict (project_id, skill_id) do update set override = excluded.override
`, projectID, skillID, override)
if err != nil {
return fmt.Errorf("add project skill: %w", err)
}
@@ -305,7 +323,9 @@ func (db *DB) RemoveProjectSkill(ctx context.Context, projectID, skillID int64)
func (db *DB) ListProjectSkills(ctx context.Context, projectID int64) ([]ext.AgentSkill, error) {
rows, err := db.pool.Query(ctx, `
select s.`+skillSelectCols+`
select s.id, s.name, s.description, s.content, s.tags::text[],
s.language_tags::text[], s.library_tags::text[], s.framework_tags::text[],
s.domain_tags::text[], s.created_at, s.updated_at, ps.override
from agent_skills s
join project_skills ps on ps.skill_id = s.id
where ps.project_id = $1
@@ -318,10 +338,13 @@ func (db *DB) ListProjectSkills(ctx context.Context, projectID int64) ([]ext.Age
var skills []ext.AgentSkill
for rows.Next() {
s, err := scanSkill(rows)
if err != nil {
var s ext.AgentSkill
if err := rows.Scan(&s.ID, &s.Name, &s.Description, &s.Content, &s.Tags,
&s.LanguageTags, &s.LibraryTags, &s.FrameworkTags, &s.DomainTags,
&s.CreatedAt, &s.UpdatedAt, &s.Override); err != nil {
return nil, fmt.Errorf("scan project skill: %w", err)
}
normalizeSkillSlices(&s)
skills = append(skills, s)
}
return skills, rows.Err()
+2 -1
View File
@@ -422,6 +422,7 @@ func (t *AgentPersonasTool) RemovePersonaPart(ctx context.Context, _ *mcp.CallTo
type AddPersonaSkillInput struct {
PersonaID int64 `json:"persona_id" jsonschema:"persona id"`
SkillID int64 `json:"skill_id" jsonschema:"agent skill id to link"`
Override bool `json:"override,omitempty" jsonschema:"replace a project skill with the same name during world-model assembly"`
}
type AddPersonaSkillOutput struct {
@@ -430,7 +431,7 @@ type AddPersonaSkillOutput struct {
}
func (t *AgentPersonasTool) AddPersonaSkill(ctx context.Context, _ *mcp.CallToolRequest, in AddPersonaSkillInput) (*mcp.CallToolResult, AddPersonaSkillOutput, error) {
if err := t.store.AddPersonaSkill(ctx, in.PersonaID, in.SkillID); err != nil {
if err := t.store.AddPersonaSkill(ctx, in.PersonaID, in.SkillID, in.Override); err != nil {
return nil, AddPersonaSkillOutput{}, err
}
return nil, AddPersonaSkillOutput{PersonaID: in.PersonaID, SkillID: in.SkillID}, nil
+82
View File
@@ -0,0 +1,82 @@
package tools
import (
"context"
"github.com/modelcontextprotocol/go-sdk/mcp"
"git.warky.dev/wdevs/amcs/internal/session"
"git.warky.dev/wdevs/amcs/internal/store"
ext "git.warky.dev/wdevs/amcs/internal/types"
)
type ProjectPersonasTool struct {
store *store.DB
sessions *session.ActiveProjects
}
func NewProjectPersonasTool(db *store.DB, sessions *session.ActiveProjects) *ProjectPersonasTool {
return &ProjectPersonasTool{store: db, sessions: sessions}
}
type ProjectPersonaInput struct {
Project string `json:"project,omitempty" jsonschema:"project name or id (uses active project if omitted)"`
PersonaID int64 `json:"persona_id" jsonschema:"persona id"`
IsDefault bool `json:"is_default,omitempty" jsonschema:"make this the default persona for the project"`
}
type ProjectPersonaOutput struct {
ProjectID int64 `json:"project_id"`
PersonaID int64 `json:"persona_id"`
}
type ListProjectPersonasInput struct {
Project string `json:"project,omitempty" jsonschema:"project name or id (uses active project if omitted)"`
}
type ListProjectPersonasOutput struct {
ProjectID int64 `json:"project_id"`
Personas []ext.ProjectPersona `json:"personas"`
}
func (t *ProjectPersonasTool) Add(ctx context.Context, req *mcp.CallToolRequest, in ProjectPersonaInput) (*mcp.CallToolResult, ProjectPersonaOutput, error) {
project, err := resolveProject(ctx, t.store, t.sessions, req, in.Project, true)
if err != nil {
return nil, ProjectPersonaOutput{}, err
}
if err := t.store.AddProjectPersona(ctx, project.NumericID, in.PersonaID, in.IsDefault); err != nil {
return nil, ProjectPersonaOutput{}, err
}
return nil, ProjectPersonaOutput{ProjectID: project.NumericID, PersonaID: in.PersonaID}, nil
}
func (t *ProjectPersonasTool) Remove(ctx context.Context, req *mcp.CallToolRequest, in ProjectPersonaInput) (*mcp.CallToolResult, ProjectPersonaOutput, error) {
project, err := resolveProject(ctx, t.store, t.sessions, req, in.Project, true)
if err != nil {
return nil, ProjectPersonaOutput{}, err
}
if err := t.store.RemoveProjectPersona(ctx, project.NumericID, in.PersonaID); err != nil {
return nil, ProjectPersonaOutput{}, err
}
return nil, ProjectPersonaOutput{ProjectID: project.NumericID, PersonaID: in.PersonaID}, nil
}
func (t *ProjectPersonasTool) SetDefault(ctx context.Context, req *mcp.CallToolRequest, in ProjectPersonaInput) (*mcp.CallToolResult, ProjectPersonaOutput, error) {
in.IsDefault = true
return t.Add(ctx, req, in)
}
func (t *ProjectPersonasTool) List(ctx context.Context, req *mcp.CallToolRequest, in ListProjectPersonasInput) (*mcp.CallToolResult, ListProjectPersonasOutput, error) {
project, err := resolveProject(ctx, t.store, t.sessions, req, in.Project, true)
if err != nil {
return nil, ListProjectPersonasOutput{}, err
}
personas, err := t.store.ListProjectPersonas(ctx, project.NumericID)
if err != nil {
return nil, ListProjectPersonasOutput{}, err
}
if personas == nil {
personas = []ext.ProjectPersona{}
}
return nil, ListProjectPersonasOutput{ProjectID: project.NumericID, Personas: personas}, nil
}
+4 -3
View File
@@ -259,8 +259,9 @@ func (t *SkillsTool) GetGuardrail(ctx context.Context, _ *mcp.CallToolRequest, i
// add_project_skill
type AddProjectSkillInput struct {
Project string `json:"project,omitempty" jsonschema:"project name or id (uses active project if omitted)"`
SkillID int64 `json:"skill_id" jsonschema:"skill id to link"`
Project string `json:"project,omitempty" jsonschema:"project name or id (uses active project if omitted)"`
SkillID int64 `json:"skill_id" jsonschema:"skill id to link"`
Override bool `json:"override,omitempty" jsonschema:"replace a lower-precedence skill with the same name during world-model assembly"`
}
type AddProjectSkillOutput struct {
@@ -273,7 +274,7 @@ func (t *SkillsTool) AddProjectSkill(ctx context.Context, req *mcp.CallToolReque
if err != nil {
return nil, AddProjectSkillOutput{}, err
}
if err := t.store.AddProjectSkill(ctx, project.NumericID, in.SkillID); err != nil {
if err := t.store.AddProjectSkill(ctx, project.NumericID, in.SkillID, in.Override); err != nil {
return nil, AddProjectSkillOutput{}, err
}
return nil, AddProjectSkillOutput{ProjectID: project.NumericID, SkillID: in.SkillID}, nil
+183
View File
@@ -0,0 +1,183 @@
package tools
import (
"context"
"strings"
"time"
"github.com/modelcontextprotocol/go-sdk/mcp"
"git.warky.dev/wdevs/amcs/internal/session"
"git.warky.dev/wdevs/amcs/internal/store"
ext "git.warky.dev/wdevs/amcs/internal/types"
)
type WorldModelTool struct {
store *store.DB
sessions *session.ActiveProjects
}
func NewWorldModelTool(db *store.DB, sessions *session.ActiveProjects) *WorldModelTool {
return &WorldModelTool{store: db, sessions: sessions}
}
type BootstrapWorldModelInput struct {
Project string `json:"project" jsonschema:"project name or id; must be explicit for deterministic startup"`
ContextLimit int `json:"context_limit,omitempty" jsonschema:"recent project thoughts to include (default 10, maximum 50)"`
}
type EffectiveSkill struct {
ID int64 `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Content string `json:"content,omitempty"`
Tags []string `json:"tags"`
Source string `json:"source"`
Override bool `json:"override"`
}
type WorldModelContextItem struct {
ID int64 `json:"id"`
Content string `json:"content"`
Metadata ext.ThoughtMetadata `json:"metadata"`
CreatedAt time.Time `json:"created_at"`
}
type BootstrapWorldModelOutput struct {
Version int `json:"version"`
GeneratedAt time.Time `json:"generated_at"`
Project ext.Project `json:"project"`
Skills []EffectiveSkill `json:"skills"`
Guardrails []ext.AgentGuardrail `json:"guardrails"`
Personas []ext.ProjectPersona `json:"personas"`
PersonaManifests []ext.PersonaManifest `json:"persona_manifests"`
DefaultPersona *ext.PersonaFull `json:"default_persona,omitempty"`
Context []WorldModelContextItem `json:"context"`
}
func (t *WorldModelTool) Bootstrap(ctx context.Context, req *mcp.CallToolRequest, in BootstrapWorldModelInput) (*mcp.CallToolResult, BootstrapWorldModelOutput, error) {
project, err := resolveProject(ctx, t.store, t.sessions, req, in.Project, true)
if err != nil {
return nil, BootstrapWorldModelOutput{}, err
}
if t.sessions != nil && req != nil && req.Session != nil {
t.sessions.Set(req.Session.ID(), project.ID)
}
projectSkills, err := t.store.ListProjectSkills(ctx, project.NumericID)
if err != nil {
return nil, BootstrapWorldModelOutput{}, err
}
guardrails, err := t.store.ListProjectGuardrails(ctx, project.NumericID)
if err != nil {
return nil, BootstrapWorldModelOutput{}, err
}
personas, err := t.store.ListProjectPersonas(ctx, project.NumericID)
if err != nil {
return nil, BootstrapWorldModelOutput{}, err
}
manifests := make([]ext.PersonaManifest, 0, len(personas))
for _, linked := range personas {
manifest, err := t.store.GetPersonaManifest(ctx, linked.Persona.Name)
if err != nil {
return nil, BootstrapWorldModelOutput{}, err
}
manifests = append(manifests, manifest)
}
var defaultPersona *ext.PersonaFull
for _, linked := range personas {
if !linked.IsDefault {
continue
}
loaded, err := t.store.GetPersona(ctx, linked.Persona.Name, true, nil)
if err != nil {
return nil, BootstrapWorldModelOutput{}, err
}
defaultPersona = &loaded
break
}
skills := mergeWorldModelSkills(projectSkills, defaultPersona)
guardrails = mergeWorldModelGuardrails(guardrails, defaultPersona)
limit := in.ContextLimit
if limit <= 0 {
limit = 10
}
if limit > 50 {
limit = 50
}
recent, err := t.store.RecentThoughts(ctx, &project.NumericID, limit, 0)
if err != nil {
return nil, BootstrapWorldModelOutput{}, err
}
contextItems := make([]WorldModelContextItem, 0, len(recent))
for _, thought := range recent {
contextItems = append(contextItems, WorldModelContextItem{ID: thought.ID, Content: thought.Content, Metadata: thought.Metadata, CreatedAt: thought.CreatedAt})
}
if guardrails == nil {
guardrails = []ext.AgentGuardrail{}
}
if personas == nil {
personas = []ext.ProjectPersona{}
}
_ = t.store.TouchProject(ctx, project.NumericID)
return nil, BootstrapWorldModelOutput{
Version: 1, GeneratedAt: time.Now().UTC(), Project: *project, Skills: skills,
Guardrails: guardrails, Personas: personas, PersonaManifests: manifests,
DefaultPersona: defaultPersona, Context: contextItems,
}, nil
}
func mergeWorldModelSkills(projectSkills []ext.AgentSkill, persona *ext.PersonaFull) []EffectiveSkill {
result := make([]EffectiveSkill, 0, len(projectSkills))
index := make(map[string]int, len(projectSkills))
for _, skill := range projectSkills {
key := strings.ToLower(skill.Name)
index[key] = len(result)
result = append(result, EffectiveSkill{ID: skill.ID, Name: skill.Name, Description: skill.Description, Content: skill.Content, Tags: skill.Tags, Source: "project", Override: skill.Override})
}
if persona == nil {
return result
}
for _, skill := range persona.Skills {
key := strings.ToLower(skill.Name)
entry := EffectiveSkill{ID: skill.ID, Name: skill.Name, Description: skill.Description, Content: skill.Content, Tags: skill.Tags, Source: "persona", Override: skill.Override}
if i, ok := index[key]; ok {
if skill.Override {
result[i] = entry
}
continue
}
index[key] = len(result)
result = append(result, entry)
}
return result
}
func mergeWorldModelGuardrails(project []ext.AgentGuardrail, persona *ext.PersonaFull) []ext.AgentGuardrail {
result := append([]ext.AgentGuardrail(nil), project...)
index := make(map[string]int, len(result))
for i, guardrail := range result {
index[strings.ToLower(guardrail.Name)] = i
}
if persona == nil {
return result
}
severity := map[string]int{"low": 1, "medium": 2, "high": 3, "critical": 4}
for _, guardrail := range persona.Guardrails {
key := strings.ToLower(guardrail.Name)
if i, ok := index[key]; ok {
if severity[guardrail.Severity] > severity[result[i].Severity] {
result[i].Severity = guardrail.Severity
result[i].Content = guardrail.Content
}
continue
}
index[key] = len(result)
result = append(result, ext.AgentGuardrail{ID: guardrail.ID, Name: guardrail.Name, Description: guardrail.Description, Content: guardrail.Content, Severity: guardrail.Severity, Tags: guardrail.Tags})
}
return result
}
+52
View File
@@ -0,0 +1,52 @@
package tools
import (
"testing"
ext "git.warky.dev/wdevs/amcs/internal/types"
)
func TestMergeWorldModelSkills(t *testing.T) {
project := []ext.AgentSkill{
{ID: 1, Name: "go", Content: "project", Tags: []string{}},
{ID: 2, Name: "postgres", Content: "database", Tags: []string{}},
}
persona := &ext.PersonaFull{Skills: []ext.PersonaSkillEntry{
{ID: 1, Name: "go", Content: "ignored additive duplicate", Tags: []string{}},
{ID: 3, Name: "Postgres", Content: "persona override", Tags: []string{}, Override: true},
{ID: 4, Name: "writing", Content: "additive", Tags: []string{}},
}}
got := mergeWorldModelSkills(project, persona)
if len(got) != 3 {
t.Fatalf("len(skills) = %d, want 3", len(got))
}
if got[0].Content != "project" || got[0].Source != "project" {
t.Fatalf("additive duplicate replaced project skill: %#v", got[0])
}
if got[1].Content != "persona override" || got[1].Source != "persona" || !got[1].Override {
t.Fatalf("override did not replace project skill: %#v", got[1])
}
if got[2].Name != "writing" || got[2].Source != "persona" {
t.Fatalf("new additive persona skill missing: %#v", got[2])
}
}
func TestMergeWorldModelGuardrailsKeepsStrictest(t *testing.T) {
project := []ext.AgentGuardrail{{ID: 1, Name: "safe", Severity: "high", Content: "project"}}
persona := &ext.PersonaFull{Guardrails: []ext.PersonaGuardrailEntry{
{ID: 2, Name: "Safe", Severity: "low", Content: "weaker"},
{ID: 3, Name: "privacy", Severity: "critical", Content: "new"},
}}
got := mergeWorldModelGuardrails(project, persona)
if len(got) != 2 {
t.Fatalf("len(guardrails) = %d, want 2", len(got))
}
if got[0].Severity != "high" || got[0].Content != "project" {
t.Fatalf("persona weakened project guardrail: %#v", got[0])
}
if got[1].Name != "privacy" || got[1].Severity != "critical" {
t.Fatalf("new persona guardrail missing: %#v", got[1])
}
}
+19 -12
View File
@@ -81,17 +81,17 @@ type ArcStage struct {
// 2. active arc-stage parts
// 3. persona-linked parts (base)
type PersonaFull struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Body string `json:"body"` // summary or detail, depending on mode
CompiledSummary string `json:"compiled_summary,omitempty"`
Tags []string `json:"tags"`
Parts []AssembledPart `json:"parts"`
Skills []PersonaSkillEntry `json:"skills"`
Guardrails []PersonaGuardrailEntry `json:"guardrails"`
Traits []PersonaTraitEntry `json:"traits"`
Arc *PersonaArcState `json:"arc,omitempty"`
Detail bool `json:"detail"` // whether full detail was requested
Name string `json:"name"`
Description string `json:"description,omitempty"`
Body string `json:"body"` // summary or detail, depending on mode
CompiledSummary string `json:"compiled_summary,omitempty"`
Tags []string `json:"tags"`
Parts []AssembledPart `json:"parts"`
Skills []PersonaSkillEntry `json:"skills"`
Guardrails []PersonaGuardrailEntry `json:"guardrails"`
Traits []PersonaTraitEntry `json:"traits"`
Arc *PersonaArcState `json:"arc,omitempty"`
Detail bool `json:"detail"` // whether full detail was requested
}
type AssembledPart struct {
@@ -109,13 +109,14 @@ type PersonaSkillEntry struct {
Description string `json:"description,omitempty"`
Content string `json:"content,omitempty"` // only when detail=true
Tags []string `json:"tags"`
Override bool `json:"override,omitempty"`
}
type PersonaGuardrailEntry struct {
ID int64 `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Content string `json:"content,omitempty"` // only when detail=true
Content string `json:"content,omitempty"` // only when detail=true
Severity string `json:"severity,omitempty"` // only when detail=true
Tags []string `json:"tags"`
}
@@ -168,6 +169,12 @@ type ManifestSkill struct {
ID int64 `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Override bool `json:"override,omitempty"`
}
type ProjectPersona struct {
Persona Persona `json:"persona"`
IsDefault bool `json:"is_default"`
}
type ManifestGuardrail struct {
+1
View File
@@ -229,6 +229,7 @@ type AgentSkill struct {
DomainTags []string `json:"domain_tags"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Override bool `json:"override,omitempty"`
}
type AgentGuardrail struct {