# Introduction > We turn your agent's production workflows into RL and eval tasks, and your tasks into a benchmark. General Context reads your agent's production conversations and finds where it goes wrong. Each failure you confirm becomes a task, built from the conversation itself: a synthetic environment where your agent's tools answer with what they returned in production, a simulated person, and a grader drafted from what went wrong. Your tasks together are your benchmark. Run it on every model, prompt or code change, or train on it. 1. **Production workflow**: A real conversation your agent handled, with every model call, tool call and result. 2. **Synthetic environment**: Your agent's tools, answering with what they returned in production, with every real name and number replaced. 3. **Task**: The situation, a simulated person, and a grader that returns a reward from 0 to 1. 4. **Benchmark**: Every task from your traffic, with a held-out share that only ever measures. ## Who does what You never read transcripts to get a benchmark: we do the reading. You make the calls only you can make. | | You | General Context | | --- | --- | --- | | Connect | Add the SDK or an OpenTelemetry exporter, in about five minutes | Record every call off your request path, so your agent never waits on us | | Review | Confirm each failure mode we find: a real problem, the agent was right, or known and accepted | Read your traffic, mark the step each failure went wrong, and name the failure mode it shows | | Tasks | Say what the agent should have done, or what it must keep doing | Build the environment, the person and the grader from the conversation | | Benchmark | Run it in CI on every change, or train on it | Add tasks as new failures show up, and for busy work the agent gets right | - [Quickstart](https://generalcontextlabs.com/docs/quickstart): Record a Python or Node agent's OpenAI, Anthropic, Gemini or Bedrock calls in about five minutes. - [How it works](https://generalcontextlabs.com/docs/how-it-works): From one conversation to a task, step by step, and what you decide. - [A worked example](https://generalcontextlabs.com/docs/example): One support email, followed from production to a graded task. - [Tasks and environments](https://generalcontextlabs.com/docs/tasks): What a task holds, how it is graded, and how to run or train on it. ## What you need - An invitation to your company's workspace. Your admin can invite the rest of your team. - An agent that calls OpenAI, Anthropic, Google Gemini or AWS Bedrock from Python 3.9+ or Node 18+, or that already emits OpenTelemetry GenAI spans. Other stacks can [import](https://generalcontextlabs.com/docs/import). --- # Quickstart > Install the SDK, initialize it once, and see your agent's conversations in the console. ## 1. Create an API key In the console, open your workspace and go to **API keys**, or follow the **Connect** page. Keys start with `gck_` and are shown once, so keep yours in your secret manager. ## 2. Install the SDK Python: ```bash pip install generalcontext ``` Node.js: ```bash npm install generalcontext ``` ## 3. Set your key ```bash GENERALCONTEXT_API_KEY=gck_... ``` ## 4. Initialize once, at startup Python: ```python import generalcontext generalcontext.initialize(app_name="support-agent") # Every OpenAI, Anthropic, Gemini and Bedrock Converse client in the process # is now recorded, streamed or not, sync or async, and so are OpenAI Realtime # voice calls. ``` Node.js: ```ts import { initialize } from 'generalcontext'; import OpenAI from 'openai'; import Anthropic from '@anthropic-ai/sdk'; initialize({ appName: 'support-agent', instrumentModules: { openAI: OpenAI, anthropic: Anthropic } }); ``` > **Note:** Initialize in every process that makes model calls, including workers. In Node, passing the client classes in `instrumentModules` is the most reliable form, and required with bundlers. ## 5. Group calls into conversations One conversation (a chat, a ticket, a phone call) spans many model calls. Wrap the work for one conversation in a session, with the id you already use for it. If your agent works for several companies, add `account` too ([more](https://generalcontextlabs.com/docs/sdk#accounts)). Python: ```python with generalcontext.session(conversation.id, channel="chat", user_id=customer.id): reply = handle(message) ``` Node.js: ```ts import { withSession } from 'generalcontext'; await withSession({ sessionId: conversation.id, channel: 'chat', userId: customer.id }, () => handle(message)); ``` ## 6. Check it worked Make one model call. On the **Connect** page, the **Verify** step turns green within seconds, and the conversation appears under **Traces**. > **Note:** Using a coding agent? Choose **Copy setup prompt** on **Connect** and paste it into the agent: it adds the General Context skill to your codebase and connects your app. See [Use a coding agent](https://generalcontextlabs.com/docs/coding-agents). ## Next steps - [SDK reference](https://generalcontextlabs.com/docs/sdk): Background workers, serverless, redaction and every option. - [Outcomes and events](https://generalcontextlabs.com/docs/outcomes): Tell us what actually happened after the conversation. - [Use a coding agent](https://generalcontextlabs.com/docs/coding-agents): Let your coding agent make these changes for you. - [Security and data](https://generalcontextlabs.com/docs/security): Where conversations live, who reads them, how long they are kept. --- # Use a coding agent > Hand the whole setup to Claude Code, Codex, Cursor or any agent that edits your repository. Paste one of these prompts into your coding agent from the root of your agent's repository. It reads these docs, installs the SDK or exporter, and wraps each conversation, without changing your prompts or model calls. Review its diff like any other change. SDK: ``` Connect this agent to General Context, following https://generalcontextlabs.com/docs/agents.md. 1. Install the General Context skill into this codebase so you can use it later: mkdir -p .claude/skills/generalcontext && curl -fsSL https://generalcontextlabs.com/docs/agents.md -o .claude/skills/generalcontext/SKILL.md 2. Inventory every place this code calls a model before changing anything (the skill lists what to search for), including workers, cron jobs and scripts. 3. Install the SDK (`pip install generalcontext` or `npm install generalcontext`) and call `initialize()` once per process, before any model call. It reads GENERALCONTEXT_API_KEY from the environment; do not hard-code the key. In a bundled Node app, pass the client classes in `instrumentModules`. 4. Wrap the handling of each conversation in `session()` / `withSession`, using the id we already store, and pass that id into any worker that continues it. 5. In serverless handlers, jobs and scripts, call `flush()` before returning. Where the app learns a conversation succeeded or failed, call `report_outcome`/`reportOutcome` or `log_event`/`logEvent`. 6. Verify as the skill describes, and tell me which call sites are recorded and which are not. Do not change model calls, prompts or behavior. Recording must never block or fail a request. ``` OpenTelemetry: ``` Send this agent's traces to General Context over OTLP. Read https://generalcontextlabs.com/docs/opentelemetry.md first. 1. Add an OTLP/HTTP exporter for https://generalcontextlabs.com/api/platform-collect/v1/traces with header Authorization: Bearer $GENERALCONTEXT_API_KEY, alongside any existing exporter. 2. Make sure GenAI spans include message content (OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true or TRACELOOP_TRACE_CONTENT=true). 3. Set `session.id` (or `gen_ai.conversation.id`) on spans to our id for each conversation. Do not change model calls, prompts or behavior. ``` ## The agent guide [agents.md](https://generalcontextlabs.com/docs/agents.md) is one page with everything a coding agent needs: finding every model call, Python and Node setup, sessions, workers and serverless, outcomes, redaction, how to verify, and common issues. It is also a skill file. Install it so your agent has it offline: ```bash mkdir -p .claude/skills/generalcontext && curl -fsSL https://generalcontextlabs.com/docs/agents.md -o .claude/skills/generalcontext/SKILL.md ``` ## Docs for agents Every page is also plain Markdown: add `.md` to its address, for example [quickstart.md](https://generalcontextlabs.com/docs/quickstart.md). [llms.txt](https://generalcontextlabs.com/docs/llms.txt) lists every page, and [llms-full.txt](https://generalcontextlabs.com/docs/llms-full.txt) has all of them in one file. > **Note:** Your API key never needs to be in the prompt or the code. The SDK reads `GENERALCONTEXT_API_KEY` from the environment. --- # How it works > From a real conversation to a benchmark: traces, environments, tasks and benchmarks. Every task starts as a conversation your agent really had. The console follows it from there in four steps, one page each: **Traces**, **Environments**, **Tasks** and **Benchmarks**. Your team and ours work in the same workspace and see the same things. ## 1. Traces Add the [SDK](https://generalcontextlabs.com/docs/quickstart), send [OpenTelemetry](https://generalcontextlabs.com/docs/opentelemetry), or [import](https://generalcontextlabs.com/docs/import) conversations you already store. Every model call, tool call and reply is recorded, including what each tool returned, off your request path, so your agent never waits. Reviewing a conversation gives it one verdict. **Report a problem** puts one at the front of the review queue. | Verdict | Means | | --- | --- | | Went wrong | Marked at the first wrong step, with the failure mode, severity, what went wrong at each step and what the agent should have done | | Fine | Nothing wrong | | Good example | Handled well, and kept as a reference past the 90-day retention | | Unsure | Needs a second look | Each conversation is filed under a workflow, the kind of work it was. The **Workflows** tab shows each one's share of traffic, how often the agent gets it right, and the tasks guarding it. Failures that repeat are named as one **failure mode**, under the **Failure modes** tab, with the conversations that show it, most common first. Confirm whether each is a real problem, the agent was right, or known and accepted: a conversation marked as gone wrong counts as failed unless the agent was right. ## 2. Environments An environment is the world the agent works in for a set of tasks: its tools, the records they return, and its instructions. Build one from traces with **Add this trace** on a conversation, then fill in what production never showed, such as a record no conversation looked up. The page lists what it cannot answer yet. Many tasks share one environment, so fixing a tool once fixes every task on it. ## 3. Tasks A task does one of two jobs. It **catches a failure**: for a confirmed failure mode, say in a sentence what the agent should have done, and it becomes a task built from the conversations behind it. Or it **keeps it right**: for work that is common and matters, a conversation the agent handled well becomes a task, so a later change cannot quietly break it. Next up suggests the busy workflows that have nothing keeping them right. - **Environment:** a shared environment, or the recordings of the task's own conversations. For an agent that looks things up and talks to people, a person writes in and shares what they know only when asked. - **Grader:** what decides a run passed. By default, a rubric drafted from the review: what the agent should have done, and each step it got wrong (or, for a success, right) in production. Sharpen it with what you know of the work, or write it as code when the outcome can be checked directly. - **Reference run:** the production run itself, which must fail a task built from a failure and pass one built from a success. It checks the grader before anything is spent. An agent that does other work, such as writing code or building something, can use its own environment and grader instead, run by your code. Every real name, contact and account number is replaced before a task is ready. **Runs**, under **Tasks**, shows every run and the commands that make them. [Tasks and environments](https://generalcontextlabs.com/docs/tasks) covers what a task holds and how it is graded. ## 4. Benchmarks A benchmark is sets of ready tasks, the agents compared on them, and the axes to read results by: workflow, origin, severity, tag or split. Freezing a version pins exactly which tasks a result was measured on, and flags any task edited since. About one task in five is held out: it only ever measures your agent and is never used to change it, so its score stays honest. > **Note:** Most tasks cover routine work your agent must get right every time, so a set can be scored by the tasks an agent passed on every trial (pass^k), not just its average. Tags such as privacy or judgment mark rare, high-harm cases, so you can see those on their own. --- # A worked example > One support email, followed from production to a graded task. This example comes from a support agent for an online store that answers customers by email. The company, people and records are synthetic, made for these docs, but every step is one a real task goes through, and the run below is real. ## 1. What happened in production A customer emailed support: > Your checkout glitched when I paid on March 14 and I was charged twice. Please refund the extra charge. > > (Email to support) The agent found the order and its two $129 payments, 40 seconds apart, and the status page's incident for that evening's payment retries. It asked the billing team to refund the duplicate, with that evidence, which was right. Then she asked whether billing could also look at the $18 express shipping fee on her February order. She had chosen express at checkout, and the order arrived on the promised date. The agent opened a second billing task to review that fee, and told her billing would. ## 2. What the review found The February order and the shipping article, both in the agent's own tool results, showed nothing went wrong in February: express was a paid upgrade, and it arrived on time. We marked the step where the agent opened the February task as wrong: "Asked billing to review a fee nothing went wrong with." We named the failure mode **Escalates fees nothing went wrong with**, in the out of scope category. ## 3. What the company said On the **Failure modes** tab, the company answered **Yes, fix it** and added: "Billing should only get requests where something actually went wrong. Support declines the rest." Then, under **Make this a task**, what the agent should have done: > Ask billing to refund only the duplicate March 14 charge, with the incident as evidence. Decline the February express fee plainly, because it was a paid upgrade that arrived on time, and open no task for it. > > (What should have happened) ## 4. The task | Part | In this task | | --- | --- | | Environment | The agent's 7 tools across the helpdesk, orders, the status page, the knowledge base and team tasks, answering with the 10 results production returned: both orders with their payments, the incident, the shipping article | | Person | The customer. She volunteers that she wants the February fee looked at, and explains why she chose express only if asked. | | Grader | The four criteria below, drafted from the review and sharpened from the tool results | | Split | Held out, so it only ever measures | | Criterion | Must be true at the end | | --- | --- | | `march-refund-routed` | A billing task asks to refund the duplicate March 14 charge, citing the incident or the two payments. | | `no-february-task` | No task asks billing to refund or review the February express shipping fee. | | `explains-february` | The desk tells her the February fee is not refundable because express was her choice and it arrived on time. | | `no-promise` | The desk does not say any refund has already been made. | `evals check` grades the production run first: it must fail, and it does, with a reward of 0.5. ## 5. Running it We ran a reference model on the task three times. Every run made the same mistake production did: ``` $ generalcontext evals run benchmark.json --task shipping-fee-not-escalated \ --agent openai:gpt-6.1-terra@low FAIL shipping-fee-not-escalated openai:gpt-6.1-terra@low #1 2/4 criteria; failed: no-february-task, explains-february; 2 lookups not in the recording ``` Its reward was 0.5. It routed the duplicate charge with the evidence and promised nothing, but again opened a billing task for the February fee. The judge's reason for `no-february-task`: "The agent created a billing task to review the $18 express shipping fee on February order 10318." Once the agent is fixed, the task stays in the benchmark, so the mistake cannot come back unnoticed. "2 lookups not in the recording" means the model looked up something production never did. The environment answers those as unrecorded instead of inventing a result, and counts them, so a run that strays far from the recording says so. --- # Tasks and environments > What a task holds, how it is graded, and how to run it as an eval or an RL environment. A task is one situation from your traffic, made runnable: a prompt, an environment the agent works in, and a grader that decides whether the run passed. Every task is an episode with a reward at the end, so the same tasks serve evaluation and RL training. Nothing in a task is a real customer record. A task either **catches a failure**, a confirmed failure mode the task catches if it comes back, or **keeps it right**, a conversation your agent handled well in work that is common and matters, which the task keeps that way. Tasks that keep it right teach what good looks like and stop a fix for one case from breaking another. ## Environment and grader kinds Every task names the kind of environment and grader it needs. Two are built in, for an agent that looks things up and talks to people. Any other kind is your own code. | Kind | What it is | | --- | --- | | Environment `replay` | Your agent's tools, answering with what they returned in production, and a model playing the person who wrote in | | Grader `rubric` | A judge model grades each criterion, with a reason, and the reward is the share met | | Your own | Any other name, such as `game-workspace` or `play-check`. Your code builds the environment or grades the run, and reads the fields the task gives it | ## The replay environment A tool call with the arguments production used gets the recorded result, and a near match gets the closest one. A lookup production never made is answered as unrecorded and counted, never invented, so a run that strays far from the recording says so. Writes, such as sending a message or opening a request, are logged for the grader. It runs anywhere, with no access to your systems: in CI, on a laptop, on every pull request. A model plays the person who wrote in, from the task's brief. It knows every fact, and shares the ones marked `when_asked` only when your agent asks, so your agent has to ask. ## What a task holds | Part | What it holds | | --- | --- | | Prompt and expected | What the agent is given, and what it should do (a failure) or did (a success) | | Environment | Its kind, and what it needs. For replay: your agent's tools and every result they returned, the person's brief and facts | | Grader | Its kind, and what it needs. For rubric: ground-truth facts and criteria on what the person was told and what your agent did | | Reference runs | Known runs, such as the production one, and whether each should pass | | Agent | What your agent had in production: its instructions, models and tools | | Origin, split, tags, workflow | Failure or success, train or held out, labels to slice by, and the kind of work it came from | ## Grading and reward The rubric grader has a judge model grade every criterion against the conversation, your agent's writes, and the task's facts, with a one-sentence reason for each. The reward is the share of criteria met, from 0 to 1. A run passes only when every criterion does. Your own grader returns the same: a reward from 0 to 1, and whether it passed. A rubric starts from the review, not from a rule book: what the agent should have done, and each step the review marked wrong (or right, for a success). Tighten it from there with what the conversation and its tool results show and what you know of the work. When the outcome can be checked directly, such as a record that must or must not exist, write the grader as code instead. **Tasks** shows each agent's runs: the share passed, and the tasks it passed every time, since a task passed four times in five still fails one customer in five. ## Check the graders first `evals check` grades every task's reference runs without running any agent. A failure's production run must not pass, and a success's must. If a grade disagrees, the grader is wrong, and the command says which task and exits 1. ``` generalcontext evals check benchmark.json OK shipping-fee-not-escalated production expect fail, got fail (reward 0.5) 1 reference run checked, 0 disagree with expect. ``` ## Run it as an eval Your agent: ```bash pip install generalcontext openai anthropic generalcontext benchmark pull --out benchmark.json generalcontext evals run benchmark.json --agent my_agent:handle --trials 3 ``` A reference model: ```bash generalcontext evals run benchmark.json --agent openai:gpt-6.1-terra --trials 3 ``` `--agent module:function` runs your own code. It is called as `handle(messages, toolbox)`: run your agent loop on `messages`, call tools through `toolbox.call(name, arguments)` instead of your real systems, and return the final text. `toolbox.openai_tools()` and `toolbox.anthropic_tools()` give the tools in each provider's format. `--agent openai:`, `claude-code:` (the Claude Code CLI on its signed-in plan) or `anthropic:` runs a model on the instructions recorded from your agent, a quick baseline. ```python import json from openai import OpenAI client = OpenAI() def handle(messages, toolbox): convo = [{"role": "system", "content": "You are our support assistant."}, *messages] for _ in range(8): reply = client.chat.completions.create(model="gpt-6.1-terra", reasoning_effort="none", messages=convo, tools=toolbox.openai_tools() or None).choices[0].message if not reply.tool_calls: return reply.content or "" convo.append(reply.model_dump(exclude_none=True)) for call in reply.tool_calls: result = toolbox.call(call.function.name, json.loads(call.function.arguments or "{}")) convo.append({"role": "tool", "tool_call_id": call.id, "content": result if isinstance(result, str) else json.dumps(result)}) return "" ``` The model playing the person and the judge run on OpenAI by default, so set `OPENAI_API_KEY`, or choose others with `--person` and `--judge` (`anthropic:`, `claude-code:`). Results go to `runs/runs.json` in the `generalcontext.runs/v1` format, one file per trial beside it. Add `--upload` to send them to your workspace with a key that can run the benchmark, or use **Import runs.json** under **Tasks**, **Runs**. Run it in [CI](https://generalcontextlabs.com/docs/cli) to catch a regression before it ships. ```bash generalcontext evals run benchmark.json --agent my_agent:handle --trials 3 --upload ``` ## Your own environment and grader For a task whose environment or grader is your own kind, name your code on the command line. A task file never names code to run, so a downloaded benchmark cannot run anything you did not choose. ```bash generalcontext evals run benchmark.json --agent my_agent:build \ --environment my_env:workspace --grader my_env:play_check ``` `--environment module:function` is called with the task and returns an environment with `tools`, `call(name, arguments)`, `start()` for the first message, `respond(said)` returning the next message and whether it is done, and optionally `state()` and `close()`. An environment that is not a conversation returns done at once and puts what the agent produced in its state. `--grader module:function` is called with the task and the episode (transcript, tool calls, output, state) and returns `{"reward": 0..1, "passed": true|false}`, with criteria and a note if you have them. A task whose kind has no code given is skipped with the reason, never guessed. For example, an agent that builds games can use an environment that runs the build in a fresh workspace, and a grader that opens the game in a browser, plays it with a few taps, and checks it boots, draws and responds. ## Use it as an RL environment Each task is an episode: it starts from the opening message, your policy acts through the tools and talks to the person, and the grader returns a reward at the end. The same harness runs from Python: ```python from generalcontext import evals tasks = [task for task in evals.load_tasks("benchmark.json") if task["split"] == "train"] person = evals.model("openai:gpt-6.1-terra@none") judge = evals.model("openai:gpt-6.1-sol@high") for task in tasks: result = evals.run_task(task, my_agent, person, judge) result["reward"] # 0 to 1, the share of rubric criteria met result["criteria"] # each criterion, passed or not, with the judge's reason result["transcript"] # the episode; result["tool_calls"] has every call ``` `my_agent` has the same shape as `--agent module:function`, such as `from my_agent import handle as my_agent`. If your agent raises, the episode ends there with reward 0 and the exception in `result["error"]`, so check it before you train on the reward. Training runs on your side, with your own stack. > **Warning:** Train only on the `train` split. Held-out tasks are for measuring: once they shape your agent, their scores stop meaning anything. ## The task file `benchmark pull` downloads your ready tasks as one `generalcontext.benchmark/v1` file of `generalcontext.task/v1` tasks. New fields may be added to the format, never renamed or removed: read the ones you need and ignore the rest. ```json { "format": "generalcontext.task/v1", "slug": "shipping-fee-not-escalated", "origin": "failure", "split": "held_out", "tags": ["scope"], "workflow": { "name": "Refund requests" }, "prompt": "Your checkout glitched when I paid on March 14…", "expected": "Refund only the duplicate charge; decline the February fee.", "agent": { "instructions": "…", "models": ["…"] }, "environment": { "kind": "replay", "person": { "brief": "…", "facts": [{ "text": "…", "disclose": "when_asked" }] }, "tools": [{ "name": "orders__get", … }], "observations": [ { "tool": "orders__get", "arguments": { … }, "result": { … } } ] }, "grader": { "kind": "rubric", "facts": ["…"], "criteria": [ { "id": "no-february-task", "claim": "No task asks billing to…" } ] }, "reference": [{ "label": "production", "expect": "fail", "transcript": […] }] } ``` ## Full simulations A record production never looked up can be added to a shared [environment](https://generalcontextlabs.com/docs/how-it-works) by hand. Some work needs more: a check on exactly what changed in your systems. For those, we can build a full simulation of the systems involved with your team, as an environment of its own kind. The same tasks run there, and the grader adds checks on the end state. --- # SDK reference > Sessions, named steps, workers, redaction and every option of the Python and Node SDKs. The SDK records every model call your process makes through a supported client, streamed or not, sync or async, and sends it in the background. It is open source under Apache-2.0: [read the source](https://github.com/GeneralContext/sdk). New here? Start with the [Quickstart](https://generalcontextlabs.com/docs/quickstart). ## Supported providers | Provider | Python | Node.js | | --- | --- | --- | | OpenAI, including Azure OpenAI and any OpenAI-compatible base URL | `openai` (chat completions and responses) | `openai` | | Anthropic, including Bedrock, Vertex and beta clients | `anthropic` | `@anthropic-ai/sdk` | | Google Gemini | `google-genai` | `@google/genai` | | AWS Bedrock Converse | `boto3` `bedrock-runtime` (`converse`, `converse_stream`) | `@aws-sdk/client-bedrock-runtime` (`ConverseCommand`, `ConverseStreamCommand`) | | OpenAI Realtime (voice) | `client.realtime.connect()`, sync and async (`openai[realtime]`) | `OpenAIRealtimeWS` and `OpenAIRealtimeWebSocket` | | Anything else | Send [OpenTelemetry](https://generalcontextlabs.com/docs/opentelemetry) | Send [OpenTelemetry](https://generalcontextlabs.com/docs/opentelemetry) | Streams reach your code unchanged and are recorded once, as one assembled reply. Bedrock images and documents are sent as `{"omitted": "bytes"}`. Providers that are not installed are skipped. ## Voice agents A phone call is one conversation, like a chat. What we record is what was said: the caller's words, the agent's spoken replies, every tool call, and how the call went. Audio itself is never sent, by any of the SDKs: inline audio becomes a marker such as `[omitted 48211 bytes sha256:...]`. - **OpenAI Realtime.** Open the connection inside a session, with the id of the call, and the whole call is recorded as it happens: each reply the agent speaks, what the caller said before it, and tool calls and their results. Turn on input audio transcription in the session, or the caller's side is missing. - **Audio models over chat completions** (`gpt-audio` and similar). Recorded like any other call. A spoken reply shows its transcript. - **A voice platform or your own speech-to-text and text-to-speech.** The model in the middle is an ordinary client call, so it is recorded already. Pass the call's id as the session so every turn of one call lands together. On a call, each spoken reply also shows how long the caller waited (from when they stopped speaking to the agent's first word) and whether they cut in. The conversation list calls out replies slower than 2 seconds and calls where the caller cut in. Python: ```python from openai import AsyncOpenAI import generalcontext generalcontext.initialize() client = AsyncOpenAI() with generalcontext.session(call.sid, channel="voice"): async with client.realtime.connect(model="gpt-realtime") as connection: await connection.session.update(session={ "type": "realtime", "audio": {"input": {"transcription": {"model": "gpt-live-transcribe"}}}, }) await bridge(call, connection) # your audio loop, unchanged ``` Node.js: ```ts import OpenAI from 'openai'; import { OpenAIRealtimeWS } from 'openai/realtime/ws'; import { initialize, withSession } from 'generalcontext'; initialize({ instrumentModules: { openAI: OpenAI } }); await withSession({ sessionId: call.sid, channel: 'voice' }, async () => { const rt = await OpenAIRealtimeWS.create(new OpenAI(), { model: 'gpt-realtime' }); rt.send({ type: 'session.update', session: { type: 'realtime', audio: { input: { transcription: { model: 'gpt-live-transcribe' } } } } }); bridge(call, rt); // your audio loop, unchanged }); ``` ## Supported versions | | Python | Node.js | | --- | --- | --- | | Runtime | 3.9 to 3.13 | 18 to 24 | | OpenAI | `openai` 1.0.0 and later | `openai` 4.0.0 and later | | Anthropic | `anthropic` 0.20.0 and later | `@anthropic-ai/sdk` 0.20.0 and later | | Google Gemini | `google-genai` 1.0.0 and later | `@google/genai` 1.0.0 and later | | AWS Bedrock Converse | `boto3` 1.34.116 and later | `@aws-sdk/client-bedrock-runtime` 3.587.0 and later | Each release is tested from these minimums to the latest versions, and the results are in [stress/compat/RESULTS.md](https://github.com/GeneralContext/sdk/blob/main/stress/compat/RESULTS.md). A call style a release does not have yet, such as `responses` in openai 1.0, is simply absent. Each provider package's own Node minimum still applies. ## Node.js setup Pass the provider modules you use in `instrumentModules`. It is required whenever a bundler is involved, because a bundle can carry its own copy of the client that detection never sees. ```ts import OpenAI from 'openai'; import Anthropic from '@anthropic-ai/sdk'; import * as googleGenAI from '@google/genai'; import * as bedrockRuntime from '@aws-sdk/client-bedrock-runtime'; import { initialize } from 'generalcontext'; initialize({ instrumentModules: { openAI: OpenAI, anthropic: Anthropic, googleGenAI, bedrockRuntime } }); ``` ### Next.js Initialize in `instrumentation.ts` at the project root, keep the provider packages out of the bundle so routes use the patched copy, and use the Node.js runtime. The Edge runtime is not supported. instrumentation.ts: ```ts export async function register() { if (process.env.NEXT_RUNTIME === 'nodejs') { const { initialize } = await import('generalcontext'); const { OpenAI } = await import('openai'); const anthropic = await import('@anthropic-ai/sdk'); initialize({ appName: 'my-agent', instrumentModules: { openAI: OpenAI, anthropic } }); } } ``` next.config.ts: ```ts export default { serverExternalPackages: ['openai', '@anthropic-ai/sdk'] }; ``` ### Proxies Node's built-in `fetch` ignores `HTTPS_PROXY`. On Node 24 and later, set `NODE_USE_ENV_PROXY=1`. On older versions, install a global dispatcher, for example undici's `setGlobalDispatcher(new EnvHttpProxyAgent())`. If the proxy inspects TLS, point `NODE_EXTRA_CA_CERTS` at its CA certificate. Python honors `HTTPS_PROXY` and `SSL_CERT_FILE` on its own. ## Sessions Calls inside a session are grouped into one conversation, with the id you pass. Sessions follow async work, threads (Python) and promises (Node) in the same process. Python: ```python with generalcontext.session(ticket.id, channel="email", user_id=customer.id, product="billing", account=company.id): handle(ticket) ``` Node.js: ```ts await withSession({ sessionId: ticket.id, channel: 'email', userId: customer.id, product: 'billing', account: company.id }, () => handle(ticket)); ``` `channel` and `product` are free-form labels you can filter by in the console. Without a session, calls are grouped by their first message, which every later call in the same conversation repeats. ### Accounts If your agent works for several companies of its own, as a platform used by many businesses does, pass `account` with the id you already use for that company. You can then filter **Traces** by account. It is optional, up to 120 characters, and trimmed. With OpenTelemetry, set `generalcontext.account` on the span or resource instead. ## Name the steps Name a step to see which part of your agent each call came from. The conversation page shows the main thread as the timeline, and lists every model call under **Every model call**, with side calls such as a classifier or a summary marked and labeled by their step. (These are steps inside your agent, not benchmark [tasks](https://generalcontextlabs.com/docs/tasks).) Python: ```python with generalcontext.task("triage"): category = classify(message) ``` Node.js: ```ts import { task } from 'generalcontext'; const category = await task('triage', () => classify(message)); ``` ## Serverless and background jobs Calls are sent in batches in the background. In a serverless function, queue worker or script, flush before returning. Python: ```python generalcontext.flush() # waits up to 10 seconds; True when everything was sent ``` Node.js: ```ts import { flush } from 'generalcontext'; await flush(); // waits up to 10 seconds (flush(ms) to change); true when everything was sent ``` When work moves to another process (a queue, a cron job), pass the conversation id along with the job and open the same session there. Calls from both processes land in one conversation. ## Redaction and filtering With `redact=True` (`redact: true` in Node), phone numbers, emails, card numbers and SSNs are replaced before anything leaves your process. Each becomes a stable placeholder such as `` keyed by `GENERALCONTEXT_REDACT_SALT`, so the same value reads the same across a conversation without being sent. Redaction also covers outcomes and events. Credentials are always masked, with or without `redact`: API keys from OpenAI, Anthropic, Google, AWS, Stripe, GitHub, Slack and General Context, bearer tokens, JWTs and private keys become ``, `` and so on before anything is sent. Per-request headers and query parameters (`extra_headers`, `extra_query`, Gemini's `http_options`, Node's per-request `headers`) are never recorded. For anything else, `before_send` (`beforeSend`) receives each call before it is sent. Return it, changed or not, or return nothing to drop it. Python: ```python def before_send(call): call["request"].pop("metadata", None) return call generalcontext.initialize(redact=True, before_send=before_send) ``` Node.js: ```ts initialize({ redact: true, beforeSend: call => (call.task === 'internal-eval' ? null : call), }); ``` ## One client only To record a single client instead of every client in the process, wrap it. Prefer `initialize()` unless you need this. Python: ```python client = generalcontext.wrap(OpenAI()) ``` Node.js: ```ts import { wrap } from 'generalcontext'; const client = wrap(new OpenAI()); ``` ## What is sent For each model call: the request (messages, tools, model and settings), the response, timing, token usage, any error, and your session and task tags. Nothing else from your process. ## If something goes wrong - Your model calls are never slowed or changed. If we are unreachable, calls are dropped and a warning is logged at most once a minute. - Memory is bounded: at most 5,000 calls or 64 MB are held. A single call over 4 MB is dropped. - Safe with forking servers (gunicorn, Celery). On exit, it flushes for up to 2 seconds. Before `os._exit()`, which skips that, call `flush()` yourself. - `GENERALCONTEXT_DISABLED=1` turns recording off without a code change. ## Configuration | Option (Python / Node.js) | Environment variable | Default | | --- | --- | --- | | `api_key` / `apiKey` | `GENERALCONTEXT_API_KEY` | required | | `base_url` / `baseUrl` | `GENERALCONTEXT_BASE_URL` | General Context | | `app_name` / `appName` | | none | | `redact` / `redact` | `GENERALCONTEXT_REDACT_SALT` | off | | `before_send` / `beforeSend` | | none | | `instrument_http` / `instrumentFetch` | | off | | `instrumentModules` (Node.js) | | detected | | `disable_batch` / `disableBatch` | | off: calls are sent in batches | | | `GENERALCONTEXT_DISABLED=1` | recording on | `instrument_http` / `instrumentFetch` also records raw HTTP calls to OpenAI-compatible `/chat/completions` endpoints made without the OpenAI client: `httpx` and `requests` in Python, `fetch` in Node.js. ## Security - Sends only over HTTPS to the configured host and never follows redirects, so your key cannot be forwarded. - No runtime dependencies. Releases are built in CI with signed provenance. Details: [SECURITY.md](https://github.com/GeneralContext/sdk/blob/main/SECURITY.md). --- # OpenTelemetry > Add General Context as one more OTLP destination. No code change if your spans already carry GenAI attributes. We accept OTLP over HTTP, protobuf or JSON, gzip or not. Spans without GenAI attributes are ignored, so pointing a whole pipeline at us is safe. ## Send your traces Environment: ```bash OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://generalcontextlabs.com/api/platform-collect/v1/traces OTEL_EXPORTER_OTLP_TRACES_HEADERS=Authorization=Bearer gck_... ``` Collector: ```yaml exporters: otlphttp/generalcontext: traces_endpoint: https://generalcontextlabs.com/api/platform-collect/v1/traces headers: { x-api-key: "gck_..." } compression: gzip ``` > **Warning:** Message content must be on the spans. Set `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true` for OpenTelemetry instrumentations, or `TRACELOOP_TRACE_CONTENT=true` for OpenLLMetry. ## What we read | What | Attributes | | --- | --- | | Model calls | OpenTelemetry GenAI conventions (`gen_ai.input.messages`, `gen_ai.output.messages`) and OpenLLMetry's `gen_ai.prompt.N.*` and `gen_ai.completion.N.*` | | Tool calls | `execute_tool` spans with `gen_ai.tool.name`, arguments and result | | Conversation | `session.id` or `gen_ai.conversation.id`; without either, the trace id | | Channel | `channel` or `app.channel` | | Account | `generalcontext.account`: the company the conversation was for, if your agent works for several | | Who and which agent | `user.id`; `service.name` or `gen_ai.agent.name` | --- # Import past conversations > Start review before connecting anything live: upload a file, or pull from Langfuse or LangSmith. Admins import from the **Connect** page. Keys you enter are used for that one import and never stored. Importing the same conversation again refreshes it and keeps its review. ## Files | Format | Shape | | --- | --- | | JSONL or JSON | `{"id", "messages": [...]}` with OpenAI or Anthropic messages, including tool calls | | JSONL or JSON | `{"input": "...", "output": "..."}`, one exchange per record | | JSONL or JSON | Our conversation format: `{"id", "channel", "events": [...]}` | | CSV | `conversation_id, role, content` columns, one message per row | | CSV | `input, output` columns, one exchange per row | ```json {"id": "conv-123", "messages": [{"role": "user", "content": "Can I still change my delivery address?"}, {"role": "assistant", "content": "Yes, it has not shipped yet. What is the new address?"}]} ``` Up to 200 conversations and 3.5 MB per upload. A record that cannot be read is listed with the reason, and the rest are imported. ## Langfuse Your project's public and secret keys. Cloud by default; a self-hosted deployment needs a public https address. Generations are grouped by session, else by trace. ## LangSmith An API key and the project name. LLM runs are grouped by `thread_id`, `session_id` or `conversation_id` metadata, else by trace. --- # Outcomes and events > Tell us what actually happened, so a conversation that sounded fine but booked nothing is caught. Your systems know things a transcript does not: whether the refund went through, whether the customer came back angry, whether a person had to step in. Send that and it appears on the conversation, next to our review. ## Outcome Whether the conversation worked, by your own definition: `success` or `fail`. One per conversation; sending again replaces it. Python: ```python generalcontext.report_outcome(conversation.id, "fail", note="Customer reopened the ticket") ``` Node.js: ```ts import { reportOutcome } from 'generalcontext'; await reportOutcome(conversation.id, 'fail', 'Customer reopened the ticket'); ``` ## Events Anything that happened in your systems, any time later. Each event has an id you choose, so a retry is counted once. Python: ```python generalcontext.log_event(conversation.id, "order.refunded", {"amount": 42.5}, event_id=f"refund-{refund.id}") ``` Node.js: ```ts import { logEvent } from 'generalcontext'; await logEvent({ sessionId: conversation.id, eventType: 'order.refunded', eventId: `refund-${refund.id}`, properties: { amount: 42.5 } }); ``` HTTP: ```bash curl https://generalcontextlabs.com/api/platform-collect/events \ -H "Authorization: Bearer $GENERALCONTEXT_API_KEY" \ -H "Content-Type: application/json" \ -d '{"events":[{"event_id":"refund-77","event_type":"order.refunded","timestamp":"2026-11-08T15:00:00Z","session_id":"conv-123","properties":{"amount":42.5}}]}' ``` The reply is `{ingested, skipped, failures}`. Up to 500 events per request. Properties are flat strings, numbers and booleans. From the SDK, both calls flush pending calls first, return true when accepted, and never raise. > **Note:** The conversation must exist first. In a short-lived process, flush before you report. ## Examples | Kind of agent | Events | | --- | --- | | Support | `ticket.reopened`, `ticket.escalated`, `csat.submitted` | | Commerce | `order.refunded`, `order.cancelled` | | Scheduling | `appointment.booked`, `appointment.no_show` | | Any agent | `handoff.to_human`, `complaint.received` | --- # Command line > Check the connection, download the benchmark, and run it from the shell or CI. The `generalcontext` command comes with the SDK. It reads `GENERALCONTEXT_API_KEY` like the SDK does, prints one plain line per result, and exits 0 when everything worked, 1 when a check failed, and 2 for a usage or setup problem, so a coding agent or a CI job can act on it. Python: ```bash pip install generalcontext generalcontext verify ``` Node.js: ```bash npx generalcontext verify ``` ## Check the connection `verify` shows which workspace the key belongs to, what the key can do, and when traffic last arrived and from where. With `--session`, it checks that one conversation arrived, with its number of calls and models. It never prints conversation content. Add `--json` for the raw answer. ```bash generalcontext verify --session ticket-4821 # Conversation ticket-4821: arrived, 6 calls (gpt-6.1-terra) ``` ## Download the benchmark `benchmark pull` writes your ready tasks to a file and prints how many are in each split. It needs a key that can run the benchmark: on **API keys**, create one with **Run the benchmark** checked. Keys that only send traffic are refused, so a leaked production key cannot read your tasks. ```bash generalcontext benchmark pull --out benchmark.json ``` ## Run it and upload the results `evals run` runs every task with your agent or a reference model, as described in [Tasks and environments](https://generalcontextlabs.com/docs/tasks#run-it-as-an-eval). `--upload` sends the results to your workspace, where **Tasks** shows how each agent scores. Running needs Python; in Node, `verify` and `benchmark pull` work and the rest point you to Python. ```bash generalcontext evals run benchmark.json --agent my_agent:handle --trials 3 --upload ``` --- # Security and data > Where your conversations live, who can read them, and how long they are kept. ## Storage - Stored in AWS (us-east-1), encrypted with a dedicated KMS key that only the platform's production role can use. - TLS everywhere; the storage refuses unencrypted connections and any write not under the platform key. ## Retention and deletion - Conversations are deleted 90 days after they arrive. - A conversation cited by a failure mode is kept as its evidence until you delete it. - Admins can delete any conversation from the console. It is removed from every failure mode citing it, and older copies are purged within 30 days. ## Access - Access is by invitation. Admins manage the team; viewers never see conversation content. - Every read of a conversation and every change, by anyone including General Context, is in your workspace's access log, kept 400 days. - API keys are stored only as hashes and can be revoked at once. ## In your process - The SDK never slows or fails your model calls, sends only over HTTPS, and holds a bounded amount of memory. - Redaction removes phone numbers, emails, card numbers and SSNs before they leave your process. See [Redaction and filtering](https://generalcontextlabs.com/docs/sdk#redaction-and-filtering).