Skip to content
Open
5 changes: 5 additions & 0 deletions .changeset/chat-ready-core-additions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@trigger.dev/core": patch
---

Add `ChatChunkTooLargeError` and ApiClient methods for subscribing to session streams. Lays the groundwork for the upcoming `chat.agent`.
9 changes: 9 additions & 0 deletions .changeset/sessions-primitive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@trigger.dev/sdk": minor
"@trigger.dev/core": patch
---

Adds the Sessions primitive — a durable, run-aware stream channel keyed
on a stable `externalId`. Public SDK additions: `tasks.triggerAndSubscribe()`
and the `chat.agent` runtime built on top of Sessions. See
https://trigger.dev/docs/ai-chat/overview for the full feature surface.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ apps/**/public/build
/packages/trigger-sdk/src/package.json
/packages/python/src/package.json
**/.claude/settings.local.json
.claude/architecture/
.claude/docs-plans/
.claude/review-guides/
.claude/scheduled_tasks.lock
.mcp.log
.mcp.json
.cursor/debug.log
Expand Down
6 changes: 6 additions & 0 deletions .server-changes/sessions-dashboard-and-task-source-filter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
area: webapp
type: feature
---

New Sessions page in the dashboard for inspecting `chat.agent` Session rows alongside their underlying runs, plus a "Task source" filter on the Runs list (Standard / Scheduled / Agent) so agent runs can be sliced out of mixed workloads at a glance.
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ This file provides guidance to Claude Code when working with this repository. Su

This is a pnpm 10.33.2 monorepo using Turborepo. Run commands from root with `pnpm run`.

**Adding dependencies:** Edit `package.json` directly instead of using `pnpm add`, then run `pnpm i` from the repo root. See `.claude/rules/package-installation.md` for the full process.

```bash
pnpm run docker # Start Docker services (PostgreSQL, Redis, Electric)
pnpm run db:migrate # Run database migrations
Expand Down
23 changes: 23 additions & 0 deletions apps/webapp/app/components/BlankStatePanels.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
ArrowsRightLeftIcon,
BeakerIcon,
BellAlertIcon,
BookOpenIcon,
Expand Down Expand Up @@ -189,6 +190,28 @@ export function BatchesNone() {
);
}

export function SessionsNone() {
return (
<InfoPanel
title="Sessions"
icon={ArrowsRightLeftIcon}
iconClassName="text-teal-500"
panelClassName="max-w-full"
accessory={
<LinkButton to={docsPath("/ai-chat/overview")} variant="docs/small" LeadingIcon={BookOpenIcon}>
Sessions docs
</LinkButton>
}
>
<Paragraph spacing variant="small">
You have no sessions in this environment. Sessions are durable, typed, bidirectional I/O
primitives that outlive a single run — used by <InlineCode>chat.agent</InlineCode> and any
long-running task that needs streaming input and output.
</Paragraph>
</InfoPanel>
);
}

export function TestHasNoTasks() {
const organization = useOrganization();
const project = useProject();
Expand Down
13 changes: 13 additions & 0 deletions apps/webapp/app/components/BulkActionFilterSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,19 @@ export function BulkActionFilterSummary({
/>
);
}
case "sources": {
const values = Array.isArray(value) ? value : [`${value}`];
return (
<AppliedFilter
variant="minimal/medium"
key={key}
label={filterTitle(key)}
icon={filterIcon(key)}
value={appliedSummary(values)}
removable={false}
/>
);
}
default: {
assertNever(typedKey);
}
Expand Down
117 changes: 116 additions & 1 deletion apps/webapp/app/components/runs/v3/RunFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as Ariakit from "@ariakit/react";
import {
CalendarIcon,
ClockIcon,
CpuChipIcon,
FingerPrintIcon,
PlusIcon,
RectangleStackIcon,
Expand Down Expand Up @@ -190,6 +191,9 @@ export const TaskRunListSearchFilters = z.object({
`Machine presets to filter by (${machines.join(", ")})`
),
errorId: z.string().optional().describe("Error ID to filter runs by (e.g. error_abc123)"),
sources: StringOrStringArray.describe(
"Task trigger sources to filter by (STANDARD, SCHEDULED, AGENT)"
),
});

export type TaskRunListSearchFilters = z.infer<typeof TaskRunListSearchFilters>;
Expand Down Expand Up @@ -231,6 +235,8 @@ export function filterTitle(filterKey: string) {
return "Version";
case "errorId":
return "Error ID";
case "sources":
return "Source";
default:
return filterKey;
}
Expand Down Expand Up @@ -271,6 +277,8 @@ export function filterIcon(filterKey: string): ReactNode | undefined {
return <IconRotateClockwise2 className="size-4" />;
case "errorId":
return <IconBugFilled className="size-4" />;
case "sources":
return <CpuChipIcon className="size-4" />;
default:
return undefined;
}
Expand Down Expand Up @@ -318,6 +326,10 @@ export function getRunFiltersFromSearchParams(
? searchParams.getAll("versions")
: undefined,
errorId: searchParams.get("errorId") ?? undefined,
sources:
searchParams.getAll("sources").filter((v) => v.length > 0).length > 0
? searchParams.getAll("sources")
: undefined,
};

const parsed = TaskRunListSearchFilters.safeParse(params);
Expand Down Expand Up @@ -359,7 +371,8 @@ export function RunsFilters(props: RunFiltersProps) {
searchParams.has("queues") ||
searchParams.has("machines") ||
searchParams.has("versions") ||
searchParams.has("errorId");
searchParams.has("errorId") ||
searchParams.has("sources");

return (
<div className="flex flex-row flex-wrap items-center gap-1.5">
Expand Down Expand Up @@ -395,6 +408,7 @@ const filterTypes = [
{ name: "schedule", title: "Schedule ID", icon: <ClockIcon className="size-4" /> },
{ name: "bulk", title: "Bulk action", icon: <ListCheckedIcon className="size-4" /> },
{ name: "error", title: "Error ID", icon: <IconBugFilled className="size-4" /> },
{ name: "source", title: "Source", icon: <CpuChipIcon className="size-4" /> },
] as const;

type FilterType = (typeof filterTypes)[number]["name"];
Expand Down Expand Up @@ -448,6 +462,7 @@ function AppliedFilters({ bulkActions }: RunFiltersProps) {
<AppliedScheduleIdFilter />
<AppliedBulkActionsFilter bulkActions={bulkActions} />
<AppliedErrorIdFilter />
<AppliedSourceFilter />
</>
);
}
Expand Down Expand Up @@ -482,6 +497,8 @@ function Menu(props: MenuProps) {
return <VersionsDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
case "error":
return <ErrorIdDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
case "source":
return <SourceDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
}
}

Expand Down Expand Up @@ -1739,3 +1756,101 @@ function AppliedErrorIdFilter() {
</FilterMenuProvider>
);
}

const sourceOptions: { value: TaskTriggerSource; title: string }[] = [
{ value: "STANDARD", title: "Standard" },
{ value: "SCHEDULED", title: "Scheduled" },
{ value: "AGENT", title: "Agent" },
];

function SourceDropdown({
trigger,
clearSearchValue,
searchValue,
onClose,
}: {
trigger: ReactNode;
clearSearchValue: () => void;
searchValue: string;
onClose?: () => void;
}) {
const { values, replace } = useSearchParams();

const handleChange = (values: string[]) => {
clearSearchValue();
replace({ sources: values, cursor: undefined, direction: undefined });
};

const filtered = useMemo(() => {
return sourceOptions.filter((item) =>
item.title.toLowerCase().includes(searchValue.toLowerCase())
);
}, [searchValue]);

return (
<SelectProvider value={values("sources")} setValue={handleChange} virtualFocus={true}>
{trigger}
<SelectPopover
className="min-w-0 max-w-[min(240px,var(--popover-available-width))]"
hideOnEscape={() => {
if (onClose) {
onClose();
return false;
}
return true;
}}
>
<ComboBox placeholder={"Filter by source..."} value={searchValue} />
<SelectList>
{filtered.map((item, index) => (
<SelectItem
key={item.value}
value={item.value}
icon={
<TaskTriggerSourceIcon source={item.value} className="size-4 flex-none" />
}
shortcut={shortcutFromIndex(index, { shortcutsEnabled: true })}
>
{item.title}
</SelectItem>
))}
</SelectList>
</SelectPopover>
</SelectProvider>
);
}

function AppliedSourceFilter() {
const { values, del } = useSearchParams();
const sources = values("sources");

if (sources.length === 0 || sources.every((v) => v === "")) {
return null;
}

return (
<FilterMenuProvider>
{(search, setSearch) => (
<SourceDropdown
trigger={
<Ariakit.Select render={<div className="group cursor-pointer focus-custom" />}>
<AppliedFilter
label="Source"
icon={<CpuChipIcon className="size-4" />}
value={appliedSummary(
sources.map(
(v) => sourceOptions.find((o) => o.value === v)?.title ?? v
)
)}
onRemove={() => del(["sources", "cursor", "direction"])}
variant="secondary/small"
/>
</Ariakit.Select>
}
searchValue={search}
clearSearchValue={() => setSearch("")}
/>
)}
</FilterMenuProvider>
);
}
6 changes: 6 additions & 0 deletions apps/webapp/app/components/runs/v3/TaskRunsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,10 @@ import {
filterableTaskRunStatuses,
TaskRunStatusCombo,
} from "./TaskRunStatus";
import { TaskTriggerSourceIcon } from "./TaskTriggerSource";
import { useOptimisticLocation } from "~/hooks/useOptimisticLocation";
import { useSearchParams } from "~/hooks/useSearchParam";
import type { TaskTriggerSource } from "@trigger.dev/database";

type RunsTableProps = {
total: number;
Expand Down Expand Up @@ -352,6 +354,10 @@ export function TaskRunsTable({
</TableCell>
<TableCell to={path}>
<span className="flex items-center gap-x-1">
<TaskTriggerSourceIcon
source={run.taskKind as TaskTriggerSource}
className="size-3.5 flex-none"
/>
{run.taskIdentifier}
{run.rootTaskRunId === null ? <Badge variant="extra-small">Root</Badge> : null}
</span>
Expand Down
10 changes: 9 additions & 1 deletion apps/webapp/app/components/runs/v3/TaskTriggerSource.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ClockIcon } from "@heroicons/react/20/solid";
import { ClockIcon, CpuChipIcon } from "@heroicons/react/20/solid";
import type { TaskTriggerSource } from "@trigger.dev/database";
import { TaskIconSmall } from "~/assets/icons/TaskIcon";
import { cn } from "~/utils/cn";
Expand All @@ -19,6 +19,11 @@ export function TaskTriggerSourceIcon({
<ClockIcon className={cn("size-[1.125rem] min-w-[1.125rem] text-schedules", className)} />
);
}
case "AGENT": {
return (
<CpuChipIcon className={cn("size-[1.125rem] min-w-[1.125rem] text-indigo-500", className)} />
);
}
}
}

Expand All @@ -30,5 +35,8 @@ export function taskTriggerSourceDescription(source: TaskTriggerSource) {
case "SCHEDULED": {
return "Scheduled task";
}
case "AGENT": {
return "Agent";
}
}
}
Loading
Loading