From 04b814ec02a9bfa4eaba809f715f8863e951489a Mon Sep 17 00:00:00 2001 From: "rivet-docs-sync[bot]" Date: Wed, 7 Oct 2026 19:39:39 +0000 Subject: [PATCH] docs(agents): sync from rivet-dev/agents@16766dd --- .../docs/content/docs/agent-to-agent.mdx | 20 +- vendor/agents/docs/content/docs/api-keys.mdx | 2 +- .../agents/docs/content/docs/architecture.mdx | 54 ++-- .../docs/content/docs/built-in-tools.mdx | 65 +++-- .../agents/docs/content/docs/client-sdk.mdx | 25 +- .../docs/content/docs/connectors/discord.mdx | 10 +- .../docs/content/docs/connectors/github.mdx | 10 +- .../docs/content/docs/connectors/linear.mdx | 10 +- .../docs/content/docs/connectors/slack.mdx | 11 +- .../agents/docs/content/docs/custom-tools.mdx | 25 +- .../docs/content/docs/design-patterns.mdx | 28 +- .../docs/content/docs/human-in-the-loop.mdx | 14 +- vendor/agents/docs/content/docs/index.mdx | 23 +- .../agents/docs/content/docs/instructions.mdx | 56 +++- .../docs/content/docs/observability.mdx | 10 +- .../agents/docs/content/docs/pi-durable.mdx | 189 ------------- vendor/agents/docs/content/docs/pi.mdx | 258 ++++++++++++------ .../agents/docs/content/docs/quickstart.mdx | 98 ++----- vendor/agents/docs/content/docs/react-sdk.mdx | 16 +- vendor/agents/docs/content/docs/sandboxes.mdx | 92 +++++-- vendor/agents/docs/content/docs/schedules.mdx | 1 + .../docs/content/docs/security-model.mdx | 12 +- .../docs/content/docs/session-lifecycle.mdx | 47 ++-- vendor/agents/docs/content/docs/subagents.mdx | 6 +- .../docs/content/docs/user-subscriptions.mdx | 6 +- vendor/agents/docs/content/docs/workflows.mdx | 8 +- vendor/agents/docs/sidebar.json | 11 +- .../examples/docs/agent-to-agent/client.ts | 4 +- .../examples/docs/agent-to-agent/coder.ts | 14 +- .../examples/docs/agent-to-agent/server.ts | 11 +- .../examples/docs/built-in-tools/client.ts | 12 +- .../examples/docs/built-in-tools/reviewer.ts | 19 ++ .../examples/docs/built-in-tools/server.ts | 15 +- .../examples/docs/client-sdk/browser.ts | 4 +- .../agents/examples/docs/client-sdk/client.ts | 64 +++-- .../agents/examples/docs/client-sdk/server.ts | 6 + .../docs/connectors/discord/discord.ts | 9 +- .../docs/connectors/discord/server.ts | 30 +- .../examples/docs/connectors/github/github.ts | 10 +- .../examples/docs/connectors/github/server.ts | 30 +- .../examples/docs/connectors/linear/linear.ts | 4 +- .../examples/docs/connectors/linear/server.ts | 30 +- .../examples/docs/connectors/slack/server.ts | 30 +- .../examples/docs/connectors/slack/slack.ts | 4 +- .../examples/docs/credentials/api-keys.ts | 6 + .../examples/docs/custom-tools/get-order.ts | 9 +- .../examples/docs/custom-tools/server.ts | 8 +- .../design-patterns/agent-per-key/client.ts | 14 +- .../design-patterns/agent-per-key/server.ts | 12 +- .../design-patterns/background-job/server.ts | 7 +- .../docs/design-patterns/key-per-request.ts | 5 +- .../docs/design-patterns/one-agent.ts | 5 +- .../tenant-credentials/client.ts | 5 +- .../tenant-credentials/server.ts | 2 + .../docs/human-in-the-loop/ApprovalDialog.tsx | 27 +- .../examples/docs/human-in-the-loop/deploy.ts | 36 ++- .../examples/docs/human-in-the-loop/server.ts | 6 +- .../examples/docs/instructions/client.ts | 16 +- .../examples/docs/instructions/server.ts | 25 +- vendor/agents/examples/docs/package.json | 8 +- .../examples/docs/pi-durable/documents.ts | 32 --- .../agents/examples/docs/pi-durable/tasks.ts | 59 ---- vendor/agents/examples/docs/pi/actions.ts | 17 -- .../docs/{pi-durable => pi}/client.ts | 2 +- vendor/agents/examples/docs/pi/cost.ts | 20 ++ .../docs/{pi-durable => pi}/daytona.ts | 4 +- .../examples/docs/pi/documents/chambers.ts | 11 + .../examples/docs/pi/documents/client.ts | 10 + .../examples/docs/pi/documents/server.ts | 30 ++ vendor/agents/examples/docs/pi/messages.ts | 21 ++ vendor/agents/examples/docs/pi/pokedex.ts | 30 ++ vendor/agents/examples/docs/pi/server.ts | 15 +- .../docs/{pi-durable => pi}/stream.ts | 19 +- vendor/agents/examples/docs/pi/tasks.ts | 68 +++++ .../agents/examples/docs/quickstart/client.ts | 10 +- .../agents/examples/docs/quickstart/server.ts | 9 +- .../examples/docs/react-sdk/useAgentChat.ts | 107 ++++++-- .../server.ts => sandboxes/agentos.ts} | 8 +- .../agents/examples/docs/sandboxes/daytona.ts | 7 + vendor/agents/examples/docs/sandboxes/e2b.ts | 7 + .../examples/docs/sandboxes/no-sandbox.ts | 17 ++ .../agents/examples/docs/schedules/client.ts | 13 +- .../agents/examples/docs/schedules/server.ts | 6 + .../examples/docs/scoped-access/server.ts | 10 +- .../examples/docs/session-lifecycle/client.ts | 41 ++- .../agents/examples/docs/subagents/client.ts | 4 +- .../examples/docs/subagents/get-order.ts | 9 +- .../agents/examples/docs/subagents/server.ts | 6 +- .../examples/docs/subagents/specialists.ts | 31 ++- vendor/agents/examples/docs/tsconfig.json | 4 +- .../docs/user-subscriptions/client.ts | 5 +- .../docs/user-subscriptions/server.ts | 2 + .../agents/examples/docs/workflows/server.ts | 19 +- 93 files changed, 1270 insertions(+), 977 deletions(-) delete mode 100644 vendor/agents/docs/content/docs/pi-durable.mdx create mode 100644 vendor/agents/examples/docs/built-in-tools/reviewer.ts delete mode 100644 vendor/agents/examples/docs/pi-durable/documents.ts delete mode 100644 vendor/agents/examples/docs/pi-durable/tasks.ts delete mode 100644 vendor/agents/examples/docs/pi/actions.ts rename vendor/agents/examples/docs/{pi-durable => pi}/client.ts (76%) create mode 100644 vendor/agents/examples/docs/pi/cost.ts rename vendor/agents/examples/docs/{pi-durable => pi}/daytona.ts (87%) create mode 100644 vendor/agents/examples/docs/pi/documents/chambers.ts create mode 100644 vendor/agents/examples/docs/pi/documents/client.ts create mode 100644 vendor/agents/examples/docs/pi/documents/server.ts create mode 100644 vendor/agents/examples/docs/pi/messages.ts create mode 100644 vendor/agents/examples/docs/pi/pokedex.ts rename vendor/agents/examples/docs/{pi-durable => pi}/stream.ts (53%) create mode 100644 vendor/agents/examples/docs/pi/tasks.ts rename vendor/agents/examples/docs/{pi-durable/server.ts => sandboxes/agentos.ts} (71%) create mode 100644 vendor/agents/examples/docs/sandboxes/no-sandbox.ts diff --git a/vendor/agents/docs/content/docs/agent-to-agent.mdx b/vendor/agents/docs/content/docs/agent-to-agent.mdx index 7c745bd7..bfdb819d 100644 --- a/vendor/agents/docs/content/docs/agent-to-agent.mdx +++ b/vendor/agents/docs/content/docs/agent-to-agent.mdx @@ -6,7 +6,7 @@ description: "Let one agent hand work to another agent without waiting for it." A tool can send another agent a message. Here a coder agent hands a finished branch to the reviewer for that repository and keeps working.
- + @@ -15,17 +15,15 @@ A tool can send another agent a message. Here a coder agent hands a finished bra - - + Coder agent Reviewer agent key: repository - Schedule + Conversation requestReview returns right away - after(0) - prompt + submit
@@ -35,9 +33,9 @@ A tool can send another agent a message. Here a coder agent hands a finished bra -- The reviewer is keyed by repository, so every coder working on the same repository reaches the same reviewer, and its session remembers past reviews. -- The `requestReview` action schedules the reviewer's `prompt` and returns, so the coder's tool doesn't wait for the review. The schedule is saved with the reviewer, so the review runs even if the coder's request goes away. -- Passing `streamingBehavior: "followUp"` queues the request when the reviewer is busy with another review. Use it with `prompt`, not the `followUp` action, which only queues on an idle agent and never starts a run. -- The reviewer runs on a different model family than the coder. +- The reviewer is keyed by repository, so every coder working on the same repository reaches the same reviewer, and its conversation remembers past reviews. +- The `requestReview` action gets Pi's harness as `c.pi` and submits the request to the reviewer's root conversation without waiting. The request is saved before the action returns, so the review runs even if the coder's request goes away. +- When the reviewer is busy with another review, the request waits as a follow-up and runs next. +- The coder's `request_review` tool sits next to `CodingTools` in its registry. The reviewer runs on a different model family than the coder. -To wait for the other agent's answer instead, see [Subagents](/agents/docs/subagents). See [Schedules](/actors/docs/schedule) for more on `c.schedule`. +To wait for the other agent's answer instead, see [Subagents](/agents/docs/subagents). See [Pi](/agents/docs/pi#your-own-actions-and-cpi) for more on `c.pi`. diff --git a/vendor/agents/docs/content/docs/api-keys.mdx b/vendor/agents/docs/content/docs/api-keys.mdx index 129d8dd7..491185df 100644 --- a/vendor/agents/docs/content/docs/api-keys.mdx +++ b/vendor/agents/docs/content/docs/api-keys.mdx @@ -62,7 +62,7 @@ Set the key for your model provider in the environment of the process that runs ANTHROPIC_API_KEY=sk-ant-... ``` -Pi reads the provider-named variable, such as `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `XAI_API_KEY`, `DEEPSEEK_API_KEY`, `GROQ_API_KEY`, or `OPENROUTER_API_KEY`. See Pi's [provider list](https://github.com/earendil-works/pi) for the rest. +Pi reads the provider-named variable, such as `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `XAI_API_KEY`, `DEEPSEEK_API_KEY`, `GROQ_API_KEY`, or `OPENROUTER_API_KEY`. See Pi's provider list for the rest. ## Keys in code diff --git a/vendor/agents/docs/content/docs/architecture.mdx b/vendor/agents/docs/content/docs/architecture.mdx index 5a41426d..c1110594 100644 --- a/vendor/agents/docs/content/docs/architecture.mdx +++ b/vendor/agents/docs/content/docs/architecture.mdx @@ -3,11 +3,11 @@ title: "Architecture" description: "The states an agent moves through, and what survives sleep, crashes, and upgrades." --- -This page describes [`pi()`](/agents/docs/pi). A [`piDurable()`](/agents/docs/pi-durable) agent sleeps and drains the same way, and differs where noted in [Pi Durable](#pi-durable). +A [`pi()`](/agents/docs/pi) agent runs Pi Durable, Earendil's durable agent harness, inside a Rivet Actor. Pi saves each step of a run to the Actor's SQLite database before the next step starts. ## States -An agent is idle until a prompt starts a run, and goes back to idle when the run ends or is cancelled. When the context fills up during a run, the agent compacts it and keeps going. +A conversation is idle until a prompt starts a run, and goes back to idle when the run ends or is cancelled. When the context fills up during a run, the agent compacts it and keeps going.
@@ -49,7 +49,9 @@ An agent is idle until a prompt starts a run, and goes back to idle when the run ## Sleep -An idle agent sleeps and frees its memory. The next action on its key wakes it: the agent reloads its session, and reconnects to the same sandbox the first time a tool needs it, so clients don't do anything different. While the agent sleeps, its sandbox is suspended. +An idle agent sleeps and frees its memory. The next action on its key wakes it: the agent opens Pi from SQLite, resumes interrupted work, and reconnects to the same sandbox the first time a tool needs it, so clients don't do anything different. While the agent sleeps, its sandbox is suspended. + +During a retry or poll wait longer than a minute, the Actor also sleeps, and a scheduled wake resumes the work when the wait ends.
@@ -71,7 +73,7 @@ An idle agent sleeps and frees its memory. The next action on its key wakes it: Agent loop in memory SQLite - session, sandbox id + conversations, sandbox id Sandbox save each step restore on wake @@ -83,41 +85,37 @@ An idle agent sleeps and frees its memory. The next action on its key wakes it: ## What survives -| | Session | Sandbox | +| | Conversations | Sandbox | | --- | --- | --- | -| Sleep | Every message so far. | Reconnected on first use. | -| Crash | Every message written before the crash. A run in progress resumes on wake if Pi can continue it. | Reconnected on first use. | -| Upgrade | Every message so far. A run in progress gets time to finish first. See [Upgrades](#upgrades). | Reconnected on first use. | +| Sleep | Every step so far. | Reconnected on first use. | +| Crash | Every step saved before the crash. A run in progress resumes on wake from the step it reached. | Reconnected on first use. | +| Upgrade | Every step so far. A run in progress gets time to finish first. See [Upgrades](#upgrades). | Reconnected on first use. | | Destroy | Deleted. | Deleted. | -A model chosen with `setModel` is restored too, if it's still allowed. If the sandbox no longer exists when the agent wakes, a new one is created and the old files are lost. - -Actions that don't touch the sandbox, such as `getMessages` or `setModel`, don't connect to it. They keep working while the sandbox provider is down. +A model or instructions set with `conversation.configure` are part of the conversation, so they are kept too. Without a sandbox, files live in the Actor's SQLite database and survive the same way as conversations. If the sandbox no longer exists when the agent wakes, a new one is created and the old files are lost. -## Upgrades +Actions that don't run a tool, such as `conversation.context` or `conversation.configure`, don't connect to the sandbox. They keep working while the sandbox provider is down. -When you ship new code, an agent in the middle of a run lets it finish, for up to `sleepGracePeriod` (15 minutes by default), before it restarts on the new version. See [Versions](/docs/versions). +## Resuming a run -A run that is still going when the grace period ends, or that a crash cuts off, resumes when the agent wakes if Pi can continue it from the saved messages: +After a crash or a cut-off upgrade, the agent reads the saved steps on wake and carries on from the step it reached: -| Stopped while | Resumes? | +| Stopped while | On wake | | --- | --- | -| The model was answering | Yes. The model is asked again, and the partial answer is lost. | -| Parallel tool calls ran and at least one finished | Yes. Calls that didn't finish get a "No result provided" result. | -| A tool call ran and none finished | No. | -| The run was cancelled with `abort` | No. | +| The model was answering | The model call is sent again from the start. The partial answer is lost. | +| A tool marked `replay: "safe"` ran | The tool runs again. | +| Any other tool ran | The tool isn't run again. Its result tells the model the call stopped partway. | +| A task phase ran | The phase runs again, and earlier phases are kept. | +| The run was cancelled with `abort` | Nothing resumes. | -A run that doesn't resume is left as if it had been cancelled, and the next prompt continues the conversation. A client that was waiting on `prompt` gets an error. It can follow the resumed run through events, or read it with `getMessages`. +Nothing runs while a crashed Actor is down. The run resumes when the Actor starts again. A client that is still connected gets a fresh snapshot on its event stream. -Nothing runs while a crashed Actor is down. The run resumes when the Actor starts again. +## Upgrades -## Pi Durable +When you ship new code, running work gets up to `sleepGracePeriod` (15 minutes by default) to finish before the Actor restarts on the new version. See [Versions](/docs/versions). Work still running when the grace period ends resumes on wake, after your `onWake` runs. -A [`piDurable()`](/agents/docs/pi-durable) agent saves every step of a run, so it doesn't depend on what the saved messages look like: +A client waiting on `prompt` gets an error if the Actor stops first. It can send the prompt again with the same `requestId` to get the first prompt's result instead of a second run. Actions wait up to ten minutes, and a timed-out wait leaves the run going. -- **Runs carry on from the last saved step.** Model calls, tool calls, and tasks your tools launch each save their progress. After a crash, the Actor reads that record and carries on. A plain tool call that was running isn't rerun unless it's marked `replay: "safe"`, and the model learns it never completed. -- **Long waits don't keep the Actor awake.** During a long retry or poll wait, the Actor sleeps and wakes when the wait ends. -- **Streams come back on wake.** A client that is still connected gets a fresh snapshot. -- **Destroy stops at once**, without waiting for running work. +Destroying the Actor stops running work at once, without a grace period. -See [Known limitations](/agents/docs/pi-durable#known-limitations) for what this doesn't cover. +Roll forward only. An older version of Pi Durable can't open data that a newer version wrote, so don't roll back after an upgrade has run. diff --git a/vendor/agents/docs/content/docs/built-in-tools.mdx b/vendor/agents/docs/content/docs/built-in-tools.mdx index 53bd7f46..a7a871ea 100644 --- a/vendor/agents/docs/content/docs/built-in-tools.mdx +++ b/vendor/agents/docs/content/docs/built-in-tools.mdx @@ -1,46 +1,69 @@ --- title: "Built-in Tools" -description: "Pi's file and shell tools for agents with a sandbox, where they run, and how to limit them." +description: "Pi's file and shell tools, where they run with and without a sandbox, and how to choose them." --- -Built-in tools are Pi's own [coding tools](https://github.com/earendil-works/pi/tree/main/packages/coding-agent), with the same names, parameters, and output as in Pi. They run in the agent's sandbox instead of on your worker. +Built-in tools are Pi's own file and shell tools. The `CodingTools` extension from `@earendil-works/pi-durable/tools` installs all four: | Tool | What it does | | --- | --- | -| `read`, `write`, `edit` | Read, create, and change files. | -| `grep`, `find`, `ls` | Search files and list directories. | -| `bash` | Run a shell command. | +| `read` | Reads a text file. The model reads a long file in parts. | +| `write` | Creates or replaces a file. | +| `edit` | Changes part of a file. | +| `bash` | Runs a shell command. | -A sandbox is optional. Without one, the agent has no built-in tools and only uses the [custom tools](/agents/docs/custom-tools) you pass. Pi's tools run with the permissions of the process they run in, which is your worker, so they're only turned on when a [sandbox](/agents/docs/sandboxes) gives them somewhere else to run. +## Where they run + +The `sandbox` option decides where the tools run. They never touch your worker's file system or shell. + +| Tool | Without a sandbox | With a sandbox | +| --- | --- | --- | +| `read`, `write`, `edit` | Files in the Actor's own SQLite database. | Files in the sandbox. | +| `bash` | Returns an error. There is no shell. | Runs in the sandbox. |
- + - - - + + + - - + + + + Agent Actor + Actor's SQLite database + Sandbox + + read, write, edit + no sandbox + read, write, edit, bash + with a sandbox - client.ts - Reviewer agent - Sandbox - executeBash - read, grep, find, ls - bash, edit, write excluded
+Without a sandbox, there is nothing to set up. The files are saved with the conversation and survive sleep and crashes. This agent has no sandbox, so it installs the file tools without `bash`: + -- The `excludeTools` option turns tools off by name, so this reviewer can read and search files but can't run commands or change them. The `tools` option does the opposite and enables only the tools you list. -- The `executeBash` action runs a command in the agent's sandbox from your backend, without the model. It works even though the reviewer can't run `bash` itself. Its `excludeFromContext` option keeps the output out of the conversation. -- File tools only reach paths inside the sandbox's working directory. The `bash` tool accepts a timeout, and long output is truncated before it reaches the model. +The second prompt edits the file that the first prompt wrote. To run commands, give the agent a [sandbox](/agents/docs/sandboxes). + +## Choose tools + +Each tool also has its own factory: `createReadTool`, `createWriteTool`, `createEditTool`, and `createBashTool`. Install only the tools you want in your own extension. This reviewer can read files and run tests, but it has no `write` or `edit` tool: + + + +- The `bash` tool can still change files in the sandbox. For an agent that must not change anything, leave `bash` out too. +- A client can narrow the tools of one conversation with `conversation.configure(id, { tools: ["read"] })`. +- In a sandbox, the file tools reject paths outside the sandbox's working directory. +- The `bash` tool accepts a timeout, and long output is truncated before it reaches the model. +- Images aren't supported yet. The `read` tool returns an error for an image file. To give an agent your own tools, see [Custom Tools](/agents/docs/custom-tools). diff --git a/vendor/agents/docs/content/docs/client-sdk.mdx b/vendor/agents/docs/content/docs/client-sdk.mdx index fb053c50..a468020f 100644 --- a/vendor/agents/docs/content/docs/client-sdk.mdx +++ b/vendor/agents/docs/content/docs/client-sdk.mdx @@ -3,7 +3,7 @@ title: "Client SDK" description: "Call an agent from your backend, scripts, or any JavaScript runtime, and stream its events." --- -Call an agent from your backend, a script, or any JavaScript runtime with `rivetkit/client`. +The client is the `rivetkit/client` package. It calls actions on the agent and gets the agent's events over a connection. A browser connects with a short-lived token from your backend.
@@ -42,19 +42,30 @@ Call an agent from your backend, a script, or any JavaScript runtime with `rivet ## Handles and connections - With no options, `createClient()` reads `RIVET_ENDPOINT`, `RIVET_NAMESPACE`, and `RIVET_TOKEN` from the environment, and defaults to a local Rivet at `localhost:6420`. -- The `getOrCreate` method returns a handle to the agent with that key, and creates the agent on first use. Actions called on the handle, like `getMessages`, are single requests. -- Calling `connect()` opens a live connection that receives the session's events. Actions called before it opens wait until it does. +- The `getOrCreate` method returns a handle to the agent with that key, and creates the agent on first use. Actions called on the handle, like `conversation.context`, are single requests. +- An agent starts with one root conversation. The `harness.root()` action returns its id, and `prompt` uses it unless you pass a `conversationId`. +- Calling `connect()` opens a live connection that can receive events. Actions called before it opens wait until it does. - A dropped connection reconnects on its own. The `onStatusChange` callback reports `connecting`, `connected`, and `disconnected`, and `idle` after `dispose()` closes the connection for good. ## Events -A listener added with `on("event", ...)` receives every event of the session as the agent works. See [Session Lifecycle](/agents/docs/session-lifecycle#events) for the event types. +Events go only to a connection that asks for them. Call `conversation.watchEvents(id)` on the connection. It returns a `snapshot` of the conversation, including any answer in progress, and then the connection receives `pi.events` with `{ conversationId, seq, events }` as the agent works. + +- Events from before the call are not sent again. Read them from the snapshot. +- A short answer can arrive whole in `message_end`, with no `text_delta` before it, so print the rest of the text there. +- The `seq` number goes up by one for each batch. If a number is missing, call `watchEvents` again to get a new snapshot. +- To stop listening, call the function that `on` returns, or `conversation.unwatchEvents(id)`. + +See [Session Lifecycle](/agents/docs/session-lifecycle#events) for the event types. ## Results and errors -- The `prompt` action resolves when the run ends. Read the reply with `getLastAssistantText` or `getMessages`. -- The `prompt` action rejects with an `ActorError` when the action itself fails. For example, another prompt is already running (pass `streamingBehavior` to queue it instead), the model has no credential (`model_unavailable`), or the run passes the ten-minute action timeout (`action_timed_out`). A timeout only stops the wait. The run keeps going until it finishes or `abort` stops it. -- A model error doesn't reject. The run ends with an assistant message whose `stopReason` is `"error"`, and `errorMessage` explains why. +- The `prompt` action resolves when the run ends. The result is `{ status: "done", text }` with the answer, or `{ status: "unanswered", reason }`. +- A model error doesn't reject. The status is `"unanswered"`, and `reason` explains why. +- Pass a `requestId` to make retries safe. A second `prompt` with the same `requestId` returns the first one's result and doesn't run the agent again. +- While a run is in progress, a new prompt waits as a follow-up. Pass `whenBusy: "steer"` to send it into the current run, or `"reject"` to make `prompt` reject with the `ConversationBusy` code. +- The `prompt` action rejects with an `ActorError` when the action itself fails. For example, the run passes the ten-minute action timeout (`action_timed_out`). A timeout only stops the wait. The run keeps going until it finishes or `conversation.abort(id)` stops it. +- To start a run without waiting for it, call `conversation.submit(id, { type: "input", content })`. Follow its progress with events. ## Browsers diff --git a/vendor/agents/docs/content/docs/connectors/discord.mdx b/vendor/agents/docs/content/docs/connectors/discord.mdx index a0897ab4..335a0f1c 100644 --- a/vendor/agents/docs/content/docs/connectors/discord.mdx +++ b/vendor/agents/docs/content/docs/connectors/discord.mdx @@ -33,7 +33,7 @@ description: "Talk to an agent with a slash command in Discord." -In the [Discord Developer Portal](https://discord.com/developers/applications), create an application. Copy its application ID and public key, and create a bot token. +In the Discord Developer Portal, create an application. Copy its application ID and public key, and create a bot token. @@ -87,8 +87,10 @@ Under **General Information**, set the interactions endpoint URL to `https://Application command `/ask` | `/discord/interactions` | Hands the command to the channel's agent and defers. The agent edits the reply. | diff --git a/vendor/agents/docs/content/docs/connectors/github.mdx b/vendor/agents/docs/content/docs/connectors/github.mdx index 159071fa..197c5ed2 100644 --- a/vendor/agents/docs/content/docs/connectors/github.mdx +++ b/vendor/agents/docs/content/docs/connectors/github.mdx @@ -33,7 +33,7 @@ description: "Mention an agent in a GitHub issue or pull request comment." -Create a GitHub account for the agent, or use your own, and a [fine-grained personal access token](https://github.com/settings/personal-access-tokens) for the repository with the **Issues** permission set to read and write. It covers comments on pull requests too. +Create a GitHub account for the agent, or use your own, and a fine-grained personal access token for the repository with the **Issues** permission set to read and write. It covers comments on pull requests too. @@ -74,9 +74,11 @@ In the repository's **Settings > Webhooks**, add a webhook with the payload URL ## How it works - The route verifies GitHub's `X-Hub-Signature-256` signature against the raw body. -- Each issue or pull request gets its own agent, keyed by owner, repository, and number. Every mention in the thread continues the same session. +- Each issue or pull request gets its own agent, keyed by owner, repository, and number. Every mention in the thread continues the same conversation. - The route only answers new comments that mention the bot, and never the bot's own comments, so replies do not loop. -- GitHub times out a delivery after ten seconds, so the route only hands the comment to the agent's `receive` action and returns. The `receive` action adds the message to the agent's queue and schedules `drain`, which prompts the agent with the oldest message and posts the answer as a comment with Octokit. A message that arrives while the agent is answering waits in the queue. The queue is saved with the agent, so replies still run if the request that delivered a message goes away. +- GitHub times out a delivery after ten seconds, so the route only hands the comment to the agent's `receive` action and returns. The `receive` action schedules `reply`, which prompts the agent and posts the answer as a comment with Octokit. The schedule is saved with the agent, so the reply still runs if the request that delivered the message goes away. +- A message that arrives while the agent is answering waits as a follow-up, and gets its own reply when its run ends. +- The prompt's `requestId` is the comment's id, so a redelivered webhook runs the agent only once. ## Configure @@ -91,4 +93,4 @@ In the repository's **Settings > Webhooks**, add a webhook with the payload URL | GitHub event | Webhook path | What the route does | | --- | --- | --- | -| [`issue_comment`](https://docs.github.com/en/webhooks/webhook-events-and-payloads#issue_comment), `created` | `/github/webhook` | Hands the comment to the issue's agent, which replies with a comment. Covers pull request conversations too. | +| `issue_comment`, `created` | `/github/webhook` | Hands the comment to the issue's agent, which replies with a comment. Covers pull request conversations too. | diff --git a/vendor/agents/docs/content/docs/connectors/linear.mdx b/vendor/agents/docs/content/docs/connectors/linear.mdx index 5dfe17ed..82859694 100644 --- a/vendor/agents/docs/content/docs/connectors/linear.mdx +++ b/vendor/agents/docs/content/docs/connectors/linear.mdx @@ -33,7 +33,7 @@ description: "Mention an agent in a Linear issue comment." -Create a Linear user for the agent, or use your own, and a [personal API key](https://linear.app/settings/account/security) for it with read and create-comments access. +Create a Linear user for the agent, or use your own, and a personal API key for it with read and create-comments access. @@ -74,9 +74,11 @@ In Linear's **Settings > Administration > API**, which needs admin rights, creat ## How it works - The route verifies the `Linear-Signature` HMAC against the raw body and rejects deliveries older than a minute. -- Each Linear issue gets its own agent. Every comment with the trigger continues the same session. +- Each Linear issue gets its own agent. Every comment with the trigger continues the same conversation. - The route never answers the agent's own comments, so replies do not loop. -- The route only hands the comment to the agent's `receive` action and returns. The `receive` action adds the message to the agent's queue and schedules `drain`, which prompts the agent with the oldest message and posts the answer as a comment with the Linear SDK. A message that arrives while the agent is answering waits in the queue. The queue is saved with the agent, so replies still run if the request that delivered a message goes away. +- The route only hands the comment to the agent's `receive` action and returns. The `receive` action schedules `reply`, which prompts the agent and posts the answer as a comment with the Linear SDK. The schedule is saved with the agent, so the reply still runs if the request that delivered the message goes away. +- A message that arrives while the agent is answering waits as a follow-up, and gets its own reply when its run ends. +- The prompt's `requestId` is the comment's id, so a comment that Linear delivers twice runs the agent only once. ## Configure @@ -91,4 +93,4 @@ In Linear's **Settings > Administration > API**, which needs admin rights, creat | Linear event | Webhook path | What the route does | | --- | --- | --- | -| [`Comment`](https://linear.app/developers/webhooks), `create` | `/linear/webhook` | Hands the comment to the issue's agent, which replies with a comment. | +| `Comment`, `create` | `/linear/webhook` | Hands the comment to the issue's agent, which replies with a comment. | diff --git a/vendor/agents/docs/content/docs/connectors/slack.mdx b/vendor/agents/docs/content/docs/connectors/slack.mdx index 0655a88a..c00cb820 100644 --- a/vendor/agents/docs/content/docs/connectors/slack.mdx +++ b/vendor/agents/docs/content/docs/connectors/slack.mdx @@ -33,7 +33,7 @@ description: "Talk to an agent by mentioning it in a Slack channel or thread." -In [Slack's app settings](https://api.slack.com/apps), create an app, add the `app_mentions:read` and `chat:write` bot token scopes, and install it to your workspace. Copy the bot token and the signing secret. +In Slack's app settings, create an app, add the `app_mentions:read` and `chat:write` bot token scopes, and install it to your workspace. Copy the bot token and the signing secret. @@ -73,9 +73,10 @@ Under **Event Subscriptions**, set the request URL to `https:///sla ## How it works - The route verifies Slack's signature against the raw body and rejects requests older than five minutes. -- Each Slack thread gets its own agent, keyed by team, channel, and thread. Mentioning the app again in the thread continues the same session. -- Slack expects a response within three seconds and an agent run takes longer, so the route only hands the message to the agent's `receive` action and returns. The `receive` action adds the message to the agent's queue and schedules `drain`, which prompts the agent with the oldest message and posts the answer in the thread with the Slack Web API. A message that arrives while the agent is answering waits in the queue. The queue is saved with the agent, so replies still run if the request that delivered a message goes away. -- Slack retries events it thinks timed out. The route ignores retries, because the first delivery is already being answered. +- Each Slack thread gets its own agent, keyed by team, channel, and thread. Mentioning the app again in the thread continues the same conversation. +- Slack expects a response within three seconds and an agent run takes longer, so the route only hands the message to the agent's `receive` action and returns. The `receive` action schedules `reply`, which prompts the agent and posts the answer in the thread with the Slack Web API. The schedule is saved with the agent, so the reply still runs if the request that delivered the message goes away. +- A message that arrives while the agent is answering waits as a follow-up, and gets its own reply when its run ends. +- Slack retries events it thinks timed out. The route ignores retries, because the first delivery is already being answered. The prompt's `requestId` is Slack's event id, so a duplicate that gets through still runs the agent only once. ## Configure @@ -89,7 +90,7 @@ Under **Event Subscriptions**, set the request URL to `https:///sla | Slack surface | Webhook path | What the route does | | --- | --- | --- | -| [Event Subscriptions](https://docs.slack.dev/apis/events-api/), `app_mention` | `/slack/events` | Hands the message to the thread's agent, which replies in the thread. | +| Event Subscriptions, `app_mention` | `/slack/events` | Hands the message to the thread's agent, which replies in the thread. | | URL verification | `/slack/events` | Answers Slack's challenge. | To answer direct messages too, subscribe to `message.im`, add the `im:history` scope, and handle `message` events the same way. diff --git a/vendor/agents/docs/content/docs/custom-tools.mdx b/vendor/agents/docs/content/docs/custom-tools.mdx index aa1b1369..33683ac0 100644 --- a/vendor/agents/docs/content/docs/custom-tools.mdx +++ b/vendor/agents/docs/content/docs/custom-tools.mdx @@ -30,12 +30,29 @@ A custom tool is a function the model can call. It runs in the agent Actor on yo
+Tool parameters are TypeBox schemas. Pi validates every call against them, so build them with the `Type` that pi-ai exports: + +```sh +npm add @earendil-works/pi-ai +``` + -The model reads the tool's `description` and calls it with arguments that match `parameters`. It reads `content` as the result, while `details` goes to clients for your UI. A thrown error, like the 404 in `fetchOrder`, becomes an error result: the model reads the message and can try something else. The `signal` argument aborts when the run is cancelled, so a Stop from the client also cancels the request. +Both `defineTool` and `defineExtension` come from `@earendil-works/pi-durable`. A tool is installed through an extension, and by default every conversation uses every installed extension. + +- The model reads the tool's `description` and calls it with arguments that match `parameters`. Pi rejects a call whose arguments don't match. +- The model reads `content` as the result. Clients get `details` for your UI. +- A thrown error, like the 404 in `fetchOrder`, becomes an error result. The model reads the message and can try something else. +- The `context.abortSignal` aborts when the run is cancelled or the Actor stops, so a Stop from the client also cancels the request. Until the tool returns, the Actor can't sleep, and an upgrade waits for it. + +## After a crash + +If the Actor restarts while a tool runs, the agent runs that tool again on wake only if the tool sets `replay: "safe"`. The `get_order` tool only reads, so it sets it. See [Resuming a run](/agents/docs/architecture#resuming-a-run) for every case. + +Leave `replay` unset for a tool with side effects, such as one that deploys a preview. For work that must finish exactly once, launch a [task](/agents/docs/pi#tools-and-tasks) from the tool. ## Secrets @@ -45,11 +62,11 @@ The orders API token stays in your backend. The tool reads it from the worker's | --- | --- | | Your tool code | Yes, from `process.env` on the worker. | | The model | No. It sees the tool's name, description, parameters, and results. | -| The session and connected clients | No. They store and receive tool arguments and results. | +| The conversation and connected clients | No. They store and receive tool arguments and results. | | The sandbox | No. Commands run with the sandbox's own environment. | -Tool results and error messages are stored in the session and sent to every connected client, so keep secrets out of both. For model keys, see [LLM API Keys](/agents/docs/api-keys). +Tool results and error messages are stored in the conversation and sent to every watching client, so keep secrets out of both. For model keys, see [LLM API Keys](/agents/docs/api-keys). -Custom tools work without a sandbox too. This agent has one, so the model can also use the [built-in tools](/agents/docs/built-in-tools). +Custom tools need no sandbox. To also give the model file and shell tools, see [Built-in Tools](/agents/docs/built-in-tools). **Next:** [Session Lifecycle](/agents/docs/session-lifecycle), what happens while the agent answers a prompt. diff --git a/vendor/agents/docs/content/docs/design-patterns.mdx b/vendor/agents/docs/content/docs/design-patterns.mdx index b82ed19a..92639f01 100644 --- a/vendor/agents/docs/content/docs/design-patterns.mdx +++ b/vendor/agents/docs/content/docs/design-patterns.mdx @@ -49,7 +49,7 @@ The key decides which conversation a prompt continues. Pick it from what the age Keys are arrays, so a channel id or repository name from a webhook can't break the key's structure. See [Actor Keys](/actors/docs/keys). -A [`pi()`](/agents/docs/pi) agent holds one conversation, so the key is the conversation. A [`piDurable()`](/agents/docs/pi-durable) agent can hold many conversations and forks, which lets related conversations share one Actor, one sandbox, and its documents. +The `prompt` action uses the agent's root conversation, so by default the key is the conversation. An agent can also hold more conversations and forks, which lets related conversations share one Actor, one sandbox, and its documents. ## Lead Agent with Subagents @@ -83,7 +83,7 @@ A lead agent hands a focused task to a specialist with its own tools and waits f -`get-order.ts` is the `get_order` tool from [Custom Tools](/agents/docs/custom-tools). +The `get_order` tool in `get-order.ts` is the one from [Custom Tools](/agents/docs/custom-tools). See [Subagents](/agents/docs/subagents). @@ -119,7 +119,7 @@ See [Agent-to-Agent Messages](/agents/docs/agent-to-agent). ## Agent as a Workflow Step -When the work has fixed steps, waits, or retries, let a workflow drive the agent. Each step calls `abort` first, so a retried step doesn't collide with a run the previous attempt left going. +When the work has fixed steps, waits, or retries, let a workflow drive the agent. Each step prompts the agent and waits for its answer.
@@ -134,7 +134,7 @@ When the work has fixed steps, waits, or retries, let a workflow drive the agent Workflow step triage Agent - abort, prompt + prompt
@@ -180,7 +180,7 @@ See [Schedules](/agents/docs/schedules). ## Background Agents That Must Finish -When nobody is watching a run, such as an agent started by a webhook, a crash must not lose the work or repeat a side effect. Use [Pi Durable](/agents/docs/pi-durable): every model request and tool call is a durable task that continues where it stopped, and a redelivered webhook is ignored because of its `requestId`. +When nobody is watching a run, such as an agent started by a webhook, a crash must not lose the work or repeat a side effect. Every model call and tool call of a [`pi()`](/agents/docs/pi) run is saved before the next step starts, so the run continues where it stopped. Pass a `requestId`, so a redelivered webhook returns the first submission instead of starting a second run.
@@ -209,11 +209,11 @@ When nobody is watching a run, such as an agent started by a webhook, a crash mu -- `conversation.submit` returns once the input is saved, so the webhook answers right away while the agent works. -- `open_pull_request` has no `replay`, so a call cut off by a crash isn't repeated. The model is told the call was interrupted. Mark only tools that are safe to run twice as `replay: "safe"`. +- The `conversation.submit` action returns once the input is saved, so the webhook answers right away while the agent works. +- The `open_pull_request` tool has no `replay`, so a call cut off by a crash isn't repeated. The model is told the call was interrupted. Mark only tools that are safe to run twice as `replay: "safe"`. - When you ship new code, the Actor lets the run finish, for up to `sleepGracePeriod`, before it restarts on the new version. Anything still running then carries on from its last saved step. -See [Pi Durable](/agents/docs/pi-durable). +See [Architecture](/agents/docs/architecture#resuming-a-run). ## Credentials Shared per Tenant @@ -254,7 +254,7 @@ See [User Subscriptions](/agents/docs/user-subscriptions). ### One Agent for Every User -An agent runs one prompt at a time. With a single key for everyone, a prompt from one user throws while another user's prompt is running, and every user shares one session. +With a single key for everyone, every user shares one conversation and its history. A prompt from one user waits in the queue while another user's prompt runs. @@ -262,17 +262,17 @@ An agent runs one prompt at a time. With a single key for everyone, a prompt fro ### A New Key per Request -A new key creates a new agent with an empty session, so the agent forgets the conversation after every message and leaves an Actor behind. +A new key creates a new agent with an empty conversation, so the agent forgets the conversation after every message and leaves an Actor behind. **Solution:** Reuse the key of the conversation the message belongs to. -### Unattended Runs on `pi()` +### Webhooks Without a `requestId` -A `pi()` agent started by a webhook or schedule has nobody to ask it to continue. If a crash cuts off a tool call, the run doesn't resume. If the webhook is redelivered, the agent is prompted twice, and a tool with side effects can run twice. +Webhook providers deliver the same event again when your handler is slow or fails. Without a `requestId`, each delivery starts its own run, so the agent does the work twice and a tool with side effects can run twice. -**Solution:** Use [Pi Durable](/agents/docs/pi-durable) for runs nobody watches, and pass a `requestId` with every submission. See [Background Agents That Must Finish](#background-agents-that-must-finish). +**Solution:** Pass the delivery id as the `requestId` of every submission. See [Background Agents That Must Finish](#background-agents-that-must-finish). ## Where to Go Next @@ -282,7 +282,7 @@ A `pi()` agent started by a webhook or schedule has nobody to ask it to continue | Give the agent your own APIs | [Custom Tools](/agents/docs/custom-tools) | | Let the agent run commands and edit files | [Sandboxes](/agents/docs/sandboxes), then [Built-in Tools](/agents/docs/built-in-tools) | | Run agents on your users' own subscriptions | [User Subscriptions](/agents/docs/user-subscriptions) | -| Make runs finish after a crash, with no one to retry them | [Pi Durable](/agents/docs/pi-durable) | +| Make runs finish after a crash, with no one to retry them | [Architecture](/agents/docs/architecture) | | Wait for a person before a risky action | [Human in the Loop](/agents/docs/human-in-the-loop) | | Share a result through a link | [Scoped Access with JWTs](/agents/docs/scoped-access) | | Talk to users in Slack, Linear, GitHub, or Discord | [Slack](/agents/docs/connectors/slack) and the other connectors | diff --git a/vendor/agents/docs/content/docs/human-in-the-loop.mdx b/vendor/agents/docs/content/docs/human-in-the-loop.mdx index 7779e150..c5c0efbb 100644 --- a/vendor/agents/docs/content/docs/human-in-the-loop.mdx +++ b/vendor/agents/docs/content/docs/human-in-the-loop.mdx @@ -3,10 +3,10 @@ title: "Human in the Loop" description: "Make a tool wait for a person's approval before it acts." --- -A tool can refuse to act until a person approves. The check is in the tool, so the model can't skip it: `deploy` only runs after someone approves that exact deploy. +Pi has no built-in approval flow, so the check goes in the tool. A tool can refuse to act until a person approves, and the model can't skip it: `deploy` only runs after someone approves that exact deploy.
- + @@ -25,7 +25,7 @@ A tool can refuse to act until a person approves. The check is in the tool, so t approved: true or false Approval UI - consume() + create, consume() waiting prompt approve() @@ -39,9 +39,11 @@ A tool can refuse to act until a person approves. The check is in the tool, so t -- Each approval is an `approval` Actor keyed by the session and the version. Its `consume` action returns whether it was approved and resets it, so one approval allows one deploy. -- Without an approval, the tool returns `terminate: true`, which ends the turn instead of letting the model try again. The agent can sleep while it waits. -- The `ApprovalDialog` component shows the request from the `tool_execution_end` event. Approve calls `approve`, then prompts the agent, which calls `deploy` again. Passing `streamingBehavior: "followUp"` queues the answer if the agent is still running. +- The first `deploy` call creates an `approval` Actor with a random id for that version. The tool returns `control: { terminate: true }`, which ends the run instead of letting the model try again. The agent can sleep while it waits. +- The approval id is in the tool result, so the model passes it back on the next call. A made-up id fails, because no `approval` Actor has it. +- The `consume` action returns whether that version was approved and resets it, so one approval allows one deploy. +- The `ApprovalDialog` component watches the root conversation and shows the request from the `tool_execution_end` event. Approve calls `approve`, then prompts the agent, which calls `deploy` again. +- The `deploy` tool leaves `replay` unset, so a deploy cut off by a crash isn't sent twice. See [Custom Tools](/agents/docs/custom-tools#after-a-crash). - Protect the `approval` Actor with [authentication](/docs/authentication) so only approvers can call `approve`. See [React SDK](/agents/docs/react-sdk) for the rest of a chat UI. diff --git a/vendor/agents/docs/content/docs/index.mdx b/vendor/agents/docs/content/docs/index.mdx index 38c2ca29..33b5eb4b 100644 --- a/vendor/agents/docs/content/docs/index.mdx +++ b/vendor/agents/docs/content/docs/index.mdx @@ -1,9 +1,9 @@ --- title: "Agents" -description: "Run a coding agent as a durable Rivet Actor, with its tools in a separate sandbox." +description: "Run a coding agent as a durable Rivet Actor. Its files live in the Actor's database, or in a sandbox when it needs a shell." --- -Each agent on Rivet is one [Rivet Actor](/actors/docs). You define the agent once, and every key you call it with gets its own Actor, session, and sandbox. +Each agent on Rivet is one [Rivet Actor](/actors/docs). You define the agent once, and every key you call it with gets its own Actor, conversations, and files. @@ -46,15 +46,16 @@ Each agent on Rivet is one [Rivet Actor](/actors/docs). You define the agent onc - **The agent loop runs in the Actor.** Model calls, retries, and the conversation run in your backend, next to your application code, where you control credentials, permissions, and logging. -- **Tools run in a separate sandbox.** File and shell tools call into a sandbox such as E2B or Daytona. Nothing the agent runs executes on your worker, and your provider keys never enter the sandbox. -- **Agents are durable.** The Actor saves the session to its SQLite database and restores it when it wakes, so an idle agent can sleep, and a crash or upgrade does not lose the conversation. +- **Files without a sandbox.** By default, the agent's file tools work on files in the Actor's own SQLite database, so there is nothing else to set up. +- **A sandbox when it needs a shell.** Add a sandbox such as E2B or Daytona, and file and shell tools run there. Nothing the agent runs executes on your worker, and your provider keys never enter the sandbox. +- **Agents are durable.** Each agent lives in its Actor's SQLite database, not in memory, so it can sleep, crash, or upgrade mid-run and pick up where it left off. - Run your first agent, with its tools in a sandbox. + Run your first agent, with nothing else to set up. - Run the Pi coding agent as a durable Actor. + Run the Pi agent as a durable Actor. @@ -62,8 +63,8 @@ Each agent on Rivet is one [Rivet Actor](/actors/docs). You define the agent onc ### Stateful agents on any infrastructure -- **A live process per agent.** Each agent is an Actor with its own memory and SQLite database. It keeps the session loaded between prompts and runs one prompt at a time, so two servers never race on the same agent. Nothing is rebuilt from a database or replayed from an event log on each request. -- **No cloud lock-in.** Rivet is open source under Apache 2.0. Use [Rivet Cloud](https://dashboard.rivet.dev), [bring your own compute](/docs/deploy/self-host/workers), or [self-host](/docs/deploy/self-host/control-plane) everything. Workers run on Kubernetes, AWS ECS, Railway, Render, Vercel, Google Cloud Run, AWS Lambda, or any VM. +- **A live process per agent.** Each agent is an Actor with its own memory and SQLite database. It keeps its conversations loaded between prompts, so two servers never race on the same agent. Nothing is rebuilt from a database or replayed from an event log on each request. +- **No cloud lock-in.** Rivet is open source under Apache 2.0. Use Rivet Cloud, [bring your own compute](/docs/deploy/self-host/workers), or [self-host](/docs/deploy/self-host/control-plane) everything. Workers run on Kubernetes, AWS ECS, Railway, Render, Vercel, Google Cloud Run, AWS Lambda, or any VM. ### Any harness, any model @@ -72,11 +73,11 @@ Each agent on Rivet is one [Rivet Actor](/actors/docs). You define the agent onc ### Built for scale and realtime -- **Lightweight.** A running Actor adds about 72 KB of memory on top of the agent's session, and a sleeping agent uses none. Give every user, task, or thread its own agent. +- **Lightweight.** A running Actor adds about 72 KB of memory on top of the agent's conversations, and a sleeping agent uses none. Give every user, task, or thread its own agent. - **Wakes in milliseconds.** A sleeping agent wakes on its next prompt in about 12 ms, instead of the seconds it takes to boot a container. -- **Realtime and multiplayer.** Clients connect over WebSockets and stream events as the agent works. Many users can watch and steer the same session. +- **Realtime and multiplayer.** Clients connect over WebSockets and stream events as the agent works. Many users can watch and steer the same conversation. - **One primitive for your backend.** Agents are Actors, like the rest of your Rivet app, so they call other agents and Actors directly and use the same schedules, queues, and workflows. -- **Swappable sandboxes.** A sandbox crash does not lose the session, and a tool call adds about 10 ms in the same datacenter. +- **Swappable sandboxes.** A sandbox crash does not lose the conversation, and a tool call adds about 10 ms in the same datacenter. ## Core diff --git a/vendor/agents/docs/content/docs/instructions.mdx b/vendor/agents/docs/content/docs/instructions.mdx index ac749068..803ad5a5 100644 --- a/vendor/agents/docs/content/docs/instructions.mdx +++ b/vendor/agents/docs/content/docs/instructions.mdx @@ -3,40 +3,68 @@ title: "Instructions" description: "Set an agent's system prompt and project rules, and add context to a single prompt." --- -An agent's system prompt is the default prompt, which describes its tools, plus your instructions and project rules. Set them with a resource loader when you define the agent. +An agent's system prompt is built from prompt sections. Your extensions add sections that every conversation gets, and a conversation can add its own `instructions` after them. The agent renders the system prompt again before every model call.
- + - - - + + - - - + + - Default prompt - appendSystemPrompt - AGENTS.md - Agent + section() + every conversation + instructions + one conversation + Conversation system prompt Model every call
+## Sections for every conversation + +Add sections with an extension. Sections come in the order the extensions are installed. + -The resource loader's `appendSystemPrompt` adds your instructions after the default prompt. Its `agentsFilesOverride` supplies project rules as an inline `AGENTS.md`, so the agent gets the same rules wherever it runs. Call `reload()` before passing the loader. Every session of this agent gets the same instructions. +The model gets the `payments-team` sections as tagged blocks: + +```text + +You review pull requests for the payments team. Be brief and cite file paths. + + +Run `npm test` before proposing a fix. Never edit files in `migrations/`. + + +The `skills/` folder has one folder per skill. Read a skill's SKILL.md before you start a task it covers. + +``` + +The agent runs each section's function again before every model call. A section can return text that changes, such as the current date or a value from your database. + +## Instructions for one conversation + +Call `conversation.configure(id, { instructions })` to give one conversation its own instructions. They go after every extension section, and they apply from the next model call. Pass `instructions: null` to remove them. ## Context for one prompt -Put context for a single turn in the prompt text, or attach images with `prompt(text, { images })`. Each image is `{ type: "image", data, mimeType }`, with the image base64-encoded in `data`. The system prompt stays the same. +Put context for a single turn in the prompt itself. The `prompt` action takes a string, or an array of text and image blocks. Each image is `{ type: "image", data, mimeType }`, with the image base64-encoded in `data`. The system prompt stays the same. + +## AGENTS.md and skills + +Pi doesn't support `AGENTS.md` files or skills yet. Use these workarounds: + +- **Project rules:** put the rules of your `AGENTS.md` in a section, as `project-rules` does above, or in a conversation's `instructions`. +- **Skills:** put the skill files in the agent's workspace, such as the [sandbox](/agents/docs/sandboxes) or the files in the Actor's database. Then add a section that tells the model where the skills are and to read one before it acts, as `skills` does above. diff --git a/vendor/agents/docs/content/docs/observability.mdx b/vendor/agents/docs/content/docs/observability.mdx index d8371854..0286fe25 100644 --- a/vendor/agents/docs/content/docs/observability.mdx +++ b/vendor/agents/docs/content/docs/observability.mdx @@ -3,7 +3,7 @@ title: "Observability" description: "Trace every agent run with OpenTelemetry." --- -Each Pi run is traced as an `invoke_agent pi` span, a child of the `prompt` action span that started it, with [OpenTelemetry GenAI attributes](https://opentelemetry.io/docs/specs/semconv/gen-ai/). To export it, [set up OpenTelemetry for your Actors](/docs/tracing). +Each `prompt` is traced as an `invoke_agent pi` span, from the moment its input is saved until its run settles. The span is a child of the `prompt` action span and has OpenTelemetry GenAI attributes. To export it, [set up OpenTelemetry for your Actors](/docs/tracing).
@@ -31,10 +31,10 @@ Each Pi run is traced as an `invoke_agent pi` span, a child of the `prompt` acti | --- | --- | | `gen_ai.operation.name` | `invoke_agent` | | `gen_ai.agent.name` | `pi` | -| `gen_ai.conversation.id` | The Pi session id. | -| `gen_ai.request.model` | The model id. | +| `gen_ai.conversation.id` | The Pi conversation id. | +| `gen_ai.request.model` | The conversation's model, as `provider/modelId`. | | `rivet.actor.id` | The Actor's id. | -A failed run sets the span status to error, with `error.type` set to `model_error` when the model returns an error or `run_error` when the run throws. Prompt text, tool arguments, and tool results are never recorded. +A run that ends without an answer, such as after a model error, sets the span status to error with `error.type` set to `unanswered`. A `prompt` that throws sets `error.type` to `run_error`. Prompt text, tool arguments, and tool results are never recorded. -A prompt queued with `streamingBehavior` joins the run in progress, so it doesn't get a span of its own. +Only `prompt` creates this span. A run started with `conversation.submit` has no `invoke_agent pi` span, so use `prompt` for runs you want to trace. diff --git a/vendor/agents/docs/content/docs/pi-durable.mdx b/vendor/agents/docs/content/docs/pi-durable.mdx deleted file mode 100644 index 68adde5b..00000000 --- a/vendor/agents/docs/content/docs/pi-durable.mdx +++ /dev/null @@ -1,189 +0,0 @@ ---- -title: "Pi Durable" -description: "Run Pi Durable in a Rivet Actor. Each step of a run is saved to SQLite, so a run cut off by a crash carries on from the step it reached." -skill: false ---- - -`piDurable()` runs Pi Durable, Earendil's durable agent harness, in a Rivet Actor. A run cut off by a crash carries on from the step it reached. - -
- - - - - - YOUR BACKEND - - - - - Before the crash - After restart - - - - Model call - ✓ - - - - Tool call - ✓ - - - - Task phase - running - - - - Model call - ✓ - - - - Tool call - ✓ - - - - Task phase - resumes here - - - Crash - - - - - SQLite - kept through the crash - - - - - - - - saves each step - reads on restart - - -
- - -Pi Durable is in beta, and its API can change between releases. For extensions, tasks, and documents, see its README. - - -## What durable means - -A run is a series of steps: model calls, tool calls, and tasks your tools define. Each step is saved to the Actor's SQLite database before the next one begins, so after a crash the run carries on from the step it reached. - -- **Tools can launch tasks.** A task splits work into phases and saves each one before the next starts. -- **Cancelling cleans up.** Cancelling a tool call cancels the tasks it launched, and their `abort` handlers can undo the work. - -## When to use Pi Durable - -Use `piDurable()` when a run has to finish with nobody there to retry it: - -- **Background work**, such as jobs, schedules, and webhooks. -- **Multi-step side effects**, such as payments or pull requests. -- **Retrying callers.** A prompt resent to a conversation with the same `requestId` runs once. -- **Many conversations per agent**, such as forks or subagents. -- **Many clients watching one run**, including late joiners. - -For chats a person drives, use [Pi](/agents/docs/pi#when-to-use-pi). To get started, see the [Quickstart](/agents/docs/quickstart). - -## Tools and tasks - -`CodingTools` adds `read`, `write`, `edit`, and `bash`, which run in the [sandbox](/agents/docs/sandboxes). Your own tools and tasks run in your backend. For multi-step work with side effects, launch a task from the tool: - -```sh -npm add @earendil-works/pi-ai -``` - - - -After a crash, the task reruns its current phase, and the idempotency key prevents a double charge. Cancelling the run calls `abort`, which refunds it. A plain tool call cut off by a crash isn't rerun unless it's marked `replay: "safe"`. - -## Call the agent - -`prompt` sends input, waits, and returns `{ status: "done", text }` or `{ status: "unanswered", reason }`. The other actions mirror Pi Durable's methods under `harness`, `conversation`, and `submission`, with IDs in place of objects. - -| To | Call | -| --- | --- | -| Prompt and wait | `prompt(text, { requestId })` | -| Start a run without waiting | `conversation.submit(id, { type: "input", content, requestId })` | -| Steer a run | `prompt(text, { whenBusy: "steer" })` | -| Cancel a run | `conversation.abort(id)` | -| Switch models | `conversation.configure(id, { model: { provider, modelId } })` | -| Get the root conversation | `harness.root()` | -| Start a conversation | `harness.createConversation({ ownership: { kind: "ownerless" } })` | - -Actions wait up to ten minutes, and a timed-out wait leaves the run going. See the source for every action. - -## Stream a conversation - - - -`watchEvents` returns a snapshot, then sends `pi.events` to that connection only. A batch with `seq` 0 replaces the client's state, and a gap in `seq` means call `watchEvents` again. - -## Your own actions and `c.pi` - -Your own actions and lifecycle hooks get Pi Durable's `Harness` as `c.pi`. In `onSleep` and `onDestroy`, it's set only if Pi Durable is already open. Keep app state in a document, saved with the conversation: - - - -## Moving from `pi()` - -| With `pi()` | With `piDurable()` | -| --- | --- | -| `customTools: [tool]` | Pi Durable's `defineTool`, installed with `defineExtension` | -| Pi's built-in coding tools | `registry.install(CodingTools)` | -| `prompt`, then `getLastAssistantText()` | `prompt(text, { requestId })`, which returns the text | -| `prompt` throws during a run | `prompt` queues as a follow-up | -| `conn.on("event", ...)` | `conversation.watchEvents(id)`, then `conn.on("pi.events", ...)` | -| `AGENTS.md` and skills | System prompt sections you add yourself | - -Existing `pi()` conversations don't carry over, so give the `piDurable()` agent a new Actor name. - -## Upgrades and crashes - -- When you ship new code, running work gets up to `sleepGracePeriod` (15 minutes) to finish before the Actor restarts. -- Anything cut off resumes from its last saved step, after your `onWake` runs. -- Destroying the Actor stops it immediately. - -See [Architecture](/agents/docs/architecture#pi-durable). - -## Known limitations - -- **Beta API.** Install `@earendil-works/pi-durable` yourself (`^1.0.2`), and test upgrades before shipping. -- **No skills, `AGENTS.md`, or session tree.** Use system prompt sections and `conversation.fork`. -- **Plain tool calls aren't rerun after a crash.** Use a task or `replay: "safe"`. -- **Tools must honor their abort signal**, or they keep the Actor awake and delay upgrades. -- **A cut-off model request is sent again from the start.** -- **No image reads or binary writes** in the sandbox. -- **Waits cap at ten minutes.** For longer jobs, use `conversation.submit` and stream. -- **Only `prompt` is traced** in [Observability](/agents/docs/observability). -- **No `db` option.** Keep your data in `state`, documents, or another Actor. -- **No built-in approval flow** like [Human in the Loop](/agents/docs/human-in-the-loop). -- **Roll forward only.** An older Pi Durable can't open data a newer one wrote. -- **Node.js 22.19 or later.** - -## Configuration - -`piDurable()` accepts every [`actor()`](/actors/docs/actor-configuration) option except `db`, plus: - -| Option | Description | -| --- | --- | -| `registry` | Required. Your tools and tasks, from `createRegistry()`. | -| `model` | Starting model of new conversations, as `provider/modelId`. | -| `scopedModels` | Models a client may switch to. | -| `apiKeys`, `providers`, `credentials` | Model access, as for [Pi](/agents/docs/pi#configuration). | -| `sandbox` | Where `CodingTools` run. See [Sandboxes](/agents/docs/sandboxes). | -| `documents` | Documents clients may read. | -| `settings` | Pi Durable settings, such as `retry` and `compaction`. | - -Other options, such as `thinkingLevel` and `env`, pass through to Pi Durable. `ConversationBusy` reaches clients as a `UserError`, and other Pi Durable errors as internal errors. - -**Next:** [Design Patterns](/agents/docs/design-patterns), how to structure an app with many agents. diff --git a/vendor/agents/docs/content/docs/pi.mdx b/vendor/agents/docs/content/docs/pi.mdx index d4b34014..6b1a14e1 100644 --- a/vendor/agents/docs/content/docs/pi.mdx +++ b/vendor/agents/docs/content/docs/pi.mdx @@ -1,130 +1,218 @@ --- title: "Pi" -description: "Run the Pi coding agent in a Rivet Actor, with one conversation per agent." +description: "Run the Pi agent in a Rivet Actor. The agent lives in the Actor's SQLite database, so it can sleep, crash, or upgrade mid-run and pick up where it left off." skill: false --- -`@rivet-dev/pi` runs the [Pi](https://github.com/earendil-works/pi) coding agent in a Rivet Actor. The agent loop runs in the Actor, and Pi's file and shell tools run in a sandbox. +The agent runs Pi Durable, Earendil's durable agent harness, which saves every conversation, model call, tool call, and task as it happens. -Each Actor holds one Pi session and saves it in its SQLite database, so a conversation survives sleep and restarts. + +Pi Durable is still in beta, so expect its API to change between releases. +
- + - - + - - - - - + + YOUR BACKEND + + + + + Before the crash + After restart - - YOUR BACKEND - SANDBOXES + + + Model call + ✓ - - - - - - - - - + + + Tool call + ✓ - - - - - Pi Actor - Shell + files - - agent loop + SQLite + + Task phase + running - - - - - Pi Actor - Shell + files - - agent loop + SQLite + + + Model call + ✓ - - - + + + Tool call + ✓ - - - - - - - - Pi Actor - Shell + files - - agent loop + SQLite - + + + Task phase + resumes here - - Sleeping - Paused + Crash + + + + + SQLite + kept through the crash - - Tool calls - Model calls - Model provider + + + + + + saves each step + reads on restart
-## When to use Pi +## What durable means -Use `pi()` for chats a person drives, with the Pi coding agent as it is: skills, `AGENTS.md`, the session tree, and Pi's built-in tools. The [React SDK](/agents/docs/react-sdk), [Human in the Loop](/agents/docs/human-in-the-loop), and connector guides use it. +Each agent is a Rivet Actor. A run is a series of steps, such as model calls, tool calls, and task phases, and the agent saves each step to the Actor's SQLite database before the next one begins. -Use [Pi Durable](/agents/docs/pi-durable) when a run has to finish with nobody there to retry it, tools have side effects, or one agent needs many conversations. +- **One Actor per key.** Each key gets its own agent, with its own conversations, files, and sandbox. +- **Idle agents sleep.** A sleeping agent uses no memory. The next action on its key wakes it with its conversations and files. +- **Crashes and upgrades resume.** Work that was cut off carries on from its last saved step. See [Resuming a run](/agents/docs/architecture#resuming-a-run). +- **Resent prompts run once.** A client can safely resend a prompt with the same `requestId`. +- **Clients that join late catch up.** They get a snapshot of the run, then live events. To get started, see the [Quickstart](/agents/docs/quickstart). +## Without a sandbox + +An agent needs no sandbox. Without one, Pi's `read`, `write`, and `edit` tools work on files stored in the Actor's own SQLite database: + + + +- **Nothing to set up.** The files are saved with the conversation and survive sleep, crashes, and upgrades. +- **Large files are cheap.** A read of a few lines loads only those lines from the database. +- **No shell.** `bash` returns an error. To run commands, add a [sandbox](/agents/docs/sandboxes). + +## Use pi-ai + +The pi-ai package is Pi's model library, and Pi Durable is built on it. Install it next to Pi Durable: + +```sh +npm add @earendil-works/pi-ai +``` + +| To | Use from pi-ai | +| --- | --- | +| Define a tool's parameters | `Type`, which builds the schema Pi checks every call against | +| Read the messages of a conversation | `AssistantMessage`, `UserMessage`, and `ToolResultMessage` | +| Pick a thinking level for `thinkingLevel` | `ModelThinkingLevel`, from `"off"` to `"max"` | + + + + + + +Model names such as `anthropic/claude-opus-5-5` come from pi-ai's model catalog. Each assistant message records the model that wrote it and its token usage and cost. + +## Tools and tasks + +The `CodingTools` extension adds `read`, `write`, `edit`, and `bash`, which run in the [sandbox](/agents/docs/sandboxes): + + + +Your own tools and tasks run in your backend. For multi-step work with side effects, launch a task from the tool: + + + +If the Actor restarts during `comment`, the task resumes there and doesn't deploy a second preview. If the run is cancelled first, `abort` deletes the preview. To learn what happens to a plain tool call, see [Resuming a run](/agents/docs/architecture#resuming-a-run). + +## Call the agent + + + +The `prompt` action sends input, waits, and returns `{ status: "done", text }` or `{ status: "unanswered", reason }`. The other actions mirror Pi Durable's methods under `harness`, `conversation`, and `submission`, with IDs in place of objects. + +| To | Call | +| --- | --- | +| Prompt and wait | `prompt(text, { requestId })` | +| Start a run without waiting | `conversation.submit(id, { type: "input", content, requestId })` | +| Steer a run | `prompt(text, { whenBusy: "steer" })` | +| Cancel a run | `conversation.abort(id)` | +| Switch models | `conversation.configure(id, { model: { provider, modelId } })` | +| Get the root conversation | `harness.root()` | +| Start a conversation | `harness.createConversation({ ownership: { kind: "ownerless" } })` | + +Actions wait up to ten minutes, and a timed-out wait leaves the run going. A prompt sent during a run queues as a follow-up. See the source for every action. + +## Stream a conversation + + + +The `watchEvents` action returns a snapshot, then sends `pi.events` to that connection only. A short answer can arrive whole in `message_end`, with no `text_delta` before it, so print the rest of the text there. A batch with `seq` 0 replaces the client's state, and a gap in `seq` means call `watchEvents` again. + ## Your own actions and `c.pi` -Your own actions and lifecycle hooks get Pi's `AgentSession` as `c.pi`: +Your own actions and lifecycle hooks get Pi Durable's `Harness` as `c.pi`. In `onSleep` and `onDestroy`, it's set only if Pi is already open. This action reports what the agent has spent on model calls: + + + +### Keep app state in a document - +A document is typed state saved with the conversation. Change it inside `c.pi.commit`, and read it with `c.pi.snapshot`: -Settings changes such as `c.pi.setModel` are saved. In `onSleep` and `onDestroy`, `c.pi` is set only if the session is already open. + + + + + + +A fork with `fork: "current"` starts with a copy of the document. Clients can also read it by kind with the `harness.snapshot` and `harness.watchDoc` actions. + +## Upgrades and crashes + +- When you ship new code, running work gets up to `sleepGracePeriod` (15 minutes) to finish before the Actor restarts. +- Anything cut off resumes from its last saved step, after your `onWake` runs. +- A retry wait longer than a minute lets the Actor sleep, and a scheduled wake resumes it. +- Destroying the Actor stops it immediately. + +See [Architecture](/agents/docs/architecture). + +## Known limitations + +- **Skills aren't supported yet.** Put the skill files in the workspace and add a prompt section that tells the model where they are, so it reads the right one before acting. See [Instructions](/agents/docs/instructions). +- **Pi doesn't load `AGENTS.md` files.** Set `instructions` or add a prompt section. See [Instructions](/agents/docs/instructions). +- **No shell without a sandbox.** `bash` needs a [sandbox](/agents/docs/sandboxes). +- **Plain tool calls aren't rerun after a crash.** Use a task or `replay: "safe"`. +- **A tool that ignores its abort signal blocks sleep.** An upgrade then waits up to `sleepGracePeriod` for it to finish. +- **A cut-off model request is sent again from the start.** +- **No image reads, and no binary writes** in a sandbox. +- **Waits cap at ten minutes.** For longer jobs, use `conversation.submit` and stream. +- **Only `prompt` is traced** in [Observability](/agents/docs/observability). +- **No `db` option.** Keep your data in `state`, documents, or another Actor. +- **No built-in approval flow.** See [Human in the Loop](/agents/docs/human-in-the-loop). +- **Roll forward only.** An older Pi Durable can't open data a newer one wrote. +- **Node.js 22.19 or later.** ## Configuration -`pi()` accepts every [`actor()`](/actors/docs/actor-configuration) option except `db`, Pi's [session options](https://github.com/rivet-dev/agents/blob/main/packages/pi/src/runtime.ts) such as `thinkingLevel` and `customTools`, and: +The `pi()` function accepts every [`actor()`](/actors/docs/actor-configuration) option except `db`, plus: | Option | Description | | --- | --- | -| `model` | Model for a new session, as `provider/modelId`. | -| `scopedModels` | Models a client may switch to with `setModel`. | -| `apiKeys` | API keys by provider. Defaults to the environment. | +| `registry` | Required. Your tools and tasks, from `createRegistry()`. | +| `model` | Starting model of new conversations, as `provider/modelId`. | +| `scopedModels` | Models a client may switch to. | +| `apiKeys` | API keys by provider. Defaults to the environment. See [LLM API Keys](/agents/docs/api-keys). | | `providers` | Custom providers, in the shape of Pi's `models.json`. | | `credentials` | Logins your app stores. See [User Subscriptions](/agents/docs/user-subscriptions). | -| `sandbox` | Where Pi's tools run. See [Sandboxes](/agents/docs/sandboxes). | -| `settings` | Pi settings for a new session. | - -`actionTimeout` defaults to ten minutes and `sleepGracePeriod` to 15 minutes. +| `sandbox` | Where `CodingTools` run. Without one, files live in the Actor's database. See [Sandboxes](/agents/docs/sandboxes). | +| `documents` | Documents clients may read. | +| `settings` | Pi Durable settings, such as `retry` and `compaction`. | -## Known limitations - -- **One session per Actor.** Use one agent key per chat. -- **One run at a time.** `prompt` throws during a run unless you pass `streamingBehavior`. -- **Retries run again.** A retried `prompt` prompts the agent twice. -- **Partial resume.** A cut-off run resumes only if Pi can continue the transcript, and a cut-off tool call isn't repeated. See [Upgrades](/agents/docs/architecture#upgrades). -- **Long retry waits keep the Actor awake.** -- **Replaced sandboxes.** Absolute paths in earlier `bash` commands point to the old sandbox until the session reopens. -- **Pinned Pi version.** Upgrade `pi-coding-agent` by upgrading `@rivet-dev/pi`. -- **Node.js 22.19 or later.** +Other options, such as `thinkingLevel` and `env`, pass through to Pi Durable. By default, `actionTimeout` is ten minutes and `sleepGracePeriod` is 15 minutes. A `ConversationBusy` error reaches clients as a `UserError`, and other Pi Durable errors as internal errors. **Next:** [Design Patterns](/agents/docs/design-patterns), how to structure an app with many agents. diff --git a/vendor/agents/docs/content/docs/quickstart.mdx b/vendor/agents/docs/content/docs/quickstart.mdx index 58050f6e..83c77d13 100644 --- a/vendor/agents/docs/content/docs/quickstart.mdx +++ b/vendor/agents/docs/content/docs/quickstart.mdx @@ -1,14 +1,14 @@ --- title: "Quickstart" -description: "Run your first agent in a Rivet Actor, with Pi or Pi Durable, and its tools in a sandbox." +description: "Run your first Pi agent in a Rivet Actor. Its files live in the Actor's database, so there is nothing else to set up." --- import { Hosting } from "@/components/docs/Hosting"; -An agent is a Rivet Actor in your backend. Its file and shell tools run in a sandbox, and clients prompt it like any other Actor. +An agent is a Rivet Actor in your backend. Clients prompt it like any other Actor. Each step of a run is saved to the Actor's SQLite database, so a run survives sleep, crashes, and upgrades.
- + @@ -25,7 +25,8 @@ An agent is a Rivet Actor in your backend. Its file and shell tools run in a san client.ts Agent Actor server.ts - Sandbox + Sandbox + optional Model provider prompt tool calls @@ -33,40 +34,23 @@ An agent is a Rivet Actor in your backend. Its file and shell tools run in a san
-## Choose a harness - -Both harnesses keep conversations through sleep, crashes, and upgrades. They differ in what they run and how much of a cut-off run they recover: - -- **Pi** (`pi()`) runs the Pi coding agent, with skills, `AGENTS.md`, and one conversation per agent. After a crash, it resumes a run when Pi can continue the transcript. Use it for chats a person drives. -- **Pi Durable** (`piDurable()`, beta) saves each step of a run, including tool calls and tasks your tools launch, and carries a cut-off run on from the last saved step. Use it for runs nobody watches, such as background jobs, and for tools with side effects. - -Not sure? Start with Pi. See [When to use Pi Durable](/agents/docs/pi-durable#when-to-use-pi-durable). - - - - ```sh -npm add @rivet-dev/pi @rivet-dev/sandbox-adapter rivetkit e2b +npm add @rivet-dev/pi @earendil-works/pi-durable rivetkit ``` -For Daytona, replace `e2b` with `@daytonaio/sdk`. - - - - - + Set `ANTHROPIC_API_KEY`. For another model provider, change `model` and set that provider's key, such as `OPENAI_API_KEY`. See [LLM API Keys](/agents/docs/api-keys). -Pi's file and shell tools run in the sandbox. Set `E2B_API_KEY` for [E2B](https://e2b.dev) or `DAYTONA_API_KEY` for [Daytona](https://daytona.io). To write your own provider, see [Sandboxes](/agents/docs/sandboxes). +The agent needs no sandbox. Its `read`, `write`, and `edit` tools work on files stored in the Actor's own database. @@ -86,11 +70,7 @@ AGENT="rvt-namespace=default&rvt-method=getOrCreate&rvt-key=user-123&rvt-pool=de curl -X POST "$URL/prompt?$AGENT" \ -H "Content-Type: application/json" \ - -d '{"args": ["Write a Python script that prints the first 20 prime numbers, run it, and show me the output."]}' - -curl -X POST "$URL/getLastAssistantText?$AGENT" \ - -H "Content-Type: application/json" \ - -d '{"args": []}' + -d '{"args": ["Write a hello-world Rivet Actor to counter.ts, then read the file back to me.", {"requestId": "counter-1"}]}' ``` Each action is a `POST` to the gateway with its arguments in `args`. See [Call Action](/docs/api/gateway/call-action). @@ -98,75 +78,31 @@ Each action is a `POST` to the gateway with its arguments in `args`. See [Call A -The `prompt` action waits for Pi to finish, then `getLastAssistantText` reads the full reply. +The `prompt` action waits for the run to finish and returns `{ status, text }`. A model error doesn't reject it: `status` is `unanswered`, with Pi's reason. Sending the same `requestId` again returns the first answer instead of running twice. - - - - - - + - +To let the agent run commands, give it a sandbox and Pi's `CodingTools`, which add `bash`: ```sh -npm add @rivet-dev/pi @rivet-dev/sandbox-adapter @earendil-works/pi-durable@latest rivetkit e2b +npm add @rivet-dev/sandbox-adapter e2b ``` -For Daytona, replace `e2b` with `@daytonaio/sdk`. - - - - - - - + + -Set `ANTHROPIC_API_KEY`. For another model provider, change `model` and set that provider's key, such as `OPENAI_API_KEY`. See [LLM API Keys](/agents/docs/api-keys). - -`CodingTools` gives the agent `read`, `write`, `edit`, and `bash`, and they run in the sandbox. Set `E2B_API_KEY` for [E2B](https://e2b.dev) or `DAYTONA_API_KEY` for [Daytona](https://daytona.io). To write your own provider, see [Sandboxes](/agents/docs/sandboxes). - - - - - - - - - - - - - -```sh -URL="http://localhost:6420/gateway/agent/action" -AGENT="rvt-namespace=default&rvt-method=getOrCreate&rvt-key=user-123&rvt-pool=default" - -curl -X POST "$URL/prompt?$AGENT" \ - -H "Content-Type: application/json" \ - -d '{"args": ["Write a Python script that prints the first 20 prime numbers, run it, and show me the output.", {"requestId": "primes-1"}]}' -``` - -Each action is a `POST` to the gateway with its arguments in `args`. See [Call Action](/docs/api/gateway/call-action). - - - - -The `prompt` action waits for the run to finish and returns `{ status, text }`. A model error doesn't reject it: `status` is `unanswered`, with Pi's reason. Sending the same `requestId` to the same conversation again returns the first answer. +Set `E2B_API_KEY` for E2B, or install `@daytonaio/sdk` and set `DAYTONA_API_KEY` for Daytona. See [Sandboxes](/agents/docs/sandboxes). - - - ## Deploy -**Next:** the page for the harness you chose, [Pi](/agents/docs/pi) or [Pi Durable](/agents/docs/pi-durable), for how it handles crashes and upgrades, and its known limitations. +**Next:** [Pi](/agents/docs/pi), for how a run survives crashes and upgrades, and its known limitations. diff --git a/vendor/agents/docs/content/docs/react-sdk.mdx b/vendor/agents/docs/content/docs/react-sdk.mdx index 620a1e22..7e287109 100644 --- a/vendor/agents/docs/content/docs/react-sdk.mdx +++ b/vendor/agents/docs/content/docs/react-sdk.mdx @@ -3,10 +3,10 @@ title: "React SDK" description: "Build a chat UI for an agent with React." --- -Build a chat UI for an agent with `@rivetkit/react`. The agent logic lives in one hook, and the UI in one component. +The chat UI uses the `@rivetkit/react` package. The agent logic lives in one hook, and the UI in one component.
- + @@ -22,7 +22,7 @@ Build a chat UI for an agent with `@rivetkit/react`. The agent logic lives in on useAgentChat Agent Actor state - prompt, abort + submit, abort events
@@ -32,14 +32,16 @@ Build a chat UI for an agent with `@rivetkit/react`. The agent logic lives in on -- The `useActor` hook connects to the agent by name and key. Once the connection opens, the hook loads the conversation with `getMessages`. +- The `useActor` hook connects to the agent by name and key. Once the connection opens, the hook calls `conversation.watchEvents` on the root conversation. The returned snapshot has the history and any answer in progress, and from then on the connection receives `pi.events`. - The `message_update` event streams the reply as text deltas, and `message_end` adds each finished message, including your own prompt. See [Session Lifecycle](/agents/docs/session-lifecycle#events) for every event. -- The `agent_settled` event marks the agent idle once it has finished, after any retry. The `Chat` component switches its button from Send to Stop while the agent works. +- Each batch has a `seq` number. If one is missing, the hook watches again and starts from a new snapshot. +- The `send` function calls `conversation.submit`, which starts a run without waiting for it. A message sent during a run waits as a follow-up. +- The `run_start` and `run_end` events mark when the agent works. The `Chat` component switches its button from Send to Stop while it does, and Stop calls `conversation.abort`. ## Next steps -- To steer a run in progress, see [Session Lifecycle](/agents/docs/session-lifecycle#steer-follow-up-or-cancel). To handle errors, see [Client SDK](/agents/docs/client-sdk#results-and-errors). +- To steer a run in progress, see [Session Lifecycle](/agents/docs/session-lifecycle). To handle errors, see [Client SDK](/agents/docs/client-sdk#results-and-errors). - To send a login token, add `params` to `useActor`. -- To switch models from the UI, call `setModel` on the connection with one of the agent's `scopedModels`. +- To switch models from the UI, call `conversation.configure(id, { model: { provider, modelId } })` on the connection with one of the agent's `scopedModels`. See [React](/actors/docs/clients/react) in the Actors docs for everything `@rivetkit/react` can do. diff --git a/vendor/agents/docs/content/docs/sandboxes.mdx b/vendor/agents/docs/content/docs/sandboxes.mdx index 3a5a8d11..e6ca7415 100644 --- a/vendor/agents/docs/content/docs/sandboxes.mdx +++ b/vendor/agents/docs/content/docs/sandboxes.mdx @@ -1,36 +1,57 @@ --- title: "Sandboxes" -description: "Run an agent's file and shell tools in E2B, Daytona, or a sandbox provider you write." +description: "Run an agent without a sandbox, or give it a shell in E2B, Daytona, agentOS, or a sandbox provider you write." --- -An agent's file and shell tools run in a sandbox, not on your worker. [`@rivet-dev/sandbox-adapter`](https://github.com/rivet-dev/agents/tree/main/packages/sandbox-adapter) gives each Actor its own sandbox. +An agent doesn't need a sandbox. Without one, its file tools work on files in the Actor's own database. Add a sandbox only when the agent needs a shell. + +| | Files | Shell | Setup | +| --- | --- | --- | --- | +| **No sandbox** | In the Actor's SQLite database | None. `bash` returns an error. | None | +| **Sandbox** | In the sandbox | `bash` runs in the sandbox | A provider account or agentOS | + +## Without a sandbox + +Leave out the `sandbox` option. Pi's `read`, `write`, and `edit` tools then work on files stored in the Actor's own SQLite database: + + + +- **Nothing to provision.** There is no sandbox to create, pay for, pause, or delete. +- **Files are durable.** They are saved with the conversation and survive sleep, crashes, and upgrades. +- **No shell.** `bash` returns an error, so this agent installs only the file tools. + +See [Built-in Tools](/agents/docs/built-in-tools) for what each tool does. + +## With a sandbox + +When the agent needs to run commands, such as tests or a build, pass a sandbox provider. `@rivet-dev/sandbox-adapter` gives each Actor its own sandbox, and `CodingTools` run there.
- + - + SANDBOX PROVIDERS - - + + - + + - - + + - Agent Actor - Sandbox adapter - SandboxProvider + Agent Actor + Sandbox adapter + SandboxProvider E2B - Daytona - tool calls + Daytona + agentOS + tool calls
-## Quickstart - @@ -49,21 +70,20 @@ npm add @rivet-dev/sandbox-adapter e2b -Without a sandbox, Pi's built-in file and shell tools are turned off and the agent only has the `customTools` you pass. [Pi Durable](/agents/docs/pi-durable) takes the same providers as `sandbox`, and runs its `CodingTools` there. - ## Providers | Provider | SDK | While the Actor sleeps | When the Actor is destroyed | | --- | --- | --- | --- | -| [E2B](https://e2b.dev) | `e2b` | Paused | Deleted | -| [Daytona](https://daytona.io) | `@daytonaio/sdk` | Stopped | Deleted | +| E2B | `e2b` | Paused | Deleted | +| Daytona | `@daytonaio/sdk` | Stopped | Deleted | +| agentOS | None | Sleeps when idle, and keeps its files | Left in place | Provider credentials stay on your worker and never enter the sandbox. ### E2B ```sh -npm add e2b +npm add @rivet-dev/sandbox-adapter e2b ``` @@ -79,7 +99,7 @@ E2B ends a sandbox when its timeout runs out, so the provider creates sandboxes ### Daytona ```sh -npm add @daytonaio/sdk +npm add @rivet-dev/sandbox-adapter @daytonaio/sdk ``` @@ -91,26 +111,42 @@ npm add @daytonaio/sdk The working directory is the sandbox's own working directory. A stopped sandbox is started again the next time the Actor uses it. +### agentOS + +```sh +npm add @rivet-dev/sandbox-adapter +``` + + + +| Option | Description | +| --- | --- | +| `cwd` | Working directory inside the VM. Defaults to `/workspace`. | +| `software` | Packages installed when the VM is created, such as coreutils for `sh`. | + +The VM is an agentOS Actor in the `services` pool, with the key `["sandbox", ]`. It keeps its files in its own SQLite database, so they persist while the agent sleeps. The VM sleeps on its own when idle. Destroying the agent leaves the VM and its files in place. + ## How the sandbox is managed - The sandbox is created the first time the agent needs it. -- After a wake, the Actor reconnects to the same sandbox the first time a tool or `executeBash` needs it. If it no longer exists, a new one is created and the old files are lost. +- After a wake, the Actor reconnects to the same sandbox the first time a tool needs it. If it no longer exists, a new one is created and the old files are lost. - Actions that don't touch the sandbox keep working while the provider is down. - An existing Actor cannot switch to a different provider. - Commands run with the sandbox's environment, not your worker's. File tools reject paths outside the working directory, but shell commands can reach anything inside the sandbox. +- Binary file writes aren't supported yet. ## Write your own provider -Any sandbox can back an agent. Implement [`SandboxProvider`](https://github.com/rivet-dev/agents/blob/main/packages/sandbox-adapter/src/index.ts) and pass it as `sandbox`. One provider object is shared by every Actor of a definition, so keep no per-Actor state in it; the Actor stores the sandbox id. +Any sandbox can back an agent. Implement `SandboxProvider` and pass it as `sandbox`. One provider object is shared by every Actor of a definition, so keep no per-Actor state in it. The Actor stores the sandbox id. | Member | Description | | --- | --- | | `name` | Stored next to the sandbox id, so a changed provider is detected on wake. | | `create(c)` | Provisions a new sandbox and returns its id. | -| `connect(c, id)` | Connects to an existing sandbox and returns a `Sandbox`. Returns `undefined` when the sandbox no longer exists. | -| `suspend(c, id)` | Optional. Releases resources while the Actor sleeps. When omitted, the sandbox stops itself when idle or keeps running, depending on the provider. | -| `destroy(c, id)` | Optional. Deletes the sandbox when the Actor is destroyed. When omitted, the sandbox is left in place. | +| `connect(c, id)` | Connects to an existing sandbox and returns a `Sandbox`. Returns `undefined` when the sandbox no longer exists. | +| `suspend(c, id)` | Optional. Releases resources while the Actor sleeps. When omitted, the sandbox stops itself when idle or keeps running, depending on the provider. | +| `destroy(c, id)` | Optional. Deletes the sandbox when the Actor is destroyed. When omitted, the sandbox is left in place. | -The `Sandbox` that `connect` returns has a `cwd` and the operations the tools run: `exec`, `readFile`, `writeFile`, `mkdir`, `stat`, `readdir`, and `exists`. All paths are absolute paths inside the sandbox. See the [E2B provider](https://github.com/rivet-dev/agents/blob/main/packages/sandbox-adapter/src/e2b.ts) for a complete example. +The context `c` carries the owning Actor's `actorId` and a `client()` for your registry. The `Sandbox` that `connect` returns has a `cwd` and the operations the tools run: `exec`, `readFile`, `writeFile`, `mkdir`, `stat`, `readdir`, and `exists`. All paths are absolute paths inside the sandbox. See the E2B provider for a complete example. **Next:** [Custom Tools](/agents/docs/custom-tools), tools that run in your backend. diff --git a/vendor/agents/docs/content/docs/schedules.mdx b/vendor/agents/docs/content/docs/schedules.mdx index b1b264fc..6a54f1d4 100644 --- a/vendor/agents/docs/content/docs/schedules.mdx +++ b/vendor/agents/docs/content/docs/schedules.mdx @@ -34,6 +34,7 @@ Actor schedules wake an agent to run a prompt on a timer. - The cron calls the agent's `prompt` action every day at 9:00 UTC. The `onCreate` hook sets it once, when the agent is first created. - For a single run later, use `c.schedule.after(ms, "prompt", text)` or `c.schedule.at(time, "prompt", text)`. +- Every run continues the same conversation. The client reads the latest summary from the conversation with `conversation.context`. - The agent sleeps between runs, and its sandbox is suspended while it sleeps. See [Schedules](/actors/docs/schedule) for cron expressions, time zones, and history. diff --git a/vendor/agents/docs/content/docs/security-model.mdx b/vendor/agents/docs/content/docs/security-model.mdx index 14dddeb1..ac5a538e 100644 --- a/vendor/agents/docs/content/docs/security-model.mdx +++ b/vendor/agents/docs/content/docs/security-model.mdx @@ -4,7 +4,7 @@ description: "What reaches the sandbox, where secrets live, and who can connect ---
- + @@ -40,7 +40,7 @@ description: "What reaches the sandbox, where secrets live, and who can connect authenticated agent loop + keys - session + conversations own environment @@ -55,20 +55,20 @@ The agent loop runs in an Actor on your backend, and the commands the model choo ## What reaches the sandbox -- Only tool calls: commands to run, and files to read or write. Each agent has its own sandbox. +- Only calls of the built-in tools: commands to run, and files to read or write. Each agent has its own sandbox. Your [custom tools](/agents/docs/custom-tools) run on your worker. - Not your worker's environment. Commands run with the sandbox's own environment. - File tools reject paths outside the working directory. Shell commands can reach anything inside the sandbox, so the sandbox itself is the boundary. -- Without a sandbox, the agent has no file or shell tools. They never fall back to running on your worker. +- Without a sandbox, files live in the Actor's own SQLite database and there is no shell, so `bash` returns an error. Commands never fall back to running on your worker. ## Where secrets live - Model keys, sandbox provider keys, and the secrets your [custom tools](/agents/docs/custom-tools) use stay on your worker. The model and the sandbox never see them. - [User subscription](/agents/docs/user-subscriptions) logins stay in your `credentials` Actor. The agent never receives a refresh token. -- The session stores every message and tool result, and connected clients receive them, so keep secrets out of tool results and error messages. +- A conversation stores every message and tool result, and clients can read them with `conversation.context` or watch them as events, so keep secrets out of tool results and error messages. - [Traces](/agents/docs/observability) never record prompt text, tool arguments, or tool results. ## Who can connect -- Anyone who can connect to an agent can prompt it and read its whole session. Add [authentication](/docs/authentication) and check that the caller may use the agent's key. Keys aren't secret. +- Anyone who can connect to an agent can prompt it and read every one of its conversations. Add [authentication](/docs/authentication) and check that the caller may use the agent's key. Keys aren't secret. - Protect the `credentials` Actor the same way, because its actions return provider tokens. - Give a browser a short-lived token for one agent instead of your Rivet credentials. See [Client SDK](/agents/docs/client-sdk#browsers). diff --git a/vendor/agents/docs/content/docs/session-lifecycle.mdx b/vendor/agents/docs/content/docs/session-lifecycle.mdx index b2f186c9..7f0e2a72 100644 --- a/vendor/agents/docs/content/docs/session-lifecycle.mdx +++ b/vendor/agents/docs/content/docs/session-lifecycle.mdx @@ -1,18 +1,18 @@ --- title: "Session Lifecycle" -description: "What a session is, the events one prompt produces, and how to steer or cancel a run." +description: "What a conversation is, the events one prompt produces, and how to steer or cancel a run." --- -A session is one agent's conversation. Each agent key has exactly one session, and the session: +A conversation is one thread of messages inside an agent. Each agent has a root conversation, which `prompt` uses by default, and can hold more conversations. A conversation: - Keeps the full history of messages and tool calls. -- Runs one prompt at a time, calling the model and tools until the model is done. -- Streams events to every connected client while it runs. +- Handles one run at a time, calling the model and tools until the model is done. Prompts that arrive during a run wait in a queue. +- Streams events to every client that watches it. - Summarizes older messages when the context fills up. - Survives sleep, crashes, and upgrades. See [Architecture](/agents/docs/architecture).
- + @@ -38,43 +38,44 @@ A session is one agent's conversation. Each agent key has exactly one session, a
-This client connects to the [Quickstart](/agents/docs/quickstart) agent, sends one prompt, and prints the events it receives: +This client connects to the [Quickstart](/agents/docs/quickstart) agent, watches the root conversation, sends one prompt, and prints the events it receives: ## Events -Every event arrives as `event`, with a `type`: +The `conversation.watchEvents(id)` action returns a snapshot of the conversation, including a partial answer. Then the agent sends `pi.events` to that connection only. Each frame has `conversationId`, `seq`, and an `events` array. A frame with `seq` 0 replaces everything the client had, and a gap in `seq` means call `watchEvents` again. -| Event Type | When it fires | +| Event type | When it fires | | --- | --- | -| `agent_start`, `agent_end` | A run starts or ends. Then `agent_settled` follows once the agent is fully idle, after any retry. | -| `turn_start`, `turn_end` | A model response starts, or it and its tool calls are done. The `turn_end` event has the response's `usage`: tokens and cost. | -| `message_update` | The model streams output. Its `assistantMessageEvent.type` is `text_delta`, `thinking_delta`, or a tool call update. | -| `message_end` | A message is complete: your prompt, a model reply, or a tool result. | -| `tool_execution_start`, `tool_execution_end` | A tool starts or finishes. On a failure, `isError` is true. | +| `run_start`, `run_end` | A run starts or ends. | +| `turn_start`, `turn_end` | A model response starts, or it and its tool calls are done. | +| `message_start`, `message_end` | A message starts or is complete: your prompt, a model reply, or a tool result. | +| `message_update` | The model streams output. Each item in `changes` has a `type`, such as `text_delta`, `thinking_delta`, or `toolcall_delta`. | +| `tool_execution_start`, `tool_execution_end` | A tool starts or finishes. | | `auto_retry_start`, `auto_retry_end` | A failed model call is retried. | | `compaction_start`, `compaction_end` | Older messages are summarized to free up context. | +| `usage_changed` | The conversation's token and cost totals change. | -A client that connects mid-run only receives events from that point on, so read earlier messages with `getMessages`. To stop listening, call the function that `on` returns. +A short answer can arrive whole in `message_end`, with no `text_delta` before it, so print the rest of the text there. Events are not replayed. To read earlier messages, call `conversation.context(id)`, which returns `{ messages }`. To stop listening, call `conversation.unwatchEvents(id)`. ## Steer, follow up, or cancel -While a run is in progress: +While a run is in progress, `prompt(text, { whenBusy })` decides what happens to the new prompt: -| Action | What it does | +| `whenBusy` | What it does | | --- | --- | -| `steer(text)` | The agent reads the message after its current tool calls finish, before its next model call. | -| `followUp(text)` | The agent reads the message once it has no tool calls left, and the run continues. On an idle agent it only queues. | -| `abort()` | Cancels the run and stops any command still running in the sandbox. Daytona can't cancel a command, so it runs until it exits or times out. | +| `"followUp"` | The default. The agent reads the message once it has no tool calls left, and the run continues. | +| `"steer"` | The agent reads the message after its current tool calls finish, before its next model call. | +| `"reject"` | The prompt fails with an error, and nothing is queued. | -Calling `prompt` while a run is in progress throws. To queue it instead, pass `streamingBehavior: "steer"` or `"followUp"`. On an idle agent, the prompt runs right away. +On an idle conversation, the prompt runs right away. To cancel a run, call `conversation.abort(id)`. It stops any command still running in the sandbox. Daytona can't cancel a command, so it runs until it exits or times out. -Only `abort` stops a run. If `prompt` times out or its client disconnects, `prompt` rejects, and the run keeps going until it finishes. +Only `abort` stops a run. If `prompt` times out or its client disconnects, `prompt` rejects, and the run keeps going until it finishes. Send the prompt again with the same `requestId` to get its result. ## Compaction and errors -- When the context fills up, the agent summarizes older messages and keeps going. Call `compact` to do it yourself. It cancels the run in progress first. -- A failed model call is retried automatically. If it keeps failing, the run ends with an assistant message whose `stopReason` is `"error"`, and the session is kept. +- When the context fills up, the agent summarizes older messages and keeps going. Call `conversation.compact(id)` to do it yourself. The conversation keeps working while the summary is written. +- A failed model call is retried automatically. If it keeps failing, `prompt` returns `{ status: "unanswered", reason }`, and the conversation is kept. **Next:** [Client SDK](/agents/docs/client-sdk), how to call an agent from your backend or a browser. diff --git a/vendor/agents/docs/content/docs/subagents.mdx b/vendor/agents/docs/content/docs/subagents.mdx index d9dbfb14..a5677d32 100644 --- a/vendor/agents/docs/content/docs/subagents.mdx +++ b/vendor/agents/docs/content/docs/subagents.mdx @@ -33,9 +33,11 @@ A subagent is a specialist that a lead agent hands one focused task to. It has i -`get-order.ts` is the `get_order` tool from [Custom Tools](/agents/docs/custom-tools). +The `get_order` tool in `get-order.ts` is the one from [Custom Tools](/agents/docs/custom-tools). - The model picks the specialist and writes the question. The question has to carry everything the specialist needs, because the specialist can't see the lead's conversation. - Each specialist has only the tools for its job: `orders` has `get_order`, and `engineering` has a sandbox to read and run code. The support agent has neither. -- Specialists are keyed by the lead's session id, so a follow-up question on the same ticket continues the same specialist session. +- Each question goes to a new specialist with an empty conversation. The tool keys it by an id that `api.memo` saves with the tool call. +- The tool has `replay: "safe"`, so Pi runs it again after a crash. The rerun gets the same id and passes it as the `requestId`, so it waits for the run already in progress instead of asking again. +- If the lead's run is cancelled, the tool cancels the specialist's run too. - The tool waits for the answer and returns it as the tool result. To hand work off without waiting, see [Agent-to-Agent Messages](/agents/docs/agent-to-agent). diff --git a/vendor/agents/docs/content/docs/user-subscriptions.mdx b/vendor/agents/docs/content/docs/user-subscriptions.mdx index b23a0b69..f63c5eb2 100644 --- a/vendor/agents/docs/content/docs/user-subscriptions.mdx +++ b/vendor/agents/docs/content/docs/user-subscriptions.mdx @@ -3,7 +3,7 @@ title: "User Subscriptions" description: "Let your users run agents on their own Claude or ChatGPT subscription." --- -Your users can run the agent on their own Claude or ChatGPT subscription, or their own API key, instead of yours. +They can also bring their own API key. Your app saves each user's login in a credentials Actor, and the agent reads it before every model call.
@@ -43,9 +43,9 @@ Store each user's logins in a `credentials` Actor and pass it to `pi({ credentia -The agent Actor never receives a refresh token and never writes credentials back. Errors from the `credentials` Actor reject the model call that needed the credential, so keep secrets out of their messages. +The agent asks the `credentials` Actor on every model call, so a login or logout applies to the next model call, also in a run that is already going. The agent Actor never receives a refresh token and never writes credentials back. Errors from the `credentials` Actor reject the model call that needed the credential, so keep secrets out of their messages. -Your app needs its own login flow, such as a settings page, that saves the result with the `credentials` Actor's `save` action. The files above are in [`examples/docs/user-subscriptions`](https://github.com/rivet-dev/agents/tree/main/examples/docs/user-subscriptions). +Your app needs its own login flow, such as a settings page, that saves the result with the `credentials` Actor's `save` action. The files above are in `examples/docs/user-subscriptions`. ## Share credentials across a team diff --git a/vendor/agents/docs/content/docs/workflows.mdx b/vendor/agents/docs/content/docs/workflows.mdx index 7b1d9180..907f87e4 100644 --- a/vendor/agents/docs/content/docs/workflows.mdx +++ b/vendor/agents/docs/content/docs/workflows.mdx @@ -6,7 +6,7 @@ description: "Run agents as steps in durable, multi-step workflows." Run an agent as a workflow step to get retries and durable waits between steps.
- + @@ -31,8 +31,8 @@ Run an agent as a workflow step to get retries and durable waits between steps. second-run step Fixer agent - abort, prompt - abort, prompt + prompt + prompt
@@ -46,7 +46,7 @@ npm add @rivet-dev/workflows - Steps time out after 30 seconds by default, so `timeout` gives each prompt up to ten minutes. -- A step that throws is retried up to three times, while a timeout fails the step right away. Each attempt calls `abort` first, so a retry stops any run the previous attempt left going before prompting again. +- A step that throws is retried up to three times, while a timeout fails the step right away. Every attempt of a step sends the same `requestId`, so a retry waits for the run that the first attempt started instead of prompting again. - The `ctx.sleep` call waits ten minutes with nothing running. The workflow resumes on time, even after a crash or upgrade. - Both steps use the same agent key, so the second prompt continues the first conversation. - Use a workflow when the work has fixed steps, waits, or retries. Use a single agent session when the agent should decide the steps itself. diff --git a/vendor/agents/docs/sidebar.json b/vendor/agents/docs/sidebar.json index 759205e6..76054f1c 100644 --- a/vendor/agents/docs/sidebar.json +++ b/vendor/agents/docs/sidebar.json @@ -18,18 +18,9 @@ { "title": "Agents", "pages": [ - { - "title": "Pi", - "href": "/agents/docs/pi", - "badge": "Beta", - "icon": { - "src": "/images/vendors/pi.svg" - } - }, { "title": "Pi Durable", - "href": "/agents/docs/pi-durable", - "badge": "Beta", + "href": "/agents/docs/pi", "icon": { "src": "/images/vendors/pi.svg" } diff --git a/vendor/agents/examples/docs/agent-to-agent/client.ts b/vendor/agents/examples/docs/agent-to-agent/client.ts index 4691c286..c319e7f7 100644 --- a/vendor/agents/examples/docs/agent-to-agent/client.ts +++ b/vendor/agents/examples/docs/agent-to-agent/client.ts @@ -4,7 +4,7 @@ import type { registry } from "./server"; const client = createClient(); const coder = client.coder.getOrCreate(["acme/app", "fix-flaky-checkout-test"]); -await coder.prompt( +const result = await coder.prompt( "Clone https://github.com/acme/app, fix the flaky checkout test on a new fix-flaky-checkout-test branch, push it, and request a review.", ); -console.log(await coder.getLastAssistantText()); +console.log(result.status === "done" ? result.text : `Unanswered: ${result.reason}`); diff --git a/vendor/agents/examples/docs/agent-to-agent/coder.ts b/vendor/agents/examples/docs/agent-to-agent/coder.ts index 8cbf4eee..7ac1dbe2 100644 --- a/vendor/agents/examples/docs/agent-to-agent/coder.ts +++ b/vendor/agents/examples/docs/agent-to-agent/coder.ts @@ -1,5 +1,6 @@ import { Type } from "@earendil-works/pi-ai"; -import { defineTool } from "@earendil-works/pi-coding-agent"; +import { createRegistry, defineExtension, defineTool } from "@earendil-works/pi-durable"; +import { CodingTools } from "@earendil-works/pi-durable/tools"; import { pi } from "@rivet-dev/pi"; import { e2bProvider } from "@rivet-dev/sandbox-adapter/e2b"; import { createClient } from "rivetkit/client"; @@ -7,19 +8,22 @@ import type { registry } from "./server"; const requestReview = defineTool({ name: "request_review", - label: "Request review", description: "Send a finished branch to the repository's reviewer. Don't wait for the review.", parameters: Type.Object({ repo: Type.String(), branch: Type.String(), summary: Type.String() }), - async execute(_toolCallId, { repo, branch, summary }) { + execute: async ({ repo, branch, summary }) => { await client.reviewer.getOrCreate([repo]).requestReview(`Review the ${branch} branch of https://github.com/${repo}: ${summary}`); - return { content: [{ type: "text", text: `Sent ${branch} for review.` }], details: undefined }; + return { content: [{ type: "text", text: `Sent ${branch} for review.` }] }; }, }); +const extensions = createRegistry(); +extensions.install(CodingTools); +extensions.install(defineExtension({ name: "review", tools: [requestReview] })); + export const coder = pi({ model: "anthropic/claude-opus-5-5", + registry: extensions, sandbox: e2bProvider(), - customTools: [requestReview], }); const client = createClient(); diff --git a/vendor/agents/examples/docs/agent-to-agent/server.ts b/vendor/agents/examples/docs/agent-to-agent/server.ts index f19cc246..882dac66 100644 --- a/vendor/agents/examples/docs/agent-to-agent/server.ts +++ b/vendor/agents/examples/docs/agent-to-agent/server.ts @@ -1,14 +1,23 @@ +import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context"; +import { createRegistry } from "@earendil-works/pi-durable"; +import { CodingTools } from "@earendil-works/pi-durable/tools"; import { pi } from "@rivet-dev/pi"; import { e2bProvider } from "@rivet-dev/sandbox-adapter/e2b"; import { setup } from "rivetkit"; import { coder } from "./coder"; +const extensions = createRegistry(); +extensions.install(CodingTools); + const reviewer = pi({ model: "openai/gpt-5.5", + registry: extensions, sandbox: e2bProvider(), actions: { + // Saves the request and returns without waiting for the review. requestReview: async (c, request: string) => { - await c.schedule.after(0, "prompt", request, { streamingBehavior: "followUp" }); + const conversation = await c.pi.root(BACKGROUND_CONTEXT); + await conversation.submit({ type: "input", content: request }, BACKGROUND_CONTEXT); }, }, }); diff --git a/vendor/agents/examples/docs/built-in-tools/client.ts b/vendor/agents/examples/docs/built-in-tools/client.ts index 23ed3fd9..74955561 100644 --- a/vendor/agents/examples/docs/built-in-tools/client.ts +++ b/vendor/agents/examples/docs/built-in-tools/client.ts @@ -2,13 +2,9 @@ import { createClient } from "rivetkit/client"; import type { registry } from "./server"; const client = createClient(); -const reviewer = client.reviewer.getOrCreate(["pr-123"]); +const writer = client.writer.getOrCreate(["docs-123"]); -const checkout = await reviewer.executeBash( - "git clone https://github.com/acme/app . && git fetch origin pull/123/head:pr-123 && git checkout pr-123", - { excludeFromContext: true }, -); -if (checkout.exitCode !== 0) throw new Error(checkout.output); +await writer.prompt("Write a README.md for a command-line todo app."); +const result = await writer.prompt("Add an Install section to README.md."); -await reviewer.prompt("Review the changes on this branch."); -console.log(await reviewer.getLastAssistantText()); +console.log(result.status === "done" ? result.text : `Unanswered: ${result.reason}`); diff --git a/vendor/agents/examples/docs/built-in-tools/reviewer.ts b/vendor/agents/examples/docs/built-in-tools/reviewer.ts new file mode 100644 index 00000000..e7a7f6c9 --- /dev/null +++ b/vendor/agents/examples/docs/built-in-tools/reviewer.ts @@ -0,0 +1,19 @@ +import { createRegistry, defineExtension } from "@earendil-works/pi-durable"; +import { createBashTool, createReadTool } from "@earendil-works/pi-durable/tools"; +import { pi } from "@rivet-dev/pi"; +import { e2bProvider } from "@rivet-dev/sandbox-adapter/e2b"; +import { setup } from "rivetkit"; + +// read and bash, without the write and edit tools +const extensions = createRegistry(); +extensions.install(defineExtension({ name: "review-tools", tools: [createReadTool(), createBashTool()] })); + +const reviewer = pi({ + model: "anthropic/claude-opus-5-5", + registry: extensions, + sandbox: e2bProvider(), +}); + +export const registry = setup({ use: { reviewer } }); + +registry.start(); diff --git a/vendor/agents/examples/docs/built-in-tools/server.ts b/vendor/agents/examples/docs/built-in-tools/server.ts index d56e6d2e..4e70b37f 100644 --- a/vendor/agents/examples/docs/built-in-tools/server.ts +++ b/vendor/agents/examples/docs/built-in-tools/server.ts @@ -1,13 +1,18 @@ +import { createRegistry, defineExtension } from "@earendil-works/pi-durable"; +import { createEditTool, createReadTool, createWriteTool } from "@earendil-works/pi-durable/tools"; import { pi } from "@rivet-dev/pi"; -import { e2bProvider } from "@rivet-dev/sandbox-adapter/e2b"; import { setup } from "rivetkit"; -const reviewer = pi({ +// This agent has no sandbox, so its files live in the Actor's own database. +// There is no shell, so it gets read, write, and edit without bash. +const extensions = createRegistry(); +extensions.install(defineExtension({ name: "files", tools: [createReadTool(), createWriteTool(), createEditTool()] })); + +const writer = pi({ model: "anthropic/claude-opus-5-5", - sandbox: e2bProvider(), - excludeTools: ["bash", "edit", "write"], + registry: extensions, }); -export const registry = setup({ use: { reviewer } }); +export const registry = setup({ use: { writer } }); registry.start(); diff --git a/vendor/agents/examples/docs/client-sdk/browser.ts b/vendor/agents/examples/docs/client-sdk/browser.ts index cf8b8d70..23070e20 100644 --- a/vendor/agents/examples/docs/client-sdk/browser.ts +++ b/vendor/agents/examples/docs/client-sdk/browser.ts @@ -16,5 +16,5 @@ const client = createClient({ }); const conn = client.agent.getForId(agentId).connect(); -await conn.prompt("Clone https://github.com/honojs/hono and summarize its README."); -console.log(await conn.getLastAssistantText()); +const result = await conn.prompt("Clone https://github.com/honojs/hono and summarize its README."); +console.log(result.status === "done" ? result.text : `No answer: ${result.reason}`); diff --git a/vendor/agents/examples/docs/client-sdk/client.ts b/vendor/agents/examples/docs/client-sdk/client.ts index fb42dd60..483d6549 100644 --- a/vendor/agents/examples/docs/client-sdk/client.ts +++ b/vendor/agents/examples/docs/client-sdk/client.ts @@ -4,37 +4,55 @@ import type { registry } from "./server"; const client = createClient(); const agent = client.agent.getOrCreate(["support", "customer-123"]); -const history = await agent.getMessages(); -console.log(`${history.length} messages so far`); +const root = await agent.harness.root(); +const { messages } = await agent.conversation.context(root.id); +console.log(`${messages.length} messages so far`); const conn = agent.connect(); conn.onStatusChange((status) => console.log(`[connection ${status}]`)); -const unsubscribe = conn.on("event", (event) => { - switch (event.type) { - case "message_update": - if (event.assistantMessageEvent.type === "text_delta") { - process.stdout.write(event.assistantMessageEvent.delta); +// The text of the current answer printed so far. +let printed = ""; + +const unsubscribe = conn.on("pi.events", ({ events }) => { + for (const event of events) { + switch (event.type) { + case "message_update": + for (const change of event.changes) { + if (change.type === "text_delta") { + process.stdout.write(change.delta); + printed += change.delta; + } + } + break; + case "message_end": { + // A short answer can arrive whole here, with no text_delta before it. + const message = event.entry.model?.[0]; + if (message?.role === "assistant") { + const text = message.content + .flatMap((block) => (block.type === "text" ? [block.text] : [])) + .join(""); + process.stdout.write(text.slice(printed.length)); + } + printed = ""; + break; } - break; - case "tool_execution_start": - console.log(`\n> ${event.toolName}`); - break; - case "tool_execution_end": - console.log(event.isError ? " failed" : " done"); - break; - case "auto_retry_start": - console.log(`\nRetrying (${event.attempt}/${event.maxAttempts}): ${event.errorMessage}`); - break; + case "tool_execution_start": + console.log(`\n> ${event.toolName}`); + break; + case "auto_retry_start": + console.log(`\nRetrying (attempt ${event.attempt}): ${event.errorMessage}`); + break; + } } }); +// Sends this conversation's events to this connection. +await conn.conversation.watchEvents(root.id); -await conn.prompt("Clone https://github.com/honojs/hono and summarize its README."); - -const last = (await conn.getMessages()).at(-1); -if (last?.role === "assistant" && last.stopReason === "error") { - console.error(`\nModel error: ${last.errorMessage}`); -} +const result = await conn.prompt("Clone https://github.com/honojs/hono and summarize its README.", { + requestId: crypto.randomUUID(), +}); +if (result.status === "unanswered") console.error(`\nNo answer: ${result.reason}`); unsubscribe(); await conn.dispose(); diff --git a/vendor/agents/examples/docs/client-sdk/server.ts b/vendor/agents/examples/docs/client-sdk/server.ts index 561838bf..714ea0b4 100644 --- a/vendor/agents/examples/docs/client-sdk/server.ts +++ b/vendor/agents/examples/docs/client-sdk/server.ts @@ -1,9 +1,15 @@ +import { createRegistry } from "@earendil-works/pi-durable"; +import { CodingTools } from "@earendil-works/pi-durable/tools"; import { pi } from "@rivet-dev/pi"; import { e2bProvider } from "@rivet-dev/sandbox-adapter/e2b"; import { setup } from "rivetkit"; +const extensions = createRegistry(); +extensions.install(CodingTools); + const agent = pi({ model: "anthropic/claude-opus-5-5", + registry: extensions, sandbox: e2bProvider(), }); diff --git a/vendor/agents/examples/docs/connectors/discord/discord.ts b/vendor/agents/examples/docs/connectors/discord/discord.ts index 359750e9..b87396f2 100644 --- a/vendor/agents/examples/docs/connectors/discord/discord.ts +++ b/vendor/agents/examples/docs/connectors/discord/discord.ts @@ -3,7 +3,7 @@ import { Hono } from "hono"; import { createClient } from "rivetkit/client"; import type { registry } from "./server"; -export type DiscordCommand = { applicationId: string; token: string; prompt: string }; +export type DiscordCommand = { interactionId: string; applicationId: string; token: string; prompt: string }; const client = createClient(); @@ -23,7 +23,12 @@ discord.post("/interactions", async (c) => { if (interaction.type === InteractionType.APPLICATION_COMMAND && interaction.data.name === "ask") { const prompt = interaction.data.options?.find((option: { name: string }) => option.name === "prompt")?.value; const agent = client.agent.getOrCreate(["discord", interaction.channel_id ?? interaction.channel?.id]); - await agent.receive({ applicationId: interaction.application_id, token: interaction.token, prompt: String(prompt ?? "") }); + await agent.receive({ + interactionId: interaction.id, + applicationId: interaction.application_id, + token: interaction.token, + prompt: String(prompt ?? ""), + }); return c.json({ type: InteractionResponseType.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE }); } diff --git a/vendor/agents/examples/docs/connectors/discord/server.ts b/vendor/agents/examples/docs/connectors/discord/server.ts index 6202b743..10d1f57a 100644 --- a/vendor/agents/examples/docs/connectors/discord/server.ts +++ b/vendor/agents/examples/docs/connectors/discord/server.ts @@ -1,3 +1,4 @@ +import { createRegistry } from "@earendil-works/pi-durable"; import { pi } from "@rivet-dev/pi"; import { Hono } from "hono"; import { setup } from "rivetkit"; @@ -5,29 +6,18 @@ import { type DiscordCommand, discord, postReply } from "./discord"; const agent = pi({ model: "anthropic/claude-opus-5-5", - state: { pending: [] as DiscordCommand[] }, - vars: { replying: false }, + registry: createRegistry(), actions: { + // Schedules the answer and returns, so the route can respond in time. receive: async (c, command: DiscordCommand) => { - c.state.pending.push(command); - await c.schedule.after(0, "drain"); + await c.schedule.after(0, "reply", command); }, - // Answers the oldest message. Messages that arrive mid-run wait their turn. - drain: async (c): Promise => { - const command = c.state.pending[0]; - if (!command || c.vars.replying) return; - c.vars.replying = true; - try { - const self = c.client().agent.getForId(c.actorId); - await self.prompt(command.prompt); - await postReply(command, await self.getLastAssistantText()); - } catch (error) { - c.log.error({ msg: "reply failed", error }); - } finally { - c.state.pending.shift(); - c.vars.replying = false; - } - if (c.state.pending.length > 0) await c.schedule.after(0, "drain"); + reply: async (c, command: DiscordCommand): Promise => { + const self = c.client().agent.getForId(c.actorId); + // A message sent during a run waits as a follow-up. + // The requestId stops a duplicate delivery from running twice. + const result = await self.prompt(command.prompt, { requestId: command.interactionId }); + await postReply(command, result.status === "done" ? result.text : undefined); }, }, }); diff --git a/vendor/agents/examples/docs/connectors/github/github.ts b/vendor/agents/examples/docs/connectors/github/github.ts index a63a4baa..e4748664 100644 --- a/vendor/agents/examples/docs/connectors/github/github.ts +++ b/vendor/agents/examples/docs/connectors/github/github.ts @@ -4,7 +4,7 @@ import { Hono } from "hono"; import { createClient } from "rivetkit/client"; import type { registry } from "./server"; -export type GitHubComment = { owner: string; repo: string; issueNumber: number; text: string }; +export type GitHubComment = { commentId: string; owner: string; repo: string; issueNumber: number; text: string }; const botLogin = process.env.GITHUB_BOT_LOGIN ?? ""; const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN }); @@ -24,7 +24,13 @@ github.post("/webhook", async (c) => { if (action === "created" && comment.user.login !== botLogin && comment.body.includes(`@${botLogin}`)) { const owner = repository.owner.login; const agent = client.agent.getOrCreate([owner, repository.name, String(issue.number)]); - await agent.receive({ owner, repo: repository.name, issueNumber: issue.number, text: comment.body }); + await agent.receive({ + commentId: String(comment.id), + owner, + repo: repository.name, + issueNumber: issue.number, + text: comment.body, + }); } return c.body(null, 202); }); diff --git a/vendor/agents/examples/docs/connectors/github/server.ts b/vendor/agents/examples/docs/connectors/github/server.ts index b07f7acc..5c54da14 100644 --- a/vendor/agents/examples/docs/connectors/github/server.ts +++ b/vendor/agents/examples/docs/connectors/github/server.ts @@ -1,3 +1,4 @@ +import { createRegistry } from "@earendil-works/pi-durable"; import { pi } from "@rivet-dev/pi"; import { Hono } from "hono"; import { setup } from "rivetkit"; @@ -5,29 +6,18 @@ import { type GitHubComment, github, postReply } from "./github"; const agent = pi({ model: "anthropic/claude-opus-5-5", - state: { pending: [] as GitHubComment[] }, - vars: { replying: false }, + registry: createRegistry(), actions: { + // Schedules the answer and returns, so the route can respond in time. receive: async (c, comment: GitHubComment) => { - c.state.pending.push(comment); - await c.schedule.after(0, "drain"); + await c.schedule.after(0, "reply", comment); }, - // Answers the oldest message. Messages that arrive mid-run wait their turn. - drain: async (c): Promise => { - const comment = c.state.pending[0]; - if (!comment || c.vars.replying) return; - c.vars.replying = true; - try { - const self = c.client().agent.getForId(c.actorId); - await self.prompt(comment.text); - await postReply(comment, await self.getLastAssistantText()); - } catch (error) { - c.log.error({ msg: "reply failed", error }); - } finally { - c.state.pending.shift(); - c.vars.replying = false; - } - if (c.state.pending.length > 0) await c.schedule.after(0, "drain"); + reply: async (c, comment: GitHubComment): Promise => { + const self = c.client().agent.getForId(c.actorId); + // A message sent during a run waits as a follow-up. + // The requestId stops a duplicate delivery from running twice. + const result = await self.prompt(comment.text, { requestId: comment.commentId }); + await postReply(comment, result.status === "done" ? result.text : undefined); }, }, }); diff --git a/vendor/agents/examples/docs/connectors/linear/linear.ts b/vendor/agents/examples/docs/connectors/linear/linear.ts index 841b09f0..3ed89c8e 100644 --- a/vendor/agents/examples/docs/connectors/linear/linear.ts +++ b/vendor/agents/examples/docs/connectors/linear/linear.ts @@ -4,7 +4,7 @@ import { Hono } from "hono"; import { createClient } from "rivetkit/client"; import type { registry } from "./server"; -export type LinearComment = { issueId: string; text: string }; +export type LinearComment = { commentId: string; issueId: string; text: string }; const trigger = process.env.LINEAR_TRIGGER ?? "@agent"; const linear = new LinearClient({ apiKey: process.env.LINEAR_API_KEY }); @@ -31,7 +31,7 @@ linearRoutes.post("/webhook", async (c) => { comment.userId !== (await agentUserId) ) { const agent = client.agent.getOrCreate(["linear", comment.issueId]); - await agent.receive({ issueId: comment.issueId, text: comment.body }); + await agent.receive({ commentId: comment.id, issueId: comment.issueId, text: comment.body }); } return c.body(null, 200); }); diff --git a/vendor/agents/examples/docs/connectors/linear/server.ts b/vendor/agents/examples/docs/connectors/linear/server.ts index 3c4ff5be..9b843082 100644 --- a/vendor/agents/examples/docs/connectors/linear/server.ts +++ b/vendor/agents/examples/docs/connectors/linear/server.ts @@ -1,3 +1,4 @@ +import { createRegistry } from "@earendil-works/pi-durable"; import { pi } from "@rivet-dev/pi"; import { Hono } from "hono"; import { setup } from "rivetkit"; @@ -5,29 +6,18 @@ import { type LinearComment, linearRoutes, postReply } from "./linear"; const agent = pi({ model: "anthropic/claude-opus-5-5", - state: { pending: [] as LinearComment[] }, - vars: { replying: false }, + registry: createRegistry(), actions: { + // Schedules the answer and returns, so the route can respond in time. receive: async (c, comment: LinearComment) => { - c.state.pending.push(comment); - await c.schedule.after(0, "drain"); + await c.schedule.after(0, "reply", comment); }, - // Answers the oldest message. Messages that arrive mid-run wait their turn. - drain: async (c): Promise => { - const comment = c.state.pending[0]; - if (!comment || c.vars.replying) return; - c.vars.replying = true; - try { - const self = c.client().agent.getForId(c.actorId); - await self.prompt(comment.text); - await postReply(comment, await self.getLastAssistantText()); - } catch (error) { - c.log.error({ msg: "reply failed", error }); - } finally { - c.state.pending.shift(); - c.vars.replying = false; - } - if (c.state.pending.length > 0) await c.schedule.after(0, "drain"); + reply: async (c, comment: LinearComment): Promise => { + const self = c.client().agent.getForId(c.actorId); + // A message sent during a run waits as a follow-up. + // The requestId stops a duplicate delivery from running twice. + const result = await self.prompt(comment.text, { requestId: comment.commentId }); + await postReply(comment, result.status === "done" ? result.text : undefined); }, }, }); diff --git a/vendor/agents/examples/docs/connectors/slack/server.ts b/vendor/agents/examples/docs/connectors/slack/server.ts index 962d4cc5..ad7fc8f9 100644 --- a/vendor/agents/examples/docs/connectors/slack/server.ts +++ b/vendor/agents/examples/docs/connectors/slack/server.ts @@ -1,3 +1,4 @@ +import { createRegistry } from "@earendil-works/pi-durable"; import { pi } from "@rivet-dev/pi"; import { Hono } from "hono"; import { setup } from "rivetkit"; @@ -5,29 +6,18 @@ import { postReply, slack, type SlackThread } from "./slack"; const agent = pi({ model: "anthropic/claude-opus-5-5", - state: { pending: [] as SlackThread[] }, - vars: { replying: false }, + registry: createRegistry(), actions: { + // Schedules the answer and returns, so the route can respond in time. receive: async (c, thread: SlackThread) => { - c.state.pending.push(thread); - await c.schedule.after(0, "drain"); + await c.schedule.after(0, "reply", thread); }, - // Answers the oldest message. Messages that arrive mid-run wait their turn. - drain: async (c): Promise => { - const thread = c.state.pending[0]; - if (!thread || c.vars.replying) return; - c.vars.replying = true; - try { - const self = c.client().agent.getForId(c.actorId); - await self.prompt(thread.text); - await postReply(thread, await self.getLastAssistantText()); - } catch (error) { - c.log.error({ msg: "reply failed", error }); - } finally { - c.state.pending.shift(); - c.vars.replying = false; - } - if (c.state.pending.length > 0) await c.schedule.after(0, "drain"); + reply: async (c, thread: SlackThread): Promise => { + const self = c.client().agent.getForId(c.actorId); + // A message sent during a run waits as a follow-up. + // The requestId stops a duplicate delivery from running twice. + const result = await self.prompt(thread.text, { requestId: thread.eventId }); + await postReply(thread, result.status === "done" ? result.text : undefined); }, }, }); diff --git a/vendor/agents/examples/docs/connectors/slack/slack.ts b/vendor/agents/examples/docs/connectors/slack/slack.ts index 096ff119..e660982f 100644 --- a/vendor/agents/examples/docs/connectors/slack/slack.ts +++ b/vendor/agents/examples/docs/connectors/slack/slack.ts @@ -4,7 +4,7 @@ import { Hono } from "hono"; import { createClient } from "rivetkit/client"; import type { registry } from "./server"; -export type SlackThread = { channel: string; threadTs: string; text: string }; +export type SlackThread = { eventId: string; channel: string; threadTs: string; text: string }; const web = new WebClient(process.env.SLACK_BOT_TOKEN); const client = createClient(); @@ -26,7 +26,7 @@ slack.post("/events", async (c) => { if (payload.type === "event_callback" && event?.type === "app_mention") { const threadTs = event.thread_ts ?? event.ts; const agent = client.agent.getOrCreate([payload.team_id, event.channel, threadTs]); - await agent.receive({ channel: event.channel, threadTs, text: event.text }); + await agent.receive({ eventId: payload.event_id, channel: event.channel, threadTs, text: event.text }); } return c.body(null, 200); }); diff --git a/vendor/agents/examples/docs/credentials/api-keys.ts b/vendor/agents/examples/docs/credentials/api-keys.ts index 0561f590..3224562a 100644 --- a/vendor/agents/examples/docs/credentials/api-keys.ts +++ b/vendor/agents/examples/docs/credentials/api-keys.ts @@ -1,3 +1,5 @@ +import { createRegistry } from "@earendil-works/pi-durable"; +import { CodingTools } from "@earendil-works/pi-durable/tools"; import { pi } from "@rivet-dev/pi"; import { e2bProvider } from "@rivet-dev/sandbox-adapter/e2b"; import { setup } from "rivetkit"; @@ -5,8 +7,12 @@ import { setup } from "rivetkit"; const anthropicKey = process.env.MY_ANTHROPIC_KEY; if (!anthropicKey) throw new Error("MY_ANTHROPIC_KEY is not set."); +const extensions = createRegistry(); +extensions.install(CodingTools); + const agent = pi({ model: "anthropic/claude-opus-5-5", + registry: extensions, sandbox: e2bProvider(), apiKeys: { anthropic: anthropicKey }, }); diff --git a/vendor/agents/examples/docs/custom-tools/get-order.ts b/vendor/agents/examples/docs/custom-tools/get-order.ts index 1a133c9f..72fbfafe 100644 --- a/vendor/agents/examples/docs/custom-tools/get-order.ts +++ b/vendor/agents/examples/docs/custom-tools/get-order.ts @@ -1,13 +1,14 @@ import { Type } from "@earendil-works/pi-ai"; -import { defineTool } from "@earendil-works/pi-coding-agent"; +import { defineTool } from "@earendil-works/pi-durable"; export const getOrder = defineTool({ name: "get_order", - label: "Get order", description: "Look up an order's status by its id.", parameters: Type.Object({ orderId: Type.String() }), - async execute(_toolCallId, { orderId }, signal) { - const order = await fetchOrder(orderId, signal); + // A lookup changes nothing, so running it twice is harmless. + replay: "safe", + execute: async ({ orderId }, _api, context) => { + const order = await fetchOrder(orderId, context.abortSignal); const text = `Status: ${order.status}. Total: $${order.total}.`; return { content: [{ type: "text", text }], details: order }; }, diff --git a/vendor/agents/examples/docs/custom-tools/server.ts b/vendor/agents/examples/docs/custom-tools/server.ts index b39aebdd..9509c8bc 100644 --- a/vendor/agents/examples/docs/custom-tools/server.ts +++ b/vendor/agents/examples/docs/custom-tools/server.ts @@ -1,12 +1,14 @@ +import { createRegistry, defineExtension } from "@earendil-works/pi-durable"; import { pi } from "@rivet-dev/pi"; -import { e2bProvider } from "@rivet-dev/sandbox-adapter/e2b"; import { setup } from "rivetkit"; import { getOrder } from "./get-order"; +const extensions = createRegistry(); +extensions.install(defineExtension({ name: "orders", tools: [getOrder] })); + const agent = pi({ model: "anthropic/claude-opus-5-5", - sandbox: e2bProvider(), - customTools: [getOrder], + registry: extensions, }); export const registry = setup({ use: { agent } }); diff --git a/vendor/agents/examples/docs/design-patterns/agent-per-key/client.ts b/vendor/agents/examples/docs/design-patterns/agent-per-key/client.ts index 4cdb5cf4..f1c82b25 100644 --- a/vendor/agents/examples/docs/design-patterns/agent-per-key/client.ts +++ b/vendor/agents/examples/docs/design-patterns/agent-per-key/client.ts @@ -4,24 +4,22 @@ import type { registry } from "./server"; const client = createClient(); export async function chat(userId: string, message: string) { - const assistant = client.assistant.getOrCreate([userId]); - await assistant.prompt(message); - return assistant.getLastAssistantText(); + const result = await client.assistant.getOrCreate([userId]).prompt(message); + return result.status === "done" ? result.text : undefined; } export async function onSlackMessage(channel: string, threadTs: string, text: string) { - const thread = client.slackThread.getOrCreate([channel, threadTs]); - await thread.prompt(text); - return thread.getLastAssistantText(); + const result = await client.slackThread.getOrCreate([channel, threadTs]).prompt(text); + return result.status === "done" ? result.text : undefined; } export async function onIssueLabeled(repo: string, issue: { number: number; title: string; body: string }) { const fixer = client.issueFixer.getOrCreate([repo, String(issue.number)]); try { - await fixer.prompt( + const result = await fixer.prompt( `Clone https://github.com/${repo}, fix issue #${issue.number}, and open a pull request.\n\n# ${issue.title}\n\n${issue.body}`, ); - return await fixer.getLastAssistantText(); + return result.status === "done" ? result.text : undefined; } finally { await fixer.finish(); } diff --git a/vendor/agents/examples/docs/design-patterns/agent-per-key/server.ts b/vendor/agents/examples/docs/design-patterns/agent-per-key/server.ts index 8f209073..d4460085 100644 --- a/vendor/agents/examples/docs/design-patterns/agent-per-key/server.ts +++ b/vendor/agents/examples/docs/design-patterns/agent-per-key/server.ts @@ -1,13 +1,21 @@ +import { createRegistry } from "@earendil-works/pi-durable"; +import { CodingTools } from "@earendil-works/pi-durable/tools"; import { pi } from "@rivet-dev/pi"; import { e2bProvider } from "@rivet-dev/sandbox-adapter/e2b"; import { setup } from "rivetkit"; -const assistant = pi({ model: "anthropic/claude-opus-5-5" }); +// The chat agents have no tools. The issue fixer gets read, write, edit, and bash in a sandbox. +const noTools = createRegistry(); +const codingTools = createRegistry(); +codingTools.install(CodingTools); -const slackThread = pi({ model: "anthropic/claude-haiku-4-5" }); +const assistant = pi({ model: "anthropic/claude-opus-5-5", registry: noTools }); + +const slackThread = pi({ model: "anthropic/claude-haiku-4-5", registry: noTools }); const issueFixer = pi({ model: "anthropic/claude-opus-5-5", + registry: codingTools, sandbox: e2bProvider(), actions: { finish: (c) => c.destroy(), diff --git a/vendor/agents/examples/docs/design-patterns/background-job/server.ts b/vendor/agents/examples/docs/design-patterns/background-job/server.ts index c827b08e..3eb26bf3 100644 --- a/vendor/agents/examples/docs/design-patterns/background-job/server.ts +++ b/vendor/agents/examples/docs/design-patterns/background-job/server.ts @@ -1,7 +1,7 @@ import { Type } from "@earendil-works/pi-ai"; import { createRegistry, defineExtension, defineTool } from "@earendil-works/pi-durable"; import { CodingTools } from "@earendil-works/pi-durable/tools"; -import { piDurable } from "@rivet-dev/pi/durable"; +import { pi } from "@rivet-dev/pi"; import { e2bProvider } from "@rivet-dev/sandbox-adapter/e2b"; import { setup } from "rivetkit"; @@ -11,11 +11,12 @@ const openPullRequest = defineTool({ parameters: Type.Object({ repo: Type.String(), branch: Type.String(), title: Type.String() }), // No replay: if a crash cuts this call off, the model learns it never completed // and can check for an existing pull request before opening another. - execute: async ({ repo, branch, title }) => { + execute: async ({ repo, branch, title }, _api, context) => { const response = await fetch(`https://api.github.com/repos/${repo}/pulls`, { method: "POST", headers: { authorization: `Bearer ${process.env.GITHUB_TOKEN}`, accept: "application/vnd.github+json" }, body: JSON.stringify({ head: branch, base: "main", title }), + signal: context.abortSignal, }); if (!response.ok) throw new Error(`GitHub returned ${response.status}.`); const pull = (await response.json()) as { html_url: string }; @@ -27,7 +28,7 @@ const extensions = createRegistry(); extensions.install(CodingTools); extensions.install(defineExtension({ name: "github", tools: [openPullRequest] })); -const fixer = piDurable({ +const fixer = pi({ model: "anthropic/claude-opus-5-5", registry: extensions, sandbox: e2bProvider(), diff --git a/vendor/agents/examples/docs/design-patterns/key-per-request.ts b/vendor/agents/examples/docs/design-patterns/key-per-request.ts index 81f71688..7c750483 100644 --- a/vendor/agents/examples/docs/design-patterns/key-per-request.ts +++ b/vendor/agents/examples/docs/design-patterns/key-per-request.ts @@ -4,7 +4,6 @@ import type { registry } from "./agent-per-key/server"; const client = createClient(); export async function answer(message: string) { - const assistant = client.assistant.getOrCreate([crypto.randomUUID()]); - await assistant.prompt(message); - return assistant.getLastAssistantText(); + const result = await client.assistant.getOrCreate([crypto.randomUUID()]).prompt(message); + return result.status === "done" ? result.text : undefined; } diff --git a/vendor/agents/examples/docs/design-patterns/one-agent.ts b/vendor/agents/examples/docs/design-patterns/one-agent.ts index 8f74cadc..a5821402 100644 --- a/vendor/agents/examples/docs/design-patterns/one-agent.ts +++ b/vendor/agents/examples/docs/design-patterns/one-agent.ts @@ -4,7 +4,6 @@ import type { registry } from "./agent-per-key/server"; const client = createClient(); export async function answer(userId: string, message: string) { - const assistant = client.assistant.getOrCreate(["support"]); - await assistant.prompt(`${userId}: ${message}`); - return assistant.getLastAssistantText(); + const result = await client.assistant.getOrCreate(["support"]).prompt(`${userId}: ${message}`); + return result.status === "done" ? result.text : undefined; } diff --git a/vendor/agents/examples/docs/design-patterns/tenant-credentials/client.ts b/vendor/agents/examples/docs/design-patterns/tenant-credentials/client.ts index 53e2875a..df6999ce 100644 --- a/vendor/agents/examples/docs/design-patterns/tenant-credentials/client.ts +++ b/vendor/agents/examples/docs/design-patterns/tenant-credentials/client.ts @@ -10,7 +10,6 @@ export async function saveTeamKey(tenantId: string, anthropicKey: string) { // Every agent keyed under the tenant uses that key, and never sees another tenant's. export async function ask(tenantId: string, userId: string, text: string) { - const agent = client.agent.getOrCreate([tenantId, userId]); - await agent.prompt(text); - return agent.getLastAssistantText(); + const result = await client.agent.getOrCreate([tenantId, userId]).prompt(text); + return result.status === "done" ? result.text : undefined; } diff --git a/vendor/agents/examples/docs/design-patterns/tenant-credentials/server.ts b/vendor/agents/examples/docs/design-patterns/tenant-credentials/server.ts index ca6c6598..fc25d9fd 100644 --- a/vendor/agents/examples/docs/design-patterns/tenant-credentials/server.ts +++ b/vendor/agents/examples/docs/design-patterns/tenant-credentials/server.ts @@ -1,9 +1,11 @@ +import { createRegistry } from "@earendil-works/pi-durable"; import { pi } from "@rivet-dev/pi"; import { type Registry, setup } from "rivetkit"; import { credentials } from "./credentials"; const agent = pi({ model: "anthropic/claude-opus-5-5", + registry: createRegistry(), // Agent keys start with the tenant id, so every agent in a tenant reads the same credentials Actor. credentials: (c) => { const [tenantId] = c.key; diff --git a/vendor/agents/examples/docs/human-in-the-loop/ApprovalDialog.tsx b/vendor/agents/examples/docs/human-in-the-loop/ApprovalDialog.tsx index 787a15db..4b0b114e 100644 --- a/vendor/agents/examples/docs/human-in-the-loop/ApprovalDialog.tsx +++ b/vendor/agents/examples/docs/human-in-the-loop/ApprovalDialog.tsx @@ -1,29 +1,42 @@ import { createRivetKit } from "@rivetkit/react"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { createClient } from "rivetkit/client"; import type { registry } from "./server"; const { useActor } = createRivetKit(); const client = createClient(); -type Pending = { version: string; approvalKey: string[] }; +type Pending = { version: string; approvalId: string }; export function ApprovalDialog({ agentKey }: { agentKey: string[] }) { const agent = useActor({ name: "agent", key: agentKey }); const [pending, setPending] = useState(null); - agent.useEvent("event", (event) => { - if (event.type === "tool_execution_end" && event.toolName === "deploy") { - setPending(event.result.details.approvalKey ? event.result.details : null); + // Watch the root conversation, so this connection receives its pi.events. + useEffect(() => { + const conn = agent.connection; + if (!conn) return; + void conn.harness.root().then((root) => conn.conversation.watchEvents(root.id)); + }, [agent.connection]); + + agent.useEvent("pi.events", ({ events }) => { + for (const event of events) { + if (event.type !== "tool_execution_end" || event.toolName !== "deploy") continue; + const result = event.entry?.model?.[0]; + if (result?.role !== "toolResult") continue; + const details = result.details as Partial | undefined; + if (details?.version && details.approvalId) { + setPending({ version: details.version, approvalId: details.approvalId }); + } } }); async function answer(approved: boolean) { if (!pending) return; - if (approved) await client.approval.getOrCreate(pending.approvalKey).approve(); + if (approved) await client.approval.get([pending.approvalId]).approve(); const text = approved ? `Approved. Deploy ${pending.version}.` : `Rejected. Don't deploy ${pending.version}.`; setPending(null); - await agent.connection?.prompt(text, { streamingBehavior: "followUp" }); + await agent.connection?.prompt(text); } if (!pending) return null; diff --git a/vendor/agents/examples/docs/human-in-the-loop/deploy.ts b/vendor/agents/examples/docs/human-in-the-loop/deploy.ts index 21ce287f..3f6c9d0d 100644 --- a/vendor/agents/examples/docs/human-in-the-loop/deploy.ts +++ b/vendor/agents/examples/docs/human-in-the-loop/deploy.ts @@ -1,32 +1,40 @@ import { Type } from "@earendil-works/pi-ai"; -import { defineTool } from "@earendil-works/pi-coding-agent"; +import { defineTool } from "@earendil-works/pi-durable"; import { actor, type Registry } from "rivetkit"; import { createClient } from "rivetkit/client"; export const deploy = defineTool({ name: "deploy", - label: "Deploy", - description: "Deploy a version to production. Every deploy needs a person's approval.", - parameters: Type.Object({ version: Type.String() }), - async execute(_toolCallId, { version }, signal, _onUpdate, ctx) { - const approvalKey = [ctx.sessionManager.getSessionId(), version]; - if (!(await client.approval.getOrCreate(approvalKey).consume())) { - const text = `Waiting for a person to approve deploying ${version}.`; - return { content: [{ type: "text", text }], details: { version, approvalKey }, terminate: true }; + description: + "Deploy a version to production. Every deploy needs a person's approval: call it without approvalId to ask for one.", + parameters: Type.Object({ version: Type.String(), approvalId: Type.Optional(Type.String()) }), + execute: async ({ version, approvalId }, _api, context) => { + if (approvalId === undefined) { + const id = crypto.randomUUID(); + await client.approval.getOrCreate([id]).request(version); + const text = `Waiting for a person to approve deploying ${version}. After approval, call deploy again with approvalId ${id}.`; + // Ends the run, so the agent waits for the person instead of trying again. + return { content: [{ type: "text", text }], details: { version, approvalId: id }, control: { terminate: true } }; } - await fetch(`https://deploy.example.com/releases/${version}`, { method: "POST", signal }); - return { content: [{ type: "text", text: `Deployed ${version}.` }], details: { version } }; + if (!(await client.approval.get([approvalId]).consume(version))) { + throw new Error(`Deploying ${version} is not approved.`); + } + await fetch(`https://deploy.example.com/releases/${version}`, { method: "POST", signal: context.abortSignal }); + return { content: [{ type: "text", text: `Deployed ${version}.` }] }; }, }); export const approval = actor({ - state: { approved: false }, + state: { version: "", approved: false }, actions: { + request: (c, version: string) => { + c.state.version = version; + }, approve: (c) => { c.state.approved = true; }, - consume: (c) => { - const approved = c.state.approved; + consume: (c, version: string) => { + const approved = c.state.approved && c.state.version === version; c.state.approved = false; return approved; }, diff --git a/vendor/agents/examples/docs/human-in-the-loop/server.ts b/vendor/agents/examples/docs/human-in-the-loop/server.ts index b465f779..db0426df 100644 --- a/vendor/agents/examples/docs/human-in-the-loop/server.ts +++ b/vendor/agents/examples/docs/human-in-the-loop/server.ts @@ -1,8 +1,12 @@ +import { createRegistry, defineExtension } from "@earendil-works/pi-durable"; import { pi } from "@rivet-dev/pi"; import { setup } from "rivetkit"; import { approval, deploy } from "./deploy"; -const agent = pi({ model: "anthropic/claude-opus-5-5", customTools: [deploy] }); +const extensions = createRegistry(); +extensions.install(defineExtension({ name: "deploys", tools: [deploy] })); + +const agent = pi({ model: "anthropic/claude-opus-5-5", registry: extensions }); export const registry = setup({ use: { agent, approval } }); diff --git a/vendor/agents/examples/docs/instructions/client.ts b/vendor/agents/examples/docs/instructions/client.ts index 04f8dfff..2c30aaaa 100644 --- a/vendor/agents/examples/docs/instructions/client.ts +++ b/vendor/agents/examples/docs/instructions/client.ts @@ -5,9 +5,17 @@ import type { registry } from "./server"; const client = createClient(); const agent = client.agent.getOrCreate(["payments", "pr-482"]); +// Instructions for this conversation only. They go after the extension sections. +const root = await agent.harness.root(); +await agent.conversation.configure(root.id, { + instructions: "This conversation is about pr-482 in https://github.com/acme/payments. Focus on the checkout flow.", +}); + +// Context for one prompt goes in the prompt itself, text and images. const screenshot = await readFile("checkout-error.png"); +const result = await agent.prompt([ + { type: "text", text: "Clone the repository and check out pr-482. This error shows up at checkout after that PR. Which change causes it?" }, + { type: "image", data: screenshot.toString("base64"), mimeType: "image/png" }, +]); -await agent.prompt("Clone https://github.com/acme/payments and check out pr-482. This error shows up at checkout after that PR. Which change causes it?", { - images: [{ type: "image", data: screenshot.toString("base64"), mimeType: "image/png" }], -}); -console.log(await agent.getLastAssistantText()); +console.log(result.status === "done" ? result.text : `Unanswered: ${result.reason}`); diff --git a/vendor/agents/examples/docs/instructions/server.ts b/vendor/agents/examples/docs/instructions/server.ts index e56024cc..11b3b29a 100644 --- a/vendor/agents/examples/docs/instructions/server.ts +++ b/vendor/agents/examples/docs/instructions/server.ts @@ -1,24 +1,27 @@ -import { DefaultResourceLoader, getAgentDir } from "@earendil-works/pi-coding-agent"; +import { createRegistry, defineExtension, section } from "@earendil-works/pi-durable"; +import { CodingTools } from "@earendil-works/pi-durable/tools"; import { pi } from "@rivet-dev/pi"; import { e2bProvider } from "@rivet-dev/sandbox-adapter/e2b"; import { setup } from "rivetkit"; -const resourceLoader = new DefaultResourceLoader({ - cwd: process.cwd(), - agentDir: getAgentDir(), - appendSystemPrompt: ["You review pull requests for the payments team. Be brief and cite file paths."], - agentsFilesOverride: () => ({ - agentsFiles: [ - { path: "AGENTS.md", content: "Run `npm test` before proposing a fix. Never edit files in `migrations/`." }, +const extensions = createRegistry(); +extensions.install(CodingTools); +extensions.install( + defineExtension({ + name: "payments-team", + sections: [ + section("role", () => "You review pull requests for the payments team. Be brief and cite file paths."), + section("project-rules", () => "Run `npm test` before proposing a fix. Never edit files in `migrations/`."), + // The skill files are in the sandbox. This section tells the model where to find them. + section("skills", () => "The `skills/` folder has one folder per skill. Read a skill's SKILL.md before you start a task it covers."), ], }), -}); -await resourceLoader.reload(); +); const agent = pi({ model: "anthropic/claude-opus-5-5", + registry: extensions, sandbox: e2bProvider(), - resourceLoader, }); export const registry = setup({ use: { agent } }); diff --git a/vendor/agents/examples/docs/package.json b/vendor/agents/examples/docs/package.json index a8f8a660..a7a477b9 100644 --- a/vendor/agents/examples/docs/package.json +++ b/vendor/agents/examples/docs/package.json @@ -7,10 +7,10 @@ }, "dependencies": { "@daytonaio/sdk": "^0.216.0", - "@earendil-works/chord": "1.0.2", - "@earendil-works/pi-ai": "1.0.2", - "@earendil-works/pi-coding-agent": "1.0.2", - "@earendil-works/pi-durable": "1.0.2", + "@earendil-works/chord": "^1.0.4", + "@earendil-works/pi-ai": "^1.0.4", + "@earendil-works/pi-coding-agent": "^1.0.4", + "@earendil-works/pi-durable": "^1.0.4", "@linear/sdk": "^97.0.0", "@octokit/rest": "^22.0.1", "@octokit/webhooks-methods": "^6.0.0", diff --git a/vendor/agents/examples/docs/pi-durable/documents.ts b/vendor/agents/examples/docs/pi-durable/documents.ts deleted file mode 100644 index 1e9b05b5..00000000 --- a/vendor/agents/examples/docs/pi-durable/documents.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context"; -import { createRegistry, defineDoc, ROOT_CONVERSATION_ID } from "@earendil-works/pi-durable"; -import { piDurable } from "@rivet-dev/pi/durable"; -import { setup } from "rivetkit"; - -const Todos = defineDoc<{ items: string[] }>({ - kind: "app.todos", - version: 1, - scope: "conversation", - history: "latest", - fork: "current", - initial: () => ({ items: [] }), -}); - -const agent = piDurable({ - model: "anthropic/claude-opus-5-5", - registry: createRegistry(), - // Clients can read these by kind with harness.snapshot and harness.watchDoc. - documents: [Todos], - actions: { - // c.pi is the Actor's Pi Durable Harness. - addTodo: async (c, item: string) => { - await c.pi.commit(async (tx) => { - (await tx.doc(Todos, ROOT_CONVERSATION_ID)).items.push(item); - }, BACKGROUND_CONTEXT); - }, - }, -}); - -export const registry = setup({ use: { agent } }); - -registry.start(); diff --git a/vendor/agents/examples/docs/pi-durable/tasks.ts b/vendor/agents/examples/docs/pi-durable/tasks.ts deleted file mode 100644 index 01589abc..00000000 --- a/vendor/agents/examples/docs/pi-durable/tasks.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Type } from "@earendil-works/pi-ai"; -import { createRegistry, defineExtension, defineTask, defineTool } from "@earendil-works/pi-durable"; -import { piDurable } from "@rivet-dev/pi/durable"; -import { setup } from "rivetkit"; - -const PAYMENTS_API = "https://api.example.com"; - -// A durable task. Each phase saves its result before the next one starts, -// so after a crash the task carries on from the phase it reached. -const Charge = defineTask<{ card: string; cents: number }, { phase: "charge" }, string>({ - name: "shop.charge", - version: 1, - initial: () => ({ phase: "charge" }), - phases: { - charge: async (task, runtime, context) => { - // The idempotency key makes a rerun after a crash charge the card only once. - const response = await fetch(`${PAYMENTS_API}/charges`, { - method: "POST", - headers: { "idempotency-key": `charge-${task.id}` }, - body: JSON.stringify(task.input), - }); - const { receipt } = (await response.json()) as { receipt: string }; - await runtime.commit( - () => ({ status: "terminal", outcome: { status: "completed", result: receipt } }), - context, - ); - }, - }, - // Runs when the run is cancelled while the charge is in progress: undo it. - abort: async (task, runtime, context) => { - await fetch(`${PAYMENTS_API}/charges/charge-${task.id}/refund`, { method: "POST" }); - await runtime.commit(() => ({ status: "terminal", outcome: { status: "aborted" } }), context); - }, -}); - -const charge = defineTool({ - name: "charge", - description: "Charge a card for an order.", - parameters: Type.Object({ card: Type.String(), cents: Type.Number() }), - execute: async (args, api, context) => { - // Owned by this tool call, so cancelling the run aborts the charge too. - const id = await api.createTask(Charge, args, { ownership: { kind: "task", taskId: api.taskId } }, context); - const { outcome } = (await api.waitForTask(id, context)).state; - const text = outcome.status === "completed" ? `Charged. Receipt ${outcome.result}.` : `Charge ${outcome.status}.`; - return { content: [{ type: "text", text }] }; - }, -}); - -const extensions = createRegistry(); -extensions.install(defineExtension({ name: "shop", tools: [charge], tasks: [Charge] })); - -const agent = piDurable({ - model: "anthropic/claude-opus-5-5", - registry: extensions, -}); - -export const registry = setup({ use: { agent } }); - -registry.start(); diff --git a/vendor/agents/examples/docs/pi/actions.ts b/vendor/agents/examples/docs/pi/actions.ts deleted file mode 100644 index 3d4d7319..00000000 --- a/vendor/agents/examples/docs/pi/actions.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { pi } from "@rivet-dev/pi"; -import { setup } from "rivetkit"; - -const agent = pi({ - model: "anthropic/claude-opus-5-5", - actions: { - // c.pi is the Actor's Pi AgentSession. - triage: async (c, issue: string) => { - await c.pi.prompt(`Triage this issue and suggest a label: ${issue}`); - return c.pi.getLastAssistantText(); - }, - }, -}); - -export const registry = setup({ use: { agent } }); - -registry.start(); diff --git a/vendor/agents/examples/docs/pi-durable/client.ts b/vendor/agents/examples/docs/pi/client.ts similarity index 76% rename from vendor/agents/examples/docs/pi-durable/client.ts rename to vendor/agents/examples/docs/pi/client.ts index 2f876907..bc6dce16 100644 --- a/vendor/agents/examples/docs/pi-durable/client.ts +++ b/vendor/agents/examples/docs/pi/client.ts @@ -5,7 +5,7 @@ const client = createClient(); const agent = client.agent.getOrCreate(["user-123"]); const result = await agent.prompt( - "Write a Python script that prints the first 20 prime numbers, run it, and show me the output.", + "Write a Python script that rolls two dice 10,000 times, run it, and show me how often each total came up.", { requestId: crypto.randomUUID() }, ); diff --git a/vendor/agents/examples/docs/pi/cost.ts b/vendor/agents/examples/docs/pi/cost.ts new file mode 100644 index 00000000..f7fc7d0d --- /dev/null +++ b/vendor/agents/examples/docs/pi/cost.ts @@ -0,0 +1,20 @@ +import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context"; +import { createRegistry } from "@earendil-works/pi-durable"; +import { pi } from "@rivet-dev/pi"; +import { setup } from "rivetkit"; + +const agent = pi({ + model: "anthropic/claude-opus-5-5", + registry: createRegistry(), + actions: { + // What this agent has spent on model calls, in dollars, across every conversation. + cost: async (c) => { + const { models } = await c.pi.usage(BACKGROUND_CONTEXT); + return Object.values(models).reduce((total, usage) => total + usage.cost.total, 0); + }, + }, +}); + +export const registry = setup({ use: { agent } }); + +registry.start(); diff --git a/vendor/agents/examples/docs/pi-durable/daytona.ts b/vendor/agents/examples/docs/pi/daytona.ts similarity index 87% rename from vendor/agents/examples/docs/pi-durable/daytona.ts rename to vendor/agents/examples/docs/pi/daytona.ts index 3c0e4bb3..595f790f 100644 --- a/vendor/agents/examples/docs/pi-durable/daytona.ts +++ b/vendor/agents/examples/docs/pi/daytona.ts @@ -1,6 +1,6 @@ import { createRegistry } from "@earendil-works/pi-durable"; import { CodingTools } from "@earendil-works/pi-durable/tools"; -import { piDurable } from "@rivet-dev/pi/durable"; +import { pi } from "@rivet-dev/pi"; import { daytonaProvider } from "@rivet-dev/sandbox-adapter/daytona"; import { setup } from "rivetkit"; @@ -8,7 +8,7 @@ import { setup } from "rivetkit"; const extensions = createRegistry(); extensions.install(CodingTools); -const agent = piDurable({ +const agent = pi({ model: "anthropic/claude-opus-5-5", registry: extensions, sandbox: daytonaProvider(), diff --git a/vendor/agents/examples/docs/pi/documents/chambers.ts b/vendor/agents/examples/docs/pi/documents/chambers.ts new file mode 100644 index 00000000..b44dbb2d --- /dev/null +++ b/vendor/agents/examples/docs/pi/documents/chambers.ts @@ -0,0 +1,11 @@ +import { defineDoc } from "@earendil-works/pi-durable"; + +// The Portal 2 test chambers a player has solved, kept with the conversation. +export const Chambers = defineDoc<{ solved: string[] }>({ + kind: "aperture.chambers", + version: 1, + scope: "conversation", + history: "latest", + fork: "current", + initial: () => ({ solved: [] }), +}); diff --git a/vendor/agents/examples/docs/pi/documents/client.ts b/vendor/agents/examples/docs/pi/documents/client.ts new file mode 100644 index 00000000..b9d927eb --- /dev/null +++ b/vendor/agents/examples/docs/pi/documents/client.ts @@ -0,0 +1,10 @@ +import { createClient } from "rivetkit/client"; +import type { registry } from "./server"; + +const client = createClient(); +const agent = client.agent.getOrCreate(["player-1"]); + +await agent.solveChamber("1-01"); +await agent.solveChamber("1-02"); + +console.log(await agent.solvedChambers()); // ["1-01", "1-02"] diff --git a/vendor/agents/examples/docs/pi/documents/server.ts b/vendor/agents/examples/docs/pi/documents/server.ts new file mode 100644 index 00000000..43370441 --- /dev/null +++ b/vendor/agents/examples/docs/pi/documents/server.ts @@ -0,0 +1,30 @@ +import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context"; +import { createRegistry } from "@earendil-works/pi-durable"; +import { pi } from "@rivet-dev/pi"; +import { setup } from "rivetkit"; +import { Chambers } from "./chambers"; + +const agent = pi({ + model: "anthropic/claude-opus-5-5", + registry: createRegistry(), + documents: [Chambers], + actions: { + solveChamber: async (c, chamber: string) => { + // root() creates the root conversation on first use. + const root = await c.pi.root(BACKGROUND_CONTEXT); + // The change is saved in one commit, with the conversation. + await c.pi.commit(async (tx) => { + (await tx.doc(Chambers, root.id)).solved.push(chamber); + }, BACKGROUND_CONTEXT); + }, + solvedChambers: async (c) => { + const root = await c.pi.root(BACKGROUND_CONTEXT); + const chambers = await c.pi.snapshot(Chambers, root.id, BACKGROUND_CONTEXT); + return chambers?.solved ?? []; + }, + }, +}); + +export const registry = setup({ use: { agent } }); + +registry.start(); diff --git a/vendor/agents/examples/docs/pi/messages.ts b/vendor/agents/examples/docs/pi/messages.ts new file mode 100644 index 00000000..6732c614 --- /dev/null +++ b/vendor/agents/examples/docs/pi/messages.ts @@ -0,0 +1,21 @@ +import type { AssistantMessage } from "@earendil-works/pi-ai"; +import { createClient } from "rivetkit/client"; +import type { registry } from "./server"; + +const client = createClient(); +const agent = client.agent.getOrCreate(["user-123"]); + +const root = await agent.harness.root(); +const { messages } = await agent.conversation.context(root.id); + +// The messages are pi-ai messages: user, assistant, and toolResult. +const answers = messages.filter( + (message): message is AssistantMessage => message.role === "assistant", +); +for (const answer of answers) { + const text = answer.content + .flatMap((block) => (block.type === "text" ? [block.text] : [])) + .join(""); + const { totalTokens, cost } = answer.usage; + console.log(`${answer.model}, ${totalTokens} tokens, $${cost.total}: ${text}`); +} diff --git a/vendor/agents/examples/docs/pi/pokedex.ts b/vendor/agents/examples/docs/pi/pokedex.ts new file mode 100644 index 00000000..44f5802c --- /dev/null +++ b/vendor/agents/examples/docs/pi/pokedex.ts @@ -0,0 +1,30 @@ +import { Type } from "@earendil-works/pi-ai"; +import { defineTool } from "@earendil-works/pi-durable"; + +// pi-ai's Type builds the schema Pi checks every call against. +export const findPokemon = defineTool({ + name: "find_pokemon", + description: "List the areas where a wild Pokémon appears in Pokémon HeartGold or SoulSilver.", + parameters: Type.Object({ + pokemon: Type.String({ description: "Pokémon name in lowercase, such as sudowoodo." }), + version: Type.Optional(Type.Union([Type.Literal("heartgold"), Type.Literal("soulsilver")])), + }), + replay: "safe", + execute: async ({ pokemon, version = "soulsilver" }, _api, context) => { + const url = `https://pokeapi.co/api/v2/pokemon/${encodeURIComponent(pokemon)}/encounters`; + const response = await fetch(url, { signal: context.abortSignal }); + if (!response.ok) throw new Error(`PokéAPI returned ${response.status} for ${pokemon}.`); + const encounters = (await response.json()) as Encounter[]; + const areas = encounters + .filter((encounter) => encounter.version_details.some((detail) => detail.version.name === version)) + .map((encounter) => encounter.location_area.name); + const text = + areas.length > 0 ? `${pokemon} in ${version}: ${areas.join(", ")}.` : `No wild ${pokemon} in ${version}.`; + return { content: [{ type: "text", text }] }; + }, +}); + +type Encounter = { + location_area: { name: string }; + version_details: { version: { name: string } }[]; +}; diff --git a/vendor/agents/examples/docs/pi/server.ts b/vendor/agents/examples/docs/pi/server.ts index e7b60d80..1757d36e 100644 --- a/vendor/agents/examples/docs/pi/server.ts +++ b/vendor/agents/examples/docs/pi/server.ts @@ -1,10 +1,19 @@ +import { createRegistry } from "@earendil-works/pi-durable"; +import { CodingTools } from "@earendil-works/pi-durable/tools"; import { pi } from "@rivet-dev/pi"; +import { e2bProvider } from "@rivet-dev/sandbox-adapter/e2b"; import { setup } from "rivetkit"; -const agent = pi({ model: "anthropic/claude-opus-5-5" }); +// read, write, edit, and bash, running in the sandbox +const extensions = createRegistry(); +extensions.install(CodingTools); -export const registry = setup({ - use: { agent }, +const agent = pi({ + model: "anthropic/claude-opus-5-5", + registry: extensions, + sandbox: e2bProvider(), }); +export const registry = setup({ use: { agent } }); + registry.start(); diff --git a/vendor/agents/examples/docs/pi-durable/stream.ts b/vendor/agents/examples/docs/pi/stream.ts similarity index 53% rename from vendor/agents/examples/docs/pi-durable/stream.ts rename to vendor/agents/examples/docs/pi/stream.ts index bd894e64..0a91f936 100644 --- a/vendor/agents/examples/docs/pi-durable/stream.ts +++ b/vendor/agents/examples/docs/pi/stream.ts @@ -4,12 +4,29 @@ import type { registry } from "./server"; const client = createClient(); const conn = client.agent.getOrCreate(["user-123"]).connect(); +// The text of the current answer printed so far. +let printed = ""; + conn.on("pi.events", ({ events }) => { for (const event of events) { if (event.type === "message_update") { for (const change of event.changes) { - if (change.type === "text_delta") process.stdout.write(change.delta); + if (change.type === "text_delta") { + process.stdout.write(change.delta); + printed += change.delta; + } + } + } + if (event.type === "message_end") { + // A short answer can arrive whole here, with no text_delta before it. + const message = event.entry.model?.[0]; + if (message?.role === "assistant") { + const text = message.content + .flatMap((block) => (block.type === "text" ? [block.text] : [])) + .join(""); + process.stdout.write(text.slice(printed.length)); } + printed = ""; } if (event.type === "tool_execution_start") console.log(`\n> ${event.toolName}`); } diff --git a/vendor/agents/examples/docs/pi/tasks.ts b/vendor/agents/examples/docs/pi/tasks.ts new file mode 100644 index 00000000..02df0b45 --- /dev/null +++ b/vendor/agents/examples/docs/pi/tasks.ts @@ -0,0 +1,68 @@ +import { Type } from "@earendil-works/pi-ai"; +import { createRegistry, defineExtension, defineTask, defineTool } from "@earendil-works/pi-durable"; +import { pi } from "@rivet-dev/pi"; +import { setup } from "rivetkit"; + +const PREVIEWS_API = "https://api.example.com/previews"; + +// A durable task. Each phase saves its checkpoint before the next one starts, +// so after a crash the task carries on from the phase it reached. +const DeployPreview = defineTask< + { repo: string; pr: number }, + { phase: "deploy" } | { phase: "comment"; url: string }, + string +>({ + name: "previews.deploy", + version: 1, + initial: () => ({ phase: "deploy" }), + phases: { + deploy: async (task, runtime, context) => { + // The preview id comes from the task id, so a rerun after a crash finds the same preview. + const response = await fetch(`${PREVIEWS_API}/preview-${task.id}`, { + method: "PUT", + body: JSON.stringify(task.input), + }); + const { url } = (await response.json()) as { url: string }; + await runtime.commit(() => ({ status: "running", checkpoint: { phase: "comment", url } }), context); + }, + comment: async (task, runtime, context) => { + const { url } = task.state.checkpoint; + await fetch(`https://api.github.com/repos/${task.input.repo}/issues/${task.input.pr}/comments`, { + method: "POST", + headers: { authorization: `Bearer ${process.env.GITHUB_TOKEN}` }, + body: JSON.stringify({ body: `Preview: ${url}` }), + }); + await runtime.commit(() => ({ status: "terminal", outcome: { status: "completed", result: url } }), context); + }, + }, + // Runs when the run is cancelled before the task finishes: delete the preview. + abort: async (task, runtime, context) => { + await fetch(`${PREVIEWS_API}/preview-${task.id}`, { method: "DELETE" }); + await runtime.commit(() => ({ status: "terminal", outcome: { status: "aborted" } }), context); + }, +}); + +const deployPreview = defineTool({ + name: "deploy_preview", + description: "Deploy a preview of a pull request and post its URL on the pull request.", + parameters: Type.Object({ repo: Type.String({ description: "owner/name" }), pr: Type.Number() }), + execute: async (args, api, context) => { + // Owned by this tool call, so cancelling the run aborts the task too. + const id = await api.createTask(DeployPreview, args, { ownership: { kind: "task", taskId: api.taskId } }, context); + const { outcome } = (await api.waitForTask(id, context)).state; + const text = outcome.status === "completed" ? `Preview at ${outcome.result}.` : `Preview ${outcome.status}.`; + return { content: [{ type: "text", text }] }; + }, +}); + +const extensions = createRegistry(); +extensions.install(defineExtension({ name: "previews", tools: [deployPreview], tasks: [DeployPreview] })); + +const agent = pi({ + model: "anthropic/claude-opus-5-5", + registry: extensions, +}); + +export const registry = setup({ use: { agent } }); + +registry.start(); diff --git a/vendor/agents/examples/docs/quickstart/client.ts b/vendor/agents/examples/docs/quickstart/client.ts index 80fdef5d..a5b2a1fa 100644 --- a/vendor/agents/examples/docs/quickstart/client.ts +++ b/vendor/agents/examples/docs/quickstart/client.ts @@ -4,9 +4,9 @@ import type { registry } from "./server"; const client = createClient(); const agent = client.agent.getOrCreate(["user-123"]); -const conn = agent.connect(); +const result = await agent.prompt( + "Write a hello-world Rivet Actor to counter.ts, then read the file back to me.", + { requestId: crypto.randomUUID() }, +); -await conn.prompt("Write a Python script that prints the first 20 prime numbers, run it, and show me the output."); -console.log(await conn.getLastAssistantText()); - -await conn.dispose(); +console.log(result.status === "done" ? result.text : `Unanswered: ${result.reason}`); diff --git a/vendor/agents/examples/docs/quickstart/server.ts b/vendor/agents/examples/docs/quickstart/server.ts index 561838bf..e66a717b 100644 --- a/vendor/agents/examples/docs/quickstart/server.ts +++ b/vendor/agents/examples/docs/quickstart/server.ts @@ -1,10 +1,15 @@ +import { createRegistry, defineExtension } from "@earendil-works/pi-durable"; +import { createEditTool, createReadTool, createWriteTool } from "@earendil-works/pi-durable/tools"; import { pi } from "@rivet-dev/pi"; -import { e2bProvider } from "@rivet-dev/sandbox-adapter/e2b"; import { setup } from "rivetkit"; +// read, write, and edit. With no sandbox, the files live in the Actor's own database. +const extensions = createRegistry(); +extensions.install(defineExtension({ name: "files", tools: [createReadTool(), createWriteTool(), createEditTool()] })); + const agent = pi({ model: "anthropic/claude-opus-5-5", - sandbox: e2bProvider(), + registry: extensions, }); export const registry = setup({ use: { agent } }); diff --git a/vendor/agents/examples/docs/react-sdk/useAgentChat.ts b/vendor/agents/examples/docs/react-sdk/useAgentChat.ts index a3531a53..5b7275df 100644 --- a/vendor/agents/examples/docs/react-sdk/useAgentChat.ts +++ b/vendor/agents/examples/docs/react-sdk/useAgentChat.ts @@ -1,6 +1,7 @@ import type { AssistantMessage, UserMessage } from "@earendil-works/pi-ai"; +import type { AgentEvent, EntryRecord, SnapshotEvent } from "@earendil-works/pi-durable"; import { createRivetKit } from "@rivetkit/react"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import type { registry } from "../pi/server"; const { useActor } = createRivetKit(); @@ -9,47 +10,97 @@ export type ChatMessage = { id: string; role: "user" | "assistant"; text: string export function useAgentChat(key: string[]) { const agent = useActor({ name: "agent", key }); + const conn = agent.connection; + const [rootId, setRootId] = useState(null); const [messages, setMessages] = useState([]); const [streaming, setStreaming] = useState(""); const [tool, setTool] = useState(null); const [running, setRunning] = useState(false); + const lastSeq = useRef(0); + + // A snapshot replaces everything the hook has. + const applySnapshot = useCallback((snapshot: SnapshotEvent) => { + setMessages(snapshot.entries.flatMap(toChatMessages)); + setStreaming(snapshot.generation?.message ? textOf(snapshot.generation.message) : ""); + setTool(snapshot.tools.find((slot) => slot.status === "running")?.name ?? null); + setRunning(snapshot.run !== undefined); + }, []); + + // Reads the conversation, then sends its events to this connection as pi.events. + const watch = useCallback(async () => { + if (!conn) return; + const root = await conn.harness.root(); + lastSeq.current = 0; + const { snapshot } = await conn.conversation.watchEvents(root.id); + applySnapshot(snapshot); + setRootId(root.id); + }, [conn, applySnapshot]); useEffect(() => { - agent.connection?.getMessages().then((history) => setMessages(history.flatMap(toChatMessage))); - }, [agent.connection]); - - agent.useEvent("event", (event) => { - if (event.type === "agent_start") setRunning(true); - if (event.type === "agent_settled") setRunning(false); - if (event.type === "tool_execution_start") setTool(event.toolName); - if (event.type === "tool_execution_end") setTool(null); - if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") { - const { delta } = event.assistantMessageEvent; - setStreaming((text) => text + delta); - } - if (event.type === "message_end") { - setStreaming(""); - setMessages((current) => [...current, ...toChatMessage(event.message)]); - } + void watch(); + }, [watch]); + + agent.useEvent("pi.events", ({ seq, events }) => { + // A missing batch means missed events, so start again from a new snapshot. + if (seq !== 0 && seq !== lastSeq.current + 1) return void watch(); + lastSeq.current = seq; + for (const event of events) applyEvent(event); }); + function applyEvent(event: AgentEvent) { + switch (event.type) { + case "snapshot": + return applySnapshot(event); + case "run_start": + return setRunning(true); + case "run_end": + return setRunning(false); + case "tool_execution_start": + return setTool(event.toolName); + case "tool_execution_end": + return setTool(null); + case "message_start": + if (event.message.role === "assistant") setStreaming(textOf(event.message)); + return; + case "message_update": + for (const change of event.changes) { + if (change.type === "text_delta") setStreaming((text) => text + change.delta); + } + return; + case "message_end": + setStreaming(""); + setMessages((current) => [...current, ...toChatMessages(event.entry)]); + return; + } + } + return { messages, streaming, tool, running, - ready: agent.connection !== null, - send: (text: string) => agent.connection?.prompt(text), - stop: () => agent.connection?.abort(), + ready: conn !== null && rootId !== null, + // Starts a run without waiting for it. The events show the answer. + send: (text: string) => { + if (conn && rootId !== null) return conn.conversation.submit(rootId, { type: "input", content: text }); + }, + stop: () => { + if (conn && rootId !== null) return conn.conversation.abort(rootId); + }, }; } -function toChatMessage(message: { role: string }): ChatMessage[] { - if (message.role !== "user" && message.role !== "assistant") return []; - const { role, content } = message as UserMessage | AssistantMessage; - const text = - typeof content === "string" - ? content - : content.flatMap((part) => (part.type === "text" ? [part.text] : [])).join(""); - return text ? [{ id: crypto.randomUUID(), role, text }] : []; +function toChatMessages(entry: EntryRecord): ChatMessage[] { + return (entry.model ?? []).flatMap((message, index): ChatMessage[] => { + if (message.role !== "user" && message.role !== "assistant") return []; + const text = textOf(message); + return text ? [{ id: `${entry.id}:${index}`, role: message.role, text }] : []; + }); +} + +function textOf(message: UserMessage | AssistantMessage): string { + if (typeof message.content === "string") return message.content; + let text = ""; + for (const block of message.content) if (block.type === "text") text += block.text; + return text; } diff --git a/vendor/agents/examples/docs/pi-durable/server.ts b/vendor/agents/examples/docs/sandboxes/agentos.ts similarity index 71% rename from vendor/agents/examples/docs/pi-durable/server.ts rename to vendor/agents/examples/docs/sandboxes/agentos.ts index db4c90f0..f6fe1396 100644 --- a/vendor/agents/examples/docs/pi-durable/server.ts +++ b/vendor/agents/examples/docs/sandboxes/agentos.ts @@ -1,17 +1,17 @@ import { createRegistry } from "@earendil-works/pi-durable"; import { CodingTools } from "@earendil-works/pi-durable/tools"; -import { piDurable } from "@rivet-dev/pi/durable"; -import { e2bProvider } from "@rivet-dev/sandbox-adapter/e2b"; +import { pi } from "@rivet-dev/pi"; +import { agentOSProvider } from "@rivet-dev/sandbox-adapter/agentos"; import { setup } from "rivetkit"; // read, write, edit, and bash, running in the sandbox const extensions = createRegistry(); extensions.install(CodingTools); -const agent = piDurable({ +const agent = pi({ model: "anthropic/claude-opus-5-5", registry: extensions, - sandbox: e2bProvider(), + sandbox: agentOSProvider(), }); export const registry = setup({ use: { agent } }); diff --git a/vendor/agents/examples/docs/sandboxes/daytona.ts b/vendor/agents/examples/docs/sandboxes/daytona.ts index 32603535..595f790f 100644 --- a/vendor/agents/examples/docs/sandboxes/daytona.ts +++ b/vendor/agents/examples/docs/sandboxes/daytona.ts @@ -1,9 +1,16 @@ +import { createRegistry } from "@earendil-works/pi-durable"; +import { CodingTools } from "@earendil-works/pi-durable/tools"; import { pi } from "@rivet-dev/pi"; import { daytonaProvider } from "@rivet-dev/sandbox-adapter/daytona"; import { setup } from "rivetkit"; +// read, write, edit, and bash, running in the sandbox +const extensions = createRegistry(); +extensions.install(CodingTools); + const agent = pi({ model: "anthropic/claude-opus-5-5", + registry: extensions, sandbox: daytonaProvider(), }); diff --git a/vendor/agents/examples/docs/sandboxes/e2b.ts b/vendor/agents/examples/docs/sandboxes/e2b.ts index cf906ef6..79ae99c8 100644 --- a/vendor/agents/examples/docs/sandboxes/e2b.ts +++ b/vendor/agents/examples/docs/sandboxes/e2b.ts @@ -1,9 +1,16 @@ +import { createRegistry } from "@earendil-works/pi-durable"; +import { CodingTools } from "@earendil-works/pi-durable/tools"; import { pi } from "@rivet-dev/pi"; import { e2bProvider } from "@rivet-dev/sandbox-adapter/e2b"; import { setup } from "rivetkit"; +// read, write, edit, and bash, running in the sandbox +const extensions = createRegistry(); +extensions.install(CodingTools); + const agent = pi({ model: "anthropic/claude-opus-5-5", + registry: extensions, sandbox: e2bProvider({ template: "base" }), }); diff --git a/vendor/agents/examples/docs/sandboxes/no-sandbox.ts b/vendor/agents/examples/docs/sandboxes/no-sandbox.ts new file mode 100644 index 00000000..e66a717b --- /dev/null +++ b/vendor/agents/examples/docs/sandboxes/no-sandbox.ts @@ -0,0 +1,17 @@ +import { createRegistry, defineExtension } from "@earendil-works/pi-durable"; +import { createEditTool, createReadTool, createWriteTool } from "@earendil-works/pi-durable/tools"; +import { pi } from "@rivet-dev/pi"; +import { setup } from "rivetkit"; + +// read, write, and edit. With no sandbox, the files live in the Actor's own database. +const extensions = createRegistry(); +extensions.install(defineExtension({ name: "files", tools: [createReadTool(), createWriteTool(), createEditTool()] })); + +const agent = pi({ + model: "anthropic/claude-opus-5-5", + registry: extensions, +}); + +export const registry = setup({ use: { agent } }); + +registry.start(); diff --git a/vendor/agents/examples/docs/schedules/client.ts b/vendor/agents/examples/docs/schedules/client.ts index b9306e08..0d7958f4 100644 --- a/vendor/agents/examples/docs/schedules/client.ts +++ b/vendor/agents/examples/docs/schedules/client.ts @@ -5,8 +5,13 @@ const client = createClient(); const agent = client.agent.getOrCreate(["acme/app", "morning-triage"]); // The first call creates the agent, and onCreate sets its cron. -await agent.resolve(); +const root = await agent.harness.root(); -// Any time after a run, read the latest summary. -const summary = await agent.getLastAssistantText(); -console.log(summary ?? "No run yet. The first one starts at 9:00 UTC."); +// Any time after a run, read the latest summary: the last message of the conversation. +const { messages } = await agent.conversation.context(root.id); +const last = messages.at(-1); +let summary = ""; +if (last?.role === "assistant") { + for (const block of last.content) if (block.type === "text") summary += block.text; +} +console.log(summary || "No run yet. The first one starts at 9:00 UTC."); diff --git a/vendor/agents/examples/docs/schedules/server.ts b/vendor/agents/examples/docs/schedules/server.ts index c0193cc6..e21c9b4c 100644 --- a/vendor/agents/examples/docs/schedules/server.ts +++ b/vendor/agents/examples/docs/schedules/server.ts @@ -1,9 +1,15 @@ +import { createRegistry } from "@earendil-works/pi-durable"; +import { CodingTools } from "@earendil-works/pi-durable/tools"; import { pi } from "@rivet-dev/pi"; import { e2bProvider } from "@rivet-dev/sandbox-adapter/e2b"; import { setup } from "rivetkit"; +const extensions = createRegistry(); +extensions.install(CodingTools); + const agent = pi({ model: "anthropic/claude-sonnet-5-5", + registry: extensions, sandbox: e2bProvider(), onCreate: async (c) => { const [repo] = c.key; diff --git a/vendor/agents/examples/docs/scoped-access/server.ts b/vendor/agents/examples/docs/scoped-access/server.ts index 7f2a40f0..ffc5274d 100644 --- a/vendor/agents/examples/docs/scoped-access/server.ts +++ b/vendor/agents/examples/docs/scoped-access/server.ts @@ -1,15 +1,14 @@ import { Type } from "@earendil-works/pi-ai"; -import { defineTool } from "@earendil-works/pi-coding-agent"; +import { createRegistry, defineExtension, defineTool } from "@earendil-works/pi-durable"; import { pi } from "@rivet-dev/pi"; import { setup } from "rivetkit"; import { admin, report } from "./report"; const shareReport = defineTool({ name: "share_report", - label: "Share report", description: "Publish a report and return a link anyone can open for the next hour.", parameters: Type.Object({ title: Type.String(), body: Type.String() }), - async execute(_toolCallId, { title, body }) { + execute: async ({ title, body }) => { const handle = await admin.report.create([crypto.randomUUID()], { input: { title, body } }); const { token } = await handle.issueToken({ subject: "shared-report", expiresIn: 3600 }); const url = `https://app.example.com/reports/${await handle.resolve()}#${token}`; @@ -17,7 +16,10 @@ const shareReport = defineTool({ }, }); -const agent = pi({ model: "anthropic/claude-opus-5-5", customTools: [shareReport] }); +const extensions = createRegistry(); +extensions.install(defineExtension({ name: "reports", tools: [shareReport] })); + +const agent = pi({ model: "anthropic/claude-opus-5-5", registry: extensions }); export const registry = setup({ use: { agent, report } }); diff --git a/vendor/agents/examples/docs/session-lifecycle/client.ts b/vendor/agents/examples/docs/session-lifecycle/client.ts index 2937066e..8a4ac4ec 100644 --- a/vendor/agents/examples/docs/session-lifecycle/client.ts +++ b/vendor/agents/examples/docs/session-lifecycle/client.ts @@ -4,16 +4,39 @@ import type { registry } from "../quickstart/server"; const client = createClient(); const conn = client.agent.getOrCreate(["user-123"]).connect(); -conn.on("event", (event) => { - if (event.type === "turn_start") console.log("\n[turn]"); - if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") { - process.stdout.write(event.assistantMessageEvent.delta); - } - if (event.type === "turn_end" && event.message.role === "assistant") { - console.log(`\n[${event.message.usage.totalTokens} tokens]`); +// The text of the current answer printed so far. +let printed = ""; + +conn.on("pi.events", ({ events }) => { + for (const event of events) { + if (event.type === "run_start") console.log("[run]"); + if (event.type === "message_update") { + for (const change of event.changes) { + if (change.type === "text_delta") { + process.stdout.write(change.delta); + printed += change.delta; + } + } + } + if (event.type === "message_end") { + // A short answer can arrive whole here, with no text_delta before it. + const message = event.entry.model?.[0]; + if (message?.role === "assistant") { + const text = message.content + .flatMap((block) => (block.type === "text" ? [block.text] : [])) + .join(""); + process.stdout.write(text.slice(printed.length)); + } + printed = ""; + } + if (event.type === "tool_execution_start") console.log(`\n> ${event.toolName}`); + if (event.type === "compaction_start") console.log("\n[compacting]"); + if (event.type === "run_end") console.log("\n[done]"); } - if (event.type === "compaction_start") console.log("\n[compacting]"); }); -await conn.prompt("Write an isPalindrome function in TypeScript, add tests for it, and run them."); +const root = await conn.harness.root(); +await conn.conversation.watchEvents(root.id); + +await conn.prompt("Write an isPalindrome function in TypeScript to palindrome.ts, and add tests for it in palindrome.test.ts."); await conn.dispose(); diff --git a/vendor/agents/examples/docs/subagents/client.ts b/vendor/agents/examples/docs/subagents/client.ts index caa1f2e3..1bb3f395 100644 --- a/vendor/agents/examples/docs/subagents/client.ts +++ b/vendor/agents/examples/docs/subagents/client.ts @@ -4,7 +4,7 @@ import type { registry } from "./server"; const client = createClient(); const support = client.support.getOrCreate(["ticket-4821"]); -await support.prompt( +const result = await support.prompt( "Order 1042 shows as delivered but never arrived, and the tracking page throws a 500 error. Repo: https://github.com/acme/storefront", ); -console.log(await support.getLastAssistantText()); +console.log(result.status === "done" ? result.text : `Unanswered: ${result.reason}`); diff --git a/vendor/agents/examples/docs/subagents/get-order.ts b/vendor/agents/examples/docs/subagents/get-order.ts index 1a133c9f..72fbfafe 100644 --- a/vendor/agents/examples/docs/subagents/get-order.ts +++ b/vendor/agents/examples/docs/subagents/get-order.ts @@ -1,13 +1,14 @@ import { Type } from "@earendil-works/pi-ai"; -import { defineTool } from "@earendil-works/pi-coding-agent"; +import { defineTool } from "@earendil-works/pi-durable"; export const getOrder = defineTool({ name: "get_order", - label: "Get order", description: "Look up an order's status by its id.", parameters: Type.Object({ orderId: Type.String() }), - async execute(_toolCallId, { orderId }, signal) { - const order = await fetchOrder(orderId, signal); + // A lookup changes nothing, so running it twice is harmless. + replay: "safe", + execute: async ({ orderId }, _api, context) => { + const order = await fetchOrder(orderId, context.abortSignal); const text = `Status: ${order.status}. Total: $${order.total}.`; return { content: [{ type: "text", text }], details: order }; }, diff --git a/vendor/agents/examples/docs/subagents/server.ts b/vendor/agents/examples/docs/subagents/server.ts index 78c72c18..f11055b1 100644 --- a/vendor/agents/examples/docs/subagents/server.ts +++ b/vendor/agents/examples/docs/subagents/server.ts @@ -1,8 +1,12 @@ +import { createRegistry, defineExtension } from "@earendil-works/pi-durable"; import { pi } from "@rivet-dev/pi"; import { setup } from "rivetkit"; import { askSpecialist, engineering, orders } from "./specialists"; -const support = pi({ model: "anthropic/claude-opus-5-5", customTools: [askSpecialist] }); +const extensions = createRegistry(); +extensions.install(defineExtension({ name: "support", tools: [askSpecialist] })); + +const support = pi({ model: "anthropic/claude-opus-5-5", registry: extensions }); export const registry = setup({ use: { support, orders, engineering } }); diff --git a/vendor/agents/examples/docs/subagents/specialists.ts b/vendor/agents/examples/docs/subagents/specialists.ts index 32cbe98c..dd974cbf 100644 --- a/vendor/agents/examples/docs/subagents/specialists.ts +++ b/vendor/agents/examples/docs/subagents/specialists.ts @@ -1,28 +1,43 @@ import { Type } from "@earendil-works/pi-ai"; -import { defineTool } from "@earendil-works/pi-coding-agent"; +import { createRegistry, defineExtension, defineTool, ROOT_CONVERSATION_ID } from "@earendil-works/pi-durable"; +import { CodingTools } from "@earendil-works/pi-durable/tools"; import { pi } from "@rivet-dev/pi"; import { e2bProvider } from "@rivet-dev/sandbox-adapter/e2b"; import type { Registry } from "rivetkit"; import { createClient } from "rivetkit/client"; import { getOrder } from "./get-order"; -export const orders = pi({ model: "openai/gpt-5.4-mini", customTools: [getOrder] }); +const orderTools = createRegistry(); +orderTools.install(defineExtension({ name: "orders", tools: [getOrder] })); -export const engineering = pi({ model: "anthropic/claude-opus-5-5", sandbox: e2bProvider() }); +export const orders = pi({ model: "openai/gpt-5.4-mini", registry: orderTools }); + +const codingTools = createRegistry(); +codingTools.install(CodingTools); + +export const engineering = pi({ model: "anthropic/claude-opus-5-5", registry: codingTools, sandbox: e2bProvider() }); export const askSpecialist = defineTool({ name: "ask_specialist", - label: "Ask a specialist", description: "Hand one focused question to a specialist and get its answer. Use orders for order status, refunds, and shipping. Use engineering for bugs that need someone to read or run the code.", parameters: Type.Object({ specialist: Type.Union([Type.Literal("orders"), Type.Literal("engineering")]), question: Type.String({ description: "Everything the specialist needs. It can't see this conversation." }), }), - async execute(_toolCallId, { specialist, question }, _signal, _onUpdate, ctx) { - const agent = client[specialist].getOrCreate([ctx.sessionManager.getSessionId()]); - await agent.prompt(question); - const answer = (await agent.getLastAssistantText()) ?? "The specialist finished without an answer."; + // A rerun reaches the same specialist Actor, through the memo below. + replay: "safe", + execute: async ({ specialist, question }, api, context) => { + // The memo is saved with this tool call, so a rerun gets the same id. + const id = await api.memo("specialist-id", crypto.randomUUID(), context); + const agent = client[specialist].getOrCreate([id]); + context.abortSignal?.addEventListener("abort", () => void agent.conversation.abort(ROOT_CONVERSATION_ID)); + + const result = await agent.prompt(question, { requestId: id }); + if (result.status === "unanswered") { + return { content: [{ type: "text", text: `The specialist did not answer: ${result.reason}` }], isError: true }; + } + const answer = result.text || "The specialist finished without an answer."; return { content: [{ type: "text", text: answer }], details: { specialist } }; }, }); diff --git a/vendor/agents/examples/docs/tsconfig.json b/vendor/agents/examples/docs/tsconfig.json index 12273ba0..7379a0d8 100644 --- a/vendor/agents/examples/docs/tsconfig.json +++ b/vendor/agents/examples/docs/tsconfig.json @@ -8,13 +8,13 @@ "include": ["**/*.ts", "**/*.tsx"], "exclude": [ "node_modules", - // These read Pi's `event` payloads on the client. RivetKit 2.3.23 infers + // These read Pi's `pi.events` payloads on the client. RivetKit 2.3.23 infers // client event payloads as `unknown` (`InferSchemaMap` of `event()`), // so they only type-check once RivetKit fixes that inference. Chat.tsx is // excluded because it imports useAgentChat.ts. "client-sdk/client.ts", "human-in-the-loop/ApprovalDialog.tsx", - "pi-durable/stream.ts", + "pi/stream.ts", "react-sdk/Chat.tsx", "react-sdk/useAgentChat.ts", "session-lifecycle/client.ts" diff --git a/vendor/agents/examples/docs/user-subscriptions/client.ts b/vendor/agents/examples/docs/user-subscriptions/client.ts index e1256668..c0f3d1e5 100644 --- a/vendor/agents/examples/docs/user-subscriptions/client.ts +++ b/vendor/agents/examples/docs/user-subscriptions/client.ts @@ -11,7 +11,6 @@ export async function saveLogin(userId: string, provider: string, credential: Cr // The agent's key starts with the user id, so it runs on that user's credential. export async function chat(userId: string, text: string) { - const agent = client.agent.getOrCreate([userId, "chat"]); - await agent.prompt(text); - return agent.getLastAssistantText(); + const result = await client.agent.getOrCreate([userId, "chat"]).prompt(text); + return result.status === "done" ? result.text : undefined; } diff --git a/vendor/agents/examples/docs/user-subscriptions/server.ts b/vendor/agents/examples/docs/user-subscriptions/server.ts index 029aa518..634d2fa5 100644 --- a/vendor/agents/examples/docs/user-subscriptions/server.ts +++ b/vendor/agents/examples/docs/user-subscriptions/server.ts @@ -1,9 +1,11 @@ +import { createRegistry } from "@earendil-works/pi-durable"; import { pi } from "@rivet-dev/pi"; import { type Registry, setup } from "rivetkit"; import { credentials } from "./credentials"; const agent = pi({ model: "anthropic/claude-opus-5-5", + registry: createRegistry(), credentials: (c) => { const client = c.client>(); return client.credentials.getOrCreate([c.key[0]]); diff --git a/vendor/agents/examples/docs/workflows/server.ts b/vendor/agents/examples/docs/workflows/server.ts index 5ffeb46c..1974aade 100644 --- a/vendor/agents/examples/docs/workflows/server.ts +++ b/vendor/agents/examples/docs/workflows/server.ts @@ -1,9 +1,15 @@ +import { createRegistry } from "@earendil-works/pi-durable"; +import { CodingTools } from "@earendil-works/pi-durable/tools"; import { pi } from "@rivet-dev/pi"; import { e2bProvider } from "@rivet-dev/sandbox-adapter/e2b"; import { type Registry, setup, workflow } from "@rivet-dev/workflows"; +const extensions = createRegistry(); +extensions.install(CodingTools); + const agent = pi({ model: "anthropic/claude-opus-5-5", + registry: extensions, sandbox: e2bProvider(), }); @@ -17,8 +23,10 @@ const flakyTest = workflow({ timeout: 10 * 60_000, run: async (step) => { const fixer = step.client().agent.getOrCreate([step.actorId]); - await fixer.abort(); - await fixer.prompt("Clone https://github.com/acme/app, run its test suite, and note any failing tests."); + // A retried attempt sends the same requestId, so it waits for the first attempt's run. + await fixer.prompt("Clone https://github.com/acme/app, run its test suite, and note any failing tests.", { + requestId: `${step.actorId}:first-run`, + }); }, }); @@ -29,9 +37,10 @@ const flakyTest = workflow({ timeout: 10 * 60_000, run: async (step) => { const fixer = step.client().agent.getOrCreate([step.actorId]); - await fixer.abort(); - await fixer.prompt("Run the suite again. Which failures happened both times?"); - step.state.report = (await fixer.getLastAssistantText()) ?? null; + const result = await fixer.prompt("Run the suite again. Which failures happened both times?", { + requestId: `${step.actorId}:second-run`, + }); + step.state.report = result.status === "done" ? (result.text ?? null) : `Unanswered: ${result.reason}`; }, }); },