Repository navigation
docs(agents): sync from rivet-dev/agents #130
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| --- | ||
| title: "Extensions" | ||
| description: "Bundle tools, prompt sections, hooks, and tasks into named extensions, and choose which ones each conversation uses." | ||
| --- | ||
|
|
||
| An extension is a named bundle of what an agent runs with: tools, prompt sections, hooks, tool wrappers, and tasks. You install extensions in a registry and pass it to `pi()`. Each conversation stores the names of the extensions it uses. | ||
|
|
||
| <div style="overflow-x:auto"> | ||
| <svg viewBox="0 0 660 236" role="img" aria-label="Your worker's registry holds three extensions: coding-tools, audit, and read-only. Conversation A selects coding-tools and audit. Conversation B selects all three, including read-only." style="width:100%;min-width:520px;max-width:660px;height:auto;display:block;margin:2.5rem auto;font-family:system-ui,sans-serif"> | ||
| <defs> | ||
| <marker id="extensions-arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10 z" fill="rgb(var(--site-ink, 27 25 22))"/></marker> | ||
| </defs> | ||
| <rect x="20" y="40" width="260" height="178" rx="10" style="fill:rgb(var(--site-paper-mid, 250 248 243));stroke:rgb(var(--site-pine, 46 64 52));stroke-width:1.4;stroke-dasharray:7 6"/> | ||
| <text x="20" y="30" font-size="11" font-family="ui-monospace, monospace" font-weight="600" letter-spacing="0.14em" style="fill:rgb(var(--site-pine, 46 64 52))">YOUR WORKER</text> | ||
| <text x="150" y="70" text-anchor="middle" font-size="14" font-weight="600" fill="#1b1916">Registry</text> | ||
| <g fill="#ffffff" stroke="#1b1916" stroke-width="1.4"> | ||
| <rect x="50" y="86" width="200" height="30" rx="6"/> | ||
| <rect x="50" y="126" width="200" height="30" rx="6"/> | ||
| <rect x="50" y="166" width="200" height="30" rx="6"/> | ||
| </g> | ||
| <g text-anchor="middle" font-size="13" font-family="ui-monospace, monospace" fill="#1b1916"> | ||
| <text x="150" y="106">coding-tools</text> | ||
| <text x="150" y="146">audit</text> | ||
| <text x="150" y="186">read-only</text> | ||
| </g> | ||
| <rect x="400" y="52" width="240" height="60" rx="7" fill="#c7e4fb" stroke="#3d9df3" stroke-width="2"/> | ||
| <rect x="400" y="146" width="240" height="60" rx="7" fill="#c7e4fb" stroke="#3d9df3" stroke-width="2"/> | ||
| <g text-anchor="middle"> | ||
| <text x="520" y="77" font-size="14" font-weight="600" fill="#1b1916">Conversation A</text> | ||
| <text x="520" y="97" font-size="12" font-family="ui-monospace, monospace" fill="#56524a">coding-tools, audit</text> | ||
| <text x="520" y="171" font-size="14" font-weight="600" fill="#1b1916">Conversation B</text> | ||
| <text x="520" y="191" font-size="12" font-family="ui-monospace, monospace" fill="#56524a">coding-tools, audit, read-only</text> | ||
| </g> | ||
| <g stroke="rgb(var(--site-ink, 27 25 22))" stroke-width="1.4" fill="none"> | ||
| <path d="M281 112 L398 82" marker-end="url(#extensions-arrow)"/> | ||
| <path d="M281 146 L398 176" marker-end="url(#extensions-arrow)"/> | ||
| </g> | ||
| <text x="340" y="133" text-anchor="middle" font-size="12" fill="#56524a">by name</text> | ||
| </svg> | ||
| </div> | ||
|
|
||
| | Field | Adds | See | | ||
| | --- | --- | --- | | ||
| | `tools` | Functions the model can call. | [Custom Tools](/agents/docs/custom-tools) | | ||
| | `sections` | Parts of the system prompt. | [Instructions](/agents/docs/instructions) | | ||
| | `hooks` | Code that runs around model requests, tool calls, and compaction. | [Hooks](#hooks) | | ||
| | `wraps` | Decorators for a tool or section, by name. | [Wrap a tool](#wrap-a-tool) | | ||
| | `tasks` | Multi-step work that resumes after a crash. | [Tools and tasks](/agents/docs/pi#tools-and-tasks) | | ||
|
|
||
| By default, every conversation uses every installed extension, in install order. When two selected extensions have a tool with the same name, the later one wins. | ||
|
|
||
| ## Hooks | ||
|
|
||
| A hook runs inside a built-in task, such as a tool call, in every conversation that selects its extension. This extension blocks the tools that change files or run commands: | ||
|
|
||
| <CodeSnippet file="examples/docs/extensions/read-only.ts" title="read-only.ts" /> | ||
|
|
||
| | Hook | Task | Runs | Can | | ||
| | --- | --- | --- | --- | | ||
| | `beforeTool` | `ToolTask` | Before a tool call runs. | Block the call or rewrite its arguments. A throw also blocks it. | | ||
| | `afterTool` | `ToolTask` | After a tool call returns. | Replace the result. | | ||
| | `beforeRequest` | `GenerationTask` | Before every model request. | Replace the messages of that request only. | | ||
| | `afterResponse` | `GenerationTask` | After every model response. | Observe it. | | ||
| | `afterTools` | `GenerationTask` | After every tool call of a round finishes. | Observe the results. | | ||
| | `onYield` | `GenerationTask` | When the model gives a final answer. | Continue the run with another user message. | | ||
| | `beforeCompact` | `CompactionTask` | Before a compaction summarizes the conversation. | Decline it or supply your own summary. | | ||
|
|
||
| Hooks run in the agent Actor on your worker, so they can call your APIs. To enforce a rule in every conversation, keep its extension in the default selection. To approve tool calls one by one, see [Human in the Loop](/agents/docs/human-in-the-loop). | ||
|
|
||
| ## Wrap a tool | ||
|
|
||
| `wrapTool` decorates a tool by name, whichever extension supplied it. This one logs every `bash` command: | ||
|
|
||
| <CodeSnippet file="examples/docs/extensions/audit.ts" title="audit.ts" /> | ||
|
|
||
| `wrapSection` does the same for a prompt section. | ||
|
|
||
| ## Choose extensions per conversation | ||
|
|
||
| <CodeGroup> | ||
| <CodeSnippet file="examples/docs/extensions/server.ts" title="server.ts" /> | ||
| <CodeSnippet file="examples/docs/extensions/client.ts" title="client.ts" /> | ||
| </CodeGroup> | ||
|
|
||
| - **`settings.extensions`** is the default selection. Without it, conversations use every installed extension. | ||
| - **`{ add, remove }`** changes the default for one conversation. An array selects exactly those extensions, in order. `null` goes back to the default. | ||
| - **Clients pass names.** A name that isn't installed rejects with a `UserError`. | ||
| - **Changes apply to the next step.** A model request that already started keeps its tools and prompt. A tool call that hasn't started yet uses the new hooks. | ||
|
|
||
| The `conversation.agent` action returns the conversation's extensions, tools, and prompt sections by name. | ||
|
|
||
| ## Deploys | ||
|
|
||
| The registry lives in your worker's code, and every agent Actor on that worker shares it. Pi saves only the names of the extensions a conversation selects. | ||
|
|
||
| - **Install extensions at startup.** Installing one later changes only the worker that runs the code. | ||
| - **Keep names stable.** Renaming an extension is the same as removing it. | ||
| - **A removed extension stops applying.** Conversations that select it lose its tools, sections, and hooks, and get them back when a later deploy installs it again. | ||
| - **Tasks wait for their extension.** A task from an extension's `tasks` resumes only once that extension is installed, so keep it installed while its tasks can run. | ||
| - **Running tool calls finish on the old code** while the Actor drains. See [Upgrades and crashes](/agents/docs/pi#upgrades-and-crashes). | ||
|
|
||
| **Next:** [User Subscriptions](/agents/docs/user-subscriptions), how users run agents on their own Claude or ChatGPT plan. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,143 @@ | ||
| --- | ||
| title: "Sign in with ChatGPT" | ||
| description: "Let users connect their ChatGPT plan so your agents run on it, with no OpenAI API key." | ||
| --- | ||
|
|
||
| import ExampleLinkBar from "@/components/docs/ExampleLinkBar.astro"; | ||
|
|
||
| <ExampleLinkBar href="https://github.com/rivet-dev/agents/tree/main/examples/docs/sign-in-with-chatgpt" label="Prefer to read code? See the full example on GitHub." /> | ||
|
|
||
| <div style="overflow-x:auto"> | ||
| <svg viewBox="0 0 700 430" role="img" aria-label="The user calls start on the chatgptLogin Actor, which returns a sign-in URL. The user signs in at OpenAI and chooses Continue. OpenAI redirects the browser to a 127.0.0.1 URL. The user passes that URL to finish. The chatgptLogin Actor exchanges the code with OpenAI for tokens and saves the login to the user's credentials Actor." style="width:100%;min-width:600px;max-width:700px;height:auto;display:block;margin:2.5rem auto;font-family:system-ui,sans-serif"> | ||
| <defs> | ||
| <marker id="siwc-request-arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10 z" fill="rgb(var(--site-ink, 27 25 22))"/></marker> | ||
| <marker id="siwc-response-arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10 z" style="fill:rgb(var(--site-pine, 46 64 52))"/></marker> | ||
| </defs> | ||
| <rect x="20" y="20" width="120" height="44" rx="7" fill="#ffffff" stroke="#1b1916" stroke-width="1.4"/> | ||
| <rect x="190" y="20" width="140" height="44" rx="7" fill="#c7e4fb" stroke="#3d9df3" stroke-width="2"/> | ||
| <rect x="380" y="20" width="120" height="44" rx="7" fill="#ffffff" stroke="#1b1916" stroke-width="1.4"/> | ||
| <rect x="550" y="20" width="130" height="44" rx="7" fill="#c7e4fb" stroke="#3d9df3" stroke-width="2"/> | ||
| <g text-anchor="middle" font-size="14" font-weight="600" fill="#1b1916"> | ||
| <text x="80" y="47">User</text> | ||
| <text x="260" y="47" font-family="ui-monospace, monospace" font-size="13">chatgptLogin</text> | ||
| <text x="440" y="47">OpenAI</text> | ||
| <text x="615" y="47" font-family="ui-monospace, monospace" font-size="13">credentials</text> | ||
| </g> | ||
| <g stroke="#8a8578" stroke-width="1.3" stroke-dasharray="5 5"> | ||
| <line x1="80" y1="64" x2="80" y2="410"/> | ||
| <line x1="260" y1="64" x2="260" y2="410"/> | ||
| <line x1="440" y1="64" x2="440" y2="410"/> | ||
| <line x1="615" y1="64" x2="615" y2="410"/> | ||
| </g> | ||
| <g font-size="12" fill="#56524a" text-anchor="middle"> | ||
| <text x="170" y="102" font-family="ui-monospace, monospace">start()</text> | ||
| <text x="170" y="142">sign-in URL</text> | ||
| <text x="350" y="182">sign in, Continue</text> | ||
| <text x="350" y="222">redirect to 127.0.0.1</text> | ||
| <text x="170" y="262" font-family="ui-monospace, monospace">finish(url)</text> | ||
| <text x="350" y="302">exchange code</text> | ||
| <text x="350" y="342">tokens</text> | ||
| <text x="527" y="382">save login</text> | ||
| </g> | ||
| <g stroke="rgb(var(--site-ink, 27 25 22))" stroke-width="1.4" fill="none"> | ||
| <line x1="80" y1="110" x2="258" y2="110" marker-end="url(#siwc-request-arrow)"/> | ||
| <line x1="80" y1="190" x2="438" y2="190" marker-end="url(#siwc-request-arrow)"/> | ||
| <line x1="80" y1="270" x2="258" y2="270" marker-end="url(#siwc-request-arrow)"/> | ||
| <line x1="260" y1="310" x2="438" y2="310" marker-end="url(#siwc-request-arrow)"/> | ||
| <line x1="260" y1="390" x2="613" y2="390" marker-end="url(#siwc-request-arrow)"/> | ||
| </g> | ||
| <g stroke-width="1.4" stroke-dasharray="5 4" fill="none" style="stroke:rgb(var(--site-pine, 46 64 52))"> | ||
| <line x1="260" y1="150" x2="82" y2="150" marker-end="url(#siwc-response-arrow)"/> | ||
| <line x1="440" y1="230" x2="82" y2="230" marker-end="url(#siwc-response-arrow)"/> | ||
| <line x1="440" y1="350" x2="262" y2="350" marker-end="url(#siwc-response-arrow)"/> | ||
| </g> | ||
| </svg> | ||
| </div> | ||
|
|
||
| What we'll build: | ||
|
|
||
| - A **Continue with ChatGPT** flow that connects a user's ChatGPT Plus or Pro plan to your app. | ||
| - A `chatgptLogin` Actor per user that runs the sign-in with pi-ai and saves the result. | ||
| - A `credentials` Actor per user that keeps the login and refreshes its access token. | ||
| - An agent that runs on the user's ChatGPT plan, with no OpenAI API key. | ||
|
|
||
| Related docs: | ||
|
|
||
| - [User Subscriptions](/agents/docs/user-subscriptions), how agents read logins from a `credentials` Actor. | ||
| - [LLM API Keys](/agents/docs/api-keys), the order the agent looks for credentials in. | ||
| - <a href="https://developers.openai.com/siwc/token-sharing-open-source" target="_blank" rel="noopener noreferrer">ChatGPT plan usage</a>, OpenAI's guide to the flow. | ||
|
|
||
| <Note> | ||
| This flow is for open-source and self-hosted apps. To offer it in a paid or hosted app, fill out OpenAI's <a href="https://openai.com/form/sign-in-with-chatgpt-interest/" target="_blank" rel="noopener noreferrer">interest form</a>. | ||
| </Note> | ||
|
|
||
| <Steps> | ||
|
|
||
| <Step title="Install"> | ||
|
|
||
| ```sh | ||
| npm add @rivet-dev/pi @earendil-works/pi-durable @earendil-works/pi-ai @earendil-works/pi-coding-agent rivetkit | ||
| ``` | ||
|
Comment on lines
+78
to
+80
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 Low · Keep guide commands in the checked example The repository's docs convention forbids inline fenced examples: source snippets live under |
||
|
|
||
| </Step> | ||
|
|
||
| <Step title="Create a host ID"> | ||
|
|
||
| OpenAI identifies each deployment of your app by an agent host ID. Generate a UUID once and set it on every worker: | ||
|
|
||
| ```sh | ||
| export CHATGPT_HOST_ID=$(node -e 'console.log(crypto.randomUUID())') | ||
| ``` | ||
|
|
||
| Keep the same value across deploys. It isn't a secret. | ||
|
|
||
| </Step> | ||
|
|
||
| <Step title="Store the login"> | ||
|
|
||
| The `credentials` Actor holds each user's login. It gives the agent the access token, never the refresh token, and refreshes it when it's about to expire: | ||
|
|
||
| <CodeSnippet file="examples/docs/sign-in-with-chatgpt/credentials.ts" title="credentials.ts" /> | ||
|
|
||
| </Step> | ||
|
|
||
| <Step title="Run the sign-in"> | ||
|
|
||
| The `chatgptLogin` Actor drives pi-ai's Sign in with ChatGPT flow: | ||
|
|
||
| <CodeSnippet file="examples/docs/sign-in-with-chatgpt/chatgpt-login.ts" title="chatgpt-login.ts" /> | ||
|
|
||
| - `start` returns the URL to open. Calling it again cancels the sign-in in progress. | ||
| - OpenAI sends the browser back to `http://127.0.0.1:1455/auth/callback`. When the worker runs on another machine, that page doesn't load, and the user pastes its URL into your app. | ||
| - `finish` exchanges the code and saves the login with the provider id `openai`. | ||
| - A sign-in that isn't finished within ten minutes is cancelled. | ||
|
|
||
| </Step> | ||
|
|
||
| <Step title="Define the agent"> | ||
|
|
||
| <CodeSnippet file="examples/docs/sign-in-with-chatgpt/server.ts" title="server.ts" /> | ||
|
|
||
| The agent reads the user's credential before every model call, so it can use OpenAI models as soon as the user signs in. | ||
|
|
||
| </Step> | ||
|
|
||
| <Step title="Connect from your app"> | ||
|
|
||
| <CodeSnippet file="examples/docs/sign-in-with-chatgpt/client.ts" title="client.ts" /> | ||
|
|
||
| In a web app, open the URL in a new tab and show a field for the pasted URL. Label the button **Continue with ChatGPT**, as OpenAI's <a href="https://developers.openai.com/siwc/ui-ux-guidelines" target="_blank" rel="noopener noreferrer">guidelines</a> require. | ||
|
|
||
| </Step> | ||
|
|
||
| </Steps> | ||
|
|
||
| ## Limitations | ||
|
|
||
| - **One sign-in at a time per worker.** pi-ai listens on port 1455 during a sign-in, so a second user's `start` on the same worker fails until the first finishes or is cancelled. | ||
| - **Only the Responses API.** ChatGPT plan tokens can't call OpenAI classifier models. | ||
| - **Usage counts against the user's plan.** Users set limits for your app in their ChatGPT settings. | ||
|
|
||
| <Warning> | ||
| Protect both Actors with [authentication](/docs/authentication). `credentials` returns access tokens, and anyone who can call `finish` can save a login to that user. | ||
| </Warning> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| import { defineExtension, wrapTool } from "@earendil-works/pi-durable"; | ||
| import { createBashTool } from "@earendil-works/pi-durable/tools"; | ||
|
|
||
| export const audit = defineExtension({ | ||
| name: "audit", | ||
| wraps: [ | ||
| // Wraps whichever `bash` tool the conversation ends up with. | ||
| wrapTool(createBashTool(), (bash) => ({ | ||
| ...bash, | ||
| execute: (args, api, context) => { | ||
| console.log(`[audit] ${api.conversationId}: ${args.command}`); | ||
| return bash.execute(args, api, context); | ||
| }, | ||
| })), | ||
| ], | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| import { createClient } from "rivetkit/client"; | ||
| import type { registry } from "./server"; | ||
|
|
||
| const client = createClient<typeof registry>(); | ||
| const agent = client.agent.getOrCreate(["acme", "checkout-review"]); | ||
| const root = await agent.harness.root(); | ||
|
|
||
| // Add read-only to this conversation only. Extensions are passed by name. | ||
| await agent.conversation.configure(root.id, { extensions: { add: ["read-only"] } }); | ||
| const answer = await agent.prompt("Where are checkout totals computed?"); | ||
| console.log(answer.status === "done" ? answer.text : `Unanswered: ${answer.reason}`); | ||
|
|
||
| // Go back to the default selection, CodingTools and audit. | ||
| await agent.conversation.configure(root.id, { extensions: null }); | ||
| const { extensions } = await agent.conversation.agent(root.id); | ||
| console.log(extensions); // ["coding-tools", "audit"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| import { defineExtension, hook, section, ToolTask } from "@earendil-works/pi-durable"; | ||
|
|
||
| const changesFiles = new Set(["write", "edit", "bash"]); | ||
|
|
||
| export const readOnly = defineExtension({ | ||
| name: "read-only", | ||
| sections: [section("mode", () => "You are in read-only mode. Read files and explain them, but do not change anything.")], | ||
| hooks: [ | ||
| hook(ToolTask, { | ||
| // A blocked call never runs. The model gets the message as an error result. | ||
| beforeTool: (call) => (changesFiles.has(call.name) ? { block: `${call.name} is turned off in read-only mode.` } : undefined), | ||
| }), | ||
| ], | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟠 Medium · Make the new diagrams theme-aware
These hardcoded
#ffffff,#1b1916, blue, and gray SVG colors do not retint underhtml[data-theme="dark"]; the same pattern appears in the new Sign in with ChatGPT diagram. This violates the repository's theme contract and leaves both figures as light-theme islands in dark docs. Replace the fixed fills/strokes/text colors in both SVGs with the existing CSS-variable tokens (paper/inkand--runtime-highlightfor figure accents), then verify both themes.