diff --git a/apps/web/src/agent-deploy.test.ts b/apps/web/src/agent-deploy.test.ts index df152874f..1133b6116 100644 --- a/apps/web/src/agent-deploy.test.ts +++ b/apps/web/src/agent-deploy.test.ts @@ -4,6 +4,7 @@ import { agentDeploySourceAssetName, agentSlugFromSourceAssetName, buildAgentDefinitionJson, + buildScheduledRunBody, } from "./agent-deploy"; describe("buildAgentDefinitionJson", () => { @@ -34,6 +35,20 @@ describe("buildAgentDefinitionJson", () => { }); }); +describe("buildScheduledRunBody", () => { + test("asks the agent to mail the deploying person's address", () => { + const body = buildScheduledRunBody("alice@example.test"); + expect(body).toContain("alice@example.test"); + expect(body).toContain("`to` list"); + }); + + test("falls back to a bare reply instruction when no address is known", () => { + const body = buildScheduledRunBody(undefined); + expect(body).not.toContain("@"); + expect(body).toContain("Reply with the result."); + }); +}); + describe("agentSlugFromSourceAssetName", () => { test("recovers the slug agentDeploySourceAssetName wrapped", () => { expect(agentSlugFromSourceAssetName(agentDeploySourceAssetName("echo-bot"))).toBe("echo-bot"); diff --git a/apps/web/src/agent-deploy.ts b/apps/web/src/agent-deploy.ts index 3081eab53..fb55a3cfa 100644 --- a/apps/web/src/agent-deploy.ts +++ b/apps/web/src/agent-deploy.ts @@ -7,6 +7,7 @@ import { renderBundledWorkflowSourceTree } from "@corbits/workflows/client"; import { type } from "arktype"; import { WorkflowDeploymentResponse } from "@intx/types"; +import { reportError } from "@corbits/error-sink"; import { resolveExistingOffering } from "./onboarding/provider-connect-step"; import { isValidSlug, slugify } from "@/lib/slug"; @@ -17,6 +18,10 @@ const AssetCreatedShape = type({ id: "string" }); const AssetListShape = type({ id: "string", name: "string" }).array(); const GitTokenMintShape = type({ id: "string", secret: "string" }); const TenantDomainShape = type({ domain: "string" }); +// Same shape `session.ts`'s `fetchSession` parses; a person's refId is +// their better-auth user id, exactly what `threads-api.ts` builds a +// principal's mailbox address from. +const SessionUserShape = type({ user: { id: "string" } }); const PUSH_TOKEN_LIFETIME_MS = 10 * 60 * 1000; @@ -241,6 +246,48 @@ function cronPath(tenantId: string): string { return `/api/tenants/${encodeURIComponent(tenantId)}/cron`; } +/** The scheduled-run mail body: names the person to report to (when known) + * so the agent's reply has somewhere routable to go, since the cron + * sender itself has no mailbox. */ +export function buildScheduledRunBody(deployerAddress?: string): string { + const task = "Do the work your definition describes."; + if (deployerAddress === undefined) { + return `This is your scheduled run. ${task} Reply with the result.`; + } + return `This is your scheduled run. ${task} Mail the result to ${deployerAddress} (pass it as a single-item \`to\` list) with a short, descriptive subject.`; +} + +/** The deploying person's mailbox address — same source `session.ts`'s + * `fetchSession` reads, same shape `threads-api.ts` builds a person + * participant's address from (`@`). Best-effort: a + * session probe that fails or comes back signed-out just means the + * scheduled run's body falls back to naming nobody, never a failed + * deploy over it. */ +async function resolveDeployerAddress( + tenantDomain: string, + fetchImpl: typeof fetch, +): Promise { + let response: Response; + try { + response = await fetchImpl("/api/auth/get-session", { + headers: { accept: "application/json" }, + }); + } catch (cause) { + reportError(cause, { operation: "agent_deploy_resolve_deployer_address" }); + return undefined; + } + if (!response.ok) return undefined; + let body: unknown; + try { + body = await response.json(); + } catch (cause) { + reportError(cause, { operation: "agent_deploy_resolve_deployer_address" }); + return undefined; + } + const parsed = SessionUserShape(body); + return parsed instanceof type.errors ? undefined : `${parsed.user.id}@${tenantDomain}`; +} + /** Creates a `@corbits/cron` schedule row addressed at a deployed agent's * run — the only way an agent fires on a cadence, since Interchange's * `schedule` trigger is reserved but unimplemented. */ @@ -248,8 +295,10 @@ async function scheduleAgentRun( tenantId: string, expression: string, runAddress: string, + tenantDomain: string, fetchImpl: typeof fetch, ): Promise { + const deployerAddress = await resolveDeployerAddress(tenantDomain, fetchImpl); const created = await fetchImpl(cronPath(tenantId), { method: "POST", headers: { "content-type": "application/json" }, @@ -257,7 +306,7 @@ async function scheduleAgentRun( expression, toAddress: runAddress, subject: "Scheduled run", - body: "This is your scheduled run. Do the work your definition describes and reply with the result.", + body: buildScheduledRunBody(deployerAddress), }), }); if (!created.ok) { @@ -346,6 +395,7 @@ export async function deployAgentSource( // The deployment id is the top-level run id (already `run_…`), and // the run address is that id at the tenant domain. `${parsed.id}@${tenant.domain}`, + tenant.domain, fetchImpl, ); }