Skip to content

Commit 135bf73

Browse files
authored
Merge pull request #8 from run-llama/clelia/add-support-for-codex
feat: add support for codex
2 parents d7b0db3 + 96ff108 commit 135bf73

File tree

10 files changed

+634
-27
lines changed

10 files changed

+634
-27
lines changed

README.md

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# Claude + AgentFS + LlamaIndex Workflows
1+
# Coding Agent + AgentFS + LlamaIndex Workflows
22

3-
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).
3+
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).
44

55
## Set Up and Run
66

@@ -18,13 +18,27 @@ pnpm install
1818
# you can use other package managers, but pnpm is preferred
1919
```
2020

21+
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):
22+
23+
```bash
24+
pnpm add @openai/codex-sdk
25+
```
26+
27+
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):
28+
29+
```bash
30+
pnpm run mcp-start
31+
```
32+
33+
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.
34+
2135
Now run the demo with:
2236

2337
```bash
2438
# for the first time
2539
pnpm run start
2640

27-
# for follow-ups
41+
# If you want to add more files to the database
2842
pnpm run clean-start
2943
```
3044

codex/AGENTS.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
You are an expert programmer whose task is to assist the user implement their requests within the current working directory.
2+
3+
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:
4+
5+
- `read_file`: read a file, providing its path
6+
- `write_file`: write a file, providing its path and content
7+
- `edit_file`: edit a file, providing the old string and the new string to replace the old one with
8+
- `list_files`: list all the available files
9+
- `file_exists`: check whether or not a file exists, providing its path
10+
11+
Using these tools, you should be able to provide the user with the assistance that they need.

codex/config.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[features]
2+
rmcp_client = true
3+
4+
[mcp_servers.filesystem]
5+
url = "http://localhost:3000/mcp"
6+
startup_timeout_sec = 30
7+
tool_timeout_sec = 30

package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"test": "rm -rf test.db* && vitest run --testTimeout=60000",
99
"start": "pnpm exec tsx src/index.ts",
1010
"clean-start": "rm *.db* && pnpm exec tsx src/index.ts",
11+
"mcp-start": "pnpm exec tsx src/mcpServer.ts",
1112
"lint": "eslint ./src/ --fix",
1213
"format": "prettier --write ./src/",
1314
"build": "tsc",
@@ -21,6 +22,7 @@
2122
"devDependencies": {
2223
"@anthropic-ai/sdk": "^0.71.2",
2324
"@eslint/js": "^9.39.1",
25+
"@types/express": "^5.0.6",
2426
"@types/figlet": "^1.7.0",
2527
"@types/mime-types": "^3.0.1",
2628
"@types/node": "^24.10.1",
@@ -37,11 +39,18 @@
3739
"@anthropic-ai/claude-agent-sdk": "^0.1.59",
3840
"@llamaindex/workflow-core": "^1.3.3",
3941
"@modelcontextprotocol/sdk": "^1.24.3",
42+
"@openai/codex-sdk": "^0.73.0",
4043
"@visulima/colorize": "^1.4.29",
4144
"agentfs-sdk": "^0.2.1",
45+
"express": "^5.2.1",
4246
"figlet": "^1.9.4",
4347
"llama-cloud-services": "^0.4.3",
4448
"mime-types": "^3.0.2",
4549
"zod": "3.25.76"
50+
},
51+
"pnpm": {
52+
"overrides": {
53+
"@openai/codex-sdk": "-"
54+
}
4655
}
4756
}

src/cli.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export async function consoleInput(question: string): Promise<string> {
88
output: process.stdout,
99
});
1010

11-
const answer = await rl.question(question);
11+
const answer = await rl.question(bold(question));
1212
rl.close();
1313
return answer;
1414
}

src/codex.ts

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
import { Codex } from "@openai/codex-sdk";
2+
import type { ThreadEvent, Thread } from "@openai/codex-sdk";
3+
import {
4+
red,
5+
green,
6+
magentaBright,
7+
yellow,
8+
bold,
9+
cyan,
10+
} from "@visulima/colorize";
11+
12+
export const codex = new Codex({});
13+
14+
export async function runCodex(
15+
prompt: string,
16+
{ resumeSession = undefined }: { resumeSession: string | undefined },
17+
) {
18+
let thread: Thread | undefined = undefined;
19+
if (typeof resumeSession == "undefined") {
20+
thread = codex.startThread({
21+
skipGitRepoCheck: true,
22+
});
23+
} else {
24+
thread = codex.resumeThread(resumeSession, {
25+
skipGitRepoCheck: true,
26+
});
27+
}
28+
const { events } = await thread.runStreamed(prompt);
29+
30+
for await (const event of events) {
31+
switch (event.type) {
32+
case "thread.started":
33+
console.log(`Started session with ID: ${bold(event.thread_id)}`);
34+
break;
35+
case "item.started":
36+
await handleItemStart(event);
37+
break;
38+
case "item.updated":
39+
await handleItemUpdated(event);
40+
break;
41+
case "item.completed":
42+
await handleItemCompleted(event);
43+
break;
44+
case "turn.completed":
45+
await handleTurnCompletion(event);
46+
break;
47+
case "error":
48+
await handleError(event);
49+
break;
50+
}
51+
}
52+
}
53+
54+
async function handleItemStart(event: ThreadEvent) {
55+
if (event.type == "item.started") {
56+
if (event.item.type == "agent_message") {
57+
console.log(bold(magentaBright("Assistant started responding...")));
58+
console.log(event.item.text);
59+
} else if (event.item.type == "reasoning") {
60+
console.log(bold(magentaBright("Assistant started thinking...")));
61+
console.log(event.item.text);
62+
} else if (event.item.type == "mcp_tool_call") {
63+
console.log(
64+
bold(
65+
yellow(
66+
`Assistant started calling MCP tool ${event.item.tool} with input ${JSON.stringify(event.item.arguments)}`,
67+
),
68+
),
69+
);
70+
if (event.item.error) {
71+
console.log(
72+
red(
73+
bold(
74+
`An error occurred while calling the tool: ${event.item.error.message}`,
75+
),
76+
),
77+
);
78+
} else {
79+
if (event.item.result) {
80+
let finalResult = "";
81+
for (const block of event.item.result.content) {
82+
if (block.type == "text") {
83+
finalResult += block.text + "\n";
84+
}
85+
}
86+
console.log(`${green(bold("Tool result"))}: ${finalResult}`);
87+
}
88+
}
89+
} else if (event.item.type == "todo_list") {
90+
console.log(
91+
bold(green("Assistant started to produce a TODO list with items:")),
92+
);
93+
let c = 0;
94+
for (const i of event.item.items) {
95+
c += 1;
96+
console.log(
97+
`TODO item ${c}: ${i.text} (${i.completed ? "completed" : "not completed"})`,
98+
);
99+
}
100+
} else if (event.item.type == "web_search") {
101+
console.log(
102+
`Assistant started searching the web with query: ${event.item.query}`,
103+
);
104+
} else if (event.item.type == "command_execution") {
105+
console.log(
106+
`Assistant started to execute command: ${event.item.command}`,
107+
);
108+
} else if (event.item.type == "file_change") {
109+
console.log(
110+
bold(red("WARNING! The assistant is starting to change files:")),
111+
);
112+
for (const change of event.item.changes) {
113+
console.log(
114+
`The assistant would like to apply ${change.kind} to ${change.path}`,
115+
);
116+
}
117+
} else {
118+
console.log(bold(red(`An error occurred: ${event.item.message}`)));
119+
}
120+
}
121+
}
122+
123+
async function handleItemUpdated(event: ThreadEvent) {
124+
if (event.type == "item.updated") {
125+
if (event.item.type == "agent_message") {
126+
console.log(bold(magentaBright("Assistant updated its response...")));
127+
console.log(event.item.text);
128+
} else if (event.item.type == "reasoning") {
129+
console.log(bold(magentaBright("Assistant updated its thoughts...")));
130+
console.log(event.item.text);
131+
} else if (event.item.type == "mcp_tool_call") {
132+
console.log(
133+
bold(
134+
yellow(
135+
`Assistant updated its call to MCP tool ${event.item.tool} with input ${JSON.stringify(event.item.arguments)}`,
136+
),
137+
),
138+
);
139+
if (event.item.error) {
140+
console.log(
141+
red(
142+
bold(
143+
`An error occurred while calling the tool: ${event.item.error.message}`,
144+
),
145+
),
146+
);
147+
} else {
148+
if (event.item.result) {
149+
let finalResult = "";
150+
for (const block of event.item.result.content) {
151+
if (block.type == "text") {
152+
finalResult += block.text + "\n";
153+
}
154+
}
155+
console.log(`${green(bold("Tool result"))}: ${finalResult}`);
156+
}
157+
}
158+
} else if (event.item.type == "todo_list") {
159+
console.log(bold(green("Assistant updated its TODO list with items:")));
160+
let c = 0;
161+
for (const i of event.item.items) {
162+
c += 1;
163+
console.log(
164+
`TODO item ${c}: ${i.text} (${i.completed ? "completed" : "not completed"})`,
165+
);
166+
}
167+
} else if (event.item.type == "web_search") {
168+
console.log(
169+
`Assistant updated its web search with query: ${event.item.query}`,
170+
);
171+
} else if (event.item.type == "command_execution") {
172+
console.log(`Assistant updated command execution: ${event.item.command}`);
173+
} else if (event.item.type == "file_change") {
174+
console.log(
175+
bold(red("WARNING! The assistant is starting to change files:")),
176+
);
177+
for (const change of event.item.changes) {
178+
console.log(
179+
`The assistant is updating the change it would like to apply (${change.kind}) to ${change.path}`,
180+
);
181+
}
182+
} else {
183+
console.log(bold(red(`An error occurred: ${event.item.message}`)));
184+
}
185+
}
186+
}
187+
188+
async function handleItemCompleted(event: ThreadEvent) {
189+
if (event.type == "item.completed") {
190+
if (event.item.type == "agent_message") {
191+
console.log(bold(magentaBright("Assistant completed its response:")));
192+
console.log(event.item.text);
193+
} else if (event.item.type == "reasoning") {
194+
console.log(bold(magentaBright("Assistant completed its thoughts:")));
195+
console.log(event.item.text);
196+
} else if (event.item.type == "mcp_tool_call") {
197+
console.log(
198+
bold(
199+
yellow(
200+
`Assistant completed its call to MCP tool ${event.item.tool} with input ${JSON.stringify(event.item.arguments)}`,
201+
),
202+
),
203+
);
204+
if (event.item.error) {
205+
console.log(
206+
red(
207+
bold(
208+
`An error occurred while calling the tool: ${event.item.error.message}`,
209+
),
210+
),
211+
);
212+
} else {
213+
if (event.item.result) {
214+
let finalResult = "";
215+
for (const block of event.item.result.content) {
216+
if (block.type == "text") {
217+
finalResult += block.text + "\n";
218+
}
219+
}
220+
console.log(`${green(bold("Tool result"))}: ${finalResult}`);
221+
}
222+
}
223+
} else if (event.item.type == "todo_list") {
224+
console.log(bold(green("Assistant completed its TODO list with items:")));
225+
let c = 0;
226+
for (const i of event.item.items) {
227+
c += 1;
228+
console.log(
229+
`TODO item ${c}: ${i.text} (${i.completed ? "completed" : "not completed"})`,
230+
);
231+
}
232+
} else if (event.item.type == "web_search") {
233+
console.log(
234+
`Assistant updated its web search with query: ${event.item.query}`,
235+
);
236+
} else if (event.item.type == "command_execution") {
237+
console.log(`Assistant updated command execution: ${event.item.command}`);
238+
} else if (event.item.type == "file_change") {
239+
console.log(
240+
bold(red("ERROR! The assistant has completed its file changes:")),
241+
);
242+
for (const change of event.item.changes) {
243+
console.log(
244+
`The assistant has completed the change it applied to ${change.path} (${change.kind})`,
245+
);
246+
}
247+
} else {
248+
console.log(bold(red(`An error occurred: ${event.item.message}`)));
249+
}
250+
}
251+
}
252+
253+
async function handleTurnCompletion(event: ThreadEvent) {
254+
if (event.type == "turn.completed") {
255+
console.log(cyan(bold("Turn completed, usage:")));
256+
console.log(`Input tokens: ${event.usage.input_tokens}`);
257+
console.log(`Cached input tokens: ${event.usage.cached_input_tokens}`);
258+
console.log(`Output tokens: ${event.usage.output_tokens}`);
259+
}
260+
}
261+
262+
async function handleError(event: ThreadEvent) {
263+
if (event.type == "error") {
264+
console.log(bold(red(`An error occurred: ${event.message}`)));
265+
}
266+
}

0 commit comments

Comments
 (0)