Skip to content
Part 2: AI Integration
This is the part where the CLI stops being a terminal shell and starts talking to a model.
The earlier version of this chapter explained the idea. This version needs to be concrete enough that you can actually build the stage.
By the end, your app should match protoagent-tutorial-again-part-2.
What you are building in this part
Starting from the Part 1 shell, you are adding:
- the OpenAI SDK
- environment-based API key loading
- a small
Messagestructure - streaming assistant output in the terminal UI
- basic error handling around model calls
This is still deliberately simple. We are not doing provider abstraction or persisted config yet. That comes next.
Starting point
Copy your Part 1 result and continue from there.
Your target snapshot for this stage is:
protoagent-tutorial-again-part-2
Files to change
In this part you only need to change:
package.jsonsrc/App.tsx
src/cli.tsx stays effectively the same as Part 1.
Step 1: Add the runtime dependencies
Add these dependencies to package.json:
openaidotenv
The snapshot for this stage keeps the same scripts as Part 1 and adds those packages to the dependency list.
Step 2: Load environment variables
At the top of src/App.tsx, import:
ts
import OpenAI from 'openai';
import 'dotenv/config';That gives you a low-friction way to load OPENAI_API_KEY from the environment or a local .env file.
Step 3: Replace the plain string message list with structured messages
In Part 1, you were just appending strings. Now switch to a typed message array.
Use a simple interface like this:
ts
interface Message {
role: 'user' | 'assistant' | 'system';
content: string;
}Initialize state with a single system message:
ts
const [messages, setMessages] = useState<Message[]>([
{ role: 'system', content: 'You are ProtoAgent, a helpful AI coding assistant.' },
]);This matters because every later part builds on this role-based message model.
Step 4: Create a basic OpenAI client
For this stage, keep it very direct:
ts
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});That hardcodes this stage to OpenAI and to a single env var. That is okay for now. Provider abstraction and persisted config belong in Part 3.
Step 5: Turn submit into a streaming model call
Update handleSubmit() so it:
- appends the user message
- starts a loading state
- calls
openai.chat.completions.create()withstream: true - creates an empty assistant message in state
- appends each streamed text chunk into that assistant message
The stage snapshot uses model gpt-4o-mini and builds the final assistant text incrementally inside the for await ... of stream loop.
That streaming loop is the real point of this chapter. Once you have that working, the UI starts to feel like an actual agent shell instead of a prompt/response toy.
Step 6: Handle failures in-band
Wrap the API call in try/catch.
If something fails, append an assistant message like:
ts
{ role: 'assistant', content: `Error: ${error.message}` }This is not perfect error design, but it is useful at this stage because you can see failures in the same transcript area as everything else.
Step 7: Keep the UI simple
The visual structure from Part 1 stays mostly intact.
The main UI changes are:
- render user and assistant messages differently
- hide the system message from the visible transcript
- show a simple
Agent is thinking...loading state while the stream is active
That is enough for now.
What the current source does later
The current app is much more capable than this stage:
- it supports multiple providers
- it resolves API keys from config or env vars
- it rebuilds clients from provider metadata
- it streams text and tool calls through a separate loop
But the important idea is unchanged from this early stage: the UI reacts to incremental model output instead of waiting for one big blob.
Verification
Set an API key and run the app:
bash
OPENAI_API_KEY=your_key_here npm run devIf you prefer a local .env file, make sure it contains:
bash
OPENAI_API_KEY=your_key_hereThen ask something simple in the UI.
If it worked, you should see:
- your prompt added to the message list
- a loading state while the request is in flight
- the assistant response stream in incrementally
Resulting snapshot
At the end of this part, your project should match:
protoagent-tutorial-again-part-2
Pitfalls
- forgetting to add
dotenv/configand then wondering whyOPENAI_API_KEYis undefined - recreating the assistant message on every chunk instead of updating the last one
- rendering the system message in the transcript and cluttering the UI
- using a non-streaming request and missing the whole point of this stage
Core takeaway
AI integration is not just "call the model." Even this early version already introduces:
- structured message history
- streamed output
- async UI updates
- error handling around model calls
That is the base layer the rest of the agent runtime will build on.