mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 22:01:09 +02:00
feat(ai): policy is read from the owning agent (Phase 4)
Until now AiPolicy lived as a user-global setting consulted for every AI action. With agents as the principal unit of AI behavior, policy belongs on the agent — different agents can be aggressive about tasks but conservative about calendar edits, etc. Webapp (tools/executor.ts): - When an AI actor invokes a tool, the executor looks up the owning agent via getAgent(actor.principalId) and passes agent.policy into resolvePolicy. Falls back to DEFAULT_AI_POLICY when the agent record is missing (legacy write, deleted agent, race) so no tool call can silently bypass the propose/deny path. - resolvePolicy already accepted an optional policy arg, so the call site change is a single line plus the agent load. Server (mana-ai): - ServerAgent gains an optional policy field, projected off the same plaintext JSONB that the webapp writes. - Tick loop filters AI_AVAILABLE_TOOLS through filterToolsByAgentPolicy before passing them to the planner prompt. Resolution order mirrors the webapp: tools[name] → defaultsByModule → defaultForAi; 'deny' drops the tool so the LLM never even sees it. Phase 5 will surface a per-agent policy editor on the agent-detail UI. Until then all agents inherit DEFAULT_AI_POLICY (baked in during createAgent), which means no behavior change for existing users — every tool that was 'propose' before is still 'propose' now, just reached via agent.policy instead of the user-level singleton. Tests: mana-ai 41/41, webapp svelte-check clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
968e08059f
commit
f7426ab40f
3 changed files with 49 additions and 2 deletions
|
|
@ -240,7 +240,7 @@ async function planOneMission(
|
|||
const input: AiPlanInput = {
|
||||
mission,
|
||||
resolvedInputs,
|
||||
availableTools: AI_AVAILABLE_TOOLS,
|
||||
availableTools: filterToolsByAgentPolicy(AI_AVAILABLE_TOOLS, agent),
|
||||
};
|
||||
const messages = withAgentContext(buildPlannerPrompt(input), agent);
|
||||
const result = await planner.complete(messages);
|
||||
|
|
@ -265,6 +265,31 @@ async function planOneMission(
|
|||
* Ciphertext fields (`enc:1:…`) are intentionally skipped — the server
|
||||
* doesn't hold the decrypt key; the foreground runner handles those.
|
||||
*/
|
||||
/**
|
||||
* Drop tools that the agent's policy denies, so the Planner never even
|
||||
* sees a tool it can't use. Tools with policy `propose` stay in the
|
||||
* allowlist (they just get proposed rather than auto-run on the user's
|
||||
* device), and `auto` tools stay too. A missing policy or missing
|
||||
* agent leaves the list unchanged.
|
||||
*
|
||||
* Resolution order matches the webapp's `resolvePolicy`:
|
||||
* tools[name] ?? defaultsByModule[tool.module] ?? defaultForAi
|
||||
*/
|
||||
function filterToolsByAgentPolicy(
|
||||
tools: readonly import('@mana/shared-ai').AvailableTool[],
|
||||
agent: ServerAgent | null
|
||||
): import('@mana/shared-ai').AvailableTool[] {
|
||||
if (!agent?.policy) return tools as import('@mana/shared-ai').AvailableTool[];
|
||||
const policy = agent.policy;
|
||||
return tools.filter((t) => {
|
||||
const byTool = policy.tools[t.name];
|
||||
if (byTool) return byTool !== 'deny';
|
||||
const byModule = policy.defaultsByModule?.[t.module];
|
||||
if (byModule) return byModule !== 'deny';
|
||||
return policy.defaultForAi !== 'deny';
|
||||
});
|
||||
}
|
||||
|
||||
function withAgentContext(messages: PlannerMessages, agent: ServerAgent | null): PlannerMessages {
|
||||
if (!agent) return messages;
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
import type { Sql } from './connection';
|
||||
import { withUser } from './connection';
|
||||
import type { AiPolicy } from '@mana/shared-ai';
|
||||
|
||||
export interface ServerAgent {
|
||||
id: string;
|
||||
|
|
@ -31,6 +32,10 @@ export interface ServerAgent {
|
|||
* only injects plaintext. */
|
||||
systemPrompt?: string;
|
||||
memory?: string;
|
||||
/** Per-tool auto/propose/deny — drives the server-side tool
|
||||
* allowlist when Phase 4 wiring is complete. Plaintext. Undefined
|
||||
* on legacy agent records (pre-Phase-2 writes). */
|
||||
policy?: AiPolicy;
|
||||
state: 'active' | 'paused' | 'archived';
|
||||
maxConcurrentMissions: number;
|
||||
maxTokensPerDay?: number;
|
||||
|
|
@ -238,6 +243,10 @@ function toServerAgent(row: SnapshotRow): ServerAgent {
|
|||
role: String(r.role ?? ''),
|
||||
systemPrompt: typeof r.systemPrompt === 'string' ? r.systemPrompt : undefined,
|
||||
memory: typeof r.memory === 'string' ? r.memory : undefined,
|
||||
policy:
|
||||
r.policy && typeof r.policy === 'object' && !Array.isArray(r.policy)
|
||||
? (r.policy as AiPolicy)
|
||||
: undefined,
|
||||
state: (r.state as ServerAgent['state']) ?? 'active',
|
||||
maxConcurrentMissions: Number(r.maxConcurrentMissions ?? 1),
|
||||
maxTokensPerDay: typeof r.maxTokensPerDay === 'number' ? r.maxTokensPerDay : undefined,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue