By default, tools are shared and visible to all tasks via GET /{env_name}/tools. You can mark tools as task-specific so they only appear within an active session:
@tool(shared=False)def task_only_action(self, params: ActionParams) -> ToolOutput: """Only visible via /{env_name}/task_tools in an active session""" return ToolOutput(...)
You can also override list_task_tools() to return tools dynamically based on the current task:
def list_task_tools(self) -> ListToolsOutput: """Override to provide task-specific tools""" # Return different tools based on self.task_spec return ListToolsOutput(tools=[...])
Clients use GET /{env_name}/task_tools (with an active session) to get the combined set of shared + task-specific tools.
import express from 'express';import crypto from 'crypto';const app = express();app.use(express.json());const CHUNK_SIZE = 4096;// In-memory session storeconst sessions = new Map<string, EnvironmentInstance>();// Health checkapp.get('/health', (req, res) => { res.json({ status: 'ok' });});// List environmentsapp.get('/list_environments', (req, res) => { res.json(['myenvironment']);});// List toolsapp.get('/:envName/tools', (req, res) => { res.json({ tools: [ { name: 'submit', description: 'Submit answer', input_schema: { type: 'object', properties: { answer: { type: 'string' } }, required: ['answer'] } } ] });});// Create session (no X-Session-ID required)app.post('/create_session', (req, res) => { const sid = crypto.randomUUID(); res.json({ sid });});// Create episodeapp.post('/create', (req, res) => { const sid = req.headers['x-session-id'] as string; const { env_name, task_spec, split, index, secrets } = req.body; // Resolve task_spec from split/index if not provided directly let resolvedTaskSpec = task_spec; if (!task_spec && split !== undefined && index !== undefined) { resolvedTaskSpec = getTaskByIndex(split, index); } const env = new EnvironmentInstance(resolvedTaskSpec); sessions.set(sid, env); res.json({ sid });});// Call tool (SSE with chunking)app.post('/:envName/call', async (req, res) => { const sid = req.headers['x-session-id'] as string; const { name, input } = req.body; const env = sessions.get(sid); if (!env) { return res.status(404).json({ error: 'Session not found' }); } // Set SSE headers res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); // Send task ID const taskId = crypto.randomUUID(); res.write(`event: task_id\ndata: ${taskId}\n\n`); // Execute tool try { const result = await env.callTool(name, input); const resultJson = JSON.stringify({ ok: true, output: result }); // Chunk large responses (>4KB) if (resultJson.length > CHUNK_SIZE) { for (let i = 0; i < resultJson.length; i += CHUNK_SIZE) { const chunk = resultJson.slice(i, i + CHUNK_SIZE); const event = i + CHUNK_SIZE >= resultJson.length ? 'end' : 'chunk'; res.write(`event: ${event}\ndata: ${chunk}\n\n`); } } else { res.write(`event: end\ndata: ${resultJson}\n\n`); } } catch (error) { res.write(`event: error\ndata: ${error.message}\n\n`); } res.end();});app.listen(8080, () => { console.log('ORS server running on port 8080');});
The SSE chunking protocol is important for interoperability. Results larger than 4KB must be split into chunk events followed by a final end event. Clients concatenate all chunk data with the end data to reconstruct the full JSON. See the SSE specification for details.
Key Takeaway: Implementing an ORS server is straightforward. Use the Python SDK for quick development, or implement the HTTP protocol in any language for full control. Focus on proper reward design and episode termination.