Skip to content

Commit bf5ceb6

Browse files
committed
docs(openai-agent): add comprehensive README documentation
- Add detailed overview of OpenAI Agent architecture and message flow - Document installation and build instructions - Provide usage examples for Registry Launcher with custom agents file - Include standalone mode setup and direct launch instructions - Add provider-specific configuration examples (Ollama, AWS Bedrock, Azure OpenAI) - Document all environment variables and their defaults - List supported ACP methods and agent capabilities - Include content block conversion reference table - Add troubleshooting section for common issues - Provide examples for testing with raw NDJSON messages
1 parent 9bb9787 commit bf5ceb6

1 file changed

Lines changed: 350 additions & 0 deletions

File tree

Lines changed: 350 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,350 @@
1+
# OpenAI Agent
2+
3+
ACP agent for [stdio Bus kernel](https://github.com/stdiobus/stdiobus) that bridges the Agent Client Protocol to any OpenAI Chat Completions API-compatible endpoint.
4+
5+
## Overview
6+
7+
The OpenAI Agent translates ACP protocol messages (`initialize`, `newSession`, `prompt`, `cancel`) into HTTP POST requests to `/chat/completions` with `stream: true`, parses the SSE response token-by-token, and streams results back as `agent_message_chunk` session updates. It maintains per-session conversation history for multi-turn dialogues.
8+
9+
The agent is universal — it works with any endpoint implementing the OpenAI Chat Completions API:
10+
11+
| Provider | Base URL |
12+
|----------|----------|
13+
| OpenAI | `https://api.openai.com/v1` (default) |
14+
| AWS Bedrock | `https://{region}.bedrock.amazonaws.com/openai/v1` |
15+
| Azure OpenAI | `https://{resource}.openai.azure.com/openai/deployments/{deployment}` |
16+
| Ollama | `http://localhost:11434/v1` |
17+
| vLLM | `http://localhost:8000/v1` |
18+
| LiteLLM | `http://localhost:4000/v1` |
19+
20+
## Architecture
21+
22+
```
23+
stdio Bus kernel
24+
↓ (NDJSON via stdin/stdout)
25+
OpenAI Agent
26+
├── SessionIdRouter (sessionId mapping)
27+
├── AgentSideConnection (ACP JSON-RPC 2.0)
28+
├── OpenAIAgent (protocol handler)
29+
│ ├── SessionManager (conversation state)
30+
│ └── ChatCompletionsClient (HTTP + SSE)
31+
│ └── SSEParser (line-by-line parsing)
32+
└── fetch() → OpenAI-compatible API
33+
```
34+
35+
### Message Flow
36+
37+
1. Registry Launcher spawns `openai-agent` as a child process with environment variables
38+
2. stdin receives NDJSON messages from stdio Bus kernel
39+
3. `SessionIdRouter` strips `sessionId` from incoming messages, restores it on outgoing
40+
4. `AgentSideConnection` + `ndJsonStream` handle JSON-RPC 2.0 framing
41+
5. `OpenAIAgent` dispatches to the appropriate handler (`initialize`, `newSession`, `prompt`, `cancel`)
42+
6. On `prompt`: ACP content blocks are converted to OpenAI messages, `ChatCompletionsClient` sends `POST {baseUrl}/chat/completions` with `stream: true`
43+
7. SSE chunks are parsed line-by-line; `delta.content` tokens are forwarded via `sessionUpdate()`
44+
8. On stream completion (`data: [DONE]`), the full response is saved to session history
45+
46+
## Installation
47+
48+
```bash
49+
cd workers-registry/openai-agent
50+
npm install
51+
npm run build
52+
```
53+
54+
## Usage
55+
56+
### With stdio Bus kernel via Registry Launcher (recommended)
57+
58+
The standard way to run the agent is through the Registry Launcher (`acp-registry` worker), which handles agent discovery, process management, and routing.
59+
60+
**1. Create a custom agents file** (`openai-custom-agents.json`):
61+
62+
```json
63+
{
64+
"agents": [
65+
{
66+
"id": "openai-gpt4o",
67+
"name": "OpenAI GPT-4o",
68+
"version": "1.0.0",
69+
"description": "OpenAI Chat Completions API agent via ACP protocol",
70+
"distribution": {
71+
"npx": {
72+
"package": "@stdiobus/workers-registry",
73+
"args": ["openai-agent"],
74+
"env": {
75+
"OPENAI_BASE_URL": "https://api.openai.com/v1",
76+
"OPENAI_MODEL": "gpt-4o"
77+
}
78+
}
79+
}
80+
}
81+
]
82+
}
83+
```
84+
85+
**2. Add the API key** to `api-keys.json`:
86+
87+
```json
88+
{
89+
"OPENAI_API_KEY": "sk-..."
90+
}
91+
```
92+
93+
**3. Configure the stdio Bus pool** with `--custom-agents` pointing to your file:
94+
95+
```json
96+
{
97+
"pools": [{
98+
"id": "acp-worker",
99+
"command": "npx",
100+
"args": [
101+
"@stdiobus/workers-registry", "acp-registry",
102+
"--custom-agents", "./openai-custom-agents.json"
103+
],
104+
"env": {},
105+
"instances": 1
106+
}]
107+
}
108+
```
109+
110+
**4. Start stdio Bus:**
111+
112+
```bash
113+
./stdio_bus --config config.json --tcp 127.0.0.1:9000
114+
```
115+
116+
The Registry Launcher will discover the `openai-gpt4o` agent from the custom agents file and launch it on demand when a client sends a message with `"agentId": "openai-gpt4o"`.
117+
118+
Alternatively, instead of `--custom-agents` in args, you can set the environment variable:
119+
120+
```bash
121+
export ACP_CUSTOM_AGENTS_PATH="./openai-custom-agents.json"
122+
```
123+
124+
### Standalone Mode (direct launch)
125+
126+
You can also launch the worker directly via `npx` or `node` for testing:
127+
128+
```bash
129+
export OPENAI_API_KEY="sk-..."
130+
131+
# Via npx (published package)
132+
npx @stdiobus/workers-registry openai-agent
133+
134+
# Via node (local build)
135+
node dist/index.js
136+
```
137+
138+
Test with a raw NDJSON message:
139+
140+
```bash
141+
echo '{"jsonrpc":"2.0","id":"1","method":"initialize","params":{"clientInfo":{"name":"test","version":"1.0"}}}' | \
142+
npx @stdiobus/workers-registry openai-agent
143+
```
144+
145+
### With Ollama (local models)
146+
147+
Create a custom agents file for Ollama:
148+
149+
```json
150+
{
151+
"agents": [
152+
{
153+
"id": "ollama-llama3",
154+
"name": "Ollama Llama 3",
155+
"version": "1.0.0",
156+
"distribution": {
157+
"npx": {
158+
"package": "@stdiobus/workers-registry",
159+
"args": ["openai-agent"],
160+
"env": {
161+
"OPENAI_BASE_URL": "http://localhost:11434/v1",
162+
"OPENAI_MODEL": "llama3"
163+
}
164+
}
165+
}
166+
}
167+
]
168+
}
169+
```
170+
171+
No API key is needed for Ollama. Or launch directly:
172+
173+
```bash
174+
export OPENAI_BASE_URL="http://localhost:11434/v1"
175+
export OPENAI_MODEL="llama3"
176+
npx @stdiobus/workers-registry openai-agent
177+
```
178+
179+
### With AWS Bedrock
180+
181+
```json
182+
{
183+
"agents": [
184+
{
185+
"id": "bedrock-claude",
186+
"name": "AWS Bedrock Claude",
187+
"version": "1.0.0",
188+
"distribution": {
189+
"npx": {
190+
"package": "@stdiobus/workers-registry",
191+
"args": ["openai-agent"],
192+
"env": {
193+
"OPENAI_BASE_URL": "https://us-east-1.bedrock.amazonaws.com/openai/v1",
194+
"OPENAI_MODEL": "anthropic.claude-3-sonnet-20240229-v1:0"
195+
}
196+
}
197+
}
198+
}
199+
]
200+
}
201+
```
202+
203+
The `OPENAI_API_KEY` for Bedrock should be set via `api-keys.json`.
204+
205+
## Configuration
206+
207+
All configuration is via environment variables. No config files are needed.
208+
209+
| Variable | Default | Description |
210+
|----------|---------|-------------|
211+
| `OPENAI_BASE_URL` | `https://api.openai.com/v1` | Base URL of the Chat Completions API endpoint |
212+
| `OPENAI_API_KEY` | `''` (empty) | API key for authentication. A warning is logged if unset (may be fine for local endpoints like Ollama) |
213+
| `OPENAI_MODEL` | `gpt-4o` | Model identifier passed in the `model` field of API requests |
214+
| `OPENAI_SYSTEM_PROMPT` | *(unset)* | Optional system prompt prepended to every conversation |
215+
| `OPENAI_MAX_TOKENS` | *(unset)* | Optional max tokens limit. Non-numeric values are ignored |
216+
| `OPENAI_TEMPERATURE` | *(unset)* | Optional temperature (float). Non-numeric values are ignored |
217+
218+
## Protocol Support
219+
220+
### ACP Methods
221+
222+
| Method | Description | Status |
223+
|--------|-------------|--------|
224+
| `initialize` | Returns agent name, version, capabilities | ✅ Implemented |
225+
| `session/new` | Creates session with unique ID and empty history | ✅ Implemented |
226+
| `session/load` | Not supported (returns error) | ✅ Implemented |
227+
| `authenticate` | No-op (returns void) | ✅ Implemented |
228+
| `session/prompt` | Converts content → OpenAI messages, streams response | ✅ Implemented |
229+
| `cancel` | Aborts in-flight HTTP request via AbortController | ✅ Implemented |
230+
231+
### Agent Capabilities
232+
233+
```json
234+
{
235+
"protocolVersion": "2025-03-26",
236+
"agentInfo": { "name": "openai-agent", "version": "1.0.0" },
237+
"agentCapabilities": {
238+
"promptCapabilities": { "embeddedContext": true }
239+
},
240+
"authMethods": []
241+
}
242+
```
243+
244+
### Content Block Conversion
245+
246+
The agent converts ACP content blocks to OpenAI user messages:
247+
248+
| ACP Block Type | OpenAI Conversion |
249+
|----------------|-------------------|
250+
| `text` | Text content directly |
251+
| `resource_link` | `[Resource: {name}] {uri}` |
252+
| `resource` | `[Resource: {uri}]\n{text}` |
253+
| `image` | `[Image: {mimeType}]` |
254+
255+
Multiple content blocks in a single prompt are joined with newlines into one user message.
256+
257+
## Project Structure
258+
259+
```
260+
src/
261+
├── index.ts # Entry point (stdin/stdout piping, SessionIdRouter, signal handlers)
262+
├── agent.ts # OpenAIAgent class implementing ACP Agent interface
263+
├── client.ts # ChatCompletionsClient (native fetch + SSE stream reading)
264+
├── sse-parser.ts # Stateless SSE line parser (data/done/skip classification)
265+
├── session.ts # Session class (history, AbortController, cancellation)
266+
├── session-manager.ts # SessionManager (create, get, cancel by ID)
267+
├── session-id-router.ts # SessionIdRouter (stdio Bus ↔ ACP sessionId mapping)
268+
├── config.ts # Configuration loader from process.env
269+
└── types.ts # Shared TypeScript type definitions
270+
```
271+
272+
## Error Handling
273+
274+
All errors are delivered as `agent_message_chunk` session updates followed by `{ stopReason: 'end_turn' }`, so the client always receives a complete response cycle.
275+
276+
| Condition | Error Message Pattern |
277+
|-----------|----------------------|
278+
| HTTP 401/403 | `Authentication error (HTTP {status}) calling {url}. Check your OPENAI_API_KEY.` |
279+
| HTTP 429 | `Rate limit exceeded (HTTP 429) calling {url}. Please retry later.` |
280+
| HTTP 500+ | `Server error (HTTP {status}) from {url}.` |
281+
| Network failure | `Network error connecting to {url}: {message}` |
282+
| Invalid SSE JSON | Logged to stderr, chunk skipped, stream continues |
283+
| Unknown sessionId | JSON-RPC error response via ACP SDK |
284+
285+
## Graceful Shutdown
286+
287+
- `SIGTERM`: Aborts all active HTTP requests, waits for `connection.closed`, exits with code 0
288+
- `SIGINT`: Same behavior as SIGTERM
289+
- Uncaught exceptions: Logged to stderr, exits with code 1
290+
- Unhandled rejections: Logged to stderr (process continues)
291+
292+
## Development
293+
294+
### Building
295+
296+
```bash
297+
npm run build # Compile TypeScript to dist/
298+
npm run clean # Remove dist/
299+
```
300+
301+
### Testing
302+
303+
```bash
304+
npm test # Run all tests (unit + property-based)
305+
```
306+
307+
The test suite includes 11 test files covering 5 unit test suites and 6 property-based test suites:
308+
309+
**Unit tests** (`tests/*.test.ts`):
310+
- `config.test.ts` — default values, env var reading, numeric parsing, missing key warning
311+
- `session.test.ts` — session creation, history management, cancellation lifecycle
312+
- `sse-parser.test.ts` — SSE line parsing (data, done, skip, comments, invalid JSON)
313+
- `agent.test.ts` — initialize, newSession, loadSession, authenticate, prompt with mocked client
314+
- `client.test.ts` — HTTP error classification (401/403/429/500+), network errors, stream completion, cancellation
315+
316+
**Property-based tests** (`tests/*.property.test.ts`), each running 100+ iterations with `fast-check`:
317+
- `config.property.test.ts` — configuration round-trip, numeric env var parsing
318+
- `session.property.test.ts` — session uniqueness, history order preservation, cancellation semantics
319+
- `sse-parser.property.test.ts` — SSE line classification, content round-trip, invalid JSON resilience
320+
- `conversion.property.test.ts` — content block conversion, request construction with history, request invariants
321+
- `error-handling.property.test.ts` — HTTP error classification across status code ranges
322+
- `agent.property.test.ts` — initialize response field validation
323+
324+
### Key Design Decisions
325+
326+
- **Zero HTTP dependencies**: Uses native `fetch()` (Node.js 20+) instead of axios/node-fetch
327+
- **Stateless SSE parser**: `parseLine()` is a pure function — no buffering state, easy to test and reason about
328+
- **Per-session AbortController**: Each prompt gets a fresh `AbortController` via `resetCancellation()`, so cancellation of one request doesn't affect the next
329+
- **Partial responses discarded on cancel**: When a request is cancelled, the incomplete assistant response is not saved to history, preventing corrupted conversation state
330+
- **All logging to stderr**: stdout is reserved exclusively for NDJSON protocol messages
331+
332+
## Performance
333+
334+
- Session creation: O(1) — UUID generation + Map insertion
335+
- Session lookup: O(1) — Map.get by sessionId
336+
- SSE parsing: O(n) per line — single-pass string operations
337+
- Memory: proportional to conversation history length per session (no persistence, no eviction)
338+
339+
## License
340+
341+
Apache License 2.0
342+
343+
Copyright (c) 2025–present Raman Marozau, Target Insight Function.
344+
345+
## Resources
346+
347+
- [stdio Bus kernel](https://github.com/stdiobus/stdiobus) — Core protocol and daemon
348+
- [ACP SDK](https://www.npmjs.com/package/@agentclientprotocol/sdk) — Official Agent Client Protocol SDK
349+
- [ACP Registry](https://cdn.agentclientprotocol.com/registry/v1/latest/registry.json) — Available agents
350+
- [OpenAI Chat Completions API](https://platform.openai.com/docs/api-reference/chat) — API reference

0 commit comments

Comments
 (0)