Initial Feynman research agent scaffold

This commit is contained in:
Advait Paliwal
2026-03-20 11:05:58 -07:00
commit 1fe1ce04a5
25 changed files with 5079 additions and 0 deletions

34
src/feynman-prompt.ts Normal file
View File

@@ -0,0 +1,34 @@
export const FEYNMAN_SYSTEM_PROMPT = `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.
Operating rules:
- Evidence over fluency.
- Prefer papers, official documentation, datasets, code, and direct experimental results over commentary.
- Separate observations from inferences.
- 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.
- 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.
- 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 workflow:
1. Clarify the research objective if needed.
2. Search for relevant primary sources.
3. Inspect the most relevant papers or materials directly.
4. Synthesize consensus, disagreements, and missing evidence.
5. Design and run experiments when they would resolve uncertainty.
6. Write the requested output artifact.
Style:
- Concise, skeptical, and explicit.
- Avoid fake certainty.
- Do not present unverified claims as facts.`;

212
src/index.ts Normal file
View File

@@ -0,0 +1,212 @@
import "dotenv/config";
import { mkdirSync } from "node:fs";
import { stdin as input, stdout as output } from "node:process";
import { dirname, resolve } from "node:path";
import { parseArgs } from "node:util";
import { fileURLToPath } from "node:url";
import readline from "node:readline/promises";
import {
AuthStorage,
createAgentSession,
createCodingTools,
DefaultResourceLoader,
ModelRegistry,
SessionManager,
SettingsManager,
} from "@mariozechner/pi-coding-agent";
import { FEYNMAN_SYSTEM_PROMPT } from "./feynman-prompt.js";
type ThinkingLevel = "off" | "low" | "medium" | "high";
function printHelp(): void {
console.log(`Feynman commands:
/help Show this help
/new Start a fresh persisted session
/exit Quit the REPL
/lit-review <topic> Expand the literature review prompt template
/replicate <paper> Expand the replication prompt template
/reading-list <topic> Expand the reading list prompt template
/paper-code-audit <item> Expand the paper/code audit prompt template
/paper-draft <topic> Expand the paper-style writing prompt template
CLI flags:
--prompt "<text>" Run one prompt and exit
--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`);
}
function parseModelSpec(spec: string, modelRegistry: ModelRegistry) {
const trimmed = spec.trim();
const separator = trimmed.includes(":") ? ":" : trimmed.includes("/") ? "/" : null;
if (!separator) {
return undefined;
}
const [provider, ...rest] = trimmed.split(separator);
const id = rest.join(separator);
if (!provider || !id) {
return undefined;
}
return modelRegistry.find(provider, id);
}
function normalizeThinkingLevel(value: string | undefined): ThinkingLevel | undefined {
if (!value) {
return undefined;
}
const normalized = value.toLowerCase();
if (normalized === "off" || normalized === "low" || normalized === "medium" || normalized === "high") {
return normalized;
}
return undefined;
}
async function main(): Promise<void> {
const here = dirname(fileURLToPath(import.meta.url));
const appRoot = resolve(here, "..");
const { values, positionals } = parseArgs({
allowPositionals: true,
options: {
cwd: { type: "string" },
help: { type: "boolean" },
model: { type: "string" },
"new-session": { type: "boolean" },
prompt: { type: "string" },
"session-dir": { type: "string" },
thinking: { type: "string" },
},
});
if (values.help) {
printHelp();
return;
}
const workingDir = resolve(values.cwd ?? process.cwd());
const sessionDir = resolve(values["session-dir"] ?? resolve(appRoot, ".feynman", "sessions"));
mkdirSync(sessionDir, { recursive: true });
const settingsManager = SettingsManager.create(appRoot);
const authStorage = AuthStorage.create();
const modelRegistry = new ModelRegistry(authStorage);
const explicitModelSpec = values.model ?? process.env.FEYNMAN_MODEL;
const explicitModel = explicitModelSpec ? parseModelSpec(explicitModelSpec, modelRegistry) : undefined;
if (explicitModelSpec && !explicitModel) {
throw new Error(`Unknown model: ${explicitModelSpec}`);
}
if (!explicitModel) {
const available = await modelRegistry.getAvailable();
if (available.length === 0) {
throw new Error(
"No models are available. Configure pi auth or export a provider API key before starting Feynman.",
);
}
}
const thinkingLevel = normalizeThinkingLevel(values.thinking ?? process.env.FEYNMAN_THINKING) ?? "medium";
const resourceLoader = new DefaultResourceLoader({
cwd: appRoot,
additionalExtensionPaths: [resolve(appRoot, "extensions")],
additionalPromptTemplatePaths: [resolve(appRoot, "prompts")],
additionalSkillPaths: [resolve(appRoot, "skills")],
settingsManager,
systemPromptOverride: () => FEYNMAN_SYSTEM_PROMPT,
appendSystemPromptOverride: () => [],
});
await resourceLoader.reload();
const sessionManager = values["new-session"]
? SessionManager.create(workingDir, sessionDir)
: SessionManager.continueRecent(workingDir, sessionDir);
const { session } = await createAgentSession({
authStorage,
cwd: workingDir,
model: explicitModel,
modelRegistry,
resourceLoader,
sessionManager,
settingsManager,
thinkingLevel,
tools: createCodingTools(workingDir),
});
session.subscribe((event) => {
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
process.stdout.write(event.assistantMessageEvent.delta);
return;
}
if (event.type === "tool_execution_start") {
process.stderr.write(`\n[tool] ${event.toolName}\n`);
return;
}
if (event.type === "tool_execution_end" && event.isError) {
process.stderr.write(`[tool-error] ${event.toolName}\n`);
}
});
const initialPrompt = values.prompt ?? (positionals.length > 0 ? positionals.join(" ") : undefined);
if (initialPrompt) {
await session.prompt(initialPrompt);
process.stdout.write("\n");
session.dispose();
return;
}
console.log("Feynman research agent");
console.log(`working dir: ${workingDir}`);
console.log(`session dir: ${sessionDir}`);
console.log("type /help for commands");
const rl = readline.createInterface({ input, output });
try {
while (true) {
const line = (await rl.question("feynman> ")).trim();
if (!line) {
continue;
}
if (line === "/exit" || line === "/quit") {
break;
}
if (line === "/help") {
printHelp();
continue;
}
if (line === "/new") {
await session.newSession();
console.log("started a new session");
continue;
}
await session.prompt(line);
process.stdout.write("\n");
}
} finally {
rl.close();
session.dispose();
}
}
main().catch((error) => {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
});