What the agent does
The agent clones the repository into its own cloud sandbox and reads the chatbot page first. In a Streamlit app like streamlit/llm-examples, Chatbot.py keeps the whole conversation in st.session_state["messages"], a list of {"role", "content"} dictionaries seeded with one assistant greeting, renders each one with st.chat_message(...).write(...), and appends the user's prompt and the OpenAI reply on each turn. The sidebar already holds the API key input under the key chatbot_api_key. That list is the export's only source of truth, so nothing new needs to be stored.
The change is small and stays in that file. The agent adds a helper that turns st.session_state.messages into Markdown, one heading per turn with the role and the text beneath it, and an st.download_button in the sidebar with a .md file name and the text/markdown MIME type. It reads requirements.txt to confirm the floor is streamlit>=1.28, where st.download_button takes a string as data, so no new dependency is needed. Keeping the formatter a plain function means it can be tested without a browser.
Tests go in the file the project already has. app_test.py runs the page with streamlit.testing.v1.AppTest.from_file("Chatbot.py"), patches openai.resources.chat.Completions.create with a fake ChatCompletion, sets the key and sends a chat message. The agent adds a test in the same style that sends one prompt, then checks the download button is present and that the formatter's output contains both the user's message and the patched reply in order, plus a direct test of the formatter on an empty conversation.
Then it installs requirements.txt and requirements-dev.txt (which bring in pytest), runs pytest -v the way the repository's app-testing.yml workflow does through streamlit/streamlit-app-action, and runs ruff, which the same workflow turns on with a 105-character line length from .ruff.toml. It fixes what fails, commits on a new branch and opens the pull request.
Start it with one API call
POST https://api.prix.dev/o/{org}/sessions with this body:
{
"agent": "claude",
"input": "A Linear issue labeled agent says: \"Add Chat History Export to a Streamlit App. Add a download button to the chatbot page that exports the current conversation as Markdown with st.download_button.\" Do the work in this repository, add tests for it, run pytest, and open a pull request that links back to the issue.",
"resources": [
{
"url": "https://github.com/your-org/your-app"
},
{
"url": "https://linear.app/your-workspace/issue/ENG-42"
}
]
}- Get an API key. In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
- Send the request. POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
- Read what came back. Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.
What comes back
What comes back is a pull request whose diff touches two files in a repository shaped like the example: Chatbot.py, with the Markdown formatter and the sidebar download button, and app_test.py, with the new tests next to test_Chatbot. The description quotes the issue, links back to it, and shows a sample of the exported Markdown so the reviewer can see the format without running the app.
The pull request's checks are the repository's own: in the example that is the App testing workflow, which runs the app smoke test, ruff and pytest on Python 3.11. A reviewer checks that the export reads from st.session_state.messages rather than a second copy of the conversation, that the greeting message is handled the way the team wants, and that nothing from the API key field can end up in the file.
Behind the pull request, the session keeps every command the agent ran and its output: the pip installs, the pytest run with the patched OpenAI call, and the ruff check. The session read returns the repository and the pull request together.
Run it on every Linear issue labelled agent
Add an automation to the Project in the console with a Linear trigger on linear.issue.labeled, filtered to the agent label. Each issue that gains the label starts one run with that issue as context and ends in its own pull request, so a backlog of small Streamlit page requests can be worked through by labelling them.
From your own server, the same run starts with this request and the issue's URL in resources. A webhook URL on the API key gets run.completed when the turn finishes, with the session_url to read the result from.
The use case this job follows, with its automation and trigger: Turn a Linear label into a PR.
Why run it on Rush?
Streamlit pages are tested by running them, and the example's test needs the pinned LangChain and OpenAI packages installed before AppTest can import the page. Each run on Rush installs them in its own sandbox, runs the real pytest and ruff commands and keeps their output, so the pull request arrives with proof instead of a promise that the button works.
Nothing about the run touches a laptop's Python environment or anyone's OpenAI key: the tests patch the OpenAI call the same way the existing test_Chatbot does. Several labelled issues run side by side, each in its own sandbox and branch, and each comes back as a pull request with checks.
Frequently asked questions
Does the export include the API key or anything from the sidebar?
No. The export is built only from st.session_state.messages, the list of roles and message text the page already renders. The API key lives in a separate widget key, and the reviewer can confirm in the diff that the formatter never reads it.
How is a download button tested without a browser?
Streamlit's AppTest runs the page script headless and exposes its elements, so a test can send a chat message and check what the page rendered. The agent keeps the Markdown formatting in a plain function and tests that function directly, which is where the content of the file is decided.
Will the tests call OpenAI?
No. The existing test patches openai.resources.chat.Completions.create with a fixed ChatCompletion, and the agent's tests follow the same pattern. The run needs no OpenAI key and the result does not depend on a model's reply.
Can the same issue ask for JSON instead of Markdown?
Yes. The format comes from the issue text, so an issue that asks for JSON gets json.dumps of the messages with application/json instead. The steps, the tests and the pull request are the same.