diff --git a/README.md b/README.md index 5d396b7..bb2de48 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,11 @@ The AMCS directory is used to store configuration and code for the Avalon Memory | `add_project_guardrail` | Link a guardrail to a project; pass `project` if client is stateless | | `remove_project_guardrail` | Unlink a guardrail from a project; pass `project` if client is stateless | | `list_project_guardrails` | Guardrails for a project; pass `project` if client is stateless | +| `bootstrap_world_model` | Validate and activate a project, then load effective skills, guardrails, personas, and bounded context | +| `add_project_persona` | Link a persona to a project and optionally make it the default | +| `remove_project_persona` | Unlink a persona from a project | +| `set_default_project_persona` | Select the default persona for a project | +| `list_project_personas` | List project personas and identify the default | | `get_version_info` | Build version, commit, and date | | `describe_tools` | List all available MCP tools with names, descriptions, categories, and model-authored usage notes; call this at the start of a session to orient yourself | | `annotate_tool` | Persist your own usage notes for a specific tool; notes are returned by `describe_tools` in future sessions | @@ -216,9 +221,9 @@ Use `get_version_info` to retrieve the runtime build metadata: ## Agent Skills and Guardrails -Skills and guardrails are reusable agent behaviour instructions and constraints that can be attached to projects. +Skills and guardrails are reusable agent behaviour instructions and constraints that can be attached to projects. Projects can also expose personas and a default persona through the world model. -**At the start of every project session, always call `list_project_skills` and `list_project_guardrails` first.** Use the returned skills and guardrails to guide agent behaviour for that project. Only generate or create new skills/guardrails if none are returned. If your MCP client does not preserve sessions across calls, pass `project` explicitly instead of relying on `set_active_project`. +**At the start of every project session, read `amcs://world-model/intro` and call `bootstrap_world_model` with an explicit project.** Apply its skills, guardrails, and default persona before continuing. If your MCP client does not preserve sessions across calls, keep passing `project` explicitly. ### Skills @@ -243,10 +248,12 @@ Severity levels: `low`, `medium`, `high`, `critical`. Link existing skills and guardrails to a project so they are automatically available when that project is active: ```json -{ "project": "my-project", "skill_id": "" } -{ "project": "my-project", "guardrail_id": "" } +{ "project": "my-project", "skill_id": 42, "override": false } +{ "project": "my-project", "guardrail_id": 17 } ``` +Skills are additive by default. Set `override: true` on a project-skill or persona-skill link only when that skill must replace a lower-precedence skill with the same name. World-model precedence is project, selected persona, then runtime. Guardrails are additive and later layers cannot weaken them. + ## Configuration Config is YAML-driven. Copy `configs/config.example.yaml` and set: @@ -417,7 +424,7 @@ Returns `{"file": {...}, "uri": "amcs://files/"}`. Pass `thought_id`/`projec ### MCP resources -Stored files are also exposed as MCP resources at `amcs://files/{id}`. MCP clients can read raw binary content directly via `resources/read` without going through `load_file`. +Stored files are also exposed as MCP resources at `amcs://files/{id}`. MCP clients can read raw binary content directly via `resources/read` without going through `load_file`. The startup guide is available at `amcs://world-model/intro`. ### HTTP upload and download diff --git a/doc/llm/log/20260704_00.md b/doc/llm/log/20260704_00.md new file mode 100644 index 0000000..a449512 --- /dev/null +++ b/doc/llm/log/20260704_00.md @@ -0,0 +1,30 @@ +# World Model Planning + +Project: AMCS + +AMCS already exposes MCP initialization instructions from `llm/memory.md`, active-project tools, project-linked skills and guardrails, persona manifests, project context, and MCP file resources. + +Recommended design: + +- Keep a concise mandatory world-model bootstrap protocol in MCP initialization instructions. +- Add a readable static resource at `amcs://world-model/intro` containing the fuller introduction and project-selection protocol. +- Add one `bootstrap_world_model` tool. It accepts an explicit project name or ID, validates it, sets it active when the transport is stateful, and returns one bounded snapshot containing project identity, linked skills, linked guardrails, linked persona manifests/default persona, and recent project context. +- Never silently select or create a project. Return structured `project_not_found` or `project_ambiguous` errors with candidate projects; stateless clients must keep passing the project explicitly. +- Add project-to-persona associations because the current schema only associates skills and guardrails with projects. Support one optional default persona plus additional available personas. Return manifests at bootstrap and load full persona content on demand. +- Merge skills additively by default. Store an explicit `override` flag on project-skill and persona-skill association rows, not on the reusable skill itself. An overriding skill replaces lower-precedence skills with the same stable skill key; unrelated skills remain additive. +- Resolve precedence deterministically: project skills form the base, the selected persona is applied next, and explicit runtime/session skills are applied last. At each layer, `override: true` replaces the accumulated entry with the same key while `override: false` adds or de-duplicates it. +- Include a snapshot version and generation timestamp so clients can cache and refresh the world model safely. + +Delivery phases: + +1. Fix the existing `list_project_skills` SQL failure: `skillSelectCols` leaves `created_at` and other columns unqualified in a join. +2. Define the bootstrap response contract, size limits, precedence rules, and structured errors. +3. Add project-persona schema/store/tool support through DBML generation. +4. Add the intro resource and initialization instructions. +5. Implement and register `bootstrap_world_model` using existing project, skill, guardrail, persona, and context stores. +6. Add MCP lifecycle, stateful/stateless, missing/ambiguous project, payload-bound, and resource tests. +7. Add admin UI management for project personas/default persona and update client documentation. + +Confirmed decision: skills are additive unless their association has `override: true`. Guardrails remain additive, use the stricter severity when duplicates occur, and cannot be weakened by persona or runtime context. + +AMCS MCP writes and subsequent verification hung during this planning session, so this local log is the required fallback rather than a confirmed remote thought/plan. diff --git a/go.mod b/go.mod index 42b82a4..519610f 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module git.warky.dev/wdevs/amcs go 1.26.1 require ( - github.com/bitechdev/ResolveSpec v1.1.15 + github.com/bitechdev/ResolveSpec v1.1.24 github.com/google/jsonschema-go v0.4.3 github.com/google/uuid v1.6.0 github.com/jackc/pgx/v5 v5.9.2 @@ -11,7 +11,7 @@ require ( github.com/pgvector/pgvector-go v0.3.0 github.com/spf13/cobra v1.10.2 github.com/uptrace/bun v1.2.18 - github.com/uptrace/bun/dialect/pgdialect v1.2.16 + github.com/uptrace/bun/dialect/pgdialect v1.2.18 github.com/uptrace/bun/driver/pgdriver v1.1.12 github.com/uptrace/bunrouter v1.0.23 golang.org/x/sync v0.20.0 @@ -62,8 +62,8 @@ require ( github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect - github.com/uptrace/bun/dialect/mssqldialect v1.2.16 // indirect - github.com/uptrace/bun/dialect/sqlitedialect v1.2.16 // indirect + github.com/uptrace/bun/dialect/mssqldialect v1.2.18 // indirect + github.com/uptrace/bun/dialect/sqlitedialect v1.2.18 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/x448/float16 v0.8.4 // indirect diff --git a/go.sum b/go.sum index be819f7..fad3ebd 100644 --- a/go.sum +++ b/go.sum @@ -34,8 +34,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bitechdev/ResolveSpec v1.1.15 h1:Bhy1ZWGUg9AJOhOLk5Di9ilOVKmZ40SkGIGDraAlti8= -github.com/bitechdev/ResolveSpec v1.1.15/go.mod h1:GF51sMRCWbAyri2WNae3IZAFM/2s6DG6i3eTTrobbVs= +github.com/bitechdev/ResolveSpec v1.1.24 h1:+Ku3jE8ZSQ2c6IdVyqYp9CdCh4wSasCd1yLDF8GXy5U= +github.com/bitechdev/ResolveSpec v1.1.24/go.mod h1:GF51sMRCWbAyri2WNae3IZAFM/2s6DG6i3eTTrobbVs= github.com/bradfitz/gomemcache v0.0.0-20260422231931-4d751bb6e37c h1:6Gpm9YYUEQx2T9zMsYolQhr6sjwwGtFitSA0pQsa7a8= github.com/bradfitz/gomemcache v0.0.0-20260422231931-4d751bb6e37c/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= @@ -292,12 +292,12 @@ github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYm github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= github.com/uptrace/bun v1.2.18 h1:3HnRcMfS6OBPMG1eSOzlbFJ/X/AyMEJb7rMxE6VQvDU= github.com/uptrace/bun v1.2.18/go.mod h1:wNltaKJk4JtOt4SG5I5zmA7v0/Mzjh1+/S906Rayd3Y= -github.com/uptrace/bun/dialect/mssqldialect v1.2.16 h1:rKv0cKPNBviXadB/+2Y/UedA/c1JnwGzUWZkdN5FdSQ= -github.com/uptrace/bun/dialect/mssqldialect v1.2.16/go.mod h1:J5U7tGKWDsx2Q7MwDZF2417jCdpD6yD/ZMFJcCR80bk= -github.com/uptrace/bun/dialect/pgdialect v1.2.16 h1:KFNZ0LxAyczKNfK/IJWMyaleO6eI9/Z5tUv3DE1NVL4= -github.com/uptrace/bun/dialect/pgdialect v1.2.16/go.mod h1:IJdMeV4sLfh0LDUZl7TIxLI0LipF1vwTK3hBC7p5qLo= -github.com/uptrace/bun/dialect/sqlitedialect v1.2.16 h1:6wVAiYLj1pMibRthGwy4wDLa3D5AQo32Y8rvwPd8CQ0= -github.com/uptrace/bun/dialect/sqlitedialect v1.2.16/go.mod h1:Z7+5qK8CGZkDQiPMu+LSdVuDuR1I5jcwtkB1Pi3F82E= +github.com/uptrace/bun/dialect/mssqldialect v1.2.18 h1:nYzHoyJKJlIyl5i95Exi8ZTK8ooKWG+o3z3f404d/yQ= +github.com/uptrace/bun/dialect/mssqldialect v1.2.18/go.mod h1:Su45Je7z66sfeZ3d1ZsnOQEK8xfzGgaMzBvtoE8yFhk= +github.com/uptrace/bun/dialect/pgdialect v1.2.18 h1:IZ6nM2+OYrL8lkEAy7UkSEZvoa3vluTAUlZfPtlRB2k= +github.com/uptrace/bun/dialect/pgdialect v1.2.18/go.mod h1:Tqdf4QP1okrGYpXfodXvCOK6Ob1OOTwSaoAzCgBB3IU= +github.com/uptrace/bun/dialect/sqlitedialect v1.2.18 h1:Z33SY/U++XK9uGWqS4h8OZVxfCXguIG+sU9cYq2PGFQ= +github.com/uptrace/bun/dialect/sqlitedialect v1.2.18/go.mod h1:1MVOS/Ncy4FZbkJcgUFH6OqYoQinYNjkEwsmNQEXz2A= github.com/uptrace/bun/driver/pgdriver v1.1.12 h1:3rRWB1GK0psTJrHwxzNfEij2MLibggiLdTqjTtfHc1w= github.com/uptrace/bun/driver/pgdriver v1.1.12/go.mod h1:ssYUP+qwSEgeDDS1xm2XBip9el1y9Mi5mTAvLoiADLM= github.com/uptrace/bun/driver/sqliteshim v1.2.16 h1:M6Dh5kkDWFbUWBrOsIE1g1zdZ5JbSytTD4piFRBOUAI= diff --git a/internal/app/app.go b/internal/app/app.go index 5c8f1cd..480118f 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -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), diff --git a/internal/app/resolvespec_models_generated.go b/internal/app/resolvespec_models_generated.go index 0e06fcf..a377ffc 100644 --- a/internal/app/resolvespec_models_generated.go +++ b/internal/app/resolvespec_models_generated.go @@ -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{}}, diff --git a/internal/generatedmodels/sql_public_agent_guardrails.go b/internal/generatedmodels/sql_public_agent_guardrails.go index 080b84d..0b308aa 100644 --- a/internal/generatedmodels/sql_public_agent_guardrails.go +++ b/internal/generatedmodels/sql_public_agent_guardrails.go @@ -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 diff --git a/internal/generatedmodels/sql_public_agent_parts.go b/internal/generatedmodels/sql_public_agent_parts.go index 2b29fd5..04be757 100644 --- a/internal/generatedmodels/sql_public_agent_parts.go +++ b/internal/generatedmodels/sql_public_agent_parts.go @@ -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 diff --git a/internal/generatedmodels/sql_public_agent_persona_guardrails.go b/internal/generatedmodels/sql_public_agent_persona_guardrails.go index 314ff12..a5bfc0c 100644 --- a/internal/generatedmodels/sql_public_agent_persona_guardrails.go +++ b/internal/generatedmodels/sql_public_agent_persona_guardrails.go @@ -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 diff --git a/internal/generatedmodels/sql_public_agent_persona_parts.go b/internal/generatedmodels/sql_public_agent_persona_parts.go index dae158f..4e803c1 100644 --- a/internal/generatedmodels/sql_public_agent_persona_parts.go +++ b/internal/generatedmodels/sql_public_agent_persona_parts.go @@ -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 diff --git a/internal/generatedmodels/sql_public_agent_persona_skills.go b/internal/generatedmodels/sql_public_agent_persona_skills.go index d9d7a49..c8a7dfc 100644 --- a/internal/generatedmodels/sql_public_agent_persona_skills.go +++ b/internal/generatedmodels/sql_public_agent_persona_skills.go @@ -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 diff --git a/internal/generatedmodels/sql_public_agent_persona_traits.go b/internal/generatedmodels/sql_public_agent_persona_traits.go index 209eb5a..d6c0ad9 100644 --- a/internal/generatedmodels/sql_public_agent_persona_traits.go +++ b/internal/generatedmodels/sql_public_agent_persona_traits.go @@ -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 diff --git a/internal/generatedmodels/sql_public_agent_personas.go b/internal/generatedmodels/sql_public_agent_personas.go index c319161..2251ab6 100644 --- a/internal/generatedmodels/sql_public_agent_personas.go +++ b/internal/generatedmodels/sql_public_agent_personas.go @@ -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 diff --git a/internal/generatedmodels/sql_public_agent_skills.go b/internal/generatedmodels/sql_public_agent_skills.go index 9e837aa..4913cf0 100644 --- a/internal/generatedmodels/sql_public_agent_skills.go +++ b/internal/generatedmodels/sql_public_agent_skills.go @@ -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 diff --git a/internal/generatedmodels/sql_public_agent_traits.go b/internal/generatedmodels/sql_public_agent_traits.go index 0d7e3a9..867b613 100644 --- a/internal/generatedmodels/sql_public_agent_traits.go +++ b/internal/generatedmodels/sql_public_agent_traits.go @@ -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 diff --git a/internal/generatedmodels/sql_public_arc_stage_parts.go b/internal/generatedmodels/sql_public_arc_stage_parts.go index 2f488c6..dfb5cc7 100644 --- a/internal/generatedmodels/sql_public_arc_stage_parts.go +++ b/internal/generatedmodels/sql_public_arc_stage_parts.go @@ -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 diff --git a/internal/generatedmodels/sql_public_arc_stages.go b/internal/generatedmodels/sql_public_arc_stages.go index 30c58fc..e421dc6 100644 --- a/internal/generatedmodels/sql_public_arc_stages.go +++ b/internal/generatedmodels/sql_public_arc_stages.go @@ -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 diff --git a/internal/generatedmodels/sql_public_character_arcs.go b/internal/generatedmodels/sql_public_character_arcs.go index c7d194d..d2c1cab 100644 --- a/internal/generatedmodels/sql_public_character_arcs.go +++ b/internal/generatedmodels/sql_public_character_arcs.go @@ -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 diff --git a/internal/generatedmodels/sql_public_chat_histories.go b/internal/generatedmodels/sql_public_chat_histories.go index f060bb8..a1e1315 100644 --- a/internal/generatedmodels/sql_public_chat_histories.go +++ b/internal/generatedmodels/sql_public_chat_histories.go @@ -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 diff --git a/internal/generatedmodels/sql_public_embeddings.go b/internal/generatedmodels/sql_public_embeddings.go index 92d34f5..a410b1a 100644 --- a/internal/generatedmodels/sql_public_embeddings.go +++ b/internal/generatedmodels/sql_public_embeddings.go @@ -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 diff --git a/internal/generatedmodels/sql_public_learnings.go b/internal/generatedmodels/sql_public_learnings.go index 83647ae..4f7dab7 100644 --- a/internal/generatedmodels/sql_public_learnings.go +++ b/internal/generatedmodels/sql_public_learnings.go @@ -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 diff --git a/internal/generatedmodels/sql_public_oauth_clients.go b/internal/generatedmodels/sql_public_oauth_clients.go index 44d7bdf..877e2d7 100644 --- a/internal/generatedmodels/sql_public_oauth_clients.go +++ b/internal/generatedmodels/sql_public_oauth_clients.go @@ -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 diff --git a/internal/generatedmodels/sql_public_persona_arc.go b/internal/generatedmodels/sql_public_persona_arc.go index d0d2343..179ab27 100644 --- a/internal/generatedmodels/sql_public_persona_arc.go +++ b/internal/generatedmodels/sql_public_persona_arc.go @@ -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 diff --git a/internal/generatedmodels/sql_public_plan_dependencies.go b/internal/generatedmodels/sql_public_plan_dependencies.go index 3b23540..2d82802 100644 --- a/internal/generatedmodels/sql_public_plan_dependencies.go +++ b/internal/generatedmodels/sql_public_plan_dependencies.go @@ -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 diff --git a/internal/generatedmodels/sql_public_plan_guardrails.go b/internal/generatedmodels/sql_public_plan_guardrails.go index 680d930..4a1bfc1 100644 --- a/internal/generatedmodels/sql_public_plan_guardrails.go +++ b/internal/generatedmodels/sql_public_plan_guardrails.go @@ -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 diff --git a/internal/generatedmodels/sql_public_plan_related_plans.go b/internal/generatedmodels/sql_public_plan_related_plans.go index 101a686..ea733fe 100644 --- a/internal/generatedmodels/sql_public_plan_related_plans.go +++ b/internal/generatedmodels/sql_public_plan_related_plans.go @@ -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 diff --git a/internal/generatedmodels/sql_public_plan_skills.go b/internal/generatedmodels/sql_public_plan_skills.go index 62fee56..b6f1a28 100644 --- a/internal/generatedmodels/sql_public_plan_skills.go +++ b/internal/generatedmodels/sql_public_plan_skills.go @@ -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 diff --git a/internal/generatedmodels/sql_public_plans.go b/internal/generatedmodels/sql_public_plans.go index 0fabb91..2f3c12c 100644 --- a/internal/generatedmodels/sql_public_plans.go +++ b/internal/generatedmodels/sql_public_plans.go @@ -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 diff --git a/internal/generatedmodels/sql_public_project_guardrails.go b/internal/generatedmodels/sql_public_project_guardrails.go index 9db3e31..9f8493e 100644 --- a/internal/generatedmodels/sql_public_project_guardrails.go +++ b/internal/generatedmodels/sql_public_project_guardrails.go @@ -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 diff --git a/internal/generatedmodels/sql_public_project_personas.go b/internal/generatedmodels/sql_public_project_personas.go new file mode 100644 index 0000000..2051990 --- /dev/null +++ b/internal/generatedmodels/sql_public_project_personas.go @@ -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" +} diff --git a/internal/generatedmodels/sql_public_project_skills.go b/internal/generatedmodels/sql_public_project_skills.go index 9eabbbf..c0f1592 100644 --- a/internal/generatedmodels/sql_public_project_skills.go +++ b/internal/generatedmodels/sql_public_project_skills.go @@ -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 diff --git a/internal/generatedmodels/sql_public_projects.go b/internal/generatedmodels/sql_public_projects.go index 0585663..94a8375 100644 --- a/internal/generatedmodels/sql_public_projects.go +++ b/internal/generatedmodels/sql_public_projects.go @@ -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 diff --git a/internal/generatedmodels/sql_public_stored_files.go b/internal/generatedmodels/sql_public_stored_files.go index e6a1422..54d7932 100644 --- a/internal/generatedmodels/sql_public_stored_files.go +++ b/internal/generatedmodels/sql_public_stored_files.go @@ -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 diff --git a/internal/generatedmodels/sql_public_thought_links.go b/internal/generatedmodels/sql_public_thought_links.go index a945fc1..c021da8 100644 --- a/internal/generatedmodels/sql_public_thought_links.go +++ b/internal/generatedmodels/sql_public_thought_links.go @@ -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 diff --git a/internal/generatedmodels/sql_public_thoughts.go b/internal/generatedmodels/sql_public_thoughts.go index 3d81f7c..f5c2936 100644 --- a/internal/generatedmodels/sql_public_thoughts.go +++ b/internal/generatedmodels/sql_public_thoughts.go @@ -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 diff --git a/internal/generatedmodels/sql_public_tool_annotations.go b/internal/generatedmodels/sql_public_tool_annotations.go index 0c386ce..e938381 100644 --- a/internal/generatedmodels/sql_public_tool_annotations.go +++ b/internal/generatedmodels/sql_public_tool_annotations.go @@ -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 diff --git a/internal/mcpserver/server.go b/internal/mcpserver/server.go index c4d12ed..ad62aa4 100644 --- a/internal/mcpserver/server.go +++ b/internal/mcpserver/server.go @@ -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 diff --git a/internal/mcpserver/server_test.go b/internal/mcpserver/server_test.go index 7c8ebba..1a6c00d 100644 --- a/internal/mcpserver/server_test.go +++ b/internal/mcpserver/server_test.go @@ -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() diff --git a/internal/mcpserver/streamable_integration_test.go b/internal/mcpserver/streamable_integration_test.go index af5b9f6..e616ecf 100644 --- a/internal/mcpserver/streamable_integration_test.go +++ b/internal/mcpserver/streamable_integration_test.go @@ -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), } } diff --git a/internal/store/agent_personas.go b/internal/store/agent_personas.go index 6b31d1e..d37de20 100644 --- a/internal/store/agent_personas.go +++ b/internal/store/agent_personas.go @@ -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 { diff --git a/internal/store/project_personas.go b/internal/store/project_personas.go new file mode 100644 index 0000000..cdb1553 --- /dev/null +++ b/internal/store/project_personas.go @@ -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 +} diff --git a/internal/store/skills.go b/internal/store/skills.go index 4919fb1..0986412 100644 --- a/internal/store/skills.go +++ b/internal/store/skills.go @@ -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() diff --git a/internal/tools/agent_personas.go b/internal/tools/agent_personas.go index 8f6d9b6..a6143f3 100644 --- a/internal/tools/agent_personas.go +++ b/internal/tools/agent_personas.go @@ -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 diff --git a/internal/tools/project_personas.go b/internal/tools/project_personas.go new file mode 100644 index 0000000..0fbca3e --- /dev/null +++ b/internal/tools/project_personas.go @@ -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 +} diff --git a/internal/tools/skills.go b/internal/tools/skills.go index 0ece92d..f51bcc2 100644 --- a/internal/tools/skills.go +++ b/internal/tools/skills.go @@ -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 diff --git a/internal/tools/world_model.go b/internal/tools/world_model.go new file mode 100644 index 0000000..a502eee --- /dev/null +++ b/internal/tools/world_model.go @@ -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 +} diff --git a/internal/tools/world_model_test.go b/internal/tools/world_model_test.go new file mode 100644 index 0000000..1a1126f --- /dev/null +++ b/internal/tools/world_model_test.go @@ -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]) + } +} diff --git a/internal/types/agent_persona.go b/internal/types/agent_persona.go index f06e4df..1cc5590 100644 --- a/internal/types/agent_persona.go +++ b/internal/types/agent_persona.go @@ -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 { diff --git a/internal/types/extensions.go b/internal/types/extensions.go index 2c95b52..a777936 100644 --- a/internal/types/extensions.go +++ b/internal/types/extensions.go @@ -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 { diff --git a/llm/instructions.go b/llm/instructions.go index f8717c4..1134689 100644 --- a/llm/instructions.go +++ b/llm/instructions.go @@ -5,4 +5,7 @@ import _ "embed" var ( //go:embed memory.md MemoryInstructions []byte + + //go:embed world_model_intro.md + WorldModelIntro []byte ) diff --git a/llm/memory.md b/llm/memory.md index 14dfae3..aadaaef 100644 --- a/llm/memory.md +++ b/llm/memory.md @@ -28,7 +28,20 @@ Use AMCS as memory with two scopes: At the very start of any session with AMCS: -1. Call `describe_tools` to get the full list of available tools with their categories and any notes you have previously annotated. Read the notes before using a tool — they contain accumulated gotchas, workflow patterns, and field-ordering requirements you have recorded from prior sessions. +1. Read the `amcs://world-model/intro` resource. +2. Identify the current project and call `bootstrap_world_model` with its explicit name or ID and the smallest useful `context_limit`. +3. Apply the returned skills and guardrails, but keep persona manifests and memory summaries as indexes. Load detailed content only when the current task requires it. +4. Call `describe_tools` only when tool discovery or saved usage notes are needed. Prefer a category filter instead of loading the full catalog. + +## Token Budget and Progressive Loading + +- Minimize startup and working context. Do not load data merely because it is available. +- Start with names, summaries, manifests, and metadata. Fetch full skill, guardrail, persona, trait, part, thought, plan, learning, chat, or file content only when it is relevant to the current task. +- Use the smallest practical `limit` or `context_limit`, narrow queries, and project/category filters. Increase them only when the initial result is insufficient. +- Do not preload every persona, skill, guardrail, plan, file, or historical memory. Follow references on demand. +- Avoid repeating unchanged world-model content in the conversation. Keep a concise working summary and refresh only when the project or task changes, or when stale context is suspected. +- Prefer `get_persona_manifest` and compiled summaries before `get_agent_persona(detail=true)`. Prefer `list_skills` metadata before `get_skill`, and file metadata before `load_file`. +- Guardrails and directly applicable skills are mandatory even under a tight token budget; token minimization must never omit an applicable constraint. ## Project Session Startup @@ -51,7 +64,7 @@ Do not abandon the project scope or retry without a project. The project simply ## Project Memory Rules - Use project memory for code decisions, architecture, TODOs, debugging findings, and context specific to the current repo or workstream. -- Before substantial work, always retrieve context with `get_project_context` or `recall_context` so prior decisions inform your approach. +- Before substantial work, retrieve focused context with `get_project_context` or `recall_context` only when prior decisions may affect the task. Use a narrow query and small limit first. - Save durable project facts with `capture_thought` after completing meaningful work. - Use structured learnings for curated, reusable lessons that should remain distinct from raw thought capture. - Use `save_file` or `upload_file` for project assets the memory should retain, such as screenshots, PDFs, audio notes, and other documents. @@ -135,4 +148,4 @@ Notes are returned by `describe_tools` in future sessions. Annotate whenever you ## Short Operational Form -At the start of every session, call `describe_tools` to read the full tool list and any accumulated usage notes. Use AMCS memory in project scope when the current work matches a known project; if no clear project matches, global notebook memory is allowed for non-project-specific information. At the start of every project session call `list_project_skills` and `list_project_guardrails` and apply what is returned; only create new skills or guardrails if none exist. If your MCP client does not preserve sessions across calls, pass `project` explicitly instead of relying on `set_active_project`. Store raw/durable notes with `capture_thought`, store curated durable lessons with `add_learning`, and track structured multi-step goals with `create_plan`. Use `get_plan` to load a plan's full context including dependencies, related plans, and linked skills/guardrails. Stamp `last_reviewed_at` on plans you review with `update_plan mark_reviewed: true`. For binary files or files larger than 10 MB, call `upload_file` with `content_path` to stage the file and get an `amcs://files/{id}` URI, then pass that URI to `save_file` as `content_uri` to link it to a thought. For small files, use `save_file` or `upload_file` with `content_base64` directly. Browse stored files with `list_files`, and load them with `load_file` only when their contents are needed. Stored files can also be read as raw binary via MCP resources at `amcs://files/{id}`. Never store project-specific memory globally when a matching project exists, and never store memory in the wrong project. If project matching is ambiguous, ask the user. If a tool returns `project_not_found`, call `create_project` with that name and retry — never drop the project scope. Whenever you discover a non-obvious tool behaviour, gotcha, or workflow pattern, record it with `annotate_tool` so future sessions benefit. +At session start, identify the project and call `bootstrap_world_model` with a small `context_limit`. Apply mandatory skills and guardrails, but otherwise use summaries and manifests as indexes and load details only when needed. Call `describe_tools` only for discovery or saved notes, preferably with a category filter. Use project scope when the work matches a known project; pass `project` explicitly for stateless clients. Store durable notes with `capture_thought`, curated lessons with `add_learning`, and multi-step work with `create_plan`. Retrieve plans, memories, personas, and files on demand using narrow queries and the smallest useful limits. Never store memory in the wrong scope, silently choose an ambiguous project, or omit an applicable guardrail to save tokens. Record non-obvious tool behavior with `annotate_tool`. diff --git a/llm/todo.md b/llm/todo.md index 0d2f624..e35649e 100644 --- a/llm/todo.md +++ b/llm/todo.md @@ -1,5 +1,104 @@ # AMCS TODO +## Memory Consolidation and Retrieval Quality + +No prior consolidation implementation notes were found in the project. This roadmap starts clean and follows AMCS's current Go, PostgreSQL/pgvector, thoughts, learnings, projects, and model-specific embeddings architecture. + +### Current-shape decisions + +- Use bigint thought IDs for foreign keys and queue references, matching the current schema. Keep thought GUIDs as external identifiers rather than introducing UUID foreign keys without a specific interoperability need. +- Embeddings are stored in the separate, model-specific `embeddings` table. Similarity queries and indexes must include the embedding model and project scope where applicable. +- Verify the existing pgvector index strategy before adding another HNSW index. Add or change an index only when the current generated schema and query plan show it is needed. +- All consolidation writes are soft, auditable, idempotent, and reversible. + +### Phase 0 - Schema additions + +Add to `thoughts`: + +- `confidence smallint` +- `source text` +- `valid_until timestamptz` +- `superseded_by bigint` referencing `thoughts.id` +- `salience real` +- `outcome text` constrained to `confirmed`, `refuted`, or null + +Add tables: + +- `consolidation_queue(id, kind, thought_a, thought_b, status, created_at)` +- `consolidation_audit(id, action, before jsonb, after jsonb, actor, ts)` + +The audit record must contain enough information to reverse every consolidation mutation. Define queue uniqueness and status transitions so retries cannot enqueue or process the same pair twice. + +Confidence must be available through the existing admin thought resource. Return both the normalized numeric value and enough provenance to explain whether it was user-supplied, heuristic, outcome-derived, or consolidation-derived. Existing thoughts need a documented neutral default or backfill policy rather than silently appearing maximally confident. + +### Phase 1 - Algorithmic candidate generation + +Run nightly SQL candidate generation without an LLM: + +- Near duplicates: self-join model-compatible embeddings and enqueue canonical thought pairs where `1 - (a.embedding <=> b.embedding) > 0.92`. +- Contradiction suspects: combine high cosine similarity with low `pg_trgm` `similarity()` and enqueue as `kind = 'contradiction'`. +- Decay: demote thoughts past `valid_until`; apply recency weighting during retrieval rather than rewriting content. + +Candidate queries must de-duplicate ordered pairs, exclude already superseded thoughts, respect project boundaries, and avoid comparing embeddings from different models. + +### Phase 2 - Retrieval ranking (ship first) + +Change `recall_context` from cosine-only top-k ranking to a bounded composite score based on: + +```text +cosine_similarity * exp(-lambda * age_days) * confidence_weight * outcome_weight +``` + +Exclude superseded and low-salience thoughts from default recall, with explicit options for diagnostic retrieval. Tune defaults conservatively and preserve the current full-text fallback when embeddings are unavailable. + +This phase should ship first because it provides the largest recall-quality improvement without requiring consolidation writes or an adjudication model. + +### Phase 3 - LLM adjudication worker + +Add a resumable Go worker that claims queued pairs and calls a low-cost configured provider only for flagged candidates. Support two structured prompts: + +- Merge: produce one normalized schema entry from genuine duplicates. +- Resolve conflict: verify a real contradiction and select or synthesize the supported result. + +Writes remain soft: set `superseded_by`, never delete source thoughts, and append every mutation to `consolidation_audit`. Queue claiming, provider calls, and writes must be idempotent and safe across restarts. + +### Phase 4 - Salience gate + +Compute initial salience during `capture_thought` using explainable heuristics such as length, code presence, decision verbs, and thought type. Store low-salience thoughts but exclude them from default recall. Consider a logistic model only after measured retrieval outcomes show the heuristics have plateaued. + +### Phase 5 - Outcome loop + +Add a tool or update flag that marks a thought as `confirmed` or `refuted` when its decision plays out. Preserve the change in the consolidation audit and feed the outcome into retrieval ranking as the real-world error signal. + +### Phase 6 - Admin confidence and SQL job observability + +Extend the Svelte admin UI and its ResolveSpec-compatible backend resources with two read-oriented views. + +#### Thought confidence + +- Show confidence in thought list and detail views without requiring the operator to open raw metadata. +- Display the numeric value, a human-readable band, provenance, outcome, salience, validity window, and superseded state. +- Allow sorting and filtering by confidence, outcome, validity, and superseded state. +- Visually distinguish unknown/unscored confidence from low confidence; never coerce null into zero or one. +- Explain ranking inputs in the detail view so operators can understand why a thought was included or excluded from default recall. +- Keep confidence mutation behind the normal thought update/audit path rather than allowing unaudited inline table edits. + +#### Consolidation SQL jobs + +- Show configured `pg_cron` consolidation jobs and their schedule, enabled state, last start/end time, duration, status, and sanitized result/error message. +- Show currently running consolidation jobs separately from recent completed/failed runs using `cron.job` and `cron.job_run_details` through a permission-limited backend projection. +- Include queue counts by status and consolidation kind so a successful cron invocation cannot hide a stalled or growing queue. +- Add filters for job, status, and time range, plus clear empty/unavailable states when `pg_cron` is not installed or its views are inaccessible. +- Do not expose raw SQL command text, connection strings, credentials, or unrestricted `pg_stat_activity` data. +- Keep the first version read-only. Manual run, retry, pause, or cancel controls require separate authorization, audit logging, and explicit confirmation. +- Poll conservatively only while the jobs screen is visible; stop polling when hidden and avoid loading run history during normal admin startup. + +Backend tests must cover `pg_cron` unavailable/permission-denied behavior, sanitized errors, running-versus-finished classification, pagination, and project-independent admin authorization. Frontend tests must cover confidence null handling, failed/running job states, queue backlog warnings, and polling cleanup. + +### Delivery principle + +SQL handles high-volume candidate recall and ranking; the LLM handles low-volume precision decisions for a bounded number of queued pairs. Implement Phase 2 first, then Phase 0/1 foundations needed for consolidation, followed by the worker, salience gate, outcome feedback loop, and admin observability. Confidence fields should appear in the admin UI as soon as Phase 0 lands; SQL job observability should land with the first scheduled Phase 1 jobs. + ## Future Plugin: Lifestyle Tools (calendar, meals, household, CRM) The following tool groups have been removed from the core server and are candidates for a separate optional plugin or extension server. The store/tool implementations remain in the codebase but are no longer registered. diff --git a/llm/world_model_intro.md b/llm/world_model_intro.md new file mode 100644 index 0000000..087c16f --- /dev/null +++ b/llm/world_model_intro.md @@ -0,0 +1,15 @@ +# AMCS World Model + +AMCS provides a project-scoped world model containing memory, skills, guardrails, personas, and recent context. + +At the start of an agent or chat session: + +1. Identify the current project from explicit user input or the workspace. +2. Call `list_projects` when the project has not already been confirmed. +3. Call `bootstrap_world_model` with the explicit project name or ID. +4. Apply every returned guardrail and effective skill before continuing. +5. Use the default persona when present. Other linked persona manifests describe personas available on demand. + +Never silently choose or create a project. If no project matches, ask the user or create one only with explicit approval. Stateless clients must continue passing the project explicitly on project-scoped calls. + +Skills are additive by default. A linked skill with `override: true` replaces a lower-precedence skill with the same stable name. Guardrails are additive and later layers cannot weaken them. diff --git a/migrations/020_generated_schema.sql b/migrations/020_generated_schema.sql index da0ca0a..f922f2e 100644 --- a/migrations/020_generated_schema.sql +++ b/migrations/020_generated_schema.sql @@ -33,6 +33,13 @@ CREATE SEQUENCE IF NOT EXISTS public.identity_agent_persona_skills_id START 1 CACHE 1; +CREATE SEQUENCE IF NOT EXISTS public.identity_project_personas_id + INCREMENT 1 + MINVALUE 1 + MAXVALUE 9223372036854775807 + START 1 + CACHE 1; + CREATE SEQUENCE IF NOT EXISTS public.identity_agent_persona_guardrails_id INCREMENT 1 MINVALUE 1 @@ -75,7 +82,7 @@ CREATE SEQUENCE IF NOT EXISTS public.identity_arc_stage_parts_id START 1 CACHE 1; -CREATE SEQUENCE IF NOT EXISTS public.identity_persona_arc_id +CREATE SEQUENCE IF NOT EXISTS public.identity_persona_arc_persona_id INCREMENT 1 MINVALUE 1 MAXVALUE 9223372036854775807 @@ -247,10 +254,19 @@ CREATE TABLE IF NOT EXISTS public.agent_persona_parts ( CREATE TABLE IF NOT EXISTS public.agent_persona_skills ( id bigserial NOT NULL, + override boolean NOT NULL DEFAULT false, persona_id bigint NOT NULL, skill_id bigint NOT NULL ); +CREATE TABLE IF NOT EXISTS public.project_personas ( + created_at timestamptz NOT NULL DEFAULT now(), + id bigserial NOT NULL, + is_default boolean NOT NULL DEFAULT false, + persona_id bigint NOT NULL, + project_id bigint NOT NULL +); + CREATE TABLE IF NOT EXISTS public.agent_persona_guardrails ( guardrail_id bigint NOT NULL, id bigserial NOT NULL, @@ -496,6 +512,7 @@ CREATE TABLE IF NOT EXISTS public.agent_guardrails ( CREATE TABLE IF NOT EXISTS public.project_skills ( created_at timestamptz NOT NULL DEFAULT now(), id bigserial NOT NULL, + override boolean NOT NULL DEFAULT false, project_id bigint NOT NULL, skill_id bigint NOT NULL ); @@ -872,6 +889,19 @@ BEGIN END; $$; +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'agent_persona_skills' + AND column_name = 'override' + ) THEN + ALTER TABLE public.agent_persona_skills ADD COLUMN override boolean NOT NULL DEFAULT false; + END IF; +END; +$$; + DO $$ BEGIN IF NOT EXISTS ( @@ -898,6 +928,71 @@ BEGIN END; $$; +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'project_personas' + AND column_name = 'created_at' + ) THEN + ALTER TABLE public.project_personas ADD COLUMN created_at timestamptz NOT NULL DEFAULT now(); + END IF; +END; +$$; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'project_personas' + AND column_name = 'id' + ) THEN + ALTER TABLE public.project_personas ADD COLUMN id bigserial NOT NULL; + END IF; +END; +$$; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'project_personas' + AND column_name = 'is_default' + ) THEN + ALTER TABLE public.project_personas ADD COLUMN is_default boolean NOT NULL DEFAULT false; + END IF; +END; +$$; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'project_personas' + AND column_name = 'persona_id' + ) THEN + ALTER TABLE public.project_personas ADD COLUMN persona_id bigint NOT NULL; + END IF; +END; +$$; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'project_personas' + AND column_name = 'project_id' + ) THEN + ALTER TABLE public.project_personas ADD COLUMN project_id bigint NOT NULL; + END IF; +END; +$$; + DO $$ BEGIN IF NOT EXISTS ( @@ -3173,6 +3268,19 @@ BEGIN END; $$; +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'project_skills' + AND column_name = 'override' + ) THEN + ALTER TABLE public.project_skills ADD COLUMN override boolean NOT NULL DEFAULT false; + END IF; +END; +$$; + DO $$ BEGIN IF NOT EXISTS ( @@ -3896,6 +4004,29 @@ BEGIN END; $$; +DO $$ +DECLARE + current_type text; +BEGIN + SELECT pg_catalog.format_type(a.atttypid, a.atttypmod) + INTO current_type + FROM pg_attribute a + JOIN pg_class t ON t.oid = a.attrelid + JOIN pg_namespace n ON n.oid = t.relnamespace + WHERE n.nspname = 'public' + AND t.relname = 'agent_persona_skills' + AND a.attname = 'override' + AND a.attnum > 0 + AND NOT a.attisdropped; + + IF current_type IS NOT NULL + AND current_type <> ALL(ARRAY['boolean']) THEN + ALTER TABLE public.agent_persona_skills + ALTER COLUMN override TYPE boolean USING override::boolean; + END IF; +END; +$$; + DO $$ DECLARE current_type text; @@ -3942,6 +4073,121 @@ BEGIN END; $$; +DO $$ +DECLARE + current_type text; +BEGIN + SELECT pg_catalog.format_type(a.atttypid, a.atttypmod) + INTO current_type + FROM pg_attribute a + JOIN pg_class t ON t.oid = a.attrelid + JOIN pg_namespace n ON n.oid = t.relnamespace + WHERE n.nspname = 'public' + AND t.relname = 'project_personas' + AND a.attname = 'created_at' + AND a.attnum > 0 + AND NOT a.attisdropped; + + IF current_type IS NOT NULL + AND current_type <> ALL(ARRAY['timestamptz', 'timestamp with time zone']) THEN + ALTER TABLE public.project_personas + ALTER COLUMN created_at TYPE timestamptz USING created_at::timestamptz; + END IF; +END; +$$; + +DO $$ +DECLARE + current_type text; +BEGIN + SELECT pg_catalog.format_type(a.atttypid, a.atttypmod) + INTO current_type + FROM pg_attribute a + JOIN pg_class t ON t.oid = a.attrelid + JOIN pg_namespace n ON n.oid = t.relnamespace + WHERE n.nspname = 'public' + AND t.relname = 'project_personas' + AND a.attname = 'id' + AND a.attnum > 0 + AND NOT a.attisdropped; + + IF current_type IS NOT NULL + AND current_type <> ALL(ARRAY['bigint']) THEN + ALTER TABLE public.project_personas + ALTER COLUMN id TYPE bigint USING id::bigint; + END IF; +END; +$$; + +DO $$ +DECLARE + current_type text; +BEGIN + SELECT pg_catalog.format_type(a.atttypid, a.atttypmod) + INTO current_type + FROM pg_attribute a + JOIN pg_class t ON t.oid = a.attrelid + JOIN pg_namespace n ON n.oid = t.relnamespace + WHERE n.nspname = 'public' + AND t.relname = 'project_personas' + AND a.attname = 'is_default' + AND a.attnum > 0 + AND NOT a.attisdropped; + + IF current_type IS NOT NULL + AND current_type <> ALL(ARRAY['boolean']) THEN + ALTER TABLE public.project_personas + ALTER COLUMN is_default TYPE boolean USING is_default::boolean; + END IF; +END; +$$; + +DO $$ +DECLARE + current_type text; +BEGIN + SELECT pg_catalog.format_type(a.atttypid, a.atttypmod) + INTO current_type + FROM pg_attribute a + JOIN pg_class t ON t.oid = a.attrelid + JOIN pg_namespace n ON n.oid = t.relnamespace + WHERE n.nspname = 'public' + AND t.relname = 'project_personas' + AND a.attname = 'persona_id' + AND a.attnum > 0 + AND NOT a.attisdropped; + + IF current_type IS NOT NULL + AND current_type <> ALL(ARRAY['bigint']) THEN + ALTER TABLE public.project_personas + ALTER COLUMN persona_id TYPE bigint USING persona_id::bigint; + END IF; +END; +$$; + +DO $$ +DECLARE + current_type text; +BEGIN + SELECT pg_catalog.format_type(a.atttypid, a.atttypmod) + INTO current_type + FROM pg_attribute a + JOIN pg_class t ON t.oid = a.attrelid + JOIN pg_namespace n ON n.oid = t.relnamespace + WHERE n.nspname = 'public' + AND t.relname = 'project_personas' + AND a.attname = 'project_id' + AND a.attnum > 0 + AND NOT a.attisdropped; + + IF current_type IS NOT NULL + AND current_type <> ALL(ARRAY['bigint']) THEN + ALTER TABLE public.project_personas + ALTER COLUMN project_id TYPE bigint USING project_id::bigint; + END IF; +END; +$$; + DO $$ DECLARE current_type text; @@ -7967,6 +8213,29 @@ BEGIN END; $$; +DO $$ +DECLARE + current_type text; +BEGIN + SELECT pg_catalog.format_type(a.atttypid, a.atttypmod) + INTO current_type + FROM pg_attribute a + JOIN pg_class t ON t.oid = a.attrelid + JOIN pg_namespace n ON n.oid = t.relnamespace + WHERE n.nspname = 'public' + AND t.relname = 'project_skills' + AND a.attname = 'override' + AND a.attnum > 0 + AND NOT a.attisdropped; + + IF current_type IS NOT NULL + AND current_type <> ALL(ARRAY['boolean']) THEN + ALTER TABLE public.project_skills + ALTER COLUMN override TYPE boolean USING override::boolean; + END IF; +END; +$$; + DO $$ DECLARE current_type text; @@ -8282,6 +8551,50 @@ BEGIN END; $$; +DO $$ +DECLARE + current_pk_name text; + current_pk_matches boolean := false; +BEGIN + SELECT tc.constraint_name, + COALESCE( + ARRAY( + SELECT a.attname::text + FROM pg_constraint c + JOIN pg_class t ON t.oid = c.conrelid + JOIN pg_namespace n ON n.oid = t.relnamespace + JOIN unnest(c.conkey) WITH ORDINALITY AS cols(attnum, ord) + ON TRUE + JOIN pg_attribute a + ON a.attrelid = t.oid + AND a.attnum = cols.attnum + WHERE c.contype = 'p' + AND n.nspname = 'public' + AND t.relname = 'project_personas' + ORDER BY cols.ord + ), + ARRAY[]::text[] + ) = ARRAY['id'] + INTO current_pk_name, current_pk_matches + FROM information_schema.table_constraints tc + WHERE tc.table_schema = 'public' + AND tc.table_name = 'project_personas' + AND tc.constraint_type = 'PRIMARY KEY'; + + IF current_pk_name IS NOT NULL + AND NOT current_pk_matches + AND current_pk_name IN ('project_personas_pkey', 'public_project_personas_pkey') THEN + EXECUTE 'ALTER TABLE public.project_personas DROP CONSTRAINT ' || quote_ident(current_pk_name) || ' CASCADE'; + END IF; + + -- Add the desired primary key only when no matching primary key already exists. + IF current_pk_name IS NULL + OR (NOT current_pk_matches AND current_pk_name IN ('project_personas_pkey', 'public_project_personas_pkey')) THEN + ALTER TABLE public.project_personas ADD CONSTRAINT pk_public_project_personas PRIMARY KEY (id); + END IF; +END; +$$; + DO $$ DECLARE current_pk_name text; @@ -9389,6 +9702,9 @@ CREATE INDEX IF NOT EXISTS idx_agent_persona_parts_persona_id_part_id CREATE INDEX IF NOT EXISTS idx_agent_persona_skills_persona_id_skill_id ON public.agent_persona_skills USING btree (persona_id, skill_id); +CREATE INDEX IF NOT EXISTS idx_project_personas_project_id_persona_id + ON public.project_personas USING btree (project_id, persona_id); + CREATE INDEX IF NOT EXISTS idx_arc_stage_parts_stage_id_part_id ON public.arc_stage_parts USING btree (stage_id, part_id); @@ -9772,6 +10088,38 @@ BEGIN END IF; END; $$;DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE table_schema = 'public' + AND table_name = 'project_personas' + AND constraint_name = 'fk_project_personas_persona_id' + ) THEN + ALTER TABLE public.project_personas + ADD CONSTRAINT fk_project_personas_persona_id + FOREIGN KEY (persona_id) + REFERENCES public.agent_personas (id) + ON DELETE NO ACTION + ON UPDATE NO ACTION; + END IF; +END; +$$;DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE table_schema = 'public' + AND table_name = 'project_personas' + AND constraint_name = 'fk_project_personas_project_id' + ) THEN + ALTER TABLE public.project_personas + ADD CONSTRAINT fk_project_personas_project_id + FOREIGN KEY (project_id) + REFERENCES public.projects (id) + ON DELETE NO ACTION + ON UPDATE NO ACTION; + END IF; +END; +$$;DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.table_constraints @@ -10425,6 +10773,25 @@ BEGIN END; $$; DO $$ +DECLARE + m_cnt bigint; +BEGIN + IF EXISTS ( + SELECT 1 FROM pg_class c + INNER JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relname = 'identity_project_personas_id' + AND n.nspname = 'public' + AND c.relkind = 'S' + ) THEN + SELECT COALESCE(MAX(id), 0) + 1 + FROM public.project_personas + INTO m_cnt; + + PERFORM setval('public.identity_project_personas_id'::regclass, m_cnt); + END IF; +END; +$$; +DO $$ DECLARE m_cnt bigint; BEGIN @@ -10927,5 +11294,6 @@ $$; + diff --git a/schema/agent_personas.dbml b/schema/agent_personas.dbml index b6877ba..12f7d18 100644 --- a/schema/agent_personas.dbml +++ b/schema/agent_personas.dbml @@ -43,6 +43,7 @@ Table agent_persona_skills { id bigserial [pk] persona_id bigint [not null, ref: > agent_personas.id] skill_id bigint [not null, ref: > agent_skills.id] + override boolean [not null, default: false] indexes { (persona_id, skill_id) [uk] @@ -50,6 +51,19 @@ Table agent_persona_skills { } } +Table project_personas { + id bigserial [pk] + project_id bigint [not null, ref: > projects.id] + persona_id bigint [not null, ref: > agent_personas.id] + is_default boolean [not null, default: false] + created_at timestamptz [not null, default: `now()`] + + indexes { + (project_id, persona_id) [uk] + project_id + } +} + Table agent_persona_guardrails { id bigserial [pk] persona_id bigint [not null, ref: > agent_personas.id] @@ -124,6 +138,8 @@ Ref: agent_persona_parts.persona_id > agent_personas.id [delete: cascade] Ref: agent_persona_parts.part_id > agent_parts.id [delete: cascade] Ref: agent_persona_skills.persona_id > agent_personas.id [delete: cascade] Ref: agent_persona_skills.skill_id > agent_skills.id [delete: cascade] +Ref: project_personas.project_id > projects.id [delete: cascade] +Ref: project_personas.persona_id > agent_personas.id [delete: cascade] Ref: agent_persona_guardrails.persona_id > agent_personas.id [delete: cascade] Ref: agent_persona_guardrails.guardrail_id > agent_guardrails.id [delete: cascade] Ref: agent_persona_traits.persona_id > agent_personas.id [delete: cascade] diff --git a/schema/skills.dbml b/schema/skills.dbml index ebe0116..935b858 100644 --- a/schema/skills.dbml +++ b/schema/skills.dbml @@ -29,6 +29,7 @@ Table project_skills { id bigserial [pk] project_id bigint [not null, ref: > projects.id] skill_id bigint [not null, ref: > agent_skills.id] + override boolean [not null, default: false] created_at timestamptz [not null, default: `now()`] indexes { diff --git a/scripts/generate-models.sh b/scripts/generate-models.sh index e115e70..53f8f21 100755 --- a/scripts/generate-models.sh +++ b/scripts/generate-models.sh @@ -44,13 +44,15 @@ mkdir -p "${out_dir}" --from-list "${schema_list}" \ --to bun \ --to-path "${out_dir}" \ - --package generatedmodels + --package generatedmodels \ + --types resolvespec "${relspec_bin}" templ \ --from dbml \ --from-list "${schema_list}" \ --template "${resolve_spec_template}" \ --output "${resolve_spec_models_file}" + # relspec currently emits a few files with unused fmt imports; strip only when fmt is unused. for file in "${out_dir}"/*.go; do diff --git a/ui/src/App.svelte b/ui/src/App.svelte index 06a82b5..d24d72b 100644 --- a/ui/src/App.svelte +++ b/ui/src/App.svelte @@ -143,7 +143,8 @@ unique_tools: raw?.metrics?.unique_tools ?? 0, top_ips: Array.isArray(raw?.metrics?.top_ips) ? raw.metrics.top_ips : [], top_agents: Array.isArray(raw?.metrics?.top_agents) ? raw.metrics.top_agents : [], - top_tools: Array.isArray(raw?.metrics?.top_tools) ? raw.metrics.top_tools : [] + top_tools: Array.isArray(raw?.metrics?.top_tools) ? raw.metrics.top_tools : [], + recent_log: Array.isArray(raw?.metrics?.recent_log) ? raw.metrics.recent_log : [] } }; } catch (err) { diff --git a/ui/src/components/auth/LoginInfoPanel.svelte b/ui/src/components/auth/LoginInfoPanel.svelte index 7cbca83..fd4efb8 100644 --- a/ui/src/components/auth/LoginInfoPanel.svelte +++ b/ui/src/components/auth/LoginInfoPanel.svelte @@ -98,12 +98,6 @@ activeCard = id; } - function handleCardKeydown(event: KeyboardEvent, id: string) { - if (event.key === "Enter" || event.key === " ") { - event.preventDefault(); - setActiveCard(id); - } - }
{#each intelligenceCards as card} -
setActiveCard(card.id)} - onkeydown={(event) => handleCardKeydown(event, card.id)} >

{card.title} @@ -233,7 +225,7 @@ : ""} {/each}

-
+ {/each}
diff --git a/vendor/github.com/bitechdev/ResolveSpec/pkg/common/cors.go b/vendor/github.com/bitechdev/ResolveSpec/pkg/common/cors.go index 11336a4..58b7e1b 100644 --- a/vendor/github.com/bitechdev/ResolveSpec/pkg/common/cors.go +++ b/vendor/github.com/bitechdev/ResolveSpec/pkg/common/cors.go @@ -115,32 +115,39 @@ func GetHeadSpecHeaders() []string { // SetCORSHeaders sets CORS headers on a response writer func SetCORSHeaders(w ResponseWriter, r Request, config CORSConfig) { - // Set allowed origins - // if len(config.AllowedOrigins) > 0 { - // w.SetHeader("Access-Control-Allow-Origin", strings.Join(config.AllowedOrigins, ", ")) - // } - - // Todo origin list parsing - w.SetHeader("Access-Control-Allow-Origin", "*") + // Reflect the request origin; fall back to wildcard only when no origin is present + origin := r.Header("Origin") + if origin == "" { + origin = "*" + } else { + // Vary must be set so caches don't serve one origin's response to another + httpW := w.UnderlyingResponseWriter() + httpW.Header().Set("Vary", "Origin") + } + w.SetHeader("Access-Control-Allow-Origin", origin) // Set allowed methods if len(config.AllowedMethods) > 0 { w.SetHeader("Access-Control-Allow-Methods", strings.Join(config.AllowedMethods, ", ")) } - // Set allowed headers - // if len(config.AllowedHeaders) > 0 { - // w.SetHeader("Access-Control-Allow-Headers", strings.Join(config.AllowedHeaders, ", ")) - // } - w.SetHeader("Access-Control-Allow-Headers", "*") + // Reflect the preflight request headers when present; otherwise use the explicit config list + requestedHeaders := r.Header("Access-Control-Request-Headers") + if requestedHeaders != "" { + w.SetHeader("Access-Control-Allow-Headers", requestedHeaders) + } else if len(config.AllowedHeaders) > 0 { + w.SetHeader("Access-Control-Allow-Headers", strings.Join(config.AllowedHeaders, ", ")) + } // Set max age if config.MaxAge > 0 { w.SetHeader("Access-Control-Max-Age", fmt.Sprintf("%d", config.MaxAge)) } - // Allow credentials - w.SetHeader("Access-Control-Allow-Credentials", "true") + // Allow credentials only when a specific origin is reflected (not wildcard) + if origin != "*" { + w.SetHeader("Access-Control-Allow-Credentials", "true") + } // Expose headers that clients can read exposeHeaders := config.AllowedHeaders diff --git a/vendor/github.com/bitechdev/ResolveSpec/pkg/config/config.go b/vendor/github.com/bitechdev/ResolveSpec/pkg/config/config.go index b6265bf..c1d5905 100644 --- a/vendor/github.com/bitechdev/ResolveSpec/pkg/config/config.go +++ b/vendor/github.com/bitechdev/ResolveSpec/pkg/config/config.go @@ -50,6 +50,10 @@ type ServerInstanceConfig struct { // GZIP enables GZIP compression middleware GZIP bool `mapstructure:"gzip"` + // HTTP2 enables HTTP/2 with the Extended CONNECT protocol (RFC 8441) for WebSocket support. + // Requires TLS; pair with SSLCert/SSLKey, SelfSignedSSL, or AutoTLS. + HTTP2 bool `mapstructure:"http2"` + // TLS/HTTPS configuration options (mutually exclusive) // Option 1: Provide certificate and key files directly SSLCert string `mapstructure:"ssl_cert"` diff --git a/vendor/github.com/uptrace/bun/dialect/mssqldialect/dialect.go b/vendor/github.com/uptrace/bun/dialect/mssqldialect/dialect.go index 6d01664..8818e55 100644 --- a/vendor/github.com/uptrace/bun/dialect/mssqldialect/dialect.go +++ b/vendor/github.com/uptrace/bun/dialect/mssqldialect/dialect.go @@ -50,6 +50,7 @@ func New(opts ...DialectOption) *Dialect { feature.Output | feature.OffsetFetch | feature.FKDefaultOnAction | + feature.Merge | feature.UpdateFromTable | feature.MSSavepoint diff --git a/vendor/github.com/uptrace/bun/dialect/mssqldialect/version.go b/vendor/github.com/uptrace/bun/dialect/mssqldialect/version.go index 04de5d5..6eff951 100644 --- a/vendor/github.com/uptrace/bun/dialect/mssqldialect/version.go +++ b/vendor/github.com/uptrace/bun/dialect/mssqldialect/version.go @@ -2,5 +2,5 @@ package mssqldialect // Version is the current release version. func Version() string { - return "1.2.16" + return "1.2.18" } diff --git a/vendor/github.com/uptrace/bun/dialect/pgdialect/dialect.go b/vendor/github.com/uptrace/bun/dialect/pgdialect/dialect.go index be212ee..14f3839 100644 --- a/vendor/github.com/uptrace/bun/dialect/pgdialect/dialect.go +++ b/vendor/github.com/uptrace/bun/dialect/pgdialect/dialect.go @@ -57,8 +57,10 @@ func New(opts ...DialectOption) *Dialect { feature.CompositeIn | feature.FKDefaultOnAction | feature.DeleteReturning | + feature.Merge | feature.MergeReturning | - feature.AlterColumnExists + feature.AlterColumnExists | + feature.CreateIndexIfNotExists for _, opt := range opts { opt(d) diff --git a/vendor/github.com/uptrace/bun/dialect/pgdialect/inspector.go b/vendor/github.com/uptrace/bun/dialect/pgdialect/inspector.go index ea5269a..158393b 100644 --- a/vendor/github.com/uptrace/bun/dialect/pgdialect/inspector.go +++ b/vendor/github.com/uptrace/bun/dialect/pgdialect/inspector.go @@ -38,17 +38,17 @@ func (in *Inspector) Inspect(ctx context.Context) (sqlschema.Database, error) { exclude := in.ExcludeTables if len(exclude) == 0 { - // Avoid getting NOT LIKE ALL (ARRAY[NULL]) if bun.In() is called with an empty slice. + // Avoid getting NOT LIKE ALL (ARRAY[NULL]) if bun.List() is called with an empty slice. exclude = []string{""} } var tables []*InformationSchemaTable - if err := in.db.NewRaw(sqlInspectTables, in.SchemaName, bun.In(exclude)).Scan(ctx, &tables); err != nil { + if err := in.db.NewRaw(sqlInspectTables, in.SchemaName, bun.List(exclude)).Scan(ctx, &tables); err != nil { return dbSchema, err } var fks []*ForeignKey - if err := in.db.NewRaw(sqlInspectForeignKeys, in.SchemaName, bun.In(exclude), bun.In(exclude)).Scan(ctx, &fks); err != nil { + if err := in.db.NewRaw(sqlInspectForeignKeys, in.SchemaName, bun.List(exclude), bun.List(exclude)).Scan(ctx, &fks); err != nil { return dbSchema, err } dbSchema.ForeignKeys = make(map[sqlschema.ForeignKey]string, len(fks)) @@ -165,7 +165,7 @@ type PrimaryKey struct { const ( // sqlInspectTables retrieves all user-defined tables in the selected schema. - // Pass bun.In([]string{...}) to exclude tables from this inspection or bun.In([]string{''}) to include all results. + // Pass bun.List([]string{...}) to exclude tables from this inspection or bun.List([]string{''}) to include all results. sqlInspectTables = ` SELECT "t".table_schema, @@ -258,7 +258,7 @@ ORDER BY "table_schema", "table_name", "column_name" ` // sqlInspectForeignKeys get FK definitions for user-defined tables. - // Pass bun.In([]string{...}) to exclude tables from this inspection or bun.In([]string{''}) to include all results. + // Pass bun.List([]string{...}) to exclude tables from this inspection or bun.List([]string{''}) to include all results. sqlInspectForeignKeys = ` WITH "schemas" AS ( diff --git a/vendor/github.com/uptrace/bun/dialect/pgdialect/version.go b/vendor/github.com/uptrace/bun/dialect/pgdialect/version.go index e369892..f2748d8 100644 --- a/vendor/github.com/uptrace/bun/dialect/pgdialect/version.go +++ b/vendor/github.com/uptrace/bun/dialect/pgdialect/version.go @@ -2,5 +2,5 @@ package pgdialect // Version is the current release version. func Version() string { - return "1.2.16" + return "1.2.18" } diff --git a/vendor/github.com/uptrace/bun/dialect/sqlitedialect/dialect.go b/vendor/github.com/uptrace/bun/dialect/sqlitedialect/dialect.go index 7489ddb..b2bfc3f 100644 --- a/vendor/github.com/uptrace/bun/dialect/sqlitedialect/dialect.go +++ b/vendor/github.com/uptrace/bun/dialect/sqlitedialect/dialect.go @@ -42,7 +42,8 @@ func New(opts ...DialectOption) *Dialect { feature.AutoIncrement | feature.CompositeIn | feature.FKDefaultOnAction | - feature.DeleteReturning + feature.DeleteReturning | + feature.CreateIndexIfNotExists for _, opt := range opts { opt(d) diff --git a/vendor/github.com/uptrace/bun/dialect/sqlitedialect/version.go b/vendor/github.com/uptrace/bun/dialect/sqlitedialect/version.go index 0785543..b7ea661 100644 --- a/vendor/github.com/uptrace/bun/dialect/sqlitedialect/version.go +++ b/vendor/github.com/uptrace/bun/dialect/sqlitedialect/version.go @@ -2,5 +2,5 @@ package sqlitedialect // Version is the current release version. func Version() string { - return "1.2.16" + return "1.2.18" } diff --git a/vendor/modules.txt b/vendor/modules.txt index af115d3..b9291d5 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,7 +1,7 @@ # github.com/beorn7/perks v1.0.1 ## explicit; go 1.11 github.com/beorn7/perks/quantile -# github.com/bitechdev/ResolveSpec v1.1.15 +# github.com/bitechdev/ResolveSpec v1.1.24 ## explicit; go 1.25.7 github.com/bitechdev/ResolveSpec/pkg/cache github.com/bitechdev/ResolveSpec/pkg/common @@ -269,13 +269,13 @@ github.com/uptrace/bun/internal/tagparser github.com/uptrace/bun/migrate github.com/uptrace/bun/migrate/sqlschema github.com/uptrace/bun/schema -# github.com/uptrace/bun/dialect/mssqldialect v1.2.16 +# github.com/uptrace/bun/dialect/mssqldialect v1.2.18 ## explicit; go 1.24.0 github.com/uptrace/bun/dialect/mssqldialect -# github.com/uptrace/bun/dialect/pgdialect v1.2.16 +# github.com/uptrace/bun/dialect/pgdialect v1.2.18 ## explicit; go 1.24.0 github.com/uptrace/bun/dialect/pgdialect -# github.com/uptrace/bun/dialect/sqlitedialect v1.2.16 +# github.com/uptrace/bun/dialect/sqlitedialect v1.2.18 ## explicit; go 1.24.0 github.com/uptrace/bun/dialect/sqlitedialect # github.com/uptrace/bun/driver/pgdriver v1.1.12