Files
shad 15ab8ededa feat(agent): LLM proxy integration shape (Pi-AI compatible)
src/lib/llmClient.ts: browser POSTs to /internal/canvas-llm/chat with
{messages, max_tokens}. Mirrors the normalized request shape used by
earendil-works/pi's @earendil-works/pi-ai. Returns:
 - ok:true with content+provider when configured and successful
 - ok:false, configured:false when proxy 404/503/unreachable
 - ok:false, configured:true when provider returned an error

routeAgentInput now falls back to the LLM with a system prompt that
includes the user identity, tool catalogue, and the top-3 vault recall
hits when no deterministic tool matches. When the proxy is not
configured the deterministic vault-hint reply is used (no regression
in the demo tier).
2026-06-14 14:55:34 +04:00

57 lines
2.6 KiB
JavaScript

// Verify the agent memory vault round-trips: signed-in user tells the
// Assistant a fact, then asks the Assistant to recall it. Second persona
// in same tenant must NOT see the first persona's notes (isolation check
// is tenant-scoped not user-scoped since the vault key is per-user).
import { chromium } from "playwright";
const URL = "https://canvas.flow-master.ai/";
const args = ["--host-resolver-rules=MAP canvas.flow-master.ai 65.21.71.186", "--ignore-certificate-errors"];
const b = await chromium.launch({ headless: true, args });
async function asPersona(personaText, work) {
const ctx = await b.newContext({ viewport: { width: 1440, height: 900 } });
const p = await ctx.newPage();
await p.goto(URL, { waitUntil: "networkidle" });
await p.waitForTimeout(800);
await p.locator(".persona-chip", { hasText: personaText }).click();
await p.locator(".dev-login-btn").click();
await p.waitForTimeout(2500);
const enter = p.locator("button", { hasText: /Enter Mission Control/i }).first();
if (await enter.count()) { await enter.click(); await p.waitForTimeout(1200); }
await p.locator(".tab", { hasText: /^\s*Assistant\s*$/ }).click();
await p.waitForTimeout(1500);
const result = await work(p);
await ctx.close();
return result;
}
async function ask(p, text) {
const before = await p.locator(".agent-turn-agent .agent-turn-body").count();
await p.locator(".agent-composer textarea").fill(text);
await p.locator(".agent-composer button", { hasText: /Send/i }).click();
await p.waitForFunction((n) => document.querySelectorAll(".agent-turn-agent .agent-turn-body").length > n, before, { timeout: 8000 }).catch(() => {});
await p.waitForTimeout(600);
return (await p.locator(".agent-turn-agent .agent-turn-body").last().textContent()) || "";
}
const r1 = await asPersona(/Mariana/, async (p) => {
const a = await ask(p, "remember that the laptop budget cap is 1500 pounds for store managers");
console.log("[CEO remember]:", a.slice(0, 120));
await p.waitForTimeout(2000);
const b = await ask(p, "recall about laptop budget");
console.log("[CEO recall]:", b.slice(0, 200));
return { remember: a, recall: b };
});
const r2 = await asPersona(/Aisha/, async (p) => {
const a = await ask(p, "recall about laptop budget");
console.log("[HR recall]:", a.slice(0, 200));
return { recall: a };
});
console.log();
console.log("CEO remembered:", r1.remember.includes("I'll remember") ? "YES" : "NO");
console.log("CEO recalled own note:", r1.recall.includes("1500") || r1.recall.includes("laptop") ? "YES" : "NO");
console.log("HR vault isolated from CEO:", !r2.recall.includes("1500") ? "YES" : "NO (leak)");
await b.close();