Skip to content

Commit 7e91abb

Browse files
committed
feat(sdk,playground,ai-chat): chat.agent on Sessions-as-run-manager
chat.agent now runs on top of the Session-as-run-manager primitive. Public surface (`chat.agent({...})`, `useTriggerChatTransport`, `chat.store` / `chat.defer` / `chat.history`, `AgentChat`) is unchanged; the wiring underneath moves from per-run streams to the durable Session row that owns its own runs. Transport (TriggerChatTransport): - Drop `getStartToken`. Replace with `startSession({chatId, taskId, clientData}) => {publicAccessToken}` — wraps a server action that calls `chat.createStartSessionAction`. Idempotent on `(env, externalId)`. - `clientData` (typed via `withClientData`) is threaded through `startSession`'s params, so the first run's `basePayload.metadata` matches per-turn `metadata`. Live-updated via `setClientData` when the hook's `clientData` option changes. - Drop transport-level `triggerConfig` / `triggerOptions` / `idleTimeoutInSeconds`. All trigger config lives server-side in the customer's `chat.createStartSessionAction(taskId, options)`. - `transport.preload(chatId)` and lazy first `sendMessage` both route through `startSession`, deduped via the in-flight pendingStarts map. - `ChatSession` persistable shape drops `runId`; just `{lastEventId}`. chat.agent runtime: - New `chat.createStartSessionAction(taskId, options?)` — server-side wrapper that calls `sessions.start` with `basePayload.{messages:[], trigger: "preload"}` defaults plus the customer's overrides. Returns `{sessionId, runId, publicAccessToken}`. - `chat.requestUpgrade` calls `apiClient.endAndContinueSession` before emitting the `trigger:upgrade-required` chunk. Server orchestrates the swap; browser keeps streaming across the run handoff. Webapp dashboard: - Playground: `startSession` + `accessToken` both wired through the Remix action (idempotent server-side start path). Preload button now works. New session proxy routes for HEAD/GET on `/out` and POST on `/in/append`; old run-stream proxies deleted. - Run inspector Agent tab: SSE proxy now uses the canonical addressing key (externalId if set, else friendlyId), matching what the agent writes via `session.out`. Fixes the case where the Agent tab read from a different S2 stream than the agent wrote to. References (ai-chat): - `chat-view` useEffect dance gone (just hydrates `initialSession`). - `chat-app` `transport.preload(id)` routes through `startSession`. - New `upgrade-test` agent + sidebar option for exercising `chat.requestUpgrade` end-to-end. - `ChatSession` schema simplified: drop `runId` / `sessionId`, keep `publicAccessToken` + `lastEventId`. - `chat-client-test` fixed for the new transport shape. - Hello-world smoke stubs gutted to TODO placeholders — sessions are now task-bound, so standalone-session smokes need rewriting.
1 parent 31cec28 commit 7e91abb

29 files changed

Lines changed: 2053 additions & 4509 deletions

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.playground.$agentParam/route.tsx

Lines changed: 41 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
1313
import { useCallback, useEffect, useRef, useState } from "react";
1414
import { useChat } from "@ai-sdk/react";
1515
import { TriggerChatTransport } from "@trigger.dev/sdk/chat";
16-
import type { TriggerChatTaskParams, TriggerChatTaskResult } from "@trigger.dev/sdk/chat";
1716
import { MainCenteredContainer } from "~/components/layout/AppLayout";
1817
import { Badge } from "~/components/primitives/Badge";
1918
import { Button, LinkButton } from "~/components/primitives/Buttons";
@@ -219,14 +218,16 @@ function PlaygroundChat() {
219218

220219
const actionPath = `/resources/orgs/${organization.slug}/projects/${project.slug}/env/${environment.slug}/playground/action`;
221220

222-
// Server-side trigger via Remix action (acts like a Next.js server action)
223-
const triggerTask = useCallback(
224-
async (params: TriggerChatTaskParams): Promise<TriggerChatTaskResult> => {
221+
// Server-side `start` via Remix action — atomically creates the
222+
// backing Session for `chatId` and triggers the first run, returns
223+
// the session-scoped PAT. Idempotent: called on initial use AND on
224+
// 401, so the same code path serves both first-run and PAT renewal.
225+
const startSession = useCallback(
226+
async (): Promise<string> => {
225227
const formData = new FormData();
226-
formData.set("intent", "trigger");
228+
formData.set("intent", "start");
227229
formData.set("agentSlug", agent.slug);
228230
formData.set("chatId", chatId);
229-
formData.set("payload", JSON.stringify(params.payload));
230231
formData.set("clientData", clientDataJsonRef.current);
231232
if (tags.length > 0) formData.set("tags", tags.join(","));
232233
if (machine) formData.set("machine", machine);
@@ -243,32 +244,17 @@ function PlaygroundChat() {
243244
error?: string;
244245
};
245246

246-
if (!response.ok || !data.runId || !data.publicAccessToken) {
247-
throw new Error(data.error ?? "Failed to trigger agent");
247+
if (!response.ok || !data.publicAccessToken) {
248+
throw new Error(data.error ?? "Failed to start chat session");
248249
}
249250

250251
if (data.conversationId) {
251252
setConversationId(data.conversationId);
252253
}
253254

254-
return { runId: data.runId, publicAccessToken: data.publicAccessToken };
255-
},
256-
[actionPath, agent.slug, chatId, tags, machine, maxAttempts, maxDuration, version, region]
257-
);
258-
259-
// Token renewal via Remix action
260-
const renewToken = useCallback(
261-
async ({ runId }: { chatId: string; runId: string }): Promise<string | undefined> => {
262-
const formData = new FormData();
263-
formData.set("intent", "renew");
264-
formData.set("agentSlug", agent.slug);
265-
formData.set("runId", runId);
266-
267-
const response = await fetch(actionPath, { method: "POST", body: formData });
268-
const data = (await response.json()) as { publicAccessToken?: string };
269255
return data.publicAccessToken;
270256
},
271-
[actionPath, agent.slug]
257+
[actionPath, agent.slug, chatId, tags, machine, maxAttempts, maxDuration, version, region]
272258
);
273259

274260
// Resource route prefix — all realtime traffic goes through session-authed routes
@@ -280,15 +266,22 @@ function PlaygroundChat() {
280266
if (transportRef.current === null) {
281267
transportRef.current = new TriggerChatTransport({
282268
task: agent.slug,
283-
triggerTask,
284-
renewRunAccessToken: renewToken,
269+
// The Remix action is idempotent on `(env, externalId)` and
270+
// returns a fresh session PAT every time, so it serves both
271+
// first-run create and PAT renewal. `startSession` runs on
272+
// `transport.preload(chatId)` and lazily on the first
273+
// `sendMessage`; `accessToken` runs on a 401/403 from any
274+
// session-PAT-authed request. Wiring the same call to both
275+
// keeps the Preload button working without a separate refresh
276+
// route.
277+
startSession: async () => ({ publicAccessToken: await startSession() }),
278+
accessToken: () => startSession(),
285279
baseURL: playgroundBaseURL,
286280
clientData: JSON.parse(clientDataJson || "{}") as Record<string, unknown>,
287-
...(activeConversation?.runFriendlyId && activeConversation?.publicAccessToken
281+
...(activeConversation?.publicAccessToken
288282
? {
289283
sessions: {
290284
[activeConversation.chatId]: {
291-
runId: activeConversation.runFriendlyId,
292285
publicAccessToken: activeConversation.publicAccessToken,
293286
lastEventId: activeConversation.lastEventId ?? undefined,
294287
},
@@ -299,15 +292,6 @@ function PlaygroundChat() {
299292
}
300293
const transport = transportRef.current;
301294

302-
// Keep callbacks up to date
303-
useEffect(() => {
304-
transport.setTriggerTask(triggerTask);
305-
}, [triggerTask, transport]);
306-
307-
useEffect(() => {
308-
transport.setRenewRunAccessToken(renewToken);
309-
}, [renewToken, transport]);
310-
311295
// Initial messages from persisted conversation (for resume)
312296
const initialMessages = activeConversation?.messages
313297
? (activeConversation.messages as UIMessage[])
@@ -382,10 +366,7 @@ function PlaygroundChat() {
382366
const handlePreload = useCallback(async () => {
383367
setPreloading(true);
384368
try {
385-
await transport.preload(chatId, {
386-
idleTimeoutInSeconds: 60,
387-
metadata: safeParseJson(clientDataJsonRef.current),
388-
});
369+
await transport.preload(chatId);
389370
setPreloaded(true);
390371
inputRef.current?.focus();
391372
} finally {
@@ -467,8 +448,11 @@ function PlaygroundChat() {
467448
<Badge variant="extra-small">{formatAgentType(agent.type)}</Badge>
468449
</div>
469450
<div className="flex items-center gap-2">
470-
{session?.runId && (
471-
<LinkButton to={`/runs/${session.runId}`} variant="tertiary/small">
451+
{activeConversation?.runFriendlyId && (
452+
<LinkButton
453+
to={`/runs/${activeConversation.runFriendlyId}`}
454+
variant="tertiary/small"
455+
>
472456
View run
473457
</LinkButton>
474458
)}
@@ -524,7 +508,7 @@ function PlaygroundChat() {
524508
Type a message below to start testing{" "}
525509
<code className="text-text-bright">{agent.slug}</code>
526510
</Paragraph>
527-
{!session?.runId && (
511+
{!session && (
528512
<Button
529513
variant="tertiary/small"
530514
LeadingIcon={preloading ? Spinner : BoltIcon}
@@ -646,6 +630,7 @@ function PlaygroundChat() {
646630
regions={regions}
647631
isDev={isDev}
648632
session={session}
633+
runFriendlyId={activeConversation?.runFriendlyId ?? undefined}
649634
messageCount={messages.length}
650635
isStreaming={isStreaming}
651636
status={status}
@@ -696,6 +681,7 @@ function PlaygroundSidebar({
696681
regions,
697682
isDev,
698683
session,
684+
runFriendlyId,
699685
messageCount,
700686
isStreaming,
701687
status,
@@ -722,13 +708,18 @@ function PlaygroundSidebar({
722708
isDev: boolean;
723709
session:
724710
| {
725-
sessionId: string;
726-
runId?: string;
727711
publicAccessToken: string;
728712
lastEventId?: string;
729713
isStreaming?: boolean;
730714
}
731715
| undefined;
716+
/**
717+
* Friendly id of the latest run for this conversation (drawn from the
718+
* playground's own `playgroundConversation` table, which mirrors the
719+
* Session's `currentRunId`). Optional because a conversation may
720+
* exist briefly before the first run lands.
721+
*/
722+
runFriendlyId: string | undefined;
732723
messageCount: number;
733724
isStreaming: boolean;
734725
status: string;
@@ -971,9 +962,11 @@ function PlaygroundSidebar({
971962
className="min-h-0 flex-1 overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
972963
>
973964
<div className="min-w-64 space-y-3 p-3">
974-
{session?.runId ? (
965+
{session ? (
975966
<>
976-
<SessionField label="Run ID" value={session.runId} />
967+
{runFriendlyId && (
968+
<SessionField label="Run ID" value={runFriendlyId} />
969+
)}
977970
<SessionField label="Messages" value={String(messageCount)} />
978971
<div>
979972
<label className="mb-0.5 block text-[10px] font-medium uppercase tracking-wider text-text-dimmed">

0 commit comments

Comments
 (0)