Compare commits
6 Commits
v0.0.4
...
e459775740
| Author | SHA1 | Date | |
|---|---|---|---|
| e459775740 | |||
| 5718685c40 | |||
| 5c899d1635 | |||
| 81a3470407 | |||
| cfc78e0493 | |||
| c179e014ad |
@@ -34,6 +34,20 @@ jobs:
|
|||||||
- name: Tidy modules
|
- name: Tidy modules
|
||||||
run: go mod tidy
|
run: go mod tidy
|
||||||
|
|
||||||
|
- name: Set up Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 'lts/*'
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
run: npm install -g pnpm
|
||||||
|
|
||||||
|
- name: Build UI
|
||||||
|
run: |
|
||||||
|
cd ui
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
pnpm run build
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: go test ./...
|
run: go test ./...
|
||||||
|
|
||||||
|
|||||||
@@ -36,3 +36,4 @@ ui/.svelte-kit/
|
|||||||
internal/app/ui/dist/*
|
internal/app/ui/dist/*
|
||||||
!internal/app/ui/dist/placeholder.txt
|
!internal/app/ui/dist/placeholder.txt
|
||||||
.codex
|
.codex
|
||||||
|
.worktrees/
|
||||||
|
|||||||
@@ -65,10 +65,47 @@ 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 |
|
| `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 |
|
| `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 |
|
| `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 |
|
| `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 |
|
| `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 |
|
| `annotate_tool` | Persist your own usage notes for a specific tool; notes are returned by `describe_tools` in future sessions |
|
||||||
|
|
||||||
|
## Webhook ingestion
|
||||||
|
|
||||||
|
External automation can create thoughts without speaking MCP by posting JSON to `POST /webhooks/thoughts`. The endpoint is protected by the same AMCS authentication middleware as MCP and file uploads, so pass one configured API key via `x-brain-key`, an authorization bearer token header, or another enabled auth method.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8080/webhooks/thoughts \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-H 'x-brain-key: <api-key>' \
|
||||||
|
-H 'Idempotency-Key: n8n-run-123' \
|
||||||
|
-d '{
|
||||||
|
"content": "External system observed build failure on main",
|
||||||
|
"project": "amcs",
|
||||||
|
"source": "n8n",
|
||||||
|
"type": "task",
|
||||||
|
"topics": ["ci", "webhook"],
|
||||||
|
"metadata": {"workflow": "ci-monitor", "run_id": "123"}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Payload fields:
|
||||||
|
|
||||||
|
- `content` is required and becomes the thought content.
|
||||||
|
- `project` is optional; when present it must match an existing AMCS project.
|
||||||
|
- `source`, `type`, `topics`, `people`, `action_items`, and `dates_mentioned` are normalized into the standard thought metadata schema. Unknown `type` values fall back to `observation`.
|
||||||
|
- `metadata` or `source_metadata` may contain safe source-specific JSON values; unsupported values and overly deep objects are dropped rather than persisted.
|
||||||
|
- `idempotency_key` or the `Idempotency-Key` header can be supplied to make repeated webhook deliveries return the existing thought with `duplicate: true`.
|
||||||
|
- `external_id` is stored under `metadata.webhook.external_id` for source-side traceability.
|
||||||
|
|
||||||
|
Successful new ingestion returns `201` with the created thought. Duplicate idempotency-key delivery returns `200` and the previously created thought. Invalid JSON, missing content, missing/unknown projects, or unauthenticated requests are rejected before persistence. Metadata and embedding enrichment are queued after the thought is stored.
|
||||||
|
|
||||||
## Learnings
|
## Learnings
|
||||||
|
|
||||||
Learnings are curated, structured memory records for durable insights you want to keep distinct from raw thoughts. Use them for normalized lessons, decisions, and evidence-backed findings that should be easy to retrieve and review over time.
|
Learnings are curated, structured memory records for durable insights you want to keep distinct from raw thoughts. Use them for normalized lessons, decisions, and evidence-backed findings that should be easy to retrieve and review over time.
|
||||||
@@ -216,9 +253,9 @@ Use `get_version_info` to retrieve the runtime build metadata:
|
|||||||
|
|
||||||
## Agent Skills and Guardrails
|
## 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
|
### Skills
|
||||||
|
|
||||||
@@ -243,10 +280,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:
|
Link existing skills and guardrails to a project so they are automatically available when that project is active:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "project": "my-project", "skill_id": "<uuid>" }
|
{ "project": "my-project", "skill_id": 42, "override": false }
|
||||||
{ "project": "my-project", "guardrail_id": "<uuid>" }
|
{ "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
|
## Configuration
|
||||||
|
|
||||||
Config is YAML-driven. Copy `configs/config.example.yaml` and set:
|
Config is YAML-driven. Copy `configs/config.example.yaml` and set:
|
||||||
@@ -417,7 +456,7 @@ Returns `{"file": {...}, "uri": "amcs://files/<id>"}`. Pass `thought_id`/`projec
|
|||||||
|
|
||||||
### MCP resources
|
### 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
|
### HTTP upload and download
|
||||||
|
|
||||||
@@ -471,7 +510,7 @@ metadata_retry:
|
|||||||
include_archived: false
|
include_archived: false
|
||||||
```
|
```
|
||||||
|
|
||||||
**Search fallback**: when no embeddings exist for the active model in scope, `search_thoughts`, `recall_context`, `get_project_context`, `summarize_thoughts`, and `related_thoughts` automatically fall back to Postgres full-text search so results are never silently empty.
|
**Search fallback**: when no embeddings exist for the active model in scope, `search_thoughts`, `recall_context`, `get_project_context`, `summarize_thoughts`, and `related_thoughts` automatically fall back to Postgres full-text search so results are never silently empty. All five tools include a `retrieval_mode` field in their response (`"semantic"` or `"text"`) so callers can see which path was taken.
|
||||||
|
|
||||||
## Client Setup
|
## Client Setup
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -3,7 +3,7 @@ module git.warky.dev/wdevs/amcs
|
|||||||
go 1.26.1
|
go 1.26.1
|
||||||
|
|
||||||
require (
|
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/jsonschema-go v0.4.3
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/jackc/pgx/v5 v5.9.2
|
github.com/jackc/pgx/v5 v5.9.2
|
||||||
@@ -11,7 +11,7 @@ require (
|
|||||||
github.com/pgvector/pgvector-go v0.3.0
|
github.com/pgvector/pgvector-go v0.3.0
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
github.com/uptrace/bun v1.2.18
|
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/bun/driver/pgdriver v1.1.12
|
||||||
github.com/uptrace/bunrouter v1.0.23
|
github.com/uptrace/bunrouter v1.0.23
|
||||||
golang.org/x/sync v0.20.0
|
golang.org/x/sync v0.20.0
|
||||||
@@ -62,8 +62,8 @@ require (
|
|||||||
github.com/tidwall/pretty v1.2.1 // indirect
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
github.com/tidwall/sjson v1.2.5 // indirect
|
github.com/tidwall/sjson v1.2.5 // indirect
|
||||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // 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/mssqldialect v1.2.18 // indirect
|
||||||
github.com/uptrace/bun/dialect/sqlitedialect v1.2.16 // indirect
|
github.com/uptrace/bun/dialect/sqlitedialect v1.2.18 // indirect
|
||||||
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
||||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||||
github.com/x448/float16 v0.8.4 // indirect
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
|
|||||||
@@ -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/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 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
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.24 h1:+Ku3jE8ZSQ2c6IdVyqYp9CdCh4wSasCd1yLDF8GXy5U=
|
||||||
github.com/bitechdev/ResolveSpec v1.1.15/go.mod h1:GF51sMRCWbAyri2WNae3IZAFM/2s6DG6i3eTTrobbVs=
|
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 h1:6Gpm9YYUEQx2T9zMsYolQhr6sjwwGtFitSA0pQsa7a8=
|
||||||
github.com/bradfitz/gomemcache v0.0.0-20260422231931-4d751bb6e37c/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c=
|
github.com/bradfitz/gomemcache v0.0.0-20260422231931-4d751bb6e37c/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c=
|
||||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
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/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 h1:3HnRcMfS6OBPMG1eSOzlbFJ/X/AyMEJb7rMxE6VQvDU=
|
||||||
github.com/uptrace/bun v1.2.18/go.mod h1:wNltaKJk4JtOt4SG5I5zmA7v0/Mzjh1+/S906Rayd3Y=
|
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.18 h1:nYzHoyJKJlIyl5i95Exi8ZTK8ooKWG+o3z3f404d/yQ=
|
||||||
github.com/uptrace/bun/dialect/mssqldialect v1.2.16/go.mod h1:J5U7tGKWDsx2Q7MwDZF2417jCdpD6yD/ZMFJcCR80bk=
|
github.com/uptrace/bun/dialect/mssqldialect v1.2.18/go.mod h1:Su45Je7z66sfeZ3d1ZsnOQEK8xfzGgaMzBvtoE8yFhk=
|
||||||
github.com/uptrace/bun/dialect/pgdialect v1.2.16 h1:KFNZ0LxAyczKNfK/IJWMyaleO6eI9/Z5tUv3DE1NVL4=
|
github.com/uptrace/bun/dialect/pgdialect v1.2.18 h1:IZ6nM2+OYrL8lkEAy7UkSEZvoa3vluTAUlZfPtlRB2k=
|
||||||
github.com/uptrace/bun/dialect/pgdialect v1.2.16/go.mod h1:IJdMeV4sLfh0LDUZl7TIxLI0LipF1vwTK3hBC7p5qLo=
|
github.com/uptrace/bun/dialect/pgdialect v1.2.18/go.mod h1:Tqdf4QP1okrGYpXfodXvCOK6Ob1OOTwSaoAzCgBB3IU=
|
||||||
github.com/uptrace/bun/dialect/sqlitedialect v1.2.16 h1:6wVAiYLj1pMibRthGwy4wDLa3D5AQo32Y8rvwPd8CQ0=
|
github.com/uptrace/bun/dialect/sqlitedialect v1.2.18 h1:Z33SY/U++XK9uGWqS4h8OZVxfCXguIG+sU9cYq2PGFQ=
|
||||||
github.com/uptrace/bun/dialect/sqlitedialect v1.2.16/go.mod h1:Z7+5qK8CGZkDQiPMu+LSdVuDuR1I5jcwtkB1Pi3F82E=
|
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 h1:3rRWB1GK0psTJrHwxzNfEij2MLibggiLdTqjTtfHc1w=
|
||||||
github.com/uptrace/bun/driver/pgdriver v1.1.12/go.mod h1:ssYUP+qwSEgeDDS1xm2XBip9el1y9Mi5mTAvLoiADLM=
|
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=
|
github.com/uptrace/bun/driver/sqliteshim v1.2.16 h1:M6Dh5kkDWFbUWBrOsIE1g1zdZ5JbSytTD4piFRBOUAI=
|
||||||
|
|||||||
@@ -206,6 +206,9 @@ func routes(logger *slog.Logger, cfg *config.Config, info buildinfo.Info, db *st
|
|||||||
Version: tools.NewVersionTool(cfg.MCP.ServerName, info),
|
Version: tools.NewVersionTool(cfg.MCP.ServerName, info),
|
||||||
Learnings: tools.NewLearningsTool(db, activeProjects, cfg.Search),
|
Learnings: tools.NewLearningsTool(db, activeProjects, cfg.Search),
|
||||||
Plans: tools.NewPlansTool(db, activeProjects, cfg.Search),
|
Plans: tools.NewPlansTool(db, activeProjects, cfg.Search),
|
||||||
|
ProjectPersonas: tools.NewProjectPersonasTool(db, activeProjects),
|
||||||
|
WorldModel: tools.NewWorldModelTool(db, activeProjects),
|
||||||
|
ThoughtLearningLinks: tools.NewThoughtLearningLinksTool(db),
|
||||||
Context: tools.NewContextTool(db, embeddings, cfg.Search, activeProjects),
|
Context: tools.NewContextTool(db, embeddings, cfg.Search, activeProjects),
|
||||||
Recall: tools.NewRecallTool(db, embeddings, cfg.Search, activeProjects),
|
Recall: tools.NewRecallTool(db, embeddings, cfg.Search, activeProjects),
|
||||||
Summarize: tools.NewSummarizeTool(db, embeddings, metadata, cfg.Search, activeProjects),
|
Summarize: tools.NewSummarizeTool(db, embeddings, metadata, cfg.Search, activeProjects),
|
||||||
@@ -235,6 +238,7 @@ func routes(logger *slog.Logger, cfg *config.Config, info buildinfo.Info, db *st
|
|||||||
}
|
}
|
||||||
mux.Handle("/files", authMiddleware(fileHandler(filesTool)))
|
mux.Handle("/files", authMiddleware(fileHandler(filesTool)))
|
||||||
mux.Handle("/files/{id}", authMiddleware(fileHandler(filesTool)))
|
mux.Handle("/files/{id}", authMiddleware(fileHandler(filesTool)))
|
||||||
|
mux.Handle("/webhooks/thoughts", authMiddleware(newWebhookThoughtHandler(db, embeddings, cfg.Capture, enrichmentRetryer, backfillTool)))
|
||||||
mux.HandleFunc("/.well-known/oauth-authorization-server", oauthMetadataHandler())
|
mux.HandleFunc("/.well-known/oauth-authorization-server", oauthMetadataHandler())
|
||||||
mux.HandleFunc("/api/oauth/register", oauthRegisterHandler(dynClients, logger))
|
mux.HandleFunc("/api/oauth/register", oauthRegisterHandler(dynClients, logger))
|
||||||
mux.HandleFunc("/api/oauth/authorize", oauthAuthorizeHandler(dynClients, authCodes, logger))
|
mux.HandleFunc("/api/oauth/authorize", oauthAuthorizeHandler(dynClients, authCodes, logger))
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ func resolveSpecModels() []resolveSpecModel {
|
|||||||
{schema: "public", entity: "plan_skills", model: generatedmodels.ModelPublicPlanSkills{}},
|
{schema: "public", entity: "plan_skills", model: generatedmodels.ModelPublicPlanSkills{}},
|
||||||
{schema: "public", entity: "plans", model: generatedmodels.ModelPublicPlans{}},
|
{schema: "public", entity: "plans", model: generatedmodels.ModelPublicPlans{}},
|
||||||
{schema: "public", entity: "project_guardrails", model: generatedmodels.ModelPublicProjectGuardrails{}},
|
{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: "project_skills", model: generatedmodels.ModelPublicProjectSkills{}},
|
||||||
{schema: "public", entity: "projects", model: generatedmodels.ModelPublicProjects{}},
|
{schema: "public", entity: "projects", model: generatedmodels.ModelPublicProjects{}},
|
||||||
{schema: "public", entity: "stored_files", model: generatedmodels.ModelPublicStoredFiles{}},
|
{schema: "public", entity: "stored_files", model: generatedmodels.ModelPublicStoredFiles{}},
|
||||||
|
|||||||
@@ -0,0 +1,214 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.warky.dev/wdevs/amcs/internal/ai"
|
||||||
|
"git.warky.dev/wdevs/amcs/internal/config"
|
||||||
|
"git.warky.dev/wdevs/amcs/internal/metadata"
|
||||||
|
"git.warky.dev/wdevs/amcs/internal/store"
|
||||||
|
"git.warky.dev/wdevs/amcs/internal/tools"
|
||||||
|
thoughttypes "git.warky.dev/wdevs/amcs/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
const maxWebhookBodyBytes = 1 << 20
|
||||||
|
|
||||||
|
type webhookThoughtRequest struct {
|
||||||
|
Content string `json:"content"`
|
||||||
|
Project string `json:"project,omitempty"`
|
||||||
|
Source string `json:"source,omitempty"`
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
Topics []string `json:"topics,omitempty"`
|
||||||
|
People []string `json:"people,omitempty"`
|
||||||
|
ActionItems []string `json:"action_items,omitempty"`
|
||||||
|
DatesMentioned []string `json:"dates_mentioned,omitempty"`
|
||||||
|
Metadata map[string]any `json:"metadata,omitempty"`
|
||||||
|
SourceMetadata map[string]any `json:"source_metadata,omitempty"`
|
||||||
|
IDempotencyKey string `json:"idempotency_key,omitempty"`
|
||||||
|
ExternalID string `json:"external_id,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type webhookThoughtResponse struct {
|
||||||
|
Thought thoughttypes.Thought `json:"thought"`
|
||||||
|
Duplicate bool `json:"duplicate"`
|
||||||
|
WebhookMeta thoughttypes.WebhookMetadata `json:"webhook"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type webhookThoughtHandler struct {
|
||||||
|
store *store.DB
|
||||||
|
embeddings *ai.EmbeddingRunner
|
||||||
|
capture config.CaptureConfig
|
||||||
|
retryer tools.MetadataQueuer
|
||||||
|
embedRetryer tools.EmbeddingQueuer
|
||||||
|
}
|
||||||
|
|
||||||
|
func newWebhookThoughtHandler(db *store.DB, embeddings *ai.EmbeddingRunner, capture config.CaptureConfig, retryer tools.MetadataQueuer, embedRetryer tools.EmbeddingQueuer) http.Handler {
|
||||||
|
return &webhookThoughtHandler{store: db, embeddings: embeddings, capture: capture, retryer: retryer, embedRetryer: embedRetryer}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *webhookThoughtHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path != "/webhooks/thoughts" {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
w.Header().Set("Allow", http.MethodPost)
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxWebhookBodyBytes)
|
||||||
|
in, err := parseWebhookThoughtRequest(r)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
webhookMeta := buildWebhookMetadata(in, r.Header.Get("Idempotency-Key"), time.Now().UTC())
|
||||||
|
if webhookMeta.IDempotencyKey != "" {
|
||||||
|
if existing, err := h.store.GetThoughtByWebhookIDempotencyKey(r.Context(), webhookMeta.IDempotencyKey); err == nil {
|
||||||
|
writeWebhookThoughtResponse(w, http.StatusOK, webhookThoughtResponse{Thought: existing, Duplicate: true, WebhookMeta: webhookMeta})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
projectID, err := h.resolveWebhookProject(r, in.Project)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
thought := thoughttypes.Thought{
|
||||||
|
Content: strings.TrimSpace(in.Content),
|
||||||
|
Metadata: normalizeWebhookThoughtMetadata(in, webhookMeta, h.capture),
|
||||||
|
ProjectID: projectID,
|
||||||
|
}
|
||||||
|
created, err := h.store.InsertThought(r.Context(), thought, h.embeddings.PrimaryModel())
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "insert thought: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if projectID != nil {
|
||||||
|
_ = h.store.TouchProject(r.Context(), *projectID)
|
||||||
|
}
|
||||||
|
if h.retryer != nil {
|
||||||
|
h.retryer.QueueThought(created.ID)
|
||||||
|
}
|
||||||
|
if h.embedRetryer != nil {
|
||||||
|
h.embedRetryer.QueueThought(r.Context(), created.ID, created.Content)
|
||||||
|
}
|
||||||
|
|
||||||
|
writeWebhookThoughtResponse(w, http.StatusCreated, webhookThoughtResponse{Thought: created, WebhookMeta: webhookMeta})
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseWebhookThoughtRequest(r *http.Request) (webhookThoughtRequest, error) {
|
||||||
|
if !strings.Contains(r.Header.Get("Content-Type"), "application/json") {
|
||||||
|
return webhookThoughtRequest{}, errors.New("webhook requires application/json")
|
||||||
|
}
|
||||||
|
defer r.Body.Close()
|
||||||
|
decoder := json.NewDecoder(r.Body)
|
||||||
|
decoder.DisallowUnknownFields()
|
||||||
|
var in webhookThoughtRequest
|
||||||
|
if err := decoder.Decode(&in); err != nil {
|
||||||
|
return webhookThoughtRequest{}, err
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(in.Content) == "" {
|
||||||
|
return webhookThoughtRequest{}, errors.New("content is required")
|
||||||
|
}
|
||||||
|
return in, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *webhookThoughtHandler) resolveWebhookProject(r *http.Request, projectName string) (*int64, error) {
|
||||||
|
projectName = strings.TrimSpace(projectName)
|
||||||
|
if projectName == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
project, err := h.store.GetProject(r.Context(), projectName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &project.NumericID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildWebhookMetadata(in webhookThoughtRequest, headerKey string, now time.Time) thoughttypes.WebhookMetadata {
|
||||||
|
sourceMetadata := in.SourceMetadata
|
||||||
|
if len(sourceMetadata) == 0 {
|
||||||
|
sourceMetadata = in.Metadata
|
||||||
|
}
|
||||||
|
return thoughttypes.WebhookMetadata{
|
||||||
|
ReceivedAt: now.Format(time.RFC3339),
|
||||||
|
IDempotencyKey: firstNonEmpty(in.IDempotencyKey, headerKey),
|
||||||
|
ExternalID: strings.TrimSpace(in.ExternalID),
|
||||||
|
SourceMetadata: sanitizeWebhookMetadata(sourceMetadata),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeWebhookThoughtMetadata(in webhookThoughtRequest, webhookMeta thoughttypes.WebhookMetadata, capture config.CaptureConfig) thoughttypes.ThoughtMetadata {
|
||||||
|
return metadata.Normalize(thoughttypes.ThoughtMetadata{
|
||||||
|
People: in.People,
|
||||||
|
ActionItems: in.ActionItems,
|
||||||
|
DatesMentioned: in.DatesMentioned,
|
||||||
|
Topics: in.Topics,
|
||||||
|
Type: in.Type,
|
||||||
|
Source: firstNonEmpty(in.Source, "webhook"),
|
||||||
|
Webhook: &webhookMeta,
|
||||||
|
}, capture)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitizeWebhookMetadata(in map[string]any) map[string]any {
|
||||||
|
if len(in) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make(map[string]any, len(in))
|
||||||
|
for key, value := range in {
|
||||||
|
key = strings.TrimSpace(key)
|
||||||
|
if key == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if sanitized, ok := sanitizeWebhookMetadataValue(value, 0); ok {
|
||||||
|
out[key] = sanitized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(out) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitizeWebhookMetadataValue(value any, depth int) (any, bool) {
|
||||||
|
if depth > 3 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
switch v := value.(type) {
|
||||||
|
case nil, bool, float64, string:
|
||||||
|
return v, true
|
||||||
|
case []any:
|
||||||
|
if len(v) > 50 {
|
||||||
|
v = v[:50]
|
||||||
|
}
|
||||||
|
out := make([]any, 0, len(v))
|
||||||
|
for _, item := range v {
|
||||||
|
if sanitized, ok := sanitizeWebhookMetadataValue(item, depth+1); ok {
|
||||||
|
out = append(out, sanitized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, true
|
||||||
|
case map[string]any:
|
||||||
|
if len(v) > 50 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return sanitizeWebhookMetadata(v), true
|
||||||
|
default:
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeWebhookThoughtResponse(w http.ResponseWriter, status int, out webhookThoughtResponse) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(status)
|
||||||
|
_ = json.NewEncoder(w).Encode(out)
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.warky.dev/wdevs/amcs/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseWebhookThoughtRequestRequiresJSON(t *testing.T) {
|
||||||
|
req := httptestRequest("text/plain", `{"content":"hello"}`)
|
||||||
|
|
||||||
|
_, err := parseWebhookThoughtRequest(req)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for non-json content type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseWebhookThoughtRequestRequiresContent(t *testing.T) {
|
||||||
|
req := httptestRequest("application/json", `{"source":"n8n"}`)
|
||||||
|
|
||||||
|
_, err := parseWebhookThoughtRequest(req)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "content is required") {
|
||||||
|
t.Fatalf("error = %v, want content required", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildWebhookMetadataUsesHeaderIdempotencyAndSanitizesMetadata(t *testing.T) {
|
||||||
|
now := time.Date(2026, 7, 15, 4, 0, 0, 0, time.UTC)
|
||||||
|
got := buildWebhookMetadata(webhookThoughtRequest{
|
||||||
|
ExternalID: " ext-1 ",
|
||||||
|
Metadata: map[string]any{
|
||||||
|
"service": "n8n",
|
||||||
|
"unsafe": struct{}{},
|
||||||
|
"nested": map[string]any{"ok": true},
|
||||||
|
},
|
||||||
|
}, " key-1 ", now)
|
||||||
|
|
||||||
|
if got.IDempotencyKey != "key-1" {
|
||||||
|
t.Fatalf("IDempotencyKey = %q, want key-1", got.IDempotencyKey)
|
||||||
|
}
|
||||||
|
if got.ExternalID != "ext-1" {
|
||||||
|
t.Fatalf("ExternalID = %q, want ext-1", got.ExternalID)
|
||||||
|
}
|
||||||
|
if got.ReceivedAt != "2026-07-15T04:00:00Z" {
|
||||||
|
t.Fatalf("ReceivedAt = %q", got.ReceivedAt)
|
||||||
|
}
|
||||||
|
if _, ok := got.SourceMetadata["unsafe"]; ok {
|
||||||
|
t.Fatal("unsafe metadata value was not removed")
|
||||||
|
}
|
||||||
|
if got.SourceMetadata["service"] != "n8n" {
|
||||||
|
t.Fatalf("service metadata = %#v", got.SourceMetadata["service"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeWebhookThoughtMetadata(t *testing.T) {
|
||||||
|
webhookMeta := buildWebhookMetadata(webhookThoughtRequest{IDempotencyKey: "abc"}, "", time.Date(2026, 7, 15, 4, 0, 0, 0, time.UTC))
|
||||||
|
got := normalizeWebhookThoughtMetadata(webhookThoughtRequest{
|
||||||
|
Source: "github",
|
||||||
|
Type: "task",
|
||||||
|
Topics: []string{"ci", "ci", ""},
|
||||||
|
People: []string{" Sam "},
|
||||||
|
}, webhookMeta, config.CaptureConfig{})
|
||||||
|
|
||||||
|
if got.Source != "github" {
|
||||||
|
t.Fatalf("Source = %q, want github", got.Source)
|
||||||
|
}
|
||||||
|
if got.Type != "task" {
|
||||||
|
t.Fatalf("Type = %q, want task", got.Type)
|
||||||
|
}
|
||||||
|
if len(got.Topics) != 1 || got.Topics[0] != "ci" {
|
||||||
|
t.Fatalf("Topics = %#v, want [ci]", got.Topics)
|
||||||
|
}
|
||||||
|
if got.Webhook == nil || got.Webhook.IDempotencyKey != "abc" {
|
||||||
|
t.Fatalf("Webhook = %#v, want idempotency key abc", got.Webhook)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func httptestRequest(contentType, body string) *http.Request {
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/webhooks/thoughts", strings.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", contentType)
|
||||||
|
return req
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ type ModelPublicAgentGuardrails struct {
|
|||||||
GUID resolvespec_common.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
|
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"`
|
Name resolvespec_common.SqlString `bun:"name,type:text,notnull," json:"name"`
|
||||||
Severity resolvespec_common.SqlString `bun:"severity,type:text,default:'medium',notnull," json:"severity"`
|
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"`
|
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
|
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
|
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
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicAgentGuardrails) GetIDStr() string {
|
func (m ModelPublicAgentGuardrails) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ type ModelPublicAgentParts struct {
|
|||||||
Name resolvespec_common.SqlString `bun:"name,type:text,notnull," json:"name"`
|
Name resolvespec_common.SqlString `bun:"name,type:text,notnull," json:"name"`
|
||||||
PartType resolvespec_common.SqlString `bun:"part_type,type:text,notnull," json:"part_type"`
|
PartType resolvespec_common.SqlString `bun:"part_type,type:text,notnull," json:"part_type"`
|
||||||
Summary resolvespec_common.SqlString `bun:"summary,type:text,notnull," json:"summary"`
|
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"`
|
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
|
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
|
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
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicAgentParts) GetIDStr() string {
|
func (m ModelPublicAgentParts) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ func (m ModelPublicAgentPersonaGuardrails) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicAgentPersonaGuardrails) GetIDStr() string {
|
func (m ModelPublicAgentPersonaGuardrails) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ func (m ModelPublicAgentPersonaParts) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicAgentPersonaParts) GetIDStr() string {
|
func (m ModelPublicAgentPersonaParts) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
type ModelPublicAgentPersonaSkills struct {
|
type ModelPublicAgentPersonaSkills struct {
|
||||||
bun.BaseModel `bun:"table:public.agent_persona_skills,alias:agent_persona_skills"`
|
bun.BaseModel `bun:"table:public.agent_persona_skills,alias:agent_persona_skills"`
|
||||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
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"`
|
PersonaID int64 `bun:"persona_id,type:bigint,notnull," json:"persona_id"`
|
||||||
SkillID int64 `bun:"skill_id,type:bigint,notnull," json:"skill_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
|
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
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicAgentPersonaSkills) GetIDStr() string {
|
func (m ModelPublicAgentPersonaSkills) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ func (m ModelPublicAgentPersonaTraits) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicAgentPersonaTraits) GetIDStr() string {
|
func (m ModelPublicAgentPersonaTraits) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -19,10 +19,11 @@ type ModelPublicAgentPersonas struct {
|
|||||||
GUID resolvespec_common.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
|
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"`
|
Name resolvespec_common.SqlString `bun:"name,type:text,notnull," json:"name"`
|
||||||
Summary resolvespec_common.SqlString `bun:"summary,type:text,notnull," json:"summary"`
|
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"`
|
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
|
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
|
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
|
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
|
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
|
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
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicAgentPersonas) GetIDStr() string {
|
func (m ModelPublicAgentPersonas) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -13,13 +13,13 @@ type ModelPublicAgentSkills struct {
|
|||||||
Content resolvespec_common.SqlString `bun:"content,type:text,notnull," json:"content"`
|
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"`
|
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"`
|
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"`
|
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"`
|
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"`
|
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"`
|
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"`
|
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"`
|
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"`
|
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
|
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
|
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
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicAgentSkills) GetIDStr() string {
|
func (m ModelPublicAgentSkills) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ type ModelPublicAgentTraits struct {
|
|||||||
GUID resolvespec_common.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
|
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"`
|
Instruction resolvespec_common.SqlString `bun:"instruction,type:text,default:'',notnull," json:"instruction"`
|
||||||
Name resolvespec_common.SqlString `bun:"name,type:text,notnull," json:"name"`
|
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"`
|
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"`
|
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
|
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
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicAgentTraits) GetIDStr() string {
|
func (m ModelPublicAgentTraits) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ func (m ModelPublicArcStageParts) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicArcStageParts) GetIDStr() string {
|
func (m ModelPublicArcStageParts) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ func (m ModelPublicArcStages) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicArcStages) GetIDStr() string {
|
func (m ModelPublicArcStages) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ func (m ModelPublicCharacterArcs) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicCharacterArcs) GetIDStr() string {
|
func (m ModelPublicCharacterArcs) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ type ModelPublicChatHistories struct {
|
|||||||
Channel resolvespec_common.SqlString `bun:"channel,type:text,nullzero," json:"channel"`
|
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"`
|
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"`
|
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"`
|
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"`
|
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"`
|
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
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicChatHistories) GetIDStr() string {
|
func (m ModelPublicChatHistories) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ func (m ModelPublicEmbeddings) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicEmbeddings) GetIDStr() string {
|
func (m ModelPublicEmbeddings) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ type ModelPublicLearnings struct {
|
|||||||
Status resolvespec_common.SqlString `bun:"status,type:text,default:'pending',notnull," json:"status"`
|
Status resolvespec_common.SqlString `bun:"status,type:text,default:'pending',notnull," json:"status"`
|
||||||
Summary resolvespec_common.SqlString `bun:"summary,type:text,notnull," json:"summary"`
|
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"`
|
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"`
|
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
|
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
|
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
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicLearnings) GetIDStr() string {
|
func (m ModelPublicLearnings) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ type ModelPublicOauthClients struct {
|
|||||||
ClientID resolvespec_common.SqlString `bun:"client_id,type:text,notnull," json:"client_id"`
|
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"`
|
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"`
|
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
|
// TableName returns the table name for ModelPublicOauthClients
|
||||||
@@ -38,7 +38,7 @@ func (m ModelPublicOauthClients) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicOauthClients) GetIDStr() string {
|
func (m ModelPublicOauthClients) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ func (m ModelPublicPersonaArc) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicPersonaArc) GetIDStr() string {
|
func (m ModelPublicPersonaArc) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ func (m ModelPublicPlanDependencies) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicPlanDependencies) GetIDStr() string {
|
func (m ModelPublicPlanDependencies) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ func (m ModelPublicPlanGuardrails) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicPlanGuardrails) GetIDStr() string {
|
func (m ModelPublicPlanGuardrails) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ func (m ModelPublicPlanRelatedPlans) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicPlanRelatedPlans) GetIDStr() string {
|
func (m ModelPublicPlanRelatedPlans) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ func (m ModelPublicPlanSkills) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicPlanSkills) GetIDStr() string {
|
func (m ModelPublicPlanSkills) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ type ModelPublicPlans struct {
|
|||||||
ReviewedBy resolvespec_common.SqlString `bun:"reviewed_by,type:text,nullzero," json:"reviewed_by"`
|
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
|
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"`
|
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"`
|
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"`
|
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
|
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
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicPlans) GetIDStr() string {
|
func (m ModelPublicPlans) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ func (m ModelPublicProjectGuardrails) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicProjectGuardrails) GetIDStr() string {
|
func (m ModelPublicProjectGuardrails) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
// Code generated by relspecgo. DO NOT EDIT.
|
||||||
|
package generatedmodels
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ModelPublicProjectPersonas struct {
|
||||||
|
bun.BaseModel `bun:"table:public.project_personas,alias:project_personas"`
|
||||||
|
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||||
|
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||||
|
IsDefault bool `bun:"is_default,type:boolean,default:false,notnull," json:"is_default"`
|
||||||
|
PersonaID int64 `bun:"persona_id,type:bigint,notnull," json:"persona_id"`
|
||||||
|
ProjectID int64 `bun:"project_id,type:bigint,notnull," json:"project_id"`
|
||||||
|
RelPersonaID *ModelPublicAgentPersonas `bun:"rel:has-one,join:persona_id=id" json:"relpersonaid,omitempty"` // Has one ModelPublicAgentPersonas
|
||||||
|
RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName returns the table name for ModelPublicProjectPersonas
|
||||||
|
func (m ModelPublicProjectPersonas) TableName() string {
|
||||||
|
return "public.project_personas"
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableNameOnly returns the table name without schema for ModelPublicProjectPersonas
|
||||||
|
func (m ModelPublicProjectPersonas) TableNameOnly() string {
|
||||||
|
return "project_personas"
|
||||||
|
}
|
||||||
|
|
||||||
|
// SchemaName returns the schema name for ModelPublicProjectPersonas
|
||||||
|
func (m ModelPublicProjectPersonas) SchemaName() string {
|
||||||
|
return "public"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetID returns the primary key value
|
||||||
|
func (m ModelPublicProjectPersonas) GetID() int64 {
|
||||||
|
return m.ID.Int64()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIDStr returns the primary key as a string
|
||||||
|
func (m ModelPublicProjectPersonas) GetIDStr() string {
|
||||||
|
return m.ID.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetID sets the primary key value
|
||||||
|
func (m ModelPublicProjectPersonas) SetID(newid int64) {
|
||||||
|
m.UpdateID(newid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateID updates the primary key value
|
||||||
|
func (m *ModelPublicProjectPersonas) UpdateID(newid int64) {
|
||||||
|
m.ID.FromString(fmt.Sprintf("%d", newid))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIDName returns the name of the primary key column
|
||||||
|
func (m ModelPublicProjectPersonas) GetIDName() string {
|
||||||
|
return "id"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPrefix returns the table prefix
|
||||||
|
func (m ModelPublicProjectPersonas) GetPrefix() string {
|
||||||
|
return "PPR"
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ type ModelPublicProjectSkills struct {
|
|||||||
bun.BaseModel `bun:"table:public.project_skills,alias:project_skills"`
|
bun.BaseModel `bun:"table:public.project_skills,alias:project_skills"`
|
||||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
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"`
|
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"`
|
ProjectID int64 `bun:"project_id,type:bigint,notnull," json:"project_id"`
|
||||||
SkillID int64 `bun:"skill_id,type:bigint,notnull," json:"skill_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
|
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
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicProjectSkills) GetIDStr() string {
|
func (m ModelPublicProjectSkills) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ type ModelPublicProjects struct {
|
|||||||
LastActiveAt resolvespec_common.SqlTimeStamp `bun:"last_active_at,type:timestamptz,default:now(),nullzero," json:"last_active_at"`
|
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"`
|
Name resolvespec_common.SqlString `bun:"name,type:text,notnull," json:"name"`
|
||||||
ThoughtCount resolvespec_common.SqlInt64 `bun:"thought_count,scanonly" json:"thought_count"`
|
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
|
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
|
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
|
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
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicProjects) GetIDStr() string {
|
func (m ModelPublicProjects) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ func (m ModelPublicStoredFiles) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicStoredFiles) GetIDStr() string {
|
func (m ModelPublicStoredFiles) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ func (m ModelPublicThoughtLinks) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicThoughtLinks) GetIDStr() string {
|
func (m ModelPublicThoughtLinks) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ func (m ModelPublicThoughts) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicThoughts) GetIDStr() string {
|
func (m ModelPublicThoughts) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ func (m ModelPublicToolAnnotations) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicToolAnnotations) GetIDStr() string {
|
func (m ModelPublicToolAnnotations) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package mcpserver
|
package mcpserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -43,6 +44,9 @@ type ToolSet struct {
|
|||||||
Describe *tools.DescribeTool
|
Describe *tools.DescribeTool
|
||||||
Learnings *tools.LearningsTool
|
Learnings *tools.LearningsTool
|
||||||
Plans *tools.PlansTool
|
Plans *tools.PlansTool
|
||||||
|
ProjectPersonas *tools.ProjectPersonasTool
|
||||||
|
WorldModel *tools.WorldModelTool
|
||||||
|
ThoughtLearningLinks *tools.ThoughtLearningLinksTool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handlers groups the HTTP handlers produced for an MCP server instance.
|
// Handlers groups the HTTP handlers produced for an MCP server instance.
|
||||||
@@ -86,7 +90,9 @@ func NewHandlers(cfg config.MCPConfig, logger *slog.Logger, toolSet ToolSet, onS
|
|||||||
registerSystemTools,
|
registerSystemTools,
|
||||||
registerThoughtTools,
|
registerThoughtTools,
|
||||||
registerProjectTools,
|
registerProjectTools,
|
||||||
|
registerWorldModelTools,
|
||||||
registerLearningTools,
|
registerLearningTools,
|
||||||
|
registerThoughtLearningLinkTools,
|
||||||
registerPlanTools,
|
registerPlanTools,
|
||||||
registerFileTools,
|
registerFileTools,
|
||||||
registerMaintenanceTools,
|
registerMaintenanceTools,
|
||||||
@@ -123,6 +129,33 @@ func NewHandlers(cfg config.MCPConfig, logger *slog.Logger, toolSet ToolSet, onS
|
|||||||
return h, nil
|
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.
|
// 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.
|
// Returns nil when publicURL is empty so the icons field is omitted from the MCP identity.
|
||||||
func buildServerIcons(publicURL string) []mcp.Icon {
|
func buildServerIcons(publicURL string) []mcp.Icon {
|
||||||
@@ -277,6 +310,34 @@ func registerLearningTools(server *mcp.Server, logger *slog.Logger, toolSet Tool
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func registerThoughtLearningLinkTools(server *mcp.Server, logger *slog.Logger, toolSet ToolSet) error {
|
||||||
|
if err := addTool(server, logger, &mcp.Tool{
|
||||||
|
Name: "link_thought_learning",
|
||||||
|
Description: "Create or update an explicit association between a raw thought and a curated learning.",
|
||||||
|
}, toolSet.ThoughtLearningLinks.Link); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := addTool(server, logger, &mcp.Tool{
|
||||||
|
Name: "unlink_thought_learning",
|
||||||
|
Description: "Remove an explicit association between a thought and a learning.",
|
||||||
|
}, toolSet.ThoughtLearningLinks.Unlink); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := addTool(server, logger, &mcp.Tool{
|
||||||
|
Name: "get_thought_learnings",
|
||||||
|
Description: "List curated learnings explicitly linked to a thought.",
|
||||||
|
}, toolSet.ThoughtLearningLinks.GetThoughtLearnings); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := addTool(server, logger, &mcp.Tool{
|
||||||
|
Name: "get_learning_thoughts",
|
||||||
|
Description: "List raw thoughts explicitly linked to a learning.",
|
||||||
|
}, toolSet.ThoughtLearningLinks.GetLearningThoughts); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func registerPlanTools(server *mcp.Server, logger *slog.Logger, toolSet ToolSet) error {
|
func registerPlanTools(server *mcp.Server, logger *slog.Logger, toolSet ToolSet) error {
|
||||||
if err := addTool(server, logger, &mcp.Tool{
|
if err := addTool(server, logger, &mcp.Tool{
|
||||||
Name: "create_plan",
|
Name: "create_plan",
|
||||||
@@ -713,12 +774,21 @@ func BuildToolCatalog() []tools.ToolEntry {
|
|||||||
{Name: "list_projects", Description: "List projects and their current thought counts.", Category: "projects"},
|
{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: "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: "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"},
|
{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
|
// learnings
|
||||||
{Name: "add_learning", Description: "Create a curated learning record distinct from raw thoughts.", Category: "projects"},
|
{Name: "add_learning", Description: "Create a curated learning record distinct from raw thoughts.", Category: "projects"},
|
||||||
{Name: "get_learning", Description: "Retrieve a structured learning by id.", Category: "projects"},
|
{Name: "get_learning", Description: "Retrieve a structured learning by id.", Category: "projects"},
|
||||||
{Name: "list_learnings", Description: "List structured learnings with optional project, category, area, status, priority, tag, and text filters.", Category: "projects"},
|
{Name: "list_learnings", Description: "List structured learnings with optional project, category, area, status, priority, tag, and text filters.", Category: "projects"},
|
||||||
|
{Name: "link_thought_learning", Description: "Create or update a lightweight explicit association between a raw thought and a curated learning.", Category: "projects"},
|
||||||
|
{Name: "unlink_thought_learning", Description: "Remove a lightweight explicit association between a thought and a learning.", Category: "projects"},
|
||||||
|
{Name: "get_thought_learnings", Description: "List curated learnings explicitly associated with a thought.", Category: "projects"},
|
||||||
|
{Name: "get_learning_thoughts", Description: "List raw source thoughts explicitly associated with a learning.", Category: "projects"},
|
||||||
|
|
||||||
// plans
|
// plans
|
||||||
{Name: "create_plan", Description: "Create a structured plan with status, priority, owner, due date, and optional project link.", Category: "plans"},
|
{Name: "create_plan", Description: "Create a structured plan with status, priority, owner, due date, and optional project link.", Category: "plans"},
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"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 {
|
func newStreamableTestClient(t *testing.T) *mcp.ClientSession {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
|||||||
@@ -212,5 +212,8 @@ func streamableTestToolSet() ToolSet {
|
|||||||
Describe: new(tools.DescribeTool),
|
Describe: new(tools.DescribeTool),
|
||||||
Learnings: new(tools.LearningsTool),
|
Learnings: new(tools.LearningsTool),
|
||||||
Plans: new(tools.PlansTool),
|
Plans: new(tools.PlansTool),
|
||||||
|
ProjectPersonas: new(tools.ProjectPersonasTool),
|
||||||
|
WorldModel: new(tools.WorldModelTool),
|
||||||
|
ThoughtLearningLinks: new(tools.ThoughtLearningLinksTool),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ func Normalize(in thoughttypes.ThoughtMetadata, capture config.CaptureConfig) th
|
|||||||
Type: normalizeType(in.Type),
|
Type: normalizeType(in.Type),
|
||||||
Source: normalizeSource(in.Source),
|
Source: normalizeSource(in.Source),
|
||||||
Attachments: normalizeAttachments(in.Attachments),
|
Attachments: normalizeAttachments(in.Attachments),
|
||||||
|
Webhook: normalizeWebhook(in.Webhook),
|
||||||
MetadataStatus: normalizeMetadataStatus(in.MetadataStatus),
|
MetadataStatus: normalizeMetadataStatus(in.MetadataStatus),
|
||||||
MetadataUpdatedAt: strings.TrimSpace(in.MetadataUpdatedAt),
|
MetadataUpdatedAt: strings.TrimSpace(in.MetadataUpdatedAt),
|
||||||
MetadataLastAttemptedAt: strings.TrimSpace(in.MetadataLastAttemptedAt),
|
MetadataLastAttemptedAt: strings.TrimSpace(in.MetadataLastAttemptedAt),
|
||||||
@@ -201,10 +202,31 @@ func Merge(base, patch thoughttypes.ThoughtMetadata, capture config.CaptureConfi
|
|||||||
if len(patch.Attachments) > 0 {
|
if len(patch.Attachments) > 0 {
|
||||||
merged.Attachments = append(append([]thoughttypes.ThoughtAttachment{}, merged.Attachments...), patch.Attachments...)
|
merged.Attachments = append(append([]thoughttypes.ThoughtAttachment{}, merged.Attachments...), patch.Attachments...)
|
||||||
}
|
}
|
||||||
|
if patch.Webhook != nil {
|
||||||
|
merged.Webhook = patch.Webhook
|
||||||
|
}
|
||||||
|
|
||||||
return Normalize(merged, capture)
|
return Normalize(merged, capture)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeWebhook(value *thoughttypes.WebhookMetadata) *thoughttypes.WebhookMetadata {
|
||||||
|
if value == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := &thoughttypes.WebhookMetadata{
|
||||||
|
ReceivedAt: strings.TrimSpace(value.ReceivedAt),
|
||||||
|
IDempotencyKey: strings.TrimSpace(value.IDempotencyKey),
|
||||||
|
ExternalID: strings.TrimSpace(value.ExternalID),
|
||||||
|
}
|
||||||
|
if len(value.SourceMetadata) > 0 {
|
||||||
|
out.SourceMetadata = value.SourceMetadata
|
||||||
|
}
|
||||||
|
if out.ReceivedAt == "" && out.IDempotencyKey == "" && out.ExternalID == "" && len(out.SourceMetadata) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
func normalizeAttachments(values []thoughttypes.ThoughtAttachment) []thoughttypes.ThoughtAttachment {
|
func normalizeAttachments(values []thoughttypes.ThoughtAttachment) []thoughttypes.ThoughtAttachment {
|
||||||
seen := make(map[string]struct{}, len(values))
|
seen := make(map[string]struct{}, len(values))
|
||||||
result := make([]thoughttypes.ThoughtAttachment, 0, len(values))
|
result := make([]thoughttypes.ThoughtAttachment, 0, len(values))
|
||||||
|
|||||||
@@ -234,6 +234,7 @@ func (db *DB) GetPersona(ctx context.Context, name string, detail bool, override
|
|||||||
Name: s.Name,
|
Name: s.Name,
|
||||||
Description: s.Description,
|
Description: s.Description,
|
||||||
Tags: s.Tags,
|
Tags: s.Tags,
|
||||||
|
Override: s.Override,
|
||||||
}
|
}
|
||||||
if detail {
|
if detail {
|
||||||
entry.Content = s.Content
|
entry.Content = s.Content
|
||||||
@@ -341,6 +342,7 @@ func (db *DB) GetPersonaManifest(ctx context.Context, name string) (ext.PersonaM
|
|||||||
ID: s.ID,
|
ID: s.ID,
|
||||||
Name: s.Name,
|
Name: s.Name,
|
||||||
Description: s.Description,
|
Description: s.Description,
|
||||||
|
Override: s.Override,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
for _, g := range guardrails {
|
for _, g := range guardrails {
|
||||||
@@ -567,11 +569,12 @@ func (db *DB) RemovePersonaPart(ctx context.Context, personaName, partName strin
|
|||||||
// Persona-Skill links
|
// 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, `
|
_, err := db.pool.Exec(ctx, `
|
||||||
insert into agent_persona_skills (persona_id, skill_id)
|
insert into agent_persona_skills (persona_id, skill_id, override)
|
||||||
values ($1, $2) on conflict do nothing
|
values ($1, $2, $3)
|
||||||
`, personaID, skillID)
|
on conflict (persona_id, skill_id) do update set override = excluded.override
|
||||||
|
`, personaID, skillID, override)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("add persona skill: %w", err)
|
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) {
|
func (db *DB) listPersonaSkills(ctx context.Context, personaID int64) ([]AgentSkillRow, error) {
|
||||||
rows, err := db.pool.Query(ctx, `
|
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
|
from agent_skills s
|
||||||
join agent_persona_skills aps on aps.skill_id = s.id
|
join agent_persona_skills aps on aps.skill_id = s.id
|
||||||
where aps.persona_id = $1
|
where aps.persona_id = $1
|
||||||
@@ -1073,7 +1076,7 @@ func (db *DB) listPersonaSkills(ctx context.Context, personaID int64) ([]AgentSk
|
|||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var s AgentSkillRow
|
var s AgentSkillRow
|
||||||
var tags []string
|
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)
|
return nil, fmt.Errorf("scan persona skill: %w", err)
|
||||||
}
|
}
|
||||||
s.Tags = nilToEmptyStrings(tags)
|
s.Tags = nilToEmptyStrings(tags)
|
||||||
@@ -1139,6 +1142,7 @@ type AgentSkillRow struct {
|
|||||||
Description string
|
Description string
|
||||||
Content string
|
Content string
|
||||||
Tags []string
|
Tags []string
|
||||||
|
Override bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentGuardrailRow struct {
|
type AgentGuardrailRow struct {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -116,6 +116,24 @@ func scanSkill(row skillScanner) (ext.AgentSkill, error) {
|
|||||||
}, nil
|
}, 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) {
|
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)
|
row := db.pool.QueryRow(ctx, `select `+skillSelectCols+` from agent_skills where id = $1`, id)
|
||||||
s, err := scanSkill(row)
|
s, err := scanSkill(row)
|
||||||
@@ -278,12 +296,12 @@ func (db *DB) GetGuardrail(ctx context.Context, id int64) (ext.AgentGuardrail, e
|
|||||||
|
|
||||||
// Project Skills
|
// 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, `
|
_, err := db.pool.Exec(ctx, `
|
||||||
insert into project_skills (project_id, skill_id)
|
insert into project_skills (project_id, skill_id, override)
|
||||||
values ($1, $2)
|
values ($1, $2, $3)
|
||||||
on conflict do nothing
|
on conflict (project_id, skill_id) do update set override = excluded.override
|
||||||
`, projectID, skillID)
|
`, projectID, skillID, override)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("add project skill: %w", err)
|
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) {
|
func (db *DB) ListProjectSkills(ctx context.Context, projectID int64) ([]ext.AgentSkill, error) {
|
||||||
rows, err := db.pool.Query(ctx, `
|
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
|
from agent_skills s
|
||||||
join project_skills ps on ps.skill_id = s.id
|
join project_skills ps on ps.skill_id = s.id
|
||||||
where ps.project_id = $1
|
where ps.project_id = $1
|
||||||
@@ -318,10 +338,13 @@ func (db *DB) ListProjectSkills(ctx context.Context, projectID int64) ([]ext.Age
|
|||||||
|
|
||||||
var skills []ext.AgentSkill
|
var skills []ext.AgentSkill
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
s, err := scanSkill(rows)
|
var s ext.AgentSkill
|
||||||
if err != nil {
|
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)
|
return nil, fmt.Errorf("scan project skill: %w", err)
|
||||||
}
|
}
|
||||||
|
normalizeSkillSlices(&s)
|
||||||
skills = append(skills, s)
|
skills = append(skills, s)
|
||||||
}
|
}
|
||||||
return skills, rows.Err()
|
return skills, rows.Err()
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
|
||||||
|
"git.warky.dev/wdevs/amcs/internal/generatedmodels"
|
||||||
|
thoughttypes "git.warky.dev/wdevs/amcs/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LinkThoughtLearning creates or updates an association between a thought (by GUID) and a learning (by numeric ID).
|
||||||
|
// If the pair already exists, the relation label is updated.
|
||||||
|
func (db *DB) LinkThoughtLearning(ctx context.Context, thoughtGUID uuid.UUID, learningID int64, relation string) error {
|
||||||
|
_, err := db.pool.Exec(ctx, `
|
||||||
|
insert into thought_learning_links (thought_id, learning_id, relation)
|
||||||
|
select t.id, $2, $3
|
||||||
|
from thoughts t
|
||||||
|
where t.guid = $1
|
||||||
|
on conflict (thought_id, learning_id) do update set relation = excluded.relation
|
||||||
|
`, thoughtGUID, learningID, relation)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("link thought learning: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnlinkThoughtLearning removes the association between a thought (by GUID) and a learning (by numeric ID).
|
||||||
|
func (db *DB) UnlinkThoughtLearning(ctx context.Context, thoughtGUID uuid.UUID, learningID int64) error {
|
||||||
|
_, err := db.pool.Exec(ctx, `
|
||||||
|
delete from thought_learning_links
|
||||||
|
where thought_id = (select id from thoughts where guid = $1)
|
||||||
|
and learning_id = $2
|
||||||
|
`, thoughtGUID, learningID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unlink thought learning: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLinkedLearnings returns all learnings explicitly associated with a thought (by GUID).
|
||||||
|
func (db *DB) GetLinkedLearnings(ctx context.Context, thoughtGUID uuid.UUID) ([]thoughttypes.LinkedLearning, error) {
|
||||||
|
rows, err := db.pool.Query(ctx, `
|
||||||
|
select l.id, l.guid, l.summary, l.details, l.category, l.area, l.status, l.priority, l.confidence,
|
||||||
|
l.action_required, l.source_type, l.source_ref, l.project_id, l.related_thought_id,
|
||||||
|
l.related_skill_id, l.reviewed_by, l.reviewed_at, l.duplicate_of_learning_id,
|
||||||
|
l.supersedes_learning_id, l.tags::text[], l.created_at, l.updated_at,
|
||||||
|
tll.relation, tll.created_at as linked_at
|
||||||
|
from thought_learning_links tll
|
||||||
|
join learnings l on l.id = tll.learning_id
|
||||||
|
where tll.thought_id = (select id from thoughts where guid = $1)
|
||||||
|
order by tll.created_at desc
|
||||||
|
`, thoughtGUID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("query linked learnings: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
items := make([]thoughttypes.LinkedLearning, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
var tags []string
|
||||||
|
var linked thoughttypes.LinkedLearning
|
||||||
|
var m generatedmodels.ModelPublicLearnings
|
||||||
|
|
||||||
|
if err := rows.Scan(
|
||||||
|
&m.ID,
|
||||||
|
&m.GUID,
|
||||||
|
&m.Summary,
|
||||||
|
&m.Details,
|
||||||
|
&m.Category,
|
||||||
|
&m.Area,
|
||||||
|
&m.Status,
|
||||||
|
&m.Priority,
|
||||||
|
&m.Confidence,
|
||||||
|
&m.ActionRequired,
|
||||||
|
&m.SourceType,
|
||||||
|
&m.SourceRef,
|
||||||
|
&m.ProjectID,
|
||||||
|
&m.RelatedThoughtID,
|
||||||
|
&m.RelatedSkillID,
|
||||||
|
&m.ReviewedBy,
|
||||||
|
&m.ReviewedAt,
|
||||||
|
&m.DuplicateOfLearningID,
|
||||||
|
&m.SupersedesLearningID,
|
||||||
|
&tags,
|
||||||
|
&m.CreatedAt,
|
||||||
|
&m.UpdatedAt,
|
||||||
|
&linked.Relation,
|
||||||
|
&linked.CreatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, fmt.Errorf("scan linked learning: %w", err)
|
||||||
|
}
|
||||||
|
linked.Learning = learningFromModel(m, tags)
|
||||||
|
items = append(items, linked)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("iterate linked learnings: %w", err)
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLinkedThoughtsForLearning returns all thoughts explicitly associated with a learning (by numeric ID).
|
||||||
|
func (db *DB) GetLinkedThoughtsForLearning(ctx context.Context, learningID int64) ([]thoughttypes.LinkedLearningThought, error) {
|
||||||
|
rows, err := db.pool.Query(ctx, `
|
||||||
|
select t.id, t.guid, t.content, t.metadata, t.project_id, t.archived_at, t.created_at, t.updated_at,
|
||||||
|
tll.relation, tll.created_at as linked_at
|
||||||
|
from thought_learning_links tll
|
||||||
|
join thoughts t on t.id = tll.thought_id
|
||||||
|
where tll.learning_id = $1
|
||||||
|
order by tll.created_at desc
|
||||||
|
`, learningID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("query linked thoughts for learning: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
items := make([]thoughttypes.LinkedLearningThought, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
var linked thoughttypes.LinkedLearningThought
|
||||||
|
var m generatedmodels.ModelPublicThoughts
|
||||||
|
|
||||||
|
if err := rows.Scan(
|
||||||
|
&m.ID,
|
||||||
|
&m.GUID,
|
||||||
|
&m.Content,
|
||||||
|
&m.Metadata,
|
||||||
|
&m.ProjectID,
|
||||||
|
&m.ArchivedAt,
|
||||||
|
&m.CreatedAt,
|
||||||
|
&m.UpdatedAt,
|
||||||
|
&linked.Relation,
|
||||||
|
&linked.CreatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, fmt.Errorf("scan linked thought for learning: %w", err)
|
||||||
|
}
|
||||||
|
thought, err := thoughtFromModel(m)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("map linked thought for learning: %w", err)
|
||||||
|
}
|
||||||
|
linked.Thought = thought
|
||||||
|
items = append(items, linked)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("iterate linked thoughts for learning: %w", err)
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
@@ -68,6 +68,22 @@ func (db *DB) InsertThought(ctx context.Context, thought thoughttypes.Thought, e
|
|||||||
return created, nil
|
return created, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (db *DB) GetThoughtByWebhookIDempotencyKey(ctx context.Context, key string) (thoughttypes.Thought, error) {
|
||||||
|
row := db.pool.QueryRow(ctx, `
|
||||||
|
select id, guid, content, metadata, project_id, archived_at, created_at, updated_at
|
||||||
|
from thoughts
|
||||||
|
where metadata->'webhook'->>'idempotency_key' = $1
|
||||||
|
order by created_at desc
|
||||||
|
limit 1
|
||||||
|
`, strings.TrimSpace(key))
|
||||||
|
|
||||||
|
var model generatedmodels.ModelPublicThoughts
|
||||||
|
if err := row.Scan(&model.ID, &model.GUID, &model.Content, &model.Metadata, &model.ProjectID, &model.ArchivedAt, &model.CreatedAt, &model.UpdatedAt); err != nil {
|
||||||
|
return thoughttypes.Thought{}, err
|
||||||
|
}
|
||||||
|
return thoughtFromModel(model)
|
||||||
|
}
|
||||||
|
|
||||||
func (db *DB) SearchThoughts(ctx context.Context, embedding []float32, embeddingModel string, threshold float64, limit int, filter map[string]any) ([]thoughttypes.SearchResult, error) {
|
func (db *DB) SearchThoughts(ctx context.Context, embedding []float32, embeddingModel string, threshold float64, limit int, filter map[string]any) ([]thoughttypes.SearchResult, error) {
|
||||||
filterJSON, err := json.Marshal(filter)
|
filterJSON, err := json.Marshal(filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -422,6 +422,7 @@ func (t *AgentPersonasTool) RemovePersonaPart(ctx context.Context, _ *mcp.CallTo
|
|||||||
type AddPersonaSkillInput struct {
|
type AddPersonaSkillInput struct {
|
||||||
PersonaID int64 `json:"persona_id" jsonschema:"persona id"`
|
PersonaID int64 `json:"persona_id" jsonschema:"persona id"`
|
||||||
SkillID int64 `json:"skill_id" jsonschema:"agent skill id to link"`
|
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 {
|
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) {
|
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{}, err
|
||||||
}
|
}
|
||||||
return nil, AddPersonaSkillOutput{PersonaID: in.PersonaID, SkillID: in.SkillID}, nil
|
return nil, AddPersonaSkillOutput{PersonaID: in.PersonaID, SkillID: in.SkillID}, nil
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ type ProjectContextOutput struct {
|
|||||||
Project thoughttypes.Project `json:"project"`
|
Project thoughttypes.Project `json:"project"`
|
||||||
Context string `json:"context"`
|
Context string `json:"context"`
|
||||||
Items []ContextItem `json:"items"`
|
Items []ContextItem `json:"items"`
|
||||||
|
RetrievalMode string `json:"retrieval_mode,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewContextTool(db *store.DB, embeddings *ai.EmbeddingRunner, search config.SearchConfig, sessions *session.ActiveProjects) *ContextTool {
|
func NewContextTool(db *store.DB, embeddings *ai.EmbeddingRunner, search config.SearchConfig, sessions *session.ActiveProjects) *ContextTool {
|
||||||
@@ -70,12 +71,14 @@ func (t *ContextTool) Handle(ctx context.Context, req *mcp.CallToolRequest, in P
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var retrievalMode string
|
||||||
query := strings.TrimSpace(in.Query)
|
query := strings.TrimSpace(in.Query)
|
||||||
if query != "" {
|
if query != "" {
|
||||||
semantic, err := semanticSearch(ctx, t.store, t.embeddings, t.search, query, limit, t.search.DefaultThreshold, &project.NumericID, nil)
|
semantic, mode, err := semanticSearch(ctx, t.store, t.embeddings, t.search, query, limit, t.search.DefaultThreshold, &project.NumericID, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ProjectContextOutput{}, err
|
return nil, ProjectContextOutput{}, err
|
||||||
}
|
}
|
||||||
|
retrievalMode = mode
|
||||||
for _, result := range semantic {
|
for _, result := range semantic {
|
||||||
key := fmt.Sprint(result.ID)
|
key := fmt.Sprint(result.ID)
|
||||||
if _, ok := seen[key]; ok {
|
if _, ok := seen[key]; ok {
|
||||||
@@ -103,5 +106,6 @@ func (t *ContextTool) Handle(ctx context.Context, req *mcp.CallToolRequest, in P
|
|||||||
Project: *project,
|
Project: *project,
|
||||||
Context: contextBlock,
|
Context: contextBlock,
|
||||||
Items: items,
|
Items: items,
|
||||||
|
RetrievalMode: retrievalMode,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ type RelatedThought struct {
|
|||||||
|
|
||||||
type RelatedOutput struct {
|
type RelatedOutput struct {
|
||||||
Related []RelatedThought `json:"related"`
|
Related []RelatedThought `json:"related"`
|
||||||
|
RetrievalMode string `json:"retrieval_mode,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLinksTool(db *store.DB, embeddings *ai.EmbeddingRunner, search config.SearchConfig) *LinksTool {
|
func NewLinksTool(db *store.DB, embeddings *ai.EmbeddingRunner, search config.SearchConfig) *LinksTool {
|
||||||
@@ -117,11 +118,13 @@ func (t *LinksTool) Related(ctx context.Context, _ *mcp.CallToolRequest, in Rela
|
|||||||
includeSemantic = *in.IncludeSemantic
|
includeSemantic = *in.IncludeSemantic
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var retrievalMode string
|
||||||
if includeSemantic {
|
if includeSemantic {
|
||||||
semantic, err := semanticSearch(ctx, t.store, t.embeddings, t.search, thought.Content, t.search.DefaultLimit, t.search.DefaultThreshold, thought.ProjectID, &thought.GUID)
|
semantic, mode, err := semanticSearch(ctx, t.store, t.embeddings, t.search, thought.Content, t.search.DefaultLimit, t.search.DefaultThreshold, thought.ProjectID, &thought.GUID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, RelatedOutput{}, err
|
return nil, RelatedOutput{}, err
|
||||||
}
|
}
|
||||||
|
retrievalMode = mode
|
||||||
for _, item := range semantic {
|
for _, item := range semantic {
|
||||||
key := fmt.Sprint(item.ID)
|
key := fmt.Sprint(item.ID)
|
||||||
if _, ok := seen[key]; ok {
|
if _, ok := seen[key]; ok {
|
||||||
@@ -138,5 +141,5 @@ func (t *LinksTool) Related(ctx context.Context, _ *mcp.CallToolRequest, in Rela
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, RelatedOutput{Related: related}, nil
|
return nil, RelatedOutput{Related: related, RetrievalMode: retrievalMode}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ type RecallInput struct {
|
|||||||
type RecallOutput struct {
|
type RecallOutput struct {
|
||||||
Context string `json:"context"`
|
Context string `json:"context"`
|
||||||
Items []ContextItem `json:"items"`
|
Items []ContextItem `json:"items"`
|
||||||
|
RetrievalMode string `json:"retrieval_mode,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRecallTool(db *store.DB, embeddings *ai.EmbeddingRunner, search config.SearchConfig, sessions *session.ActiveProjects) *RecallTool {
|
func NewRecallTool(db *store.DB, embeddings *ai.EmbeddingRunner, search config.SearchConfig, sessions *session.ActiveProjects) *RecallTool {
|
||||||
@@ -53,7 +54,7 @@ func (t *RecallTool) Handle(ctx context.Context, req *mcp.CallToolRequest, in Re
|
|||||||
projectID = &project.NumericID
|
projectID = &project.NumericID
|
||||||
}
|
}
|
||||||
|
|
||||||
semantic, err := semanticSearch(ctx, t.store, t.embeddings, t.search, query, limit, t.search.DefaultThreshold, projectID, nil)
|
semantic, mode, err := semanticSearch(ctx, t.store, t.embeddings, t.search, query, limit, t.search.DefaultThreshold, projectID, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, RecallOutput{}, err
|
return nil, RecallOutput{}, err
|
||||||
}
|
}
|
||||||
@@ -102,5 +103,6 @@ func (t *RecallTool) Handle(ctx context.Context, req *mcp.CallToolRequest, in Re
|
|||||||
return nil, RecallOutput{
|
return nil, RecallOutput{
|
||||||
Context: formatContextBlock(header, lines),
|
Context: formatContextBlock(header, lines),
|
||||||
Items: items,
|
Items: items,
|
||||||
|
RetrievalMode: mode,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,10 +11,16 @@ import (
|
|||||||
thoughttypes "git.warky.dev/wdevs/amcs/internal/types"
|
thoughttypes "git.warky.dev/wdevs/amcs/internal/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
RetrievalModeSemantic = "semantic"
|
||||||
|
RetrievalModeText = "text"
|
||||||
|
)
|
||||||
|
|
||||||
// semanticSearch runs vector similarity search if embeddings exist for the
|
// semanticSearch runs vector similarity search if embeddings exist for the
|
||||||
// primary embedding model in the given scope, otherwise falls back to Postgres
|
// primary embedding model in the given scope, otherwise falls back to Postgres
|
||||||
// full-text search. Search always uses the primary model so query vectors
|
// full-text search. Search always uses the primary model so query vectors
|
||||||
// match rows stored under the primary model name.
|
// match rows stored under the primary model name.
|
||||||
|
// It returns the results and the retrieval mode used ("semantic" or "text").
|
||||||
func semanticSearch(
|
func semanticSearch(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
db *store.DB,
|
db *store.DB,
|
||||||
@@ -25,20 +31,22 @@ func semanticSearch(
|
|||||||
threshold float64,
|
threshold float64,
|
||||||
projectID *int64,
|
projectID *int64,
|
||||||
excludeID *uuid.UUID,
|
excludeID *uuid.UUID,
|
||||||
) ([]thoughttypes.SearchResult, error) {
|
) ([]thoughttypes.SearchResult, string, error) {
|
||||||
model := embeddings.PrimaryModel()
|
model := embeddings.PrimaryModel()
|
||||||
hasEmbeddings, err := db.HasEmbeddingsForModel(ctx, model, projectID)
|
hasEmbeddings, err := db.HasEmbeddingsForModel(ctx, model, projectID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasEmbeddings {
|
if hasEmbeddings {
|
||||||
embedding, err := embeddings.EmbedPrimary(ctx, query)
|
embedding, err := embeddings.EmbedPrimary(ctx, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
return db.SearchSimilarThoughts(ctx, embedding, model, threshold, limit, projectID, excludeID)
|
results, err := db.SearchSimilarThoughts(ctx, embedding, model, threshold, limit, projectID, excludeID)
|
||||||
|
return results, RetrievalModeSemantic, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return db.SearchThoughtsText(ctx, query, limit, projectID, excludeID)
|
results, err := db.SearchThoughtsText(ctx, query, limit, projectID, excludeID)
|
||||||
|
return results, RetrievalModeText, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package tools
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestRetrievalModeConstants(t *testing.T) {
|
||||||
|
if RetrievalModeSemantic != "semantic" {
|
||||||
|
t.Fatalf("RetrievalModeSemantic = %q, want %q", RetrievalModeSemantic, "semantic")
|
||||||
|
}
|
||||||
|
if RetrievalModeText != "text" {
|
||||||
|
t.Fatalf("RetrievalModeText = %q, want %q", RetrievalModeText, "text")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSearchOutputIncludesRetrievalMode(t *testing.T) {
|
||||||
|
out := SearchOutput{RetrievalMode: RetrievalModeSemantic}
|
||||||
|
if out.RetrievalMode != RetrievalModeSemantic {
|
||||||
|
t.Fatalf("SearchOutput.RetrievalMode = %q, want %q", out.RetrievalMode, RetrievalModeSemantic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRelatedOutputIncludesRetrievalMode(t *testing.T) {
|
||||||
|
out := RelatedOutput{RetrievalMode: RetrievalModeText}
|
||||||
|
if out.RetrievalMode != RetrievalModeText {
|
||||||
|
t.Fatalf("RelatedOutput.RetrievalMode = %q, want %q", out.RetrievalMode, RetrievalModeText)
|
||||||
|
}
|
||||||
|
// retrieval_mode is omitempty — empty string means semantic search was skipped
|
||||||
|
out2 := RelatedOutput{}
|
||||||
|
if out2.RetrievalMode != "" {
|
||||||
|
t.Fatalf("RelatedOutput.RetrievalMode = %q, want empty when include_semantic is false", out2.RetrievalMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSummarizeOutputIncludesRetrievalModeOnlyWhenQueryUsed(t *testing.T) {
|
||||||
|
withQuery := SummarizeOutput{Summary: "s", Count: 1, RetrievalMode: RetrievalModeSemantic}
|
||||||
|
if withQuery.RetrievalMode != RetrievalModeSemantic {
|
||||||
|
t.Fatalf("SummarizeOutput.RetrievalMode = %q, want %q", withQuery.RetrievalMode, RetrievalModeSemantic)
|
||||||
|
}
|
||||||
|
withoutQuery := SummarizeOutput{Summary: "s", Count: 1}
|
||||||
|
if withoutQuery.RetrievalMode != "" {
|
||||||
|
t.Fatalf("SummarizeOutput.RetrievalMode = %q, want empty when no query", withoutQuery.RetrievalMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProjectContextOutputIncludesRetrievalModeOnlyWhenQueryUsed(t *testing.T) {
|
||||||
|
withQuery := ProjectContextOutput{RetrievalMode: RetrievalModeText}
|
||||||
|
if withQuery.RetrievalMode != RetrievalModeText {
|
||||||
|
t.Fatalf("ProjectContextOutput.RetrievalMode = %q, want %q", withQuery.RetrievalMode, RetrievalModeText)
|
||||||
|
}
|
||||||
|
withoutQuery := ProjectContextOutput{}
|
||||||
|
if withoutQuery.RetrievalMode != "" {
|
||||||
|
t.Fatalf("ProjectContextOutput.RetrievalMode = %q, want empty when no query", withoutQuery.RetrievalMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRecallOutputIncludesRetrievalMode(t *testing.T) {
|
||||||
|
out := RecallOutput{RetrievalMode: RetrievalModeSemantic}
|
||||||
|
if out.RetrievalMode != RetrievalModeSemantic {
|
||||||
|
t.Fatalf("RecallOutput.RetrievalMode = %q, want %q", out.RetrievalMode, RetrievalModeSemantic)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ type SearchInput struct {
|
|||||||
|
|
||||||
type SearchOutput struct {
|
type SearchOutput struct {
|
||||||
Results []thoughttypes.SearchResult `json:"results"`
|
Results []thoughttypes.SearchResult `json:"results"`
|
||||||
|
RetrievalMode string `json:"retrieval_mode,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSearchTool(db *store.DB, embeddings *ai.EmbeddingRunner, search config.SearchConfig, sessions *session.ActiveProjects) *SearchTool {
|
func NewSearchTool(db *store.DB, embeddings *ai.EmbeddingRunner, search config.SearchConfig, sessions *session.ActiveProjects) *SearchTool {
|
||||||
@@ -55,10 +56,10 @@ func (t *SearchTool) Handle(ctx context.Context, req *mcp.CallToolRequest, in Se
|
|||||||
_ = t.store.TouchProject(ctx, project.NumericID)
|
_ = t.store.TouchProject(ctx, project.NumericID)
|
||||||
}
|
}
|
||||||
|
|
||||||
results, err := semanticSearch(ctx, t.store, t.embeddings, t.search, query, limit, threshold, projectID, nil)
|
results, mode, err := semanticSearch(ctx, t.store, t.embeddings, t.search, query, limit, threshold, projectID, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, SearchOutput{}, err
|
return nil, SearchOutput{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, SearchOutput{Results: results}, nil
|
return nil, SearchOutput{Results: results, RetrievalMode: mode}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -261,6 +261,7 @@ func (t *SkillsTool) GetGuardrail(ctx context.Context, _ *mcp.CallToolRequest, i
|
|||||||
type AddProjectSkillInput struct {
|
type AddProjectSkillInput struct {
|
||||||
Project string `json:"project,omitempty" jsonschema:"project name or id (uses active project if omitted)"`
|
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"`
|
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 {
|
type AddProjectSkillOutput struct {
|
||||||
@@ -273,7 +274,7 @@ func (t *SkillsTool) AddProjectSkill(ctx context.Context, req *mcp.CallToolReque
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, AddProjectSkillOutput{}, err
|
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{}, err
|
||||||
}
|
}
|
||||||
return nil, AddProjectSkillOutput{ProjectID: project.NumericID, SkillID: in.SkillID}, nil
|
return nil, AddProjectSkillOutput{ProjectID: project.NumericID, SkillID: in.SkillID}, nil
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ type SummarizeInput struct {
|
|||||||
type SummarizeOutput struct {
|
type SummarizeOutput struct {
|
||||||
Summary string `json:"summary"`
|
Summary string `json:"summary"`
|
||||||
Count int `json:"count"`
|
Count int `json:"count"`
|
||||||
|
RetrievalMode string `json:"retrieval_mode,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSummarizeTool(db *store.DB, embeddings *ai.EmbeddingRunner, metadata *ai.MetadataRunner, search config.SearchConfig, sessions *session.ActiveProjects) *SummarizeTool {
|
func NewSummarizeTool(db *store.DB, embeddings *ai.EmbeddingRunner, metadata *ai.MetadataRunner, search config.SearchConfig, sessions *session.ActiveProjects) *SummarizeTool {
|
||||||
@@ -47,15 +48,17 @@ func (t *SummarizeTool) Handle(ctx context.Context, req *mcp.CallToolRequest, in
|
|||||||
lines := make([]string, 0, limit)
|
lines := make([]string, 0, limit)
|
||||||
count := 0
|
count := 0
|
||||||
|
|
||||||
|
var retrievalMode string
|
||||||
if query != "" {
|
if query != "" {
|
||||||
var projectID *int64
|
var projectID *int64
|
||||||
if project != nil {
|
if project != nil {
|
||||||
projectID = &project.NumericID
|
projectID = &project.NumericID
|
||||||
}
|
}
|
||||||
results, err := semanticSearch(ctx, t.store, t.embeddings, t.search, query, limit, t.search.DefaultThreshold, projectID, nil)
|
results, mode, err := semanticSearch(ctx, t.store, t.embeddings, t.search, query, limit, t.search.DefaultThreshold, projectID, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, SummarizeOutput{}, err
|
return nil, SummarizeOutput{}, err
|
||||||
}
|
}
|
||||||
|
retrievalMode = mode
|
||||||
for i, result := range results {
|
for i, result := range results {
|
||||||
lines = append(lines, thoughtContextLine(i, result.Content, result.Metadata, result.Similarity))
|
lines = append(lines, thoughtContextLine(i, result.Content, result.Metadata, result.Similarity))
|
||||||
}
|
}
|
||||||
@@ -85,5 +88,5 @@ func (t *SummarizeTool) Handle(ctx context.Context, req *mcp.CallToolRequest, in
|
|||||||
_ = t.store.TouchProject(ctx, project.NumericID)
|
_ = t.store.TouchProject(ctx, project.NumericID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, SummarizeOutput{Summary: summary, Count: count}, nil
|
return nil, SummarizeOutput{Summary: summary, Count: count, RetrievalMode: retrievalMode}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||||
|
|
||||||
|
"git.warky.dev/wdevs/amcs/internal/store"
|
||||||
|
thoughttypes "git.warky.dev/wdevs/amcs/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ThoughtLearningLinksTool struct {
|
||||||
|
store *store.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
type LinkThoughtLearningInput struct {
|
||||||
|
ThoughtID string `json:"thought_id" jsonschema:"UUID of the thought to link"`
|
||||||
|
LearningID int64 `json:"learning_id" jsonschema:"numeric id of the learning to link"`
|
||||||
|
Relation string `json:"relation,omitempty" jsonschema:"relationship label, e.g. source, derived_from, related; defaults to source"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LinkThoughtLearningOutput struct {
|
||||||
|
Linked bool `json:"linked"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnlinkThoughtLearningInput struct {
|
||||||
|
ThoughtID string `json:"thought_id" jsonschema:"UUID of the thought"`
|
||||||
|
LearningID int64 `json:"learning_id" jsonschema:"numeric id of the learning"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnlinkThoughtLearningOutput struct {
|
||||||
|
Unlinked bool `json:"unlinked"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetThoughtLearningsInput struct {
|
||||||
|
ThoughtID string `json:"thought_id" jsonschema:"UUID of the thought"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetThoughtLearningsOutput struct {
|
||||||
|
Learnings []thoughttypes.LinkedLearning `json:"learnings"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetLearningThoughtsInput struct {
|
||||||
|
LearningID int64 `json:"learning_id" jsonschema:"numeric id of the learning"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetLearningThoughtsOutput struct {
|
||||||
|
Thoughts []thoughttypes.LinkedLearningThought `json:"thoughts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewThoughtLearningLinksTool(db *store.DB) *ThoughtLearningLinksTool {
|
||||||
|
return &ThoughtLearningLinksTool{store: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ThoughtLearningLinksTool) Link(ctx context.Context, _ *mcp.CallToolRequest, in LinkThoughtLearningInput) (*mcp.CallToolResult, LinkThoughtLearningOutput, error) {
|
||||||
|
if err := t.ensureConfigured(); err != nil {
|
||||||
|
return nil, LinkThoughtLearningOutput{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
thoughtGUID, err := parseUUID(in.ThoughtID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, LinkThoughtLearningOutput{}, err
|
||||||
|
}
|
||||||
|
if in.LearningID <= 0 {
|
||||||
|
return nil, LinkThoughtLearningOutput{}, errRequiredField("learning_id")
|
||||||
|
}
|
||||||
|
relation := strings.TrimSpace(in.Relation)
|
||||||
|
if relation == "" {
|
||||||
|
relation = "source"
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := t.store.GetThought(ctx, thoughtGUID); err != nil {
|
||||||
|
return nil, LinkThoughtLearningOutput{}, errEntityNotFound("thought", "thought_id", in.ThoughtID)
|
||||||
|
}
|
||||||
|
if _, err := t.store.GetLearning(ctx, in.LearningID); err != nil {
|
||||||
|
return nil, LinkThoughtLearningOutput{}, errEntityNotFound("learning", "learning_id", strconv.FormatInt(in.LearningID, 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := t.store.LinkThoughtLearning(ctx, thoughtGUID, in.LearningID, relation); err != nil {
|
||||||
|
return nil, LinkThoughtLearningOutput{}, err
|
||||||
|
}
|
||||||
|
return nil, LinkThoughtLearningOutput{Linked: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ThoughtLearningLinksTool) Unlink(ctx context.Context, _ *mcp.CallToolRequest, in UnlinkThoughtLearningInput) (*mcp.CallToolResult, UnlinkThoughtLearningOutput, error) {
|
||||||
|
if err := t.ensureConfigured(); err != nil {
|
||||||
|
return nil, UnlinkThoughtLearningOutput{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
thoughtGUID, err := parseUUID(in.ThoughtID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, UnlinkThoughtLearningOutput{}, err
|
||||||
|
}
|
||||||
|
if in.LearningID <= 0 {
|
||||||
|
return nil, UnlinkThoughtLearningOutput{}, errRequiredField("learning_id")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := t.store.UnlinkThoughtLearning(ctx, thoughtGUID, in.LearningID); err != nil {
|
||||||
|
return nil, UnlinkThoughtLearningOutput{}, err
|
||||||
|
}
|
||||||
|
return nil, UnlinkThoughtLearningOutput{Unlinked: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ThoughtLearningLinksTool) GetThoughtLearnings(ctx context.Context, _ *mcp.CallToolRequest, in GetThoughtLearningsInput) (*mcp.CallToolResult, GetThoughtLearningsOutput, error) {
|
||||||
|
if err := t.ensureConfigured(); err != nil {
|
||||||
|
return nil, GetThoughtLearningsOutput{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
thoughtGUID, err := parseUUID(in.ThoughtID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, GetThoughtLearningsOutput{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := t.store.GetThought(ctx, thoughtGUID); err != nil {
|
||||||
|
return nil, GetThoughtLearningsOutput{}, errEntityNotFound("thought", "thought_id", in.ThoughtID)
|
||||||
|
}
|
||||||
|
|
||||||
|
learnings, err := t.store.GetLinkedLearnings(ctx, thoughtGUID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, GetThoughtLearningsOutput{}, err
|
||||||
|
}
|
||||||
|
return nil, GetThoughtLearningsOutput{Learnings: learnings}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ThoughtLearningLinksTool) GetLearningThoughts(ctx context.Context, _ *mcp.CallToolRequest, in GetLearningThoughtsInput) (*mcp.CallToolResult, GetLearningThoughtsOutput, error) {
|
||||||
|
if err := t.ensureConfigured(); err != nil {
|
||||||
|
return nil, GetLearningThoughtsOutput{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if in.LearningID <= 0 {
|
||||||
|
return nil, GetLearningThoughtsOutput{}, errRequiredField("learning_id")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := t.store.GetLearning(ctx, in.LearningID); err != nil {
|
||||||
|
return nil, GetLearningThoughtsOutput{}, errEntityNotFound("learning", "learning_id", strconv.FormatInt(in.LearningID, 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
thoughts, err := t.store.GetLinkedThoughtsForLearning(ctx, in.LearningID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, GetLearningThoughtsOutput{}, err
|
||||||
|
}
|
||||||
|
return nil, GetLearningThoughtsOutput{Thoughts: thoughts}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ThoughtLearningLinksTool) ensureConfigured() error {
|
||||||
|
if t == nil || t.store == nil {
|
||||||
|
return errInvalidInput("thought learning links tool is not configured")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -109,6 +109,7 @@ type PersonaSkillEntry struct {
|
|||||||
Description string `json:"description,omitempty"`
|
Description string `json:"description,omitempty"`
|
||||||
Content string `json:"content,omitempty"` // only when detail=true
|
Content string `json:"content,omitempty"` // only when detail=true
|
||||||
Tags []string `json:"tags"`
|
Tags []string `json:"tags"`
|
||||||
|
Override bool `json:"override,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PersonaGuardrailEntry struct {
|
type PersonaGuardrailEntry struct {
|
||||||
@@ -168,6 +169,12 @@ type ManifestSkill struct {
|
|||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description string `json:"description,omitempty"`
|
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 {
|
type ManifestGuardrail struct {
|
||||||
|
|||||||
@@ -229,6 +229,7 @@ type AgentSkill struct {
|
|||||||
DomainTags []string `json:"domain_tags"`
|
DomainTags []string `json:"domain_tags"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
Override bool `json:"override,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentGuardrail struct {
|
type AgentGuardrail struct {
|
||||||
|
|||||||
@@ -39,3 +39,22 @@ type LinkedThought struct {
|
|||||||
Direction string `json:"direction"`
|
Direction string `json:"direction"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ThoughtLearningLink struct {
|
||||||
|
ThoughtID int64 `json:"thought_id"`
|
||||||
|
LearningID int64 `json:"learning_id"`
|
||||||
|
Relation string `json:"relation"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LinkedLearning struct {
|
||||||
|
Learning Learning `json:"learning"`
|
||||||
|
Relation string `json:"relation"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LinkedLearningThought struct {
|
||||||
|
Thought Thought `json:"thought"`
|
||||||
|
Relation string `json:"relation"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,12 +14,20 @@ type ThoughtMetadata struct {
|
|||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Source string `json:"source"`
|
Source string `json:"source"`
|
||||||
Attachments []ThoughtAttachment `json:"attachments,omitempty"`
|
Attachments []ThoughtAttachment `json:"attachments,omitempty"`
|
||||||
|
Webhook *WebhookMetadata `json:"webhook,omitempty"`
|
||||||
MetadataStatus string `json:"metadata_status,omitempty"`
|
MetadataStatus string `json:"metadata_status,omitempty"`
|
||||||
MetadataUpdatedAt string `json:"metadata_updated_at,omitempty"`
|
MetadataUpdatedAt string `json:"metadata_updated_at,omitempty"`
|
||||||
MetadataLastAttemptedAt string `json:"metadata_last_attempted_at,omitempty"`
|
MetadataLastAttemptedAt string `json:"metadata_last_attempted_at,omitempty"`
|
||||||
MetadataError string `json:"metadata_error,omitempty"`
|
MetadataError string `json:"metadata_error,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type WebhookMetadata struct {
|
||||||
|
ReceivedAt string `json:"received_at"`
|
||||||
|
IDempotencyKey string `json:"idempotency_key,omitempty"`
|
||||||
|
ExternalID string `json:"external_id,omitempty"`
|
||||||
|
SourceMetadata map[string]any `json:"source_metadata,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type ThoughtAttachment struct {
|
type ThoughtAttachment struct {
|
||||||
FileID uuid.UUID `json:"file_id"`
|
FileID uuid.UUID `json:"file_id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
|||||||
@@ -5,4 +5,7 @@ import _ "embed"
|
|||||||
var (
|
var (
|
||||||
//go:embed memory.md
|
//go:embed memory.md
|
||||||
MemoryInstructions []byte
|
MemoryInstructions []byte
|
||||||
|
|
||||||
|
//go:embed world_model_intro.md
|
||||||
|
WorldModelIntro []byte
|
||||||
)
|
)
|
||||||
|
|||||||
+16
-3
@@ -28,7 +28,20 @@ Use AMCS as memory with two scopes:
|
|||||||
|
|
||||||
At the very start of any session with AMCS:
|
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
|
## Project Session Startup
|
||||||
|
|
||||||
@@ -51,7 +64,7 @@ Do not abandon the project scope or retry without a project. The project simply
|
|||||||
## Project Memory Rules
|
## Project Memory Rules
|
||||||
|
|
||||||
- Use project memory for code decisions, architecture, TODOs, debugging findings, and context specific to the current repo or workstream.
|
- 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.
|
- 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 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.
|
- 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
|
## 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`.
|
||||||
|
|||||||
+99
@@ -1,5 +1,104 @@
|
|||||||
# AMCS TODO
|
# 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)
|
## 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.
|
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.
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -33,6 +33,13 @@ CREATE SEQUENCE IF NOT EXISTS public.identity_agent_persona_skills_id
|
|||||||
START 1
|
START 1
|
||||||
CACHE 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
|
CREATE SEQUENCE IF NOT EXISTS public.identity_agent_persona_guardrails_id
|
||||||
INCREMENT 1
|
INCREMENT 1
|
||||||
MINVALUE 1
|
MINVALUE 1
|
||||||
@@ -75,7 +82,7 @@ CREATE SEQUENCE IF NOT EXISTS public.identity_arc_stage_parts_id
|
|||||||
START 1
|
START 1
|
||||||
CACHE 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
|
INCREMENT 1
|
||||||
MINVALUE 1
|
MINVALUE 1
|
||||||
MAXVALUE 9223372036854775807
|
MAXVALUE 9223372036854775807
|
||||||
@@ -247,10 +254,19 @@ CREATE TABLE IF NOT EXISTS public.agent_persona_parts (
|
|||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS public.agent_persona_skills (
|
CREATE TABLE IF NOT EXISTS public.agent_persona_skills (
|
||||||
id bigserial NOT NULL,
|
id bigserial NOT NULL,
|
||||||
|
override boolean NOT NULL DEFAULT false,
|
||||||
persona_id bigint NOT NULL,
|
persona_id bigint NOT NULL,
|
||||||
skill_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 (
|
CREATE TABLE IF NOT EXISTS public.agent_persona_guardrails (
|
||||||
guardrail_id bigint NOT NULL,
|
guardrail_id bigint NOT NULL,
|
||||||
id bigserial 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 (
|
CREATE TABLE IF NOT EXISTS public.project_skills (
|
||||||
created_at timestamptz NOT NULL DEFAULT now(),
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
id bigserial NOT NULL,
|
id bigserial NOT NULL,
|
||||||
|
override boolean NOT NULL DEFAULT false,
|
||||||
project_id bigint NOT NULL,
|
project_id bigint NOT NULL,
|
||||||
skill_id bigint NOT NULL
|
skill_id bigint NOT NULL
|
||||||
);
|
);
|
||||||
@@ -872,6 +889,19 @@ BEGIN
|
|||||||
END;
|
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 $$
|
DO $$
|
||||||
BEGIN
|
BEGIN
|
||||||
IF NOT EXISTS (
|
IF NOT EXISTS (
|
||||||
@@ -898,6 +928,71 @@ BEGIN
|
|||||||
END;
|
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 $$
|
DO $$
|
||||||
BEGIN
|
BEGIN
|
||||||
IF NOT EXISTS (
|
IF NOT EXISTS (
|
||||||
@@ -3173,6 +3268,19 @@ BEGIN
|
|||||||
END;
|
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 $$
|
DO $$
|
||||||
BEGIN
|
BEGIN
|
||||||
IF NOT EXISTS (
|
IF NOT EXISTS (
|
||||||
@@ -3896,6 +4004,29 @@ BEGIN
|
|||||||
END;
|
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 $$
|
DO $$
|
||||||
DECLARE
|
DECLARE
|
||||||
current_type text;
|
current_type text;
|
||||||
@@ -3942,6 +4073,121 @@ BEGIN
|
|||||||
END;
|
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 $$
|
DO $$
|
||||||
DECLARE
|
DECLARE
|
||||||
current_type text;
|
current_type text;
|
||||||
@@ -7967,6 +8213,29 @@ BEGIN
|
|||||||
END;
|
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 $$
|
DO $$
|
||||||
DECLARE
|
DECLARE
|
||||||
current_type text;
|
current_type text;
|
||||||
@@ -8282,6 +8551,50 @@ BEGIN
|
|||||||
END;
|
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 $$
|
DO $$
|
||||||
DECLARE
|
DECLARE
|
||||||
current_pk_name text;
|
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
|
CREATE INDEX IF NOT EXISTS idx_agent_persona_skills_persona_id_skill_id
|
||||||
ON public.agent_persona_skills USING btree (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
|
CREATE INDEX IF NOT EXISTS idx_arc_stage_parts_stage_id_part_id
|
||||||
ON public.arc_stage_parts USING btree (stage_id, part_id);
|
ON public.arc_stage_parts USING btree (stage_id, part_id);
|
||||||
|
|
||||||
@@ -9772,6 +10088,38 @@ BEGIN
|
|||||||
END IF;
|
END IF;
|
||||||
END;
|
END;
|
||||||
$$;DO $$
|
$$;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
|
BEGIN
|
||||||
IF NOT EXISTS (
|
IF NOT EXISTS (
|
||||||
SELECT 1 FROM information_schema.table_constraints
|
SELECT 1 FROM information_schema.table_constraints
|
||||||
@@ -10425,6 +10773,25 @@ BEGIN
|
|||||||
END;
|
END;
|
||||||
$$;
|
$$;
|
||||||
DO $$
|
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
|
DECLARE
|
||||||
m_cnt bigint;
|
m_cnt bigint;
|
||||||
BEGIN
|
BEGIN
|
||||||
@@ -10927,5 +11294,6 @@ $$;
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
-- Many-to-many join table between thoughts and learnings.
|
||||||
|
-- Allows a learning to reference multiple source thoughts and a thought to
|
||||||
|
-- expose multiple related learnings, queryable in both directions.
|
||||||
|
|
||||||
|
CREATE SEQUENCE IF NOT EXISTS public.identity_thought_learning_links_id
|
||||||
|
INCREMENT 1
|
||||||
|
MINVALUE 1
|
||||||
|
MAXVALUE 9223372036854775807
|
||||||
|
START 1
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS public.thought_learning_links (
|
||||||
|
id bigint NOT NULL DEFAULT nextval('public.identity_thought_learning_links_id'),
|
||||||
|
thought_id bigint NOT NULL REFERENCES public.thoughts(id) ON DELETE CASCADE,
|
||||||
|
learning_id bigint NOT NULL REFERENCES public.learnings(id) ON DELETE CASCADE,
|
||||||
|
relation text NOT NULL DEFAULT 'source',
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
CONSTRAINT thought_learning_links_pkey PRIMARY KEY (id),
|
||||||
|
CONSTRAINT thought_learning_links_unique UNIQUE (thought_id, learning_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_thought_learning_links_thought_id
|
||||||
|
ON public.thought_learning_links (thought_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_thought_learning_links_learning_id
|
||||||
|
ON public.thought_learning_links (learning_id);
|
||||||
@@ -43,6 +43,7 @@ Table agent_persona_skills {
|
|||||||
id bigserial [pk]
|
id bigserial [pk]
|
||||||
persona_id bigint [not null, ref: > agent_personas.id]
|
persona_id bigint [not null, ref: > agent_personas.id]
|
||||||
skill_id bigint [not null, ref: > agent_skills.id]
|
skill_id bigint [not null, ref: > agent_skills.id]
|
||||||
|
override boolean [not null, default: false]
|
||||||
|
|
||||||
indexes {
|
indexes {
|
||||||
(persona_id, skill_id) [uk]
|
(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 {
|
Table agent_persona_guardrails {
|
||||||
id bigserial [pk]
|
id bigserial [pk]
|
||||||
persona_id bigint [not null, ref: > agent_personas.id]
|
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_parts.part_id > agent_parts.id [delete: cascade]
|
||||||
Ref: agent_persona_skills.persona_id > agent_personas.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: 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.persona_id > agent_personas.id [delete: cascade]
|
||||||
Ref: agent_persona_guardrails.guardrail_id > agent_guardrails.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]
|
Ref: agent_persona_traits.persona_id > agent_personas.id [delete: cascade]
|
||||||
|
|||||||
@@ -32,6 +32,20 @@ Table thought_links {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Table thought_learning_links {
|
||||||
|
id bigserial [pk]
|
||||||
|
thought_id bigint [not null, ref: > thoughts.id]
|
||||||
|
learning_id bigint [not null, ref: > learnings.id]
|
||||||
|
relation text [not null, default: `'source'`]
|
||||||
|
created_at timestamptz [not null, default: `now()`]
|
||||||
|
|
||||||
|
indexes {
|
||||||
|
(thought_id, learning_id) [unique]
|
||||||
|
thought_id
|
||||||
|
learning_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Table embeddings {
|
Table embeddings {
|
||||||
id bigserial [pk]
|
id bigserial [pk]
|
||||||
guid uuid [unique, not null, default: `gen_random_uuid()`]
|
guid uuid [unique, not null, default: `gen_random_uuid()`]
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ Table project_skills {
|
|||||||
id bigserial [pk]
|
id bigserial [pk]
|
||||||
project_id bigint [not null, ref: > projects.id]
|
project_id bigint [not null, ref: > projects.id]
|
||||||
skill_id bigint [not null, ref: > agent_skills.id]
|
skill_id bigint [not null, ref: > agent_skills.id]
|
||||||
|
override boolean [not null, default: false]
|
||||||
created_at timestamptz [not null, default: `now()`]
|
created_at timestamptz [not null, default: `now()`]
|
||||||
|
|
||||||
indexes {
|
indexes {
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ mkdir -p "${out_dir}"
|
|||||||
--from-list "${schema_list}" \
|
--from-list "${schema_list}" \
|
||||||
--to bun \
|
--to bun \
|
||||||
--to-path "${out_dir}" \
|
--to-path "${out_dir}" \
|
||||||
--package generatedmodels
|
--package generatedmodels \
|
||||||
|
--types resolvespec
|
||||||
|
|
||||||
"${relspec_bin}" templ \
|
"${relspec_bin}" templ \
|
||||||
--from dbml \
|
--from dbml \
|
||||||
@@ -52,6 +53,7 @@ mkdir -p "${out_dir}"
|
|||||||
--template "${resolve_spec_template}" \
|
--template "${resolve_spec_template}" \
|
||||||
--output "${resolve_spec_models_file}"
|
--output "${resolve_spec_models_file}"
|
||||||
|
|
||||||
|
|
||||||
# relspec currently emits a few files with unused fmt imports; strip only when fmt is unused.
|
# relspec currently emits a few files with unused fmt imports; strip only when fmt is unused.
|
||||||
for file in "${out_dir}"/*.go; do
|
for file in "${out_dir}"/*.go; do
|
||||||
sed -i 's/fmt.Sprintf("%d", m.ID)/fmt.Sprintf("%v", m.ID)/g' "${file}"
|
sed -i 's/fmt.Sprintf("%d", m.ID)/fmt.Sprintf("%v", m.ID)/g' "${file}"
|
||||||
|
|||||||
+2
-1
@@ -143,7 +143,8 @@
|
|||||||
unique_tools: raw?.metrics?.unique_tools ?? 0,
|
unique_tools: raw?.metrics?.unique_tools ?? 0,
|
||||||
top_ips: Array.isArray(raw?.metrics?.top_ips) ? raw.metrics.top_ips : [],
|
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_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) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -98,12 +98,6 @@
|
|||||||
activeCard = id;
|
activeCard = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCardKeydown(event: KeyboardEvent, id: string) {
|
|
||||||
if (event.key === "Enter" || event.key === " ") {
|
|
||||||
event.preventDefault();
|
|
||||||
setActiveCard(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -205,17 +199,15 @@
|
|||||||
|
|
||||||
<div class="mt-4 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
<div class="mt-4 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||||
{#each intelligenceCards as card}
|
{#each intelligenceCards as card}
|
||||||
<article
|
<button
|
||||||
class={`rounded-xl border p-4 transition ${
|
type="button"
|
||||||
|
class={`w-full rounded-xl border p-4 text-left transition ${
|
||||||
activeCard === card.id
|
activeCard === card.id
|
||||||
? "border-cyan-300/40 bg-cyan-400/[0.08] shadow-lg shadow-cyan-950/30"
|
? "border-cyan-300/40 bg-cyan-400/[0.08] shadow-lg shadow-cyan-950/30"
|
||||||
: "border-white/10 bg-white/[0.03] hover:border-white/20 hover:bg-white/[0.05]"
|
: "border-white/10 bg-white/[0.03] hover:border-white/20 hover:bg-white/[0.05]"
|
||||||
}`}
|
}`}
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
aria-pressed={activeCard === card.id}
|
aria-pressed={activeCard === card.id}
|
||||||
onclick={() => setActiveCard(card.id)}
|
onclick={() => setActiveCard(card.id)}
|
||||||
onkeydown={(event) => handleCardKeydown(event, card.id)}
|
|
||||||
>
|
>
|
||||||
<p class={`text-xs uppercase tracking-[0.16em] ${card.accentClass}`}>
|
<p class={`text-xs uppercase tracking-[0.16em] ${card.accentClass}`}>
|
||||||
{card.title}
|
{card.title}
|
||||||
@@ -233,7 +225,7 @@
|
|||||||
: ""}
|
: ""}
|
||||||
{/each}
|
{/each}
|
||||||
</p>
|
</p>
|
||||||
</article>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+20
-13
@@ -115,32 +115,39 @@ func GetHeadSpecHeaders() []string {
|
|||||||
|
|
||||||
// SetCORSHeaders sets CORS headers on a response writer
|
// SetCORSHeaders sets CORS headers on a response writer
|
||||||
func SetCORSHeaders(w ResponseWriter, r Request, config CORSConfig) {
|
func SetCORSHeaders(w ResponseWriter, r Request, config CORSConfig) {
|
||||||
// Set allowed origins
|
// Reflect the request origin; fall back to wildcard only when no origin is present
|
||||||
// if len(config.AllowedOrigins) > 0 {
|
origin := r.Header("Origin")
|
||||||
// w.SetHeader("Access-Control-Allow-Origin", strings.Join(config.AllowedOrigins, ", "))
|
if origin == "" {
|
||||||
// }
|
origin = "*"
|
||||||
|
} else {
|
||||||
// Todo origin list parsing
|
// Vary must be set so caches don't serve one origin's response to another
|
||||||
w.SetHeader("Access-Control-Allow-Origin", "*")
|
httpW := w.UnderlyingResponseWriter()
|
||||||
|
httpW.Header().Set("Vary", "Origin")
|
||||||
|
}
|
||||||
|
w.SetHeader("Access-Control-Allow-Origin", origin)
|
||||||
|
|
||||||
// Set allowed methods
|
// Set allowed methods
|
||||||
if len(config.AllowedMethods) > 0 {
|
if len(config.AllowedMethods) > 0 {
|
||||||
w.SetHeader("Access-Control-Allow-Methods", strings.Join(config.AllowedMethods, ", "))
|
w.SetHeader("Access-Control-Allow-Methods", strings.Join(config.AllowedMethods, ", "))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set allowed headers
|
// Reflect the preflight request headers when present; otherwise use the explicit config list
|
||||||
// if len(config.AllowedHeaders) > 0 {
|
requestedHeaders := r.Header("Access-Control-Request-Headers")
|
||||||
// w.SetHeader("Access-Control-Allow-Headers", strings.Join(config.AllowedHeaders, ", "))
|
if requestedHeaders != "" {
|
||||||
// }
|
w.SetHeader("Access-Control-Allow-Headers", requestedHeaders)
|
||||||
w.SetHeader("Access-Control-Allow-Headers", "*")
|
} else if len(config.AllowedHeaders) > 0 {
|
||||||
|
w.SetHeader("Access-Control-Allow-Headers", strings.Join(config.AllowedHeaders, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
// Set max age
|
// Set max age
|
||||||
if config.MaxAge > 0 {
|
if config.MaxAge > 0 {
|
||||||
w.SetHeader("Access-Control-Max-Age", fmt.Sprintf("%d", config.MaxAge))
|
w.SetHeader("Access-Control-Max-Age", fmt.Sprintf("%d", config.MaxAge))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow credentials
|
// Allow credentials only when a specific origin is reflected (not wildcard)
|
||||||
|
if origin != "*" {
|
||||||
w.SetHeader("Access-Control-Allow-Credentials", "true")
|
w.SetHeader("Access-Control-Allow-Credentials", "true")
|
||||||
|
}
|
||||||
|
|
||||||
// Expose headers that clients can read
|
// Expose headers that clients can read
|
||||||
exposeHeaders := config.AllowedHeaders
|
exposeHeaders := config.AllowedHeaders
|
||||||
|
|||||||
+4
@@ -50,6 +50,10 @@ type ServerInstanceConfig struct {
|
|||||||
// GZIP enables GZIP compression middleware
|
// GZIP enables GZIP compression middleware
|
||||||
GZIP bool `mapstructure:"gzip"`
|
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)
|
// TLS/HTTPS configuration options (mutually exclusive)
|
||||||
// Option 1: Provide certificate and key files directly
|
// Option 1: Provide certificate and key files directly
|
||||||
SSLCert string `mapstructure:"ssl_cert"`
|
SSLCert string `mapstructure:"ssl_cert"`
|
||||||
|
|||||||
+1
@@ -50,6 +50,7 @@ func New(opts ...DialectOption) *Dialect {
|
|||||||
feature.Output |
|
feature.Output |
|
||||||
feature.OffsetFetch |
|
feature.OffsetFetch |
|
||||||
feature.FKDefaultOnAction |
|
feature.FKDefaultOnAction |
|
||||||
|
feature.Merge |
|
||||||
feature.UpdateFromTable |
|
feature.UpdateFromTable |
|
||||||
feature.MSSavepoint
|
feature.MSSavepoint
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -2,5 +2,5 @@ package mssqldialect
|
|||||||
|
|
||||||
// Version is the current release version.
|
// Version is the current release version.
|
||||||
func Version() string {
|
func Version() string {
|
||||||
return "1.2.16"
|
return "1.2.18"
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -57,8 +57,10 @@ func New(opts ...DialectOption) *Dialect {
|
|||||||
feature.CompositeIn |
|
feature.CompositeIn |
|
||||||
feature.FKDefaultOnAction |
|
feature.FKDefaultOnAction |
|
||||||
feature.DeleteReturning |
|
feature.DeleteReturning |
|
||||||
|
feature.Merge |
|
||||||
feature.MergeReturning |
|
feature.MergeReturning |
|
||||||
feature.AlterColumnExists
|
feature.AlterColumnExists |
|
||||||
|
feature.CreateIndexIfNotExists
|
||||||
|
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
opt(d)
|
opt(d)
|
||||||
|
|||||||
+5
-5
@@ -38,17 +38,17 @@ func (in *Inspector) Inspect(ctx context.Context) (sqlschema.Database, error) {
|
|||||||
|
|
||||||
exclude := in.ExcludeTables
|
exclude := in.ExcludeTables
|
||||||
if len(exclude) == 0 {
|
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{""}
|
exclude = []string{""}
|
||||||
}
|
}
|
||||||
|
|
||||||
var tables []*InformationSchemaTable
|
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
|
return dbSchema, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var fks []*ForeignKey
|
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
|
return dbSchema, err
|
||||||
}
|
}
|
||||||
dbSchema.ForeignKeys = make(map[sqlschema.ForeignKey]string, len(fks))
|
dbSchema.ForeignKeys = make(map[sqlschema.ForeignKey]string, len(fks))
|
||||||
@@ -165,7 +165,7 @@ type PrimaryKey struct {
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// sqlInspectTables retrieves all user-defined tables in the selected schema.
|
// 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 = `
|
sqlInspectTables = `
|
||||||
SELECT
|
SELECT
|
||||||
"t".table_schema,
|
"t".table_schema,
|
||||||
@@ -258,7 +258,7 @@ ORDER BY "table_schema", "table_name", "column_name"
|
|||||||
`
|
`
|
||||||
|
|
||||||
// sqlInspectForeignKeys get FK definitions for user-defined tables.
|
// 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 = `
|
sqlInspectForeignKeys = `
|
||||||
WITH
|
WITH
|
||||||
"schemas" AS (
|
"schemas" AS (
|
||||||
|
|||||||
+1
-1
@@ -2,5 +2,5 @@ package pgdialect
|
|||||||
|
|
||||||
// Version is the current release version.
|
// Version is the current release version.
|
||||||
func Version() string {
|
func Version() string {
|
||||||
return "1.2.16"
|
return "1.2.18"
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -42,7 +42,8 @@ func New(opts ...DialectOption) *Dialect {
|
|||||||
feature.AutoIncrement |
|
feature.AutoIncrement |
|
||||||
feature.CompositeIn |
|
feature.CompositeIn |
|
||||||
feature.FKDefaultOnAction |
|
feature.FKDefaultOnAction |
|
||||||
feature.DeleteReturning
|
feature.DeleteReturning |
|
||||||
|
feature.CreateIndexIfNotExists
|
||||||
|
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
opt(d)
|
opt(d)
|
||||||
|
|||||||
+1
-1
@@ -2,5 +2,5 @@ package sqlitedialect
|
|||||||
|
|
||||||
// Version is the current release version.
|
// Version is the current release version.
|
||||||
func Version() string {
|
func Version() string {
|
||||||
return "1.2.16"
|
return "1.2.18"
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+4
-4
@@ -1,7 +1,7 @@
|
|||||||
# github.com/beorn7/perks v1.0.1
|
# github.com/beorn7/perks v1.0.1
|
||||||
## explicit; go 1.11
|
## explicit; go 1.11
|
||||||
github.com/beorn7/perks/quantile
|
github.com/beorn7/perks/quantile
|
||||||
# github.com/bitechdev/ResolveSpec v1.1.15
|
# github.com/bitechdev/ResolveSpec v1.1.24
|
||||||
## explicit; go 1.25.7
|
## explicit; go 1.25.7
|
||||||
github.com/bitechdev/ResolveSpec/pkg/cache
|
github.com/bitechdev/ResolveSpec/pkg/cache
|
||||||
github.com/bitechdev/ResolveSpec/pkg/common
|
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
|
||||||
github.com/uptrace/bun/migrate/sqlschema
|
github.com/uptrace/bun/migrate/sqlschema
|
||||||
github.com/uptrace/bun/schema
|
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
|
## explicit; go 1.24.0
|
||||||
github.com/uptrace/bun/dialect/mssqldialect
|
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
|
## explicit; go 1.24.0
|
||||||
github.com/uptrace/bun/dialect/pgdialect
|
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
|
## explicit; go 1.24.0
|
||||||
github.com/uptrace/bun/dialect/sqlitedialect
|
github.com/uptrace/bun/dialect/sqlitedialect
|
||||||
# github.com/uptrace/bun/driver/pgdriver v1.1.12
|
# github.com/uptrace/bun/driver/pgdriver v1.1.12
|
||||||
|
|||||||
Reference in New Issue
Block a user