From 6545498dc25b7bed38c8d3c5db1b1417927bc415 Mon Sep 17 00:00:00 2001 From: Till JS Date: Fri, 24 Apr 2026 16:36:20 +0200 Subject: [PATCH] =?UTF-8?q?feat(writing):=20agent.defaultWritingStyleId=20?= =?UTF-8?q?=E2=80=94=20M8=20persona-linkage=20follow-up?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agents can now pin a default writing style. When an AI-actor runs `create_draft` without an explicit styleId, the tool resolves to the agent's `defaultWritingStyleId` so e.g. a "Marketing-Agent" always drafts in the Corporate-Tone style and a "Memoir-Agent" in Memoir. - @mana/shared-ai: optional `defaultWritingStyleId?: string` added to the Agent interface (plaintext FK, format `preset:` or a custom WritingStyle uuid). No migration — existing rows stay undefined and the fallback path no-ops for them. - ai-agents store: field threaded through CreateAgentInput + AgentPatch + the create function's copy-list. `updateAgent` already deep-clones the patch so nothing else to change there. - ai-agents ListView: new "Writing" section in the agent detail panel with a StylePicker (reuses the writing module's component — Vorlagen + Meine Stile optgroups). Empty = kein Default. - writing/tools.ts: `resolveAgentDefaultStyle()` reads the current actor, guards `isAiActor`, loads the agent row, and returns its defaultWritingStyleId. Wired into `create_draft` as a fallback when `params.styleId` is missing. User-invoked calls skip the lookup — a human omitting styleId means "ad-hoc, no style", not "my default". `generate_draft_content` needs no change because the draft's styleId is already set at create time. 107 shared-ai tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../apps/web/src/lib/data/ai/agents/store.ts | 5 ++++ .../src/lib/modules/ai-agents/ListView.svelte | 16 +++++++++++ .../apps/web/src/lib/modules/writing/tools.ts | 28 +++++++++++++++++-- packages/shared-ai/src/agents/types.ts | 8 ++++++ 4 files changed, 55 insertions(+), 2 deletions(-) diff --git a/apps/mana/apps/web/src/lib/data/ai/agents/store.ts b/apps/mana/apps/web/src/lib/data/ai/agents/store.ts index 15393dd99..ef8c8a840 100644 --- a/apps/mana/apps/web/src/lib/data/ai/agents/store.ts +++ b/apps/mana/apps/web/src/lib/data/ai/agents/store.ts @@ -40,6 +40,8 @@ export interface CreateAgentInput { policy?: AiPolicy; maxTokensPerDay?: number; maxConcurrentMissions?: number; + scopeTagIds?: string[]; + defaultWritingStyleId?: string; state?: AgentState; } @@ -66,6 +68,8 @@ export async function createAgent(input: CreateAgentInput): Promise { avatar: input.avatar, systemPrompt: input.systemPrompt, memory: input.memory, + scopeTagIds: input.scopeTagIds, + defaultWritingStyleId: input.defaultWritingStyleId, policy: deepClone(input.policy ?? DEFAULT_AI_POLICY), maxTokensPerDay: input.maxTokensPerDay, maxConcurrentMissions: input.maxConcurrentMissions ?? 1, @@ -130,6 +134,7 @@ export interface AgentPatch { maxTokensPerDay?: number; maxConcurrentMissions?: number; scopeTagIds?: string[]; + defaultWritingStyleId?: string; state?: AgentState; } diff --git a/apps/mana/apps/web/src/lib/modules/ai-agents/ListView.svelte b/apps/mana/apps/web/src/lib/modules/ai-agents/ListView.svelte index 3ecacef41..e8433d8ab 100644 --- a/apps/mana/apps/web/src/lib/modules/ai-agents/ListView.svelte +++ b/apps/mana/apps/web/src/lib/modules/ai-agents/ListView.svelte @@ -37,6 +37,7 @@ import type { AiPolicy, PolicyDecision } from '@mana/shared-ai'; import { TagSelector } from '@mana/shared-ui'; import { useAllTags } from '@mana/shared-stores'; + import StylePicker from '$lib/modules/writing/components/StylePicker.svelte'; const agents = $derived(useAgents()); const allTags = $derived(useAllTags()); @@ -89,6 +90,7 @@ let editMaxConcurrent = $state(1); let editMaxTokensPerDay = $state(null); let editScopeTagIds = $state([]); + let editDefaultWritingStyleId = $state(null); let lastSyncedId = $state(null); let saveError = $state(null); let saving = $state(false); @@ -103,6 +105,7 @@ editMaxConcurrent = selected.maxConcurrentMissions; editMaxTokensPerDay = selected.maxTokensPerDay ?? null; editScopeTagIds = [...(selected.scopeTagIds ?? [])]; + editDefaultWritingStyleId = selected.defaultWritingStyleId ?? null; lastSyncedId = selected.id; saveError = null; } @@ -121,6 +124,7 @@ maxConcurrentMissions: editMaxConcurrent, maxTokensPerDay: editMaxTokensPerDay ?? undefined, scopeTagIds: editScopeTagIds.length > 0 ? editScopeTagIds : undefined, + defaultWritingStyleId: editDefaultWritingStyleId ?? undefined, }); } catch (err) { if (err instanceof DuplicateAgentNameError) { @@ -455,6 +459,18 @@ +
+

Writing

+

+ Default-Schreibstil, den der Agent beim Anlegen eines Drafts nutzt, wenn keiner explizit + übergeben wird. +

+ (editDefaultWritingStyleId = next)} + /> +
+
{#if saveError} {saveError} diff --git a/apps/mana/apps/web/src/lib/modules/writing/tools.ts b/apps/mana/apps/web/src/lib/modules/writing/tools.ts index 83ae0ccda..952fe1609 100644 --- a/apps/mana/apps/web/src/lib/modules/writing/tools.ts +++ b/apps/mana/apps/web/src/lib/modules/writing/tools.ts @@ -27,6 +27,8 @@ import { decryptRecords, VaultLockedError } from '$lib/data/crypto'; import { toDraft, toDraftVersion } from './queries'; import { STYLE_PRESETS } from './presets/styles'; import { writingStyleTable } from './collections'; +import { getCurrentActor, isAiActor } from '$lib/data/events'; +import { getAgent } from '$lib/data/ai/agents/store'; import type { LocalDraft, LocalDraftVersion, @@ -35,6 +37,23 @@ import type { DraftStatus, } from './types'; +/** + * When an AI actor is running a writing tool without an explicit + * styleId, fall back to the agent's `defaultWritingStyleId`. User- + * invoked calls skip this — a human passing no styleId means "ad-hoc, + * no style", not "use my default". + */ +async function resolveAgentDefaultStyle(): Promise { + const actor = getCurrentActor(); + if (!isAiActor(actor)) return null; + try { + const agent = await getAgent(actor.principalId); + return agent?.defaultWritingStyleId ?? null; + } catch { + return null; + } +} + const KINDS: DraftKind[] = [ 'blog', 'essay', @@ -266,11 +285,16 @@ export const writingTools: ModuleTool[] = [ const targetWordsRaw = typeof params.targetWords === 'number' ? Math.round(params.targetWords) : null; + const explicitStyleId = + typeof params.styleId === 'string' && params.styleId.length > 0 ? params.styleId : null; + // Persona-Linkage: AI actors inherit the agent's default style + // when they don't pass one; user-invoked calls do not — a human + // omitting styleId means "ad-hoc, no style". + const styleId = explicitStyleId ?? (await resolveAgentDefaultStyle()); const { draft } = await draftsStore.createDraft({ kind, title, - styleId: - typeof params.styleId === 'string' && params.styleId.length > 0 ? params.styleId : null, + styleId, briefing: { topic, audience: typeof params.audience === 'string' ? params.audience : null, diff --git a/packages/shared-ai/src/agents/types.ts b/packages/shared-ai/src/agents/types.ts index 5d5c4afe3..50ac3b1f6 100644 --- a/packages/shared-ai/src/agents/types.ts +++ b/packages/shared-ai/src/agents/types.ts @@ -54,6 +54,14 @@ export interface Agent { * (tag IDs are not sensitive). */ scopeTagIds?: string[]; + /** Fallback writing-style id for the Writing module (M8 follow-up). + * When this agent runs create_draft / generate_draft_content without + * an explicit styleId, the tool resolves to this value so e.g. a + * "Marketing-Agent" always drafts in the Corporate-Tone style. + * Format: `preset:` for a built-in preset or the uuid of a + * custom LocalWritingStyle row. Plaintext FK. */ + defaultWritingStyleId?: string; + /** Per-tool allowlist/propose/deny. Replaces the user-level AiPolicy * in Phase 4; pre-populated with the default policy at create time * so the runner can start reading it even while still consulting