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 inworkspace_insights(sezione "da fare" della Home).auto→ esegue + logga inagent_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 azioneautologgata + 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_decisionsesiste 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 comeagent_runs(migration2026-05-21b, PR #86). Nel blocco SQL sotto: leggiagent_decisions→agent_runsper la tabella di telemetria; per S2 (autonomia) si riusa laagent_decisionsesistente. La FKworkspace_insights.agent_decision_idresta 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_decisionsdi oggi conactions_executednon vuoto (azioni autonome) → "Inviati 3 promemoria", "Riempito 1 slot da lista d'attesa". - todo[]: da
workspace_insights(statusnew/seen,insight_date=oggi) consuggested_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
Anthropicche ritorna sequenzetool_usescriptate → assert dispatch corretto, bounds (max_iter), accumulousage, gestionefinish,error→ran:true stopReason:error,no apiKey→ran:false. - PII boundary: snapshot dell'array
messages→ assert nessun email/telefono/nome reale neitool_result(solo aggregati/alias). - Idempotenza: replay evento
booking/created→ un solo reminder (guardreminder_sent_at); re-runanalyze→workspace_insightspreservaseen/dismissed/actioned. - RLS: nuove tabelle — membro legge solo il proprio workspace, non altri; scrittura solo service_role.
- Cost: dopo un run,
workspace_token_usageincrementa diresp.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;autosolo 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-onlyper le dep). - Drift Sales UI → verificare branch
feat/foundation-agenticaprima 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.