Upgrade Feynman research runtime and setup

This commit is contained in:
Advait Paliwal
2026-03-20 23:37:38 -07:00
parent 6332c3c67c
commit be97ac7a38
22 changed files with 1271 additions and 60 deletions

View File

@@ -1,6 +1,7 @@
export const FEYNMAN_SYSTEM_PROMPT = `You are Feynman, a research-first AI agent.
export function buildFeynmanSystemPrompt(): string {
return `You are Feynman, a research-first AI agent.
Your job is to investigate questions, read primary sources, design experiments, run them when useful, and produce reproducible written artifacts.
Your job is to investigate questions, read primary sources, compare evidence, design experiments when useful, and produce reproducible written artifacts.
Operating rules:
- Evidence over fluency.
@@ -9,18 +10,32 @@ Operating rules:
- State uncertainty explicitly.
- When a claim depends on recent literature or unstable facts, use tools before answering.
- When discussing papers, cite title, year, and identifier or URL when possible.
- Use the alpha-backed research tools first for literature search, paper reading, paper Q&A, and persistent annotations.
- Use the installed Pi research packages for broader web/PDF access, document parsing, session recall, background processes, experiment tracking, citations, and delegated subtasks when they reduce friction.
- Use the alpha-backed research tools for academic paper search, paper reading, paper Q&A, repository inspection, and persistent annotations.
- Use \`web_search\`, \`fetch_content\`, and \`get_search_content\` first for current topics: products, companies, markets, regulations, software releases, model availability, model pricing, benchmarks, docs, or anything phrased as latest/current/recent/today.
- For mixed topics, combine both: use web sources for current reality and paper sources for background literature.
- Never answer a latest/current question from arXiv or alpha-backed paper search alone.
- For AI model or product claims, prefer official docs/vendor pages plus recent web sources over old papers.
- Use the installed Pi research packages for broader web/PDF access, document parsing, citation workflows, background processes, memory, session recall, and delegated subtasks when they reduce friction.
- Use the visualization packages when a chart, diagram, or interactive widget would materially improve understanding. Prefer charts for quantitative comparisons, Mermaid for simple process/architecture diagrams, and interactive HTML widgets for exploratory visual explanations.
- Persistent memory is package-backed. Use \`memory_search\` to recall prior preferences and lessons, \`memory_remember\` to store explicit durable facts, and \`memory_lessons\` when prior corrections matter.
- If the user says "remember", states a stable preference, or asks for something to be the default in future sessions, call \`memory_remember\`. Do not just say you will remember it.
- Session recall is package-backed. Use \`session_search\` when the user references prior work, asks what has been done before, or when you suspect relevant past context exists.
- Feynman is intended to support always-on research work. Use the scheduling package when recurring or deferred work is appropriate instead of telling the user to remember manually.
- Use \`schedule_prompt\` for recurring scans, delayed follow-ups, reminders, and periodic research jobs.
- If the user asks you to remind, check later, run something nightly, or keep watching something over time, call \`schedule_prompt\`. Do not just promise to do it later.
- Prefer the smallest investigation or experiment that can materially reduce uncertainty before escalating to broader work.
- When an experiment is warranted, write the code or scripts, run them, capture outputs, and save artifacts to disk.
- Treat polished scientific communication as part of the job: structure reports cleanly, use Markdown deliberately, and use LaTeX math when equations clarify the argument.
- For any source-based answer, include an explicit Sources section with direct URLs, not just paper titles.
- When citing papers from alpha-backed tools, prefer direct arXiv or alphaXiv links and include the arXiv ID.
- After writing a polished artifact, use \`preview_file\` when the user wants to review it in a browser or PDF viewer.
- Default toward delivering a concrete artifact when the task naturally calls for one: reading list, memo, audit, experiment log, or draft.
- Default artifact locations:
- outputs/ for reviews, reading lists, and summaries
- experiments/ for runnable experiment code and result logs
- notes/ for scratch notes and intermediate synthesis
- papers/ for polished paper-style drafts and writeups
- Default deliverables should include: summary, strongest evidence, disagreements or gaps, open questions, and recommended next steps.
- Default deliverables should include: summary, strongest evidence, disagreements or gaps, open questions, recommended next steps, and links to the source material.
Default workflow:
1. Clarify the research objective if needed.
@@ -33,4 +48,6 @@ Default workflow:
Style:
- Concise, skeptical, and explicit.
- Avoid fake certainty.
- Do not present unverified claims as facts.`;
- Do not present unverified claims as facts.
- When greeting, introducing yourself, or answering "who are you", identify yourself explicitly as Feynman.`;
}

View File

@@ -1,9 +1,11 @@
import "dotenv/config";
import { spawn } from "node:child_process";
import { spawn, spawnSync } from "node:child_process";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { dirname, resolve } from "node:path";
import { stdin as input, stdout as output } from "node:process";
import { createInterface } from "node:readline/promises";
import { parseArgs } from "node:util";
import { fileURLToPath } from "node:url";
@@ -18,7 +20,7 @@ import {
AuthStorage,
} from "@mariozechner/pi-coding-agent";
import { FEYNMAN_SYSTEM_PROMPT } from "./feynman-prompt.js";
import { buildFeynmanSystemPrompt } from "./feynman-prompt.js";
type ThinkingLevel = "off" | "low" | "medium" | "high";
@@ -34,6 +36,8 @@ function printHelp(): void {
/replicate <paper> Expand the replication prompt template
/reading-list <topic> Expand the reading list prompt template
/research-memo <topic> Expand the general research memo prompt template
/deepresearch <topic> Expand the thorough source-heavy research prompt template
/autoresearch <idea> Expand the idea-to-paper autoresearch prompt template
/compare-sources <topic> Expand the source comparison prompt template
/paper-code-audit <item> Expand the paper/code audit prompt template
/paper-draft <topic> Expand the paper-style writing prompt template
@@ -46,7 +50,15 @@ function printHelp(): void {
--model provider:model Force a specific model
--thinking level off | low | medium | high
--cwd /path/to/workdir Working directory for tools
--session-dir /path Session storage directory`);
--session-dir /path Session storage directory
--doctor Check Feynman auth, models, preview tools, and paths
--setup-preview Install preview dependencies when possible
Top-level:
feynman setup Configure alpha login, web search, and preview deps
feynman setup alpha Configure alphaXiv login
feynman setup web Configure web search provider
feynman setup preview Install preview dependencies`);
}
function parseModelSpec(spec: string, modelRegistry: ModelRegistry) {
@@ -78,9 +90,32 @@ function normalizeThinkingLevel(value: string | undefined): ThinkingLevel | unde
return undefined;
}
function resolveExecutable(name: string, fallbackPaths: string[] = []): string | undefined {
for (const candidate of fallbackPaths) {
if (existsSync(candidate)) {
return candidate;
}
}
const result = spawnSync("sh", ["-lc", `command -v ${name}`], {
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
});
if (result.status === 0) {
const resolved = result.stdout.trim();
if (resolved) {
return resolved;
}
}
return undefined;
}
function patchEmbeddedPiBranding(piPackageRoot: string): void {
const packageJsonPath = resolve(piPackageRoot, "package.json");
const cliPath = resolve(piPackageRoot, "dist", "cli.js");
const interactiveModePath = resolve(piPackageRoot, "dist", "modes", "interactive", "interactive-mode.js");
if (existsSync(packageJsonPath)) {
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf8")) as {
@@ -101,6 +136,77 @@ function patchEmbeddedPiBranding(piPackageRoot: string): void {
writeFileSync(cliPath, cliSource.replace('process.title = "pi";', 'process.title = "feynman";'), "utf8");
}
}
if (existsSync(interactiveModePath)) {
const interactiveModeSource = readFileSync(interactiveModePath, "utf8");
if (interactiveModeSource.includes("`π - ${sessionName} - ${cwdBasename}`")) {
writeFileSync(
interactiveModePath,
interactiveModeSource
.replace("`π - ${sessionName} - ${cwdBasename}`", "`feynman - ${sessionName} - ${cwdBasename}`")
.replace("`π - ${cwdBasename}`", "`feynman - ${cwdBasename}`"),
"utf8",
);
}
}
}
function patchPackageWorkspace(appRoot: string): void {
const workspaceRoot = resolve(appRoot, ".pi", "npm", "node_modules");
const webAccessPath = resolve(workspaceRoot, "pi-web-access", "index.ts");
const sessionSearchIndexerPath = resolve(
workspaceRoot,
"@kaiserlich-dev",
"pi-session-search",
"extensions",
"indexer.ts",
);
const piMemoryPath = resolve(workspaceRoot, "@samfp", "pi-memory", "src", "index.ts");
if (existsSync(webAccessPath)) {
const source = readFileSync(webAccessPath, "utf8");
if (source.includes('pi.registerCommand("search",')) {
writeFileSync(
webAccessPath,
source.replace('pi.registerCommand("search",', 'pi.registerCommand("web-results",'),
"utf8",
);
}
}
if (existsSync(sessionSearchIndexerPath)) {
const source = readFileSync(sessionSearchIndexerPath, "utf8");
const original = 'const sessionsDir = path.join(os.homedir(), ".pi", "agent", "sessions");';
const replacement =
'const sessionsDir = process.env.FEYNMAN_SESSION_DIR ?? process.env.PI_SESSION_DIR ?? path.join(os.homedir(), ".pi", "agent", "sessions");';
if (source.includes(original)) {
writeFileSync(sessionSearchIndexerPath, source.replace(original, replacement), "utf8");
}
}
if (existsSync(piMemoryPath)) {
let source = readFileSync(piMemoryPath, "utf8");
const memoryOriginal = 'const MEMORY_DIR = join(homedir(), ".pi", "memory");';
const memoryReplacement =
'const MEMORY_DIR = process.env.FEYNMAN_MEMORY_DIR ?? process.env.PI_MEMORY_DIR ?? join(homedir(), ".pi", "memory");';
if (source.includes(memoryOriginal)) {
source = source.replace(memoryOriginal, memoryReplacement);
}
const execOriginal = 'const result = await pi.exec("pi", ["-p", prompt, "--print"], {';
const execReplacement = [
'const execBinary = process.env.FEYNMAN_NODE_EXECUTABLE || process.env.FEYNMAN_EXECUTABLE || "pi";',
' const execArgs = process.env.FEYNMAN_BIN_PATH',
' ? [process.env.FEYNMAN_BIN_PATH, "--prompt", prompt]',
' : ["-p", prompt, "--print"];',
' const result = await pi.exec(execBinary, execArgs, {',
].join("\n");
if (source.includes(execOriginal)) {
source = source.replace(execOriginal, execReplacement);
}
writeFileSync(piMemoryPath, source, "utf8");
}
}
function choosePreferredModel(
@@ -149,12 +255,6 @@ function normalizeFeynmanSettings(
}
}
if (Array.isArray(settings.packages)) {
settings.packages = settings.packages.filter(
(entry) => entry !== "npm:@kaiserlich-dev/pi-session-search",
);
}
if (!settings.defaultThinkingLevel) {
settings.defaultThinkingLevel = defaultThinkingLevel;
}
@@ -180,6 +280,217 @@ function normalizeFeynmanSettings(
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
}
function readJson(path: string): Record<string, unknown> {
if (!existsSync(path)) {
return {};
}
try {
return JSON.parse(readFileSync(path, "utf8"));
} catch {
return {};
}
}
function getWebSearchConfigPath(): string {
return resolve(homedir(), ".pi", "web-search.json");
}
function loadWebSearchConfig(): Record<string, unknown> {
return readJson(getWebSearchConfigPath());
}
function saveWebSearchConfig(config: Record<string, unknown>): void {
const path = getWebSearchConfigPath();
mkdirSync(dirname(path), { recursive: true });
writeFileSync(path, JSON.stringify(config, null, 2) + "\n", "utf8");
}
function hasConfiguredWebProvider(): boolean {
const config = loadWebSearchConfig();
return typeof config.perplexityApiKey === "string" && config.perplexityApiKey.trim().length > 0
|| typeof config.geminiApiKey === "string" && config.geminiApiKey.trim().length > 0;
}
async function promptText(question: string, defaultValue = ""): Promise<string> {
if (!input.isTTY || !output.isTTY) {
throw new Error("feynman setup requires an interactive terminal.");
}
const rl = createInterface({ input, output });
try {
const suffix = defaultValue ? ` [${defaultValue}]` : "";
const value = (await rl.question(`${question}${suffix}: `)).trim();
return value || defaultValue;
} finally {
rl.close();
}
}
async function promptChoice(question: string, choices: string[], defaultIndex = 0): Promise<number> {
console.log(question);
for (const [index, choice] of choices.entries()) {
const marker = index === defaultIndex ? "*" : " ";
console.log(` ${marker} ${index + 1}. ${choice}`);
}
const answer = await promptText("Select", String(defaultIndex + 1));
const parsed = Number(answer);
if (!Number.isFinite(parsed) || parsed < 1 || parsed > choices.length) {
return defaultIndex;
}
return parsed - 1;
}
async function setupWebProvider(): Promise<void> {
const config = loadWebSearchConfig();
const choices = [
"Gemini API key",
"Perplexity API key",
"Browser Gemini (manual sign-in only)",
"Skip",
];
const selection = await promptChoice("Choose a web search provider for Feynman:", choices, hasConfiguredWebProvider() ? 3 : 0);
if (selection === 0) {
const key = await promptText("Gemini API key");
if (key) {
config.geminiApiKey = key;
delete config.perplexityApiKey;
saveWebSearchConfig(config);
console.log("Saved Gemini API key to ~/.pi/web-search.json");
}
return;
}
if (selection === 1) {
const key = await promptText("Perplexity API key");
if (key) {
config.perplexityApiKey = key;
delete config.geminiApiKey;
saveWebSearchConfig(config);
console.log("Saved Perplexity API key to ~/.pi/web-search.json");
}
return;
}
if (selection === 2) {
console.log("Sign into gemini.google.com in Chrome, Chromium, Brave, or Edge, then restart Feynman.");
return;
}
}
async function runSetup(
section: string | undefined,
settingsPath: string,
bundledSettingsPath: string,
authPath: string,
workingDir: string,
sessionDir: string,
): Promise<void> {
if (section === "alpha" || !section) {
if (!isAlphaLoggedIn()) {
await loginAlpha();
console.log("alphaXiv login complete");
} else {
console.log("alphaXiv login already configured");
}
if (section === "alpha") return;
}
if (section === "web" || !section) {
await setupWebProvider();
if (section === "web") return;
}
if (section === "preview" || !section) {
setupPreviewDependencies();
if (section === "preview") return;
}
normalizeFeynmanSettings(settingsPath, bundledSettingsPath, "medium", authPath);
runDoctor(settingsPath, authPath, sessionDir, workingDir);
}
function runDoctor(
settingsPath: string,
authPath: string,
sessionDir: string,
workingDir: string,
): void {
const settings = readJson(settingsPath);
const modelRegistry = new ModelRegistry(AuthStorage.create(authPath));
const availableModels = modelRegistry.getAvailable();
const pandocPath = resolveExecutable("pandoc", [
"/opt/homebrew/bin/pandoc",
"/usr/local/bin/pandoc",
]);
const browserPath =
process.env.PUPPETEER_EXECUTABLE_PATH ??
resolveExecutable("google-chrome", [
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"/Applications/Chromium.app/Contents/MacOS/Chromium",
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
]);
console.log("Feynman doctor");
console.log("");
console.log(`working dir: ${workingDir}`);
console.log(`session dir: ${sessionDir}`);
console.log("");
console.log(`alphaXiv auth: ${isAlphaLoggedIn() ? "ok" : "missing"}`);
if (isAlphaLoggedIn()) {
const name = getAlphaUserName();
if (name) {
console.log(` user: ${name}`);
}
}
console.log(`models available: ${availableModels.length}`);
if (availableModels.length > 0) {
const sample = availableModels
.slice(0, 6)
.map((model) => `${model.provider}/${model.id}`)
.join(", ");
console.log(` sample: ${sample}`);
}
console.log(
`default model: ${typeof settings.defaultProvider === "string" && typeof settings.defaultModel === "string"
? `${settings.defaultProvider}/${settings.defaultModel}`
: "not set"}`,
);
console.log(`pandoc: ${pandocPath ?? "missing"}`);
console.log(`browser preview runtime: ${browserPath ?? "missing"}`);
console.log(`web research provider: ${hasConfiguredWebProvider() ? "configured" : "missing"}`);
console.log(`quiet startup: ${settings.quietStartup === true ? "enabled" : "disabled"}`);
console.log(`theme: ${typeof settings.theme === "string" ? settings.theme : "not set"}`);
console.log(`setup hint: feynman setup`);
}
function setupPreviewDependencies(): void {
const pandocPath = resolveExecutable("pandoc", [
"/opt/homebrew/bin/pandoc",
"/usr/local/bin/pandoc",
]);
if (pandocPath) {
console.log(`pandoc already installed at ${pandocPath}`);
return;
}
const brewPath = resolveExecutable("brew", [
"/opt/homebrew/bin/brew",
"/usr/local/bin/brew",
]);
if (process.platform === "darwin" && brewPath) {
const result = spawnSync(brewPath, ["install", "pandoc"], { stdio: "inherit" });
if (result.status !== 0) {
throw new Error("Failed to install pandoc via Homebrew.");
}
console.log("Preview dependency installed: pandoc");
return;
}
throw new Error("Automatic preview setup is only supported on macOS with Homebrew right now.");
}
function syncFeynmanTheme(appRoot: string, agentDir: string): void {
const sourceThemePath = resolve(appRoot, ".pi", "themes", "feynman.json");
const targetThemeDir = resolve(agentDir, "themes");
@@ -201,11 +512,13 @@ async function main(): Promise<void> {
const feynmanAgentDir = resolve(homedir(), ".feynman", "agent");
const bundledSettingsPath = resolve(appRoot, ".pi", "settings.json");
patchEmbeddedPiBranding(piPackageRoot);
patchPackageWorkspace(appRoot);
const { values, positionals } = parseArgs({
allowPositionals: true,
options: {
cwd: { type: "string" },
doctor: { type: "boolean" },
help: { type: "boolean" },
"alpha-login": { type: "boolean" },
"alpha-logout": { type: "boolean" },
@@ -214,6 +527,7 @@ async function main(): Promise<void> {
"new-session": { type: "boolean" },
prompt: { type: "string" },
"session-dir": { type: "string" },
"setup-preview": { type: "boolean" },
thinking: { type: "string" },
},
});
@@ -233,6 +547,21 @@ async function main(): Promise<void> {
const thinkingLevel = normalizeThinkingLevel(values.thinking ?? process.env.FEYNMAN_THINKING) ?? "medium";
normalizeFeynmanSettings(feynmanSettingsPath, bundledSettingsPath, thinkingLevel, feynmanAuthPath);
if (positionals[0] === "setup") {
await runSetup(positionals[1], feynmanSettingsPath, bundledSettingsPath, feynmanAuthPath, workingDir, sessionDir);
return;
}
if (values.doctor) {
runDoctor(feynmanSettingsPath, feynmanAuthPath, sessionDir, workingDir);
return;
}
if (values["setup-preview"]) {
setupPreviewDependencies();
return;
}
if (values["alpha-login"]) {
const result = await loginAlpha();
normalizeFeynmanSettings(feynmanSettingsPath, bundledSettingsPath, thinkingLevel, feynmanAuthPath);
@@ -273,6 +602,7 @@ async function main(): Promise<void> {
}
const oneShotPrompt = values.prompt;
const initialPrompt = oneShotPrompt ?? (positionals.length > 0 ? positionals.join(" ") : undefined);
const systemPrompt = buildFeynmanSystemPrompt();
const piArgs = [
"--session-dir",
@@ -284,7 +614,7 @@ async function main(): Promise<void> {
"--prompt-template",
resolve(appRoot, "prompts"),
"--system-prompt",
FEYNMAN_SYSTEM_PROMPT,
systemPrompt,
];
if (explicitModelSpec) {
@@ -307,6 +637,33 @@ async function main(): Promise<void> {
...process.env,
PI_CODING_AGENT_DIR: feynmanAgentDir,
FEYNMAN_CODING_AGENT_DIR: feynmanAgentDir,
FEYNMAN_PI_NPM_ROOT: resolve(appRoot, ".pi", "npm", "node_modules"),
FEYNMAN_SESSION_DIR: sessionDir,
PI_SESSION_DIR: sessionDir,
FEYNMAN_MEMORY_DIR: resolve(dirname(feynmanAgentDir), "memory"),
FEYNMAN_NODE_EXECUTABLE: process.execPath,
FEYNMAN_BIN_PATH: resolve(appRoot, "bin", "feynman.js"),
PANDOC_PATH:
process.env.PANDOC_PATH ??
resolveExecutable("pandoc", [
"/opt/homebrew/bin/pandoc",
"/usr/local/bin/pandoc",
]),
PI_SKIP_VERSION_CHECK: process.env.PI_SKIP_VERSION_CHECK ?? "1",
MERMAID_CLI_PATH:
process.env.MERMAID_CLI_PATH ??
resolveExecutable("mmdc", [
"/opt/homebrew/bin/mmdc",
"/usr/local/bin/mmdc",
]),
PUPPETEER_EXECUTABLE_PATH:
process.env.PUPPETEER_EXECUTABLE_PATH ??
resolveExecutable("google-chrome", [
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"/Applications/Chromium.app/Contents/MacOS/Chromium",
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
]),
},
});