Merge pull request #8 from run-llama/clelia/add-support-for-codex

feat: add support for codex
This commit is contained in:
Clelia (Astra) Bertelli
2025-12-19 13:49:50 +01:00
committed by GitHub
10 changed files with 634 additions and 27 deletions
+17 -3
View File
@@ -1,6 +1,6 @@
# Claude + AgentFS + LlamaIndex Workflows
# Coding Agent + AgentFS + LlamaIndex Workflows
A demo where we run Claude Code within a fully-virtualized file system ([AgentFS](https://github.com/tursodatabase/agentfs)), orchestrating it with [LlamaIndex Workflows](https://github.com/run-llama/workflows-ts) and adding the possibility of reading unstructured files (e.g. PDFs or Word/Google docs) with [LlamaCloud](https://cloud.llamaindex.ai).
A demo where we run Claude Code or Codex within a fully-virtualized file system ([AgentFS](https://github.com/tursodatabase/agentfs)), orchestrating it with [LlamaIndex Workflows](https://github.com/run-llama/workflows-ts) and adding the possibility of reading unstructured files (e.g. PDFs or Word/Google docs) with [LlamaCloud](https://cloud.llamaindex.ai).
## Set Up and Run
@@ -18,13 +18,27 @@ pnpm install
# you can use other package managers, but pnpm is preferred
```
If you wish to use the demo with Codex, you need to install the Codex SDK separately (given the size of the library - 140+ MB - its download it disabled by default):
```bash
pnpm add @openai/codex-sdk
```
Moreover, if you wish to run the demo with Codex, you also need to start the MCP server (from a different terminal window, but within the same directory):
```bash
pnpm run mcp-start
```
The MCP will be live on `http://localhost:3000/mcp`, and you will need to add the MCP configuration in [config.toml](./codex/config.toml) to the global Codex configuration in `$HOME/.codex/config.toml`. If you want Codex to use the filesystem MCP by default, you will also need to copy the [AGENTS.md](./codex/AGENTS.md) file, containing the instructions on how to use the server.
Now run the demo with:
```bash
# for the first time
pnpm run start
# for follow-ups
# If you want to add more files to the database
pnpm run clean-start
```
+11
View File
@@ -0,0 +1,11 @@
You are an expert programmer whose task is to assist the user implement their requests within the current working directory.
In order to perform file system operations, you MUST NOT USE the built-in tools you have (Read, Write, Glob, Edit): instead, you MUST USE the `filesystem` MCP server, which provides the following tools:
- `read_file`: read a file, providing its path
- `write_file`: write a file, providing its path and content
- `edit_file`: edit a file, providing the old string and the new string to replace the old one with
- `list_files`: list all the available files
- `file_exists`: check whether or not a file exists, providing its path
Using these tools, you should be able to provide the user with the assistance that they need.
+7
View File
@@ -0,0 +1,7 @@
[features]
rmcp_client = true
[mcp_servers.filesystem]
url = "http://localhost:3000/mcp"
startup_timeout_sec = 30
tool_timeout_sec = 30
+9
View File
@@ -8,6 +8,7 @@
"test": "rm -rf test.db* && vitest run --testTimeout=60000",
"start": "pnpm exec tsx src/index.ts",
"clean-start": "rm *.db* && pnpm exec tsx src/index.ts",
"mcp-start": "pnpm exec tsx src/mcpServer.ts",
"lint": "eslint ./src/ --fix",
"format": "prettier --write ./src/",
"build": "tsc",
@@ -21,6 +22,7 @@
"devDependencies": {
"@anthropic-ai/sdk": "^0.71.2",
"@eslint/js": "^9.39.1",
"@types/express": "^5.0.6",
"@types/figlet": "^1.7.0",
"@types/mime-types": "^3.0.1",
"@types/node": "^24.10.1",
@@ -37,11 +39,18 @@
"@anthropic-ai/claude-agent-sdk": "^0.1.59",
"@llamaindex/workflow-core": "^1.3.3",
"@modelcontextprotocol/sdk": "^1.24.3",
"@openai/codex-sdk": "^0.73.0",
"@visulima/colorize": "^1.4.29",
"agentfs-sdk": "^0.2.1",
"express": "^5.2.1",
"figlet": "^1.9.4",
"llama-cloud-services": "^0.4.3",
"mime-types": "^3.0.2",
"zod": "3.25.76"
},
"pnpm": {
"overrides": {
"@openai/codex-sdk": "-"
}
}
}
+1 -1
View File
@@ -8,7 +8,7 @@ export async function consoleInput(question: string): Promise<string> {
output: process.stdout,
});
const answer = await rl.question(question);
const answer = await rl.question(bold(question));
rl.close();
return answer;
}
+266
View File
@@ -0,0 +1,266 @@
import { Codex } from "@openai/codex-sdk";
import type { ThreadEvent, Thread } from "@openai/codex-sdk";
import {
red,
green,
magentaBright,
yellow,
bold,
cyan,
} from "@visulima/colorize";
export const codex = new Codex({});
export async function runCodex(
prompt: string,
{ resumeSession = undefined }: { resumeSession: string | undefined },
) {
let thread: Thread | undefined = undefined;
if (typeof resumeSession == "undefined") {
thread = codex.startThread({
skipGitRepoCheck: true,
});
} else {
thread = codex.resumeThread(resumeSession, {
skipGitRepoCheck: true,
});
}
const { events } = await thread.runStreamed(prompt);
for await (const event of events) {
switch (event.type) {
case "thread.started":
console.log(`Started session with ID: ${bold(event.thread_id)}`);
break;
case "item.started":
await handleItemStart(event);
break;
case "item.updated":
await handleItemUpdated(event);
break;
case "item.completed":
await handleItemCompleted(event);
break;
case "turn.completed":
await handleTurnCompletion(event);
break;
case "error":
await handleError(event);
break;
}
}
}
async function handleItemStart(event: ThreadEvent) {
if (event.type == "item.started") {
if (event.item.type == "agent_message") {
console.log(bold(magentaBright("Assistant started responding...")));
console.log(event.item.text);
} else if (event.item.type == "reasoning") {
console.log(bold(magentaBright("Assistant started thinking...")));
console.log(event.item.text);
} else if (event.item.type == "mcp_tool_call") {
console.log(
bold(
yellow(
`Assistant started calling MCP tool ${event.item.tool} with input ${JSON.stringify(event.item.arguments)}`,
),
),
);
if (event.item.error) {
console.log(
red(
bold(
`An error occurred while calling the tool: ${event.item.error.message}`,
),
),
);
} else {
if (event.item.result) {
let finalResult = "";
for (const block of event.item.result.content) {
if (block.type == "text") {
finalResult += block.text + "\n";
}
}
console.log(`${green(bold("Tool result"))}: ${finalResult}`);
}
}
} else if (event.item.type == "todo_list") {
console.log(
bold(green("Assistant started to produce a TODO list with items:")),
);
let c = 0;
for (const i of event.item.items) {
c += 1;
console.log(
`TODO item ${c}: ${i.text} (${i.completed ? "completed" : "not completed"})`,
);
}
} else if (event.item.type == "web_search") {
console.log(
`Assistant started searching the web with query: ${event.item.query}`,
);
} else if (event.item.type == "command_execution") {
console.log(
`Assistant started to execute command: ${event.item.command}`,
);
} else if (event.item.type == "file_change") {
console.log(
bold(red("WARNING! The assistant is starting to change files:")),
);
for (const change of event.item.changes) {
console.log(
`The assistant would like to apply ${change.kind} to ${change.path}`,
);
}
} else {
console.log(bold(red(`An error occurred: ${event.item.message}`)));
}
}
}
async function handleItemUpdated(event: ThreadEvent) {
if (event.type == "item.updated") {
if (event.item.type == "agent_message") {
console.log(bold(magentaBright("Assistant updated its response...")));
console.log(event.item.text);
} else if (event.item.type == "reasoning") {
console.log(bold(magentaBright("Assistant updated its thoughts...")));
console.log(event.item.text);
} else if (event.item.type == "mcp_tool_call") {
console.log(
bold(
yellow(
`Assistant updated its call to MCP tool ${event.item.tool} with input ${JSON.stringify(event.item.arguments)}`,
),
),
);
if (event.item.error) {
console.log(
red(
bold(
`An error occurred while calling the tool: ${event.item.error.message}`,
),
),
);
} else {
if (event.item.result) {
let finalResult = "";
for (const block of event.item.result.content) {
if (block.type == "text") {
finalResult += block.text + "\n";
}
}
console.log(`${green(bold("Tool result"))}: ${finalResult}`);
}
}
} else if (event.item.type == "todo_list") {
console.log(bold(green("Assistant updated its TODO list with items:")));
let c = 0;
for (const i of event.item.items) {
c += 1;
console.log(
`TODO item ${c}: ${i.text} (${i.completed ? "completed" : "not completed"})`,
);
}
} else if (event.item.type == "web_search") {
console.log(
`Assistant updated its web search with query: ${event.item.query}`,
);
} else if (event.item.type == "command_execution") {
console.log(`Assistant updated command execution: ${event.item.command}`);
} else if (event.item.type == "file_change") {
console.log(
bold(red("WARNING! The assistant is starting to change files:")),
);
for (const change of event.item.changes) {
console.log(
`The assistant is updating the change it would like to apply (${change.kind}) to ${change.path}`,
);
}
} else {
console.log(bold(red(`An error occurred: ${event.item.message}`)));
}
}
}
async function handleItemCompleted(event: ThreadEvent) {
if (event.type == "item.completed") {
if (event.item.type == "agent_message") {
console.log(bold(magentaBright("Assistant completed its response:")));
console.log(event.item.text);
} else if (event.item.type == "reasoning") {
console.log(bold(magentaBright("Assistant completed its thoughts:")));
console.log(event.item.text);
} else if (event.item.type == "mcp_tool_call") {
console.log(
bold(
yellow(
`Assistant completed its call to MCP tool ${event.item.tool} with input ${JSON.stringify(event.item.arguments)}`,
),
),
);
if (event.item.error) {
console.log(
red(
bold(
`An error occurred while calling the tool: ${event.item.error.message}`,
),
),
);
} else {
if (event.item.result) {
let finalResult = "";
for (const block of event.item.result.content) {
if (block.type == "text") {
finalResult += block.text + "\n";
}
}
console.log(`${green(bold("Tool result"))}: ${finalResult}`);
}
}
} else if (event.item.type == "todo_list") {
console.log(bold(green("Assistant completed its TODO list with items:")));
let c = 0;
for (const i of event.item.items) {
c += 1;
console.log(
`TODO item ${c}: ${i.text} (${i.completed ? "completed" : "not completed"})`,
);
}
} else if (event.item.type == "web_search") {
console.log(
`Assistant updated its web search with query: ${event.item.query}`,
);
} else if (event.item.type == "command_execution") {
console.log(`Assistant updated command execution: ${event.item.command}`);
} else if (event.item.type == "file_change") {
console.log(
bold(red("ERROR! The assistant has completed its file changes:")),
);
for (const change of event.item.changes) {
console.log(
`The assistant has completed the change it applied to ${change.path} (${change.kind})`,
);
}
} else {
console.log(bold(red(`An error occurred: ${event.item.message}`)));
}
}
}
async function handleTurnCompletion(event: ThreadEvent) {
if (event.type == "turn.completed") {
console.log(cyan(bold("Turn completed, usage:")));
console.log(`Input tokens: ${event.usage.input_tokens}`);
console.log(`Cached input tokens: ${event.usage.cached_input_tokens}`);
console.log(`Output tokens: ${event.usage.output_tokens}`);
}
}
async function handleError(event: ThreadEvent) {
if (event.type == "error") {
console.log(bold(red(`An error occurred: ${event.message}`)));
}
}
+59 -16
View File
@@ -4,9 +4,10 @@ import { recordFiles } from "./filesystem";
import { getAgentFS } from "./mcp";
import { Agent } from "./claude";
import { queryOptions } from "./options";
import { bold } from "@visulima/colorize";
import { bold, green, red } from "@visulima/colorize";
import { consoleInput, renderLogo } from "./cli";
import * as fs from "fs";
import { runCodex } from "./codex";
async function main() {
const { withState } = createStatefulMiddleware(() => ({}));
@@ -15,6 +16,7 @@ async function main() {
const filesRegisteredEvent = workflowEvent<void>();
const requestPromptEvent = workflowEvent<void>();
const promptEvent = workflowEvent<{
chosenAgent: string;
prompt: string;
resume: string | undefined;
plan: boolean;
@@ -28,6 +30,10 @@ async function main() {
workflow.handle([startEvent], async (_context, event) => {
await renderLogo();
if (notFromScratch) {
fs.copyFileSync("fs.db", "fsMcp.db");
if (fs.existsSync("fs.db-wal")) {
fs.copyFileSync("fs.db-wal", "fsMcp.db-wal");
}
return filesRegisteredEvent.with();
}
const wd = event.data.workingDirectory;
@@ -43,6 +49,10 @@ async function main() {
"Could not register the files within the AgentFS file system: check writing permissions in the current directory",
});
} else {
fs.copyFileSync("fs.db", "fsMcp.db");
if (fs.existsSync("fs.db-wal")) {
fs.copyFileSync("fs.db-wal", "fsMcp.db-wal");
}
return filesRegisteredEvent.with();
}
});
@@ -51,7 +61,9 @@ async function main() {
workflow.handle([filesRegisteredEvent], async (_context, _event) => {
console.log(
bold(
"All the files have been uploaded to the AgentFS filesystem, what would you like to do now?",
green(
"All the files have been uploaded to the AgentFS filesystem, what would you like to do now?",
),
),
);
return requestPromptEvent.with();
@@ -59,15 +71,25 @@ async function main() {
workflow.handle([promptEvent], async (_context, event) => {
const prompt = event.data.prompt;
const agent = new Agent(queryOptions, {
resume: event.data.resume,
plan: event.data.plan,
});
try {
await agent.run(prompt);
return stopEvent.with({ success: true, error: null });
} catch (error) {
return stopEvent.with({ success: false, error: JSON.stringify(error) });
if (event.data.chosenAgent == "claude") {
const agent = new Agent(queryOptions, {
resume: event.data.resume,
plan: event.data.plan,
});
try {
await agent.run(prompt);
return stopEvent.with({ success: true, error: null });
} catch (error) {
return stopEvent.with({ success: false, error: JSON.stringify(error) });
}
} else {
try {
await runCodex(event.data.prompt, { resumeSession: event.data.resume });
return stopEvent.with({ success: true, error: null });
} catch (error) {
console.error(error);
return stopEvent.with({ success: false, error: JSON.stringify(error) });
}
}
});
@@ -75,6 +97,13 @@ async function main() {
sendEvent(startEvent.with({ workingDirectory: "./" }));
await stream.until(requestPromptEvent).toArray();
const snapshotData = await snapshot();
let agentOfChoice = "claude";
const chosenAgent = await consoleInput(
"What agent would you like to use? [codex/claude] ",
);
if (chosenAgent.trim() != "") {
agentOfChoice = chosenAgent;
}
const humanResponse = await consoleInput("Your prompt: ");
console.log(
bold("Would you like to resume a previous session? Leave blank if not"),
@@ -84,21 +113,35 @@ async function main() {
if (resumeSession.trim() != "") {
sessionId = resumeSession;
}
console.log(bold("Would you like to activate plan mode? [y/n]"));
const activatePlan = await consoleInput("Your answer: ");
let planMode = false;
if (["yes", "y", "yse"].includes(activatePlan.trim().toLowerCase())) {
planMode = true;
if (agentOfChoice == "claude") {
console.log(bold("Would you like to activate plan mode? [y/n]"));
const activatePlan = await consoleInput("Your answer: ");
if (["yes", "y", "yse"].includes(activatePlan.trim().toLowerCase())) {
planMode = true;
}
}
const resumedContext = workflow.resume(snapshotData);
resumedContext.sendEvent(
promptEvent.with({
chosenAgent: agentOfChoice,
prompt: humanResponse,
resume: sessionId,
plan: planMode,
}),
);
await resumedContext.stream.until(stopEvent).toArray();
const finalEvent = (
await resumedContext.stream.until(stopEvent).toArray()
).at(-1);
if (typeof finalEvent != "undefined") {
if ("error" in finalEvent.data) {
if (typeof finalEvent.data.error == "string") {
console.log(
`${bold(red("An error occurred during the workflow execution:"))} ${finalEvent.data.error}`,
);
}
}
}
}
await main().catch(console.error);
+5 -5
View File
@@ -10,16 +10,16 @@ import {
} from "./filesystem";
import { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
const readSchemaShape = {
export const readSchemaShape = {
filePath: z.string().describe("Path of the file to read"),
};
const writeSchemaShape = {
export const writeSchemaShape = {
filePath: z.string().describe("Path of the file to write"),
fileContent: z.string().describe("Content of the file"),
};
const editSchemaShape = {
export const editSchemaShape = {
filePath: z.string().describe("Path of the file to write"),
oldString: z.string().describe("Old string in the file (to replace)"),
newString: z
@@ -27,13 +27,13 @@ const editSchemaShape = {
.describe("New string with which to replace the old string"),
};
const fileExistsSchemaShape = {
export const fileExistsSchemaShape = {
filePath: z
.string()
.describe("Path of the file whose existence has to be checked"),
};
const listFilesSchemaShape = {};
export const listFilesSchemaShape = {};
// eslint-disable-next-line
const readSchema = z.object(readSchemaShape);
+257
View File
@@ -0,0 +1,257 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { Request, Response } from "express";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import {
type CallToolResult,
isInitializeRequest,
} from "@modelcontextprotocol/sdk/types.js";
import { randomUUID } from "node:crypto";
import {
readSchemaShape,
fileExistsSchemaShape,
writeSchemaShape,
listFilesSchemaShape,
editSchemaShape,
getAgentFS,
} from "./mcp";
import {
readFile,
writeFile,
editFile,
fileExists,
listFiles,
} from "./filesystem";
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
const getServer = () => {
const mcpServer = new McpServer({
name: "filesystem-mcp",
version: "1.0.0",
});
mcpServer.registerTool(
"read_file",
{
description: "Read a file by passing its path.",
inputSchema: readSchemaShape,
},
async ({ filePath }) => {
const agentfs = await getAgentFS({ filePath: "fsMcp.db" });
const content = await readFile(filePath, agentfs);
if (typeof content == "string") {
return { content: [{ type: "text", text: content }] };
} else {
return {
content: [
{
type: "text",
text: `Could not read ${filePath}. Please check that the file exists and submit the request again.`,
},
],
isError: true,
};
}
},
);
mcpServer.registerTool(
"file_exists",
{
description: "Check whether a file exists or not by passing its path.",
inputSchema: fileExistsSchemaShape,
},
async ({ filePath }) => {
const agentfs = await getAgentFS({ filePath: "fsMcp.db" });
const exists = await fileExists(filePath, agentfs);
if (exists) {
return {
content: [{ type: "text", text: `File ${filePath} exists` }],
};
} else {
return {
content: [{ type: "text", text: `File ${filePath} does not exist.` }],
};
}
},
);
mcpServer.registerTool(
"write_file",
{
description: "Write a file by passing its path and content.",
inputSchema: writeSchemaShape,
},
async ({ filePath, fileContent }) => {
const agentfs = await getAgentFS({ filePath: "fsMcp.db" });
const success = await writeFile(filePath, fileContent, agentfs);
if (success) {
return {
content: [
{
type: "text",
text: `File ${filePath} successfully written with content:\n\n'''\n${fileContent}\n'''`,
},
],
};
} else {
return {
content: [
{
type: "text",
text: `There was an error while writing file ${filePath}`,
},
],
};
}
},
);
mcpServer.registerTool(
"edit_file",
{
description:
"Edit a file by passing its path, the old string and the new string.",
inputSchema: editSchemaShape,
},
async ({ filePath, oldString, newString }) => {
const agentfs = await getAgentFS({ filePath: "fsMcp.db" });
const editedContent = await editFile(
filePath,
oldString,
newString,
agentfs,
);
if (typeof editedContent == "string") {
return {
content: [
{
type: "text",
text: `Successfully edited ${filePath}. New content:\n\n'''\n${editedContent}\n'''`,
},
],
} as CallToolResult;
} else {
return {
content: [
{
type: "text",
text: `Could not edit ${filePath}. Please check that the file exists and submit the request again.`,
},
],
isError: true,
};
}
},
);
mcpServer.registerTool(
"list_files",
{
description: "List all the available files",
inputSchema: listFilesSchemaShape,
},
async () => {
const agentfs = await getAgentFS({ filePath: "fsMcp.db" });
const files = await listFiles(agentfs);
if (files != "") {
return { content: [{ type: "text", text: files }] };
} else {
return {
content: [
{
type: "text",
text: `Could not list files. Please report this failure to the user`,
},
],
isError: true,
};
}
},
);
return mcpServer;
};
const app = createMcpExpressApp();
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
app.post("/mcp", async (req: Request, res: Response) => {
console.log("Received MCP request:", req.body);
try {
// Check for existing session ID
const sessionId = req.headers["mcp-session-id"] as string | undefined;
let transport: StreamableHTTPServerTransport;
if (sessionId && transports[sessionId]) {
// Reuse existing transport
transport = transports[sessionId];
} else if (!sessionId && isInitializeRequest(req.body)) {
// New initialization request - use JSON response mode
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
enableJsonResponse: true, // Enable JSON response mode
onsessioninitialized: (sessionId) => {
// Store the transport by session ID when session is initialized
// This avoids race conditions where requests might come in before the session is stored
console.log(`Session initialized with ID: ${sessionId}`);
transports[sessionId] = transport;
},
});
// Connect the transport to the MCP server BEFORE handling the request
const server = getServer();
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
return; // Already handled
} else {
// Invalid request - no session ID or not initialization request
res.status(400).json({
jsonrpc: "2.0",
error: {
code: -32000,
message: "Bad Request: No valid session ID provided",
},
id: null,
});
return;
}
// Handle the request with existing transport - no need to reconnect
await transport.handleRequest(req, res, req.body);
} catch (error) {
console.error("Error handling MCP request:", error);
if (!res.headersSent) {
res.status(500).json({
jsonrpc: "2.0",
error: {
code: -32603,
message: "Internal server error",
},
id: null,
});
}
}
});
// Handle GET requests for SSE streams according to spec
app.get("/mcp", async (req: Request, res: Response) => {
// Since this is a very simple example, we don't support GET requests for this server
// The spec requires returning 405 Method Not Allowed in this case
res.status(405).set("Allow", "POST").send("Method Not Allowed");
});
// Start the server
const PORT = 3000;
app.listen(PORT, (error) => {
if (error) {
console.error("Failed to start server:", error);
process.exit(1);
}
console.log(`MCP Streamable HTTP Server listening on port ${PORT}`);
});
// Handle server shutdown
process.on("SIGINT", async () => {
console.log("Shutting down server...");
process.exit(0);
});
+2 -2
View File
@@ -131,9 +131,9 @@ const hooks: Partial<Record<HookEvent, HookCallbackMatcher[]>> = {
};
export const systemPrompt = `
You are an expert programmer whose task is to assist the user implement their requsts within the current working directory.
You are an expert programmer whose task is to assist the user implement their requests within the current working directory.
In order to perform file system operations, you MUST NOT USE the built-in tools you have (Read, Write, Glob, Edit): instead, you MUST USE the 'filesystem' MCP server, wich provides the following tools:
In order to perform file system operations, you MUST NOT USE the built-in tools you have (Read, Write, Glob, Edit): instead, you MUST USE the 'filesystem' MCP server, which provides the following tools:
- 'read_file': read a file, providing its path
- 'write_file': write a file, providing its path and content