- Deploy code that registers an actor to
- see it here.
+ Deploy code that registers an actor or
+ add a service to see it here.
diff --git a/frontend/src/app/compute-deploy.tsx b/frontend/src/app/compute-deploy.tsx
index 3e14538f75..bd43911faa 100644
--- a/frontend/src/app/compute-deploy.tsx
+++ b/frontend/src/app/compute-deploy.tsx
@@ -16,6 +16,7 @@ import { type HostedTarget, hostedUrl } from "@/components/mcp/scope";
import {
getAgentInstructionsPrompt,
getComputeAddendum,
+ getDurableStreamsServiceUrl,
type McpSetup,
type OnboardingTarget,
} from "@/content/agent-prompts";
@@ -75,6 +76,7 @@ export function useAgentInstructionsCode({
const secretToken = useRivetDsn({ kind: "secret", endpoint });
const mcp = useMcpSetup();
const namespace = useEngineCompatDataProvider().engineNamespace;
+ const durableStreamsServiceUrl = useDurableStreamsServiceUrl();
return getAgentInstructionsPrompt({
providerStr,
@@ -89,9 +91,19 @@ export function useAgentInstructionsCode({
cliDeploy: provider === "rivet",
target,
mcp,
+ durableStreamsServiceUrl,
});
}
+// Where the managed Durable Streams service for this namespace is served on
+// Rivet Cloud. Self-hosted flavors run the worker themselves, so there is no
+// fixed URL to hand out.
+export function useDurableStreamsServiceUrl(): string | undefined {
+ const namespace = useEngineCompatDataProvider().engineNamespace;
+ if (!features.compute) return undefined;
+ return getDurableStreamsServiceUrl(getRivetRunUrl(namespace));
+}
+
// The MCP setup the copy-prompt should instruct the agent to perform. The hosted
// connection needs the user to approve an OAuth window, so the agent has to hand
// that step back; the local stdio server it can wire up itself.
diff --git a/frontend/src/app/engine-namespace-landing.tsx b/frontend/src/app/engine-namespace-landing.tsx
index 1054419f7a..1420b255b4 100644
--- a/frontend/src/app/engine-namespace-landing.tsx
+++ b/frontend/src/app/engine-namespace-landing.tsx
@@ -5,8 +5,8 @@ import { Button, H1, ScrollArea, SmallText, WithTooltip } from "@/components";
import { useEngineNamespaceDataProvider } from "@/components/actors";
import { NoProvidersAlert } from "@/components/actors/no-providers-alert";
import { VisibilitySensor } from "@/components/visibility-sensor";
-import { AddComponentButton, AddComponentCard } from "./add-component-card";
import { ActorBuildCard, ActorGridCardSkeleton } from "./actors-grid";
+import { AddComponentButton, AddComponentCard } from "./add-component-card";
// Engine (OSS / enterprise) namespace landing shown when no Actor name is
// selected. This is the engine counterpart to the cloud `ActorsGrid`; keep the
@@ -79,7 +79,7 @@ export function EngineNamespaceLanding() {
+ );
+}
+
+// Durable Streams is a service, so "run locally" means starting the bundled dev
+// server and pointing the client SDK at it. There is no project to scaffold, so
+// the prompt is the generic one (no compute addendum) on every flavor.
+function DurableStreamsRunLocally() {
+ const code = useAgentInstructionsCode({ target: "durable-streams" });
+ const copy = getOnboardingTargetCopy("durable-streams");
+ return (
+
+
+
+
+
+
+
Start the dev server
+
+ Runs a local Rivet control plane and the Durable Streams
+ worker. Streams are served at{" "}
+
+ {getDurableStreamUrl(
+ DURABLE_STREAMS_LOCAL_URL,
+ "",
+ )}
+
+ .
-
- {copy.quickstartDescription}
+
+
+
+
+
+
+
Connect a client
+
+ Install the official client and append to your first
+ stream.
+
+
+
+
-
+ );
+}
+
+function DurableStreamsClientSnippet({ serviceUrl }: { serviceUrl: string }) {
+ const code = getDurableStreamsClientSnippet(serviceUrl);
+ return (
+
+ {[
+ code}
+ className="m-0"
>
-
+ ,
+ ]}
+
+ );
+}
+
+// Final step on the Durable Streams path. On Rivet Cloud the service is
+// managed per namespace, so this hands out the service URL. Self-hosted
+// flavors run the worker container against their own control plane instead.
+// Also rendered by the "Add Durable Streams" sheet from namespace settings.
+function DurableStreamsConnect() {
+ const code = useAgentInstructionsCode({ target: "durable-streams" });
+ const serviceUrl = useDurableStreamsServiceUrl();
+ return (
+
+ The worker runs your streams and connects to this
+ namespace.{" "}
+
+ RIVET_ENDPOINT
+ {" "}
+ contains an admin credential, so keep it out of source
+ control and browser code.
+
+
+
+
+
+
+
+
Point your client at it
+
+ Streams are served at{" "}
+
+ http://<host>:8642/durable-streams/v1/stream/<path>
+
+ . See the{" "}
+
+ integration guide
+ {" "}
+ for the control plane flags Durable Streams needs.
+
+ :8642/durable-streams/"}
+ />
+
+
+ >
);
}
diff --git a/frontend/src/app/settings-drawer.tsx b/frontend/src/app/settings-drawer.tsx
index 380dee2d8a..4ed5b34512 100644
--- a/frontend/src/app/settings-drawer.tsx
+++ b/frontend/src/app/settings-drawer.tsx
@@ -5,6 +5,7 @@ import {
faClose,
faCreditCard,
faGear,
+ faPuzzlePiece,
faRivet,
faSliders,
faSparkles,
@@ -30,6 +31,7 @@ import { BillingUsageGauge } from "./billing/billing-usage-gauge";
import { PoolSwitcher, poolHeaderText, resolvePoolName } from "./pool-switcher";
import { BillingPanel } from "./settings-pages/billing-panel";
import { NamespaceComputeContent } from "./settings-pages/namespace-compute";
+import { NamespaceServicesContent } from "./settings-pages/namespace-services";
import {
NamespaceAdvancedContent,
NamespaceSettingsContent,
@@ -42,6 +44,7 @@ import { WhatsNewPanel } from "./settings-pages/whats-new-panel";
export type SettingsTab =
| "profile"
| "settings"
+ | "services"
| "compute"
| "advanced"
| "billing"
@@ -64,6 +67,7 @@ const NAV_SECTIONS: Array<{
label: "Namespace",
items: [
{ key: "settings", label: "Settings", icon: faGear },
+ { key: "services", label: "Services", icon: faPuzzlePiece },
{ key: "compute", label: "Compute", icon: faRivet },
{ key: "advanced", label: "Advanced", icon: faSliders },
],
@@ -96,6 +100,11 @@ const TAB_META: Record = {
? "Connect your RivetKit application to Rivet Cloud. Use your cloud of choice to run Rivet Actors."
: "Connect providers and runners to this namespace. Use your cloud of choice to run Rivet Actors.",
},
+ services: {
+ title: "Services",
+ description:
+ "Managed services you can connect to this namespace, such as Durable Streams.",
+ },
compute: {
title: "Compute",
description: "Manage the Rivet Compute deployment for this namespace.",
@@ -366,6 +375,8 @@ function TabContent({ tab }: { tab: SettingsTab }) {
return ;
case "settings":
return ;
+ case "services":
+ return ;
case "compute":
return ;
case "advanced":
@@ -660,12 +671,50 @@ function CloudAdvancedTabBody() {
return ;
}
+function ServicesTabBody() {
+ if (!features.platform) {
+ return ;
+ }
+ return ;
+}
+
+function EngineNamespaceServices() {
+ return useEngineNamespaceReady() ? (
+
+ ) : (
+
+ );
+}
+
+function CloudServicesTabBody() {
+ const namespaceMatch = useMatch({
+ from: "/_context/orgs/$organization/projects/$project/ns/$namespace",
+ shouldThrow: false,
+ });
+
+ if (!namespaceMatch) {
+ return (
+
+ );
+ }
+ if (!namespaceMatch.loaderData) {
+ return ;
+ }
+ return ;
+}
+
export function settingsParamToTab(
param: string | undefined,
): SettingsTab | null {
switch (param) {
case "profile":
case "settings":
+ case "services":
case "advanced":
case "billing":
case "organization":
diff --git a/frontend/src/app/settings-pages/namespace-services.tsx b/frontend/src/app/settings-pages/namespace-services.tsx
new file mode 100644
index 0000000000..1fcfd373ff
--- /dev/null
+++ b/frontend/src/app/settings-pages/namespace-services.tsx
@@ -0,0 +1,147 @@
+import { faExternalLink, faPlus, Icon } from "@rivet-gg/icons";
+import { useInfiniteQuery } from "@tanstack/react-query";
+import {
+ Button,
+ cn,
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+ Ping,
+} from "@/components";
+import { useEngineCompatDataProvider } from "@/components/actors";
+import {
+ getProduct,
+ getProductDocsUrl,
+ ProductMark,
+} from "@/components/products/product-picker";
+import { SettingsCard } from "./settings-card";
+
+// Services are Rivet-run workers the user connects to a namespace rather than
+// code they deploy. Each one registers well-known actor names, so a namespace
+// "has" the service once any of those names shows up in its builds. Setup
+// instructions live in the docs so they are not duplicated here.
+const SERVICES = [
+ {
+ product: getProduct("durable-streams"),
+ actorNames: ["durableStream"],
+ },
+];
+
+function openDocs(target: (typeof SERVICES)[number]["product"]["target"]) {
+ window.open(getProductDocsUrl(target), "_blank", "noopener,noreferrer");
+}
+
+export function NamespaceServicesContent() {
+ const dataProvider = useEngineCompatDataProvider();
+ const { data: builds = [] } = useInfiniteQuery(
+ dataProvider.buildsQueryOptions(),
+ );
+
+ return (
+
+ );
+}
diff --git a/frontend/src/components/products/product-picker.tsx b/frontend/src/components/products/product-picker.tsx
index e360ade462..a241a5c515 100644
--- a/frontend/src/components/products/product-picker.tsx
+++ b/frontend/src/components/products/product-picker.tsx
@@ -1,4 +1,5 @@
import type { ReactNode } from "react";
+import { cn } from "@/components/lib/utils";
import { Badge } from "@/components/ui/badge";
import {
getOnboardingTargetCopy,
@@ -7,8 +8,14 @@ import {
import { features } from "@/lib/features";
import { publicUrl } from "@/lib/utils";
+// Products are SDKs the user builds on and deploys themselves. Services are
+// managed by Rivet: the user only points a client at them, so they onboard
+// through a different path and are listed under their own heading.
+export type ProductSection = "products" | "services";
+
type Product = {
target: OnboardingTarget;
+ section: ProductSection;
label: string;
description: string;
markFileName: string;
@@ -19,6 +26,7 @@ type Product = {
const PRODUCTS: Product[] = [
{
target: "actor",
+ section: "products",
label: "Actors",
description: "The primitive for realtime, stateful workloads",
markFileName: "actors-mark.svg",
@@ -26,6 +34,7 @@ const PRODUCTS: Product[] = [
},
{
target: "agent-os",
+ section: "products",
label: "agentOS",
description: "Hand every agent a computer of its own",
markFileName: "agentos-mark.svg",
@@ -33,6 +42,7 @@ const PRODUCTS: Product[] = [
},
{
target: "workflows",
+ section: "products",
label: "Workflows",
description: "Write multi-step operations that survive restarts",
markFileName: "workflows-mark.svg",
@@ -40,32 +50,92 @@ const PRODUCTS: Product[] = [
},
{
target: "dynamic-apps",
+ section: "products",
label: "Dynamic Apps",
description: "Deploy AI-generated apps for your users",
markFileName: "dynamic-apps-mark.svg",
badge: "Preview",
isAvailable: () => true,
},
+ {
+ target: "durable-streams",
+ section: "services",
+ label: "Durable Streams",
+ description: "Real-time streams with durable, replayable history",
+ markFileName: "durable-streams-mark.svg",
+ isAvailable: () => true,
+ },
+];
+
+const SECTIONS: { id: ProductSection; label: string }[] = [
+ { id: "products", label: "Products" },
+ { id: "services", label: "Services" },
];
export function getAvailableProducts() {
return PRODUCTS.filter((p) => p.isAvailable());
}
+export function getProductSections() {
+ const available = getAvailableProducts();
+ return SECTIONS.map((section) => ({
+ ...section,
+ products: available.filter((p) => p.section === section.id),
+ })).filter((section) => section.products.length > 0);
+}
+
+export function getProduct(target: OnboardingTarget) {
+ const product = PRODUCTS.find((p) => p.target === target);
+ if (!product) {
+ throw new Error(`Unknown product: ${target}`);
+ }
+ return product;
+}
+
export function getProductDocsUrl(target: OnboardingTarget) {
return getOnboardingTargetCopy(target).quickstartUrl;
}
-export function ProductMark({ fileName }: { fileName: string }) {
- return (
+// Product marks ship as tiles (logo inside a colored rounded square with an
+// inner ring). Service marks are the bare third-party logo, so give them a
+// neutral tile here to match the products' visual weight.
+export function ProductMark({
+ fileName,
+ section,
+ size = "md",
+}: {
+ fileName: string;
+ section: ProductSection;
+ /** `md` is the picker card size; `sm` fits inline in menus and text. */
+ size?: "sm" | "md";
+}) {
+ const isService = section === "services";
+ const img = (
);
+ if (!isService) {
+ return img;
+ }
+ return (
+
+ {img}
+
+ );
}
export function ProductCard({
@@ -122,17 +192,34 @@ export function ProductPicker({
diff --git a/frontend/src/content/agent-prompts.test.ts b/frontend/src/content/agent-prompts.test.ts
index 70f4ddc9d4..a3e432df75 100644
--- a/frontend/src/content/agent-prompts.test.ts
+++ b/frontend/src/content/agent-prompts.test.ts
@@ -83,6 +83,55 @@ describe("onboarding product prompts", () => {
expect(prompt).not.toContain("Drive actors via the inspector HTTP API");
});
+ it("treats Durable Streams as a client-only service", () => {
+ const prompt = getAgentInstructionsPrompt({
+ ...agentPromptOptions,
+ target: "durable-streams",
+ });
+
+ expect(prompt).toContain("# Durable Streams Setup");
+ expect(prompt).toContain("npx @rivet-dev/services dev");
+ expect(prompt).toContain("npm install @durable-streams/client");
+ expect(prompt).toContain(
+ "http://127.0.0.1:8642/durable-streams/v1/stream/demo",
+ );
+ expect(prompt).toContain(
+ "https://rivet.dev/actors/integrations/durable-streams",
+ );
+ expect(prompt).not.toContain("npm install rivetkit");
+ expect(prompt).not.toContain("registry.listen");
+ expect(prompt).not.toContain("RIVET_PUBLIC_ENDPOINT");
+ });
+
+ it("hands out the managed service URL for Durable Streams on Rivet Cloud", () => {
+ const prompt = getAgentInstructionsPrompt({
+ ...agentPromptOptions,
+ target: "durable-streams",
+ durableStreamsServiceUrl:
+ "https://onboarding-test.staging.rivet.run/durable-streams/",
+ });
+
+ expect(prompt).toContain(
+ "https://onboarding-test.staging.rivet.run/durable-streams/v1/stream/",
+ );
+ expect(prompt).toContain(
+ "managed service in the `onboarding-test` namespace",
+ );
+ expect(prompt).not.toContain("docker run");
+ expect(prompt).not.toContain("secret_test");
+ });
+
+ it("runs the Durable Streams worker against the control plane when self-hosting", () => {
+ const prompt = getAgentInstructionsPrompt({
+ ...agentPromptOptions,
+ target: "durable-streams",
+ });
+
+ expect(prompt).toContain("rivetdev/services");
+ expect(prompt).toContain('RIVET_ENDPOINT="secret_test"');
+ expect(prompt).not.toContain("rivet.run/durable-streams/");
+ });
+
it("replaces Actor verification with an app URL for Dynamic Apps on Compute", () => {
const prompt = getComputeAddendum({
...computePromptOptions,
diff --git a/frontend/src/content/agent-prompts.ts b/frontend/src/content/agent-prompts.ts
index 3be81ebe07..dca44ee16a 100644
--- a/frontend/src/content/agent-prompts.ts
+++ b/frontend/src/content/agent-prompts.ts
@@ -2,7 +2,47 @@ export type OnboardingTarget =
| "actor"
| "agent-os"
| "workflows"
- | "dynamic-apps";
+ | "dynamic-apps"
+ | "durable-streams";
+
+// Durable Streams is a managed service rather than user code, so the local
+// dev server and the client SDK are fixed and the prompts can name them.
+export const DURABLE_STREAMS_DOCS_URL =
+ "https://rivet.dev/actors/integrations/durable-streams";
+export const DURABLE_STREAMS_DEV_COMMAND = "npx @rivet-dev/services dev";
+export const DURABLE_STREAMS_LOCAL_URL =
+ "http://127.0.0.1:8642/durable-streams/";
+export const DURABLE_STREAMS_CLIENT_PACKAGE = "@durable-streams/client";
+
+// Path under a Rivet Run origin where the managed Durable Streams service is
+// served, e.g. `https://.rivet.run/durable-streams/`.
+export function getDurableStreamsServiceUrl(rivetRunUrl: string) {
+ return `${rivetRunUrl.replace(/\/?$/, "/")}durable-streams/`;
+}
+
+// The Durable Streams protocol mounts streams at `v1/stream/` under the
+// service origin.
+export function getDurableStreamUrl(serviceUrl: string, streamPath: string) {
+ return `${serviceUrl.replace(/\/?$/, "/")}v1/stream/${streamPath}`;
+}
+
+export function getDurableStreamsClientSnippet(serviceUrl: string) {
+ return `import { DurableStream } from "${DURABLE_STREAMS_CLIENT_PACKAGE}";
+
+const stream = await DurableStream.create({
+ url: "${getDurableStreamUrl(serviceUrl, "demo")}",
+ contentType: "application/json",
+});
+
+await stream.append(JSON.stringify({ message: "hello" }));
+
+const res = await stream.stream<{ message: string }>();
+res.subscribeJson(async (batch) => {
+ for (const item of batch.items) {
+ console.log(item.message);
+ }
+});`;
+}
const onboardingTargetCopy: Record<
OnboardingTarget,
@@ -35,6 +75,12 @@ const onboardingTargetCopy: Record<
"Build a Dynamic Apps host and deploy a sample app by hand.",
quickstartUrl: "https://rivet.dev/dynamic-apps/docs/quickstart/",
},
+ "durable-streams": {
+ promptObject: "a Durable Streams client",
+ quickstartDescription:
+ "Run Durable Streams locally and connect a client by hand.",
+ quickstartUrl: DURABLE_STREAMS_DOCS_URL,
+ },
};
export function getOnboardingTargetCopy(target: OnboardingTarget) {
@@ -172,6 +218,100 @@ ${mcpSection}## Step 4: Verify the workflow end-to-end
Report the workflow host URL, command used, action or queue invoked, observed result, and any remaining setup the user must complete.`;
}
+// Durable Streams ships as a service, so there is no user code to deploy. The
+// prompt walks the agent through the local dev server, the client SDK, and then
+// either the managed service URL (Rivet Cloud) or running the worker container
+// against the user's own control plane (self-hosted).
+function getDurableStreamsPrompt({
+ secretToken,
+ namespace,
+ durableStreamsServiceUrl,
+ mcpSection,
+}: {
+ secretToken: string;
+ namespace?: string;
+ durableStreamsServiceUrl?: string;
+ mcpSection: string;
+}) {
+ const localStreamUrl = getDurableStreamUrl(
+ DURABLE_STREAMS_LOCAL_URL,
+ "",
+ );
+ const deploySteps = durableStreamsServiceUrl
+ ? `Durable Streams runs as a managed service in the \`${namespace ?? "selected"}\` namespace on Rivet Cloud. There is nothing to build or deploy.
+
+1. Point the client at the managed service instead of the local server. Streams live at \`${getDurableStreamUrl(durableStreamsServiceUrl, "")}\`.
+2. Read the service URL from an environment variable (for example \`DURABLE_STREAMS_URL\`) so local development keeps using \`${DURABLE_STREAMS_LOCAL_URL}\` and production uses \`${durableStreamsServiceUrl}\`.
+3. Verify against the managed service: append one record and read it back with \`curl '${getDurableStreamUrl(durableStreamsServiceUrl, "demo")}?offset=-1'\`.`
+ : `Durable Streams runs as a single worker container that connects to the user's Rivet control plane.
+
+1. Run the worker next to the application and point it at the control plane. \`RIVET_ENDPOINT\` contains a secret admin credential, so write it to the platform's secret store or a local \`.env\` that is listed in \`.gitignore\`. Never commit it and never expose it to browser code:
+ \`\`\`bash
+ docker run -p 8642:8642 \\
+ -e RIVET_ENDPOINT="${secretToken}" \\
+ -e HOST=0.0.0.0 \\
+ rivetdev/services
+ \`\`\`
+2. Point the client at the worker. Streams live at \`http://:8642/durable-streams/v1/stream/\`. Read the URL from an environment variable (for example \`DURABLE_STREAMS_URL\`) so local development keeps using \`${DURABLE_STREAMS_LOCAL_URL}\`.
+3. Verify against the deployed worker: append one record and read it back with \`curl 'http://:8642/durable-streams/v1/stream/demo?offset=-1'\`.`;
+
+ return `# Durable Streams Setup
+
+Read the Durable Streams integration guide before changing the project: ${DURABLE_STREAMS_DOCS_URL}
+
+Durable Streams is an open standard for real-time streams with durable, replayable history. Rivet runs it as a service backed by Rivet Actors, so the project only needs a client. Do not scaffold Rivet Actors, a RivetKit registry, or a Dockerfile for this.
+
+## Step 1: Understand the project
+
+Determine whether the user wants a new project or wants to add Durable Streams to the existing application. Inspect the package manager, runtime, and any existing streaming or realtime code first. Good fits are agent session transcripts, CRDT sync, and change feeds that clients resume from an offset.
+
+## Step 2: Run Durable Streams locally
+
+Start the local Rivet control plane and the Durable Streams worker in a separate terminal:
+
+\`\`\`bash
+${DURABLE_STREAMS_DEV_COMMAND}
+\`\`\`
+
+Streams are then available at \`${localStreamUrl}\`. If the project already runs Rivet Actors with the RivetKit TypeScript SDK (2.3.12 or newer), Durable Streams is already served by that process at \`http://127.0.0.1:6420/durable-streams/v1/stream/\` and this command is not needed.
+
+## Step 3: Connect a client
+
+- Install the official client with the project's package manager: \`npm install ${DURABLE_STREAMS_CLIENT_PACKAGE}\`.
+- Create a stream and append to it. Keep the base URL in one place so it can point at production later:
+
+\`\`\`ts
+${getDurableStreamsClientSnippet(DURABLE_STREAMS_LOCAL_URL)}
+\`\`\`
+
+- Readers can catch up from offset \`-1\` (the beginning), resume from a saved offset, or tail live. Persist the offset from the \`Stream-Next-Offset\` header when the application needs to resume.
+- For structured data, JSON mode, Yjs, TanStack AI, or the Vercel AI SDK, follow https://durablestreams.com instead of inventing a wire format.
+
+## Step 4: Verify locally
+
+Create a stream with one record, append a second, and read both back over HTTP, then run the application's own read path:
+
+\`\`\`bash
+curl -i -X PUT -H 'content-type: application/json' --data '[{"message":"hello"}]' ${getDurableStreamUrl(DURABLE_STREAMS_LOCAL_URL, "demo")}
+curl -i -X POST -H 'content-type: application/json' --data '{"message":"world"}' ${getDurableStreamUrl(DURABLE_STREAMS_LOCAL_URL, "demo")}
+curl '${getDurableStreamUrl(DURABLE_STREAMS_LOCAL_URL, "demo")}?offset=-1'
+\`\`\`
+
+Report the commands run and the observed result.
+
+## Step 5: Deploy
+
+${deploySteps}
+
+${mcpSection}## If you get stuck
+
+Check ${DURABLE_STREAMS_DOCS_URL} and https://durablestreams.com. If that doesn't help, point the user at:
+- Discord: https://rivet.dev/discord
+- GitHub issues: https://github.com/rivet-dev/rivet-durable-streams
+
+Include in the report: symptoms, what was tried, the client version, and the stream URL in use.`;
+}
+
// The hosted connection authorizes against the user's Rivet account through a
// browser window, so the agent has to hand that step back. The local server is
// plain stdio and the agent can run it itself.
@@ -390,6 +530,7 @@ export function getAgentInstructionsPrompt({
cliDeploy,
target = "actor",
mcp,
+ durableStreamsServiceUrl,
}: {
providerStr: string;
publishableToken: string;
@@ -403,7 +544,19 @@ export function getAgentInstructionsPrompt({
cliDeploy?: boolean;
target?: OnboardingTarget;
mcp?: McpSetup;
+ // Managed Durable Streams service URL for this namespace on Rivet Cloud.
+ // Omitted when self-hosting, where the agent runs the worker itself.
+ durableStreamsServiceUrl?: string;
}) {
+ if (target === "durable-streams") {
+ return getDurableStreamsPrompt({
+ secretToken,
+ namespace,
+ durableStreamsServiceUrl,
+ mcpSection: mcp ? getMcpSection(mcp) : "",
+ });
+ }
+
const poolLine =
runnerName !== "default" ? `\n RIVET_POOL=${runnerName}` : "";
// Compute appends its own addendum with the same section; emitting it twice