name: foundation-agentica-architecture description: Architettura della Foundation Agentica Multiverbe (Opus) · runtime agent riusabile agent-core + tool model read/write + autonomia 3-approval-promote + osservabilità agent_decisions + token quota + Home digest · grounded sul codice reale 2026-05-21 last_update: 2026-05-21 v30 · Opus 4.7 architecture, grounded su codice repo Multiverbe owner: gregorio scope_repo: D:\Desktop\Multiverbe (frontend/multiverbe-app) ref: execution/mvp-plus-scope-2026-05-20.md + execution/decisions-log.md v24/v29 + canon/02-product.md § Home agentica

🧠 Foundation Agentica · Architettura (Opus)

Doc di architettura + handoff. Il codice si esegue nel repo D:\Desktop\Multiverbe\ (sessione Claude Code dedicata · validazione su CI Linux, lint/tsc locale rotto su Windows). Qui c'è il "modo deciso da Opus": decisioni, codice pronto, SQL, sequenza, test, handoff.

0. Stato reale (verificato sul codice 2026-05-21)

Pezzo Stato Dove
Inngest client + endpoint api/inngest.ts · api/_inngest/client.ts (id multiverbe, eventi booking/created, insights/analyze.workspace)
Workflow booking-reminder-smart api/_inngest/functions/booking-reminder-smart.ts (event-driven, sleepUntil, idempotente via reminder_sent_at)
Workflow analyze-planet-data (cron 06:00 + fan-out per-workspace) api/_inngest/functions/analyze-planet-data.ts
Agent loop Anthropic Tool Use ✅ ma DUPLICATO api/ai/sole-crm-suggestions.ts (6 iter) + analyze-planet-data.ts (10 iter) — stessa struttura copiata
PII pseudonymize src/lib/pii-pseudonymize.ts (+ mirror inline in sole-crm)
knowledge_packs (30 seed) + benchmark loader api/_inngest/sector-benchmark.ts · loadSectorBenchmark()
Supabase service-role REST helper api/booking/_lib/supabase-admin.ts (selectRows/insertRow/upsertRows/deleteRows, filtro workspace_id manuale obbligatorio)
workspace_insights (+ idempotenza seen/dismissed) migration 2026-05-21a_workspace_insights.sql
Sales tabelle Boss-View (quotas/territories/commissions + deals.call_script + stage.call_script_template) migration 2026-05-20b_sales_boss_view_foundation.sql
agent-core runtime riusabile ❌ MANCA da creare packages/agent-core
agent_decisions (audit/osservabilità · FK già citata in workspace_insights) ❌ MANCA da creare
workspace_token_usage (quota token per tier) ❌ MANCA da creare
write-tool + autonomia (il sistema fa, non solo propone) ❌ MANCA framework in agent-core
Home digest compose ("cosa ho fatto / da fare") ❌ MANCA endpoint + UI
Sales Boss-View UI (tabelle ok, UI?) ⚠️ verificare branch feat/foundation-agentica prima di costruire (Explore non l'ha trovata; possibile WIP non mergiato)

1. Tesi architetturale

Un solo runtime "Planet Agent" riusabile, non N loop bespoke. Oggi il loop è copiato 2 volte e ne arriveranno 6+ (uno per pianeta) → senza astrazione = caos + nessuna osservabilità/quota centralizzata + rischio bypass della pseudonimizzazione. Il runtime centralizza: loop bounded, dispatch tool, cattura token usage (oggi NON catturata → quota impossibile), logging decisione, guardrail PII, fallback. I loop esistenti si rifattorizzano sopra (behavior-preserving).

Secondo principio · il sistema deve fare, non solo proporre. La missione founder è "il sistema ha lavorato per te". Oggi gli agent solo propongono (insights). Serve il modello write-tool gated da autonomia: lo stesso agent, gli stessi tool; l'impostazione di autonomia per-workspace/pianeta decide propose-vs-execute. booking-reminder-smart è già il primo write autonomo → diventa il riferimento.

2. packages/agent-core · il runtime

API minima, framework-agnostic (TS puro, nessuna API Node version-specific → safe su CI Linux):

// packages/agent-core/src/run-agent.ts
import Anthropic from '@anthropic-ai/sdk';

export type ToolKind = 'read' | 'write';

export interface AgentTool {
  schema: Anthropic.Tool;
  kind: ToolKind;
  /** ritorna il content del tool_result (stringa); isError opzionale */
  handler: (input: unknown) => Promise<{ content: string; isError?: boolean }>;
}

export interface AgentRunOptions {
  system: string;                 // prompt di sistema (verrà cache_control ephemeral)
  firstMessage: string;           // primo messaggio user
  tools: Record<string, AgentTool>;
  finishToolName?: string;        // default 'finish'
  model?: string;                 // default 'claude-sonnet-4-6'
  maxTokens?: number;             // default 800
  maxIterations?: number;         // default 8
  apiKey?: string;                // default process.env.ANTHROPIC_API_KEY
}

export interface AgentRunResult {
  ran: boolean;                   // false se apiKey assente → il caller usa fallback
  stopReason: 'finished' | 'no_tool_use' | 'max_iter' | 'error';
  iterations: number;
  toolCalls: Array<{ name: string; ok: boolean }>;
  usage: { input_tokens: number; output_tokens: number };
  model: string;
}

/**
 * Loop agentico bounded · generalizza il pattern di sole-crm-suggestions + analyze-planet-data.
 * Non possiede accumulatori: i tool handler chiudono sul proprio stato (il caller lo legge dopo).
 * Cattura SEMPRE token usage (per quota) e l'esito (per agent_decisions).
 */
export async function runAgent(opts: AgentRunOptions): Promise<AgentRunResult> {
  const apiKey = opts.apiKey ?? process.env.ANTHROPIC_API_KEY;
  const model = opts.model ?? 'claude-sonnet-4-6';
  const usage = { input_tokens: 0, output_tokens: 0 };
  const toolCalls: Array<{ name: string; ok: boolean }> = [];
  if (!apiKey) return { ran: false, stopReason: 'no_tool_use', iterations: 0, toolCalls, usage, model };

  const client = new Anthropic({ apiKey });
  const finishName = opts.finishToolName ?? 'finish';
  const maxIter = opts.maxIterations ?? 8;
  const toolSchemas = Object.values(opts.tools).map((t) => t.schema);
  const messages: Anthropic.MessageParam[] = [{ role: 'user', content: opts.firstMessage }];

  try {
    for (let i = 0; i < maxIter; i++) {
      const resp = await client.messages.create({
        model,
        max_tokens: opts.maxTokens ?? 800,
        system: [{ type: 'text', text: opts.system, cache_control: { type: 'ephemeral' } }],
        tools: toolSchemas,
        messages,
      });
      usage.input_tokens += resp.usage?.input_tokens ?? 0;
      usage.output_tokens += resp.usage?.output_tokens ?? 0;
      messages.push({ role: 'assistant', content: resp.content });
      if (resp.stop_reason !== 'tool_use') {
        return { ran: true, stopReason: 'no_tool_use', iterations: i + 1, toolCalls, usage, model };
      }

      const results: Anthropic.ToolResultBlockParam[] = [];
      let finished = false;
      for (const block of resp.content) {
        if (block.type !== 'tool_use') continue;
        if (block.name === finishName) {
          finished = true;
          results.push({ type: 'tool_result', tool_use_id: block.id, content: 'ok' });
          continue;
        }
        const tool = opts.tools[block.name];
        if (!tool) {
          toolCalls.push({ name: block.name, ok: false });
          results.push({ type: 'tool_result', tool_use_id: block.id, content: 'unknown_tool', is_error: true });
          continue;
        }
        const out = await tool.handler(block.input);
        toolCalls.push({ name: block.name, ok: !out.isError });
        results.push({ type: 'tool_result', tool_use_id: block.id, content: out.content, is_error: out.isError });
      }
      messages.push({ role: 'user', content: results });
      if (finished) return { ran: true, stopReason: 'finished', iterations: i + 1, toolCalls, usage, model };
    }
    return { ran: true, stopReason: 'max_iter', iterations: maxIter, toolCalls, usage, model };
  } catch (err) {
    console.error('[agent-core] loop error', err);
    return { ran: true, stopReason: 'error', iterations: 0, toolCalls, usage, model };
  }
}

Refactor dei 2 loop esistenti sopra runAgent (behavior-preserving): si passano i tool come {schema, kind:'read', handler} con gli handler che chiudono sulle Map già esistenti (proposals / collected). Si elimina la duplicazione del loop. sole-crm resta maxIterations:6, analyze-planet resta 10. Dopo il run, il caller persiste agent_decisions + incrementa workspace_token_usage con result.usage.

3. Tool model · read vs write + autonomia (3-approval-promote)

  • read-tool: legge aggregati (pseudonimizzati). Sempre permessi. (Es. query_planet_metrics, query_sector_benchmark.)
  • write-tool: esegue un'azione reale (manda reminder, riempie slot da waitlist, manda win-back). Gated dall'autonomia per-workspace/pianeta:
    • off → il pianeta non agisce.
    • propose (default) → la write-tool non muta: registra un'azione proposta → finisce in workspace_insights (sezione "da fare" della Home).
    • auto → esegue + logga in agent_decisions.actions_executed (sezione "cosa ho fatto" della Home).
  • 3-approval-promote (pattern canon): se il titolare approva 3× lo stesso tipo di azione proposta, la Home suggerisce di promuoverla ad auto. Mai auto silenzioso senza opt-in.
  • kill-switch per-workspace e per-pianeta (autonomia off). Ogni azione auto loggata + reversibile dove possibile.

Tabella settaggi:

create table workspace_planet_autonomy (
  workspace_id uuid not null references workspaces(id) on delete cascade,
  planet text not null,                         -- web|booking|content|crm|sales|marketing|cross
  autonomy text not null default 'propose'      -- off|propose|auto
    check (autonomy in ('off','propose','auto')),
  updated_at timestamptz not null default now(),
  primary key (workspace_id, planet)
);

4. Osservabilità + costo (SQL · da applicare via MCP/migration)

🔑 Correzione 2026-05-21 (S1a · verificato sul DB prod): agent_decisions esiste già ed è il workflow di approvazione azioni (proposed_action/autonomy_tier/status/approved_by/executed_at) → è il substrato dell'autonomia (S2), NON un log di run. Perciò la telemetria per-esecuzione è stata creata come agent_runs (migration 2026-05-21b, PR #86). Nel blocco SQL sotto: leggi agent_decisionsagent_runs per la tabella di telemetria; per S2 (autonomia) si riusa la agent_decisions esistente. La FK workspace_insights.agent_decision_id resta valida (punta all'azione proposta).

-- agent_decisions · audit di ogni run agentico (sorgente "cosa ho fatto" + §9 quality + compliance)
create table agent_decisions (
  id uuid primary key default gen_random_uuid(),
  workspace_id uuid not null references workspaces(id) on delete cascade,
  planet text not null,
  trigger text not null,            -- 'cron:analyze_planet_data' | 'event:booking/created' | 'user:sole-crm'
  model text not null,
  stop_reason text not null,        -- finished|no_tool_use|max_iter|error
  iterations int not null default 0,
  tool_calls jsonb not null default '[]'::jsonb,   -- [{name, ok}]
  input_tokens int not null default 0,
  output_tokens int not null default 0,
  duration_ms int,
  outcome jsonb,                    -- sintesi decisione
  actions_executed jsonb not null default '[]'::jsonb,  -- write auto → feed "cosa ho fatto"
  created_at timestamptz not null default now()
);
create index idx_agent_decisions_ws_date on agent_decisions (workspace_id, created_at desc);
alter table agent_decisions enable row level security;
create policy agent_decisions_read on agent_decisions for select
  using (workspace_id in (select workspace_id from team_members where user_id = (select auth.uid())));
-- write SOLO service_role (no policy insert per ruoli anon)

-- workspace_token_usage · quota token AI/mese per tier (gate pre-run + incremento post-run)
create table workspace_token_usage (
  workspace_id uuid not null references workspaces(id) on delete cascade,
  period_month date not null,       -- date_trunc('month')
  input_tokens bigint not null default 0,
  output_tokens bigint not null default 0,
  updated_at timestamptz not null default now(),
  primary key (workspace_id, period_month)
);
create or replace function increment_token_usage(p_ws uuid, p_in bigint, p_out bigint)
returns void language sql as $$
  insert into workspace_token_usage (workspace_id, period_month, input_tokens, output_tokens)
  values (p_ws, date_trunc('month', now())::date, p_in, p_out)
  on conflict (workspace_id, period_month)
  do update set input_tokens = workspace_token_usage.input_tokens + excluded.input_tokens,
                output_tokens = workspace_token_usage.output_tokens + excluded.output_tokens,
                updated_at = now();
$$;

La FK workspace_insights.agent_decision_id ora ha una tabella valida da referenziare.

5. Contratto Home · compose "cosa ho fatto / da fare"

MVP = compose a read-time (niente tabella materializzata finché la perf non lo richiede). Endpoint GET /api/home/daily-brief?workspace_id=…:

  • pulse (1 riga): rollup veloce (prenotazioni oggi, lead 7gg, Δ fatturato settimana) da computePlanetMetrics.
  • done[]: da agent_decisions di oggi con actions_executed non vuoto (azioni autonome) → "Inviati 3 promemoria", "Riempito 1 slot da lista d'attesa".
  • todo[]: da workspace_insights (status new/seen, insight_date=oggi) con suggested_actions → card approva/ignora.
  • planets[]: health per pianeta (riusa metrics rollup → mappa su rotation/colore della metafora 3D).
  • personalizzazione: pin sezioni/pianeti in workspace_home_prefs (JSONB per-workspace).

Promozione futura a workspace_daily_brief materializzata (scritta dall'aggregatore Inngest daily_brief dopo analyze_planet_data) se il compose read-time diventa pesante.

6. Sequenza build (Fase 1 residua + framework) · per la sessione codice

Step Cosa Owner Note
S0 Vercel env: INNGEST_EVENT_KEY + INNGEST_SIGNING_KEY (+ confermare ANTHROPIC_API_KEY) founder Senza, i workflow sono no-op. Sblocca tutto.
S1 Crea packages/agent-core (runAgent + types + usage). Refactor sole-crm-suggestions + analyze-planet-data sopra il runtime (behavior-preserving). Migration agent_decisions + workspace_token_usage + persist run metadata + increment_token_usage. Claude (code) Keystone. DRY + osservabilità + base quota.
S2 Framework write-tool + workspace_planet_autonomy + gating propose/auto + 3-approval-promote. Wire booking-reminder-smart come azione auto loggata in agent_decisions. Claude (code) Il sistema inizia a "fare".
S3 GET /api/home/daily-brief compose (done/todo/pulse/planets) + Home UI minimal mobile + workspace_home_prefs. Claude (code) La scena demo. Dipende da S1/S2.
S4 Sales Boss-View UI sulle tabelle esistenti: filtro pipeline owner_user_id, modale call-script, Leaderboard. Verifica prima il branch feat/foundation-agentica (UI forse già WIP). Claude (code)
S5 Token quota gate (read usage vs tier cap pre-run) + surface dashboard. Claude (code) Fase 2 pricing.
QA CI Linux verde · RLS test nuove tabelle · idempotenza · PII boundary · usage increment. Claude (code)

7. Test strategy

  • agent-core: unit test con mock Anthropic che ritorna sequenze tool_use scriptate → assert dispatch corretto, bounds (max_iter), accumulo usage, gestione finish, errorran:true stopReason:error, no apiKeyran:false.
  • PII boundary: snapshot dell'array messages → assert nessun email/telefono/nome reale nei tool_result (solo aggregati/alias).
  • Idempotenza: replay evento booking/created → un solo reminder (guard reminder_sent_at); re-run analyzeworkspace_insights preserva seen/dismissed/actioned.
  • RLS: nuove tabelle — membro legge solo il proprio workspace, non altri; scrittura solo service_role.
  • Cost: dopo un run, workspace_token_usage incrementa di resp.usage (input+output).

8. Rischi / guardrail

  • Cost runaway → bounds (iter + max_tokens) + quota gate + concurrency: { limit: 5 } (già presente).
  • Leak PII → la pseudonimizzazione è hard rule; centralizzarla a monte dei tool così nessun loop la bypassa. Mai PII raw ad Anthropic (US, fuori EU Data Boundary).
  • Autonomia → default propose; auto solo con opt-in / 3-approval-promote; kill-switch; azioni reversibili dove possibile.
  • Windows local rotto → niente API Node version-specific in agent-core; ci si affida a CI Linux (pnpm install --lockfile-only per le dep).
  • Drift Sales UI → verificare branch feat/foundation-agentica prima di S4 per non duplicare lavoro.

9. Handoff prompt (copia-incolla nella sessione Claude Code aperta in D:\Desktop\Multiverbe\)

Vedi blocco nel messaggio di handoff. Sintesi: leggi questo doc, parti da S0 (founder env) → S1 keystone (agent-core + refactor + migration osservabilità/quota), procedi S2→S5 con QA su CI Linux, output a Generator-Verifier (§7) per i write-tool e le migration.


Owner: Gregorio (decisioni) · esecuzione Claude Code (repo codice) · architettura Opus 4.7 2026-05-21.