feat(ai-chat): agent roles — parallel impl (agent 180 / model B) #22

Closed
Ghost wants to merge 4 commits from feat/ai-agent-roles-agent180 into develop

Context — model-comparison pair

This is the second of two parallel implementations of the same backlog feature (ai-agent-roles), produced by a different model for side-by-side comparison. The other implementation is PR #11 (feat/ai-agent-roles, agent 227). Both branch from common ancestor c8af6376; they share no commits.

Key differences vs PR #11 (agent 227):

  • This branch has no unit tests (listed as a follow-up); PR #11 ships ~674 lines of specs.
  • Model-override validation is inline in ai.service.ts / ai-settings.service.ts (PR #11 uses a dedicated role-model-config.ts).
  • DTOs split into create/update/id files (PR #11 uses one combined dto).
  • Client queries/services under features/workspace/ (PR #11 under features/ai-chat/).

What

Workspace-admin presets that customize the AI agent's system-prompt persona and optionally the model, attached to a chat at creation. A role changes ONLY instructions + optional model; the toolset stays full, so the CASL security boundary is unchanged.

Backend

  • Migration 20260620T150000-ai-agent-roles: ai_agent_roles table + ai_chats.role_id (ON DELETE SET NULL).
  • New repo/service/controller at /workspace/ai-agent-roles. LIST (picker view) open to all workspace members; create/update/delete admin-only. Picker view omits instructions/modelConfig so they never leak to non-admins.
  • buildSystemPrompt: optional roleInstructions REPLACES the admin persona (priority: role > admin > default). Non-removable SAFETY_FRAMEWORK always appended.
  • AiChatService.stream: persists roleId on the first turn; later turns read it from the chat row, never the request body. Instructions stay applied even if the role is later disabled or soft-deleted.
  • AiService.getChatModel(workspaceId, override?): same-driver overrides reuse the workspace key; cross-driver (openai/gemini) loads alternate creds and throws a clean 503 if missing; cross-driver ollama rejected with a clear message. Controller resolves the role model BEFORE res.hijack().

Client

  • New-chat picker (Mantine Select) with default 'Universal assistant' (roleId null); roleId sent only when starting a new chat.
  • Role badge in chat window header and conversation list.
  • Settings -> AI: 'Agent roles' management section (add/edit/delete + enable toggle + optional model override).

Verification

  • tsc --noEmit server + client clean; pnpm --filter client test 58/58; server tests unchanged vs baseline (6 pre-existing DI failures in unrelated modules).

Known follow-ups

  • Unit tests for the new code (PR #11 already has these).
  • Seed presets; per-driver base URLs for cross-driver ollama; dedupe chat/role lookups between controller and stream().
## Context — model-comparison pair This is the **second of two parallel implementations** of the same backlog feature (ai-agent-roles), produced by a different model for side-by-side comparison. The other implementation is **PR #11** (`feat/ai-agent-roles`, agent 227). Both branch from common ancestor `c8af6376`; they share no commits. Key differences vs PR #11 (agent 227): - **This branch has no unit tests** (listed as a follow-up); PR #11 ships ~674 lines of specs. - Model-override validation is inline in `ai.service.ts` / `ai-settings.service.ts` (PR #11 uses a dedicated `role-model-config.ts`). - DTOs split into `create`/`update`/`id` files (PR #11 uses one combined dto). - Client queries/services under `features/workspace/` (PR #11 under `features/ai-chat/`). ## What Workspace-admin presets that customize the AI agent's system-prompt persona and optionally the model, attached to a chat at creation. A role changes ONLY instructions + optional model; the toolset stays full, so the CASL security boundary is unchanged. ## Backend - Migration `20260620T150000-ai-agent-roles`: `ai_agent_roles` table + `ai_chats.role_id` (ON DELETE SET NULL). - New repo/service/controller at `/workspace/ai-agent-roles`. LIST (picker view) open to all workspace members; create/update/delete admin-only. Picker view omits `instructions`/`modelConfig` so they never leak to non-admins. - `buildSystemPrompt`: optional `roleInstructions` REPLACES the admin persona (priority: role > admin > default). Non-removable `SAFETY_FRAMEWORK` always appended. - `AiChatService.stream`: persists `roleId` on the first turn; later turns read it from the chat row, never the request body. Instructions stay applied even if the role is later disabled or soft-deleted. - `AiService.getChatModel(workspaceId, override?)`: same-driver overrides reuse the workspace key; cross-driver (openai/gemini) loads alternate creds and throws a clean 503 if missing; cross-driver ollama rejected with a clear message. Controller resolves the role model BEFORE `res.hijack()`. ## Client - New-chat picker (Mantine Select) with default 'Universal assistant' (roleId null); roleId sent only when starting a new chat. - Role badge in chat window header and conversation list. - Settings -> AI: 'Agent roles' management section (add/edit/delete + enable toggle + optional model override). ## Verification - `tsc --noEmit` server + client clean; `pnpm --filter client test` 58/58; server tests unchanged vs baseline (6 pre-existing DI failures in unrelated modules). ## Known follow-ups - Unit tests for the new code (PR #11 already has these). - Seed presets; per-driver base URLs for cross-driver ollama; dedupe chat/role lookups between controller and stream().
Ghost added 4 commits 2026-06-20 16:03:44 +03:00
Two feature plans grounded in the current develop:
- search-language-morphology-plan.md: make the FTS text-search config
  selectable (env SEARCH_TS_CONFIG, whitelist english|russian|simple|ru_en;
  recommend ru_en for the RU/EN wiki). Documents every hardcoded 'english'
  touchpoint (pages.tsv trigger, page_embeddings.fts generated column,
  attachments.tsv, search.service.ts, hybrid lexical CTE), the DDL-baked
  config constraint, reindex strategy, and the regconfig SQL-injection guard.
- hybrid-search-general-plan.md: expose the existing pgvector/RRF hybrid
  search (today agent-only via searchPages) on the user-facing /search, the
  UI, and the MCP search tool, reusing page-embedding.repo.hybridSearch with
  identical CASL/permission post-filtering and lexical fallback.
Record outdated-deps and security-audit findings for the fork as of
2026-06-20 (pnpm outdated -r + pnpm audit --prod): 162 outdated entries,
50 major-behind, 51 vulnerabilities (16 high).

Key finding: pnpm.overrides pin several packages to versions flagged by
the audit (ws, undici, tmp, hono, protobufjs, dompurify) — cheapest fix
is bumping the pins. Also flags direct-dep highs (@nestjs/platform-fastify
auth middleware bypass, nodemailer, form-data, react-router-dom),
risky majors to schedule separately (Mantine9/React19, Hocuspocus 4,
CASL 7, TypeScript 6, zod 4, stripe), the deprecated @types/form-data,
and @types/node drift across the workspace.
Add docs/history-diff-perf-plan.md: deep-dive into the page-history
inline diff performance problem and a phased redesign.

- Root causes: O(K·D) recreateTransform (rfc6902 full-doc rebuild per op),
  full recompute on the "Highlight changes" toggle, a second full TipTap
  instance, all synchronous on the main thread.
- Fix: drop recreateTransform; diff directly via prosemirror-changeset
  (getReplaceStep + ChangeSet.addSteps/computeDiff), keeping the existing
  decoration contract for visual parity.
- Split the diff useEffect so the toggle no longer re-diffs.
- Phased plan (P0 core, P1 large-doc guard + error handling, P2 worker),
  testing/parity strategy, risks and rollback.
Roles are workspace-admin presets that customize the AI agent's system-
prompt persona and, optionally, the model, attached to a chat at creation
time. Examples: a 'Proofreader' that only touches grammar, a 'Fact-
checker' that cites web sources. A role changes ONLY instructions and
( optional ) the model; the toolset stays full, so the security boundary
(CASL via the per-user loopback token) is unchanged.

Backend:
- Migration 20260620T150000-ai-agent-roles: ai_agent_roles table
  (workspace-scoped, soft-delete, model_config jsonb) + ai_chats.role_id
  (ON DELETE SET NULL).
- AiAgentRoleRepo / AiAgentRolesService / AiAgentRolesController at
  /workspace/ai-agent-roles. LIST (picker view) is open to all workspace
  members; create/update/delete are admin-only. The picker view omits
  instructions and model_config so they never leak to non-admins.
- buildSystemPrompt: optional roleInstructions REPLACES the admin persona
  (priority order: role > admin > default). The non-removable
  SAFETY_FRAMEWORK is always appended - a role cannot strip it.
- AiChatService.stream: persists roleId on first turn; subsequent turns
  read role_id from the chat row, never from the request body. The role's
  instructions are applied even if it was later disabled or soft-deleted
  (existing chats keep their persona).
- AiService.getChatModel accepts an optional override. Same-driver
  overrides reuse the workspace key; cross-driver (openai/gemini) loads
  alternate creds from ai_provider_credentials and throws a clean 503 if
  they are missing (no silent fallback). Cross-driver ollama is rejected
  with a clear message (no per-driver ollama base URL exists yet).
- Controller resolves the role model BEFORE res.hijack so misconfigured
  overrides return JSON 503, not a broken stream.

Client:
- New chat picker (Mantine Select) lists enabled roles, default
  'Universal assistant' (roleId null). The roleId is sent only when
  starting a new chat; existing chats show the role as a fixed badge.
- Role badge in the chat window header and conversation list.
- Settings -> AI: new 'Agent roles' management section mirrors the
  external MCP servers UI (add/edit/delete + enable toggle + optional
  model override). Form fields: name, emoji, description, instructions,
  model override (driver + chatModel), with a reminder that the safety
  framework is always appended.

Hardening after review:
- Empty-string roleId coerced to null on both client and server (picker
  'Universal assistant' option used to crash the uuid INSERT).
- New-chat insert validates picker-eligibility (enabled + not soft-deleted
  + workspace-scoped); ineligible ids silently fall back to null.
- findByCreator's role JOIN is workspace-scoped and every column ref is
  table-qualified (avoids Postgres ambiguous-column errors).
- getChatModelForRole applies the same picker-eligibility gate as stream
  on the new-chat path, so model and persona resolve from one source.
Ghost closed this pull request 2026-06-20 22:26:27 +03:00

Pull request closed

Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: vvzvlad/gitmost#22