Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/agent-summary-grouped-channels.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": patch
---

The Vercel agent summary (`.eve/agent-summary.json`) now groups channels one entry per authored channel with routes nested under a `routes` array, instead of one flat entry per HTTP route. This is `schemaVersion` 4: a channel like `channels/eve.ts` reads as a single channel carrying its session, follow-up, and stream routes, so dashboards no longer over-count channels or repeat the same name per route.
Original file line number Diff line number Diff line change
Expand Up @@ -70,16 +70,21 @@ function makeSchedule(name: string, cron: string): CompiledScheduleDefinition {
};
}

function makeChannel(input: { name: string; adapterKind?: string }): CompiledChannelDefinition {
function makeChannel(input: {
name: string;
adapterKind?: string;
method?: CompiledChannelDefinition["method"];
urlPath?: string;
}): CompiledChannelDefinition {
return {
adapterKind: input.adapterKind,
kind: "channel",
logicalPath: `channels/${input.name}.ts`,
method: "POST",
method: input.method ?? "POST",
name: input.name,
sourceId: `channels/${input.name}.ts`,
sourceKind: "module",
urlPath: `/${input.name}`,
urlPath: input.urlPath ?? `/${input.name}`,
};
}

Expand Down Expand Up @@ -143,7 +148,13 @@ describe("buildVercelAgentSummary", () => {
channels: [
makeChannel({ name: "slack", adapterKind: "slack" }),
makeChannel({ name: "weather-bot", adapterKind: "weather-slack" }),
makeChannel({ name: "messages", adapterKind: "http" }),
makeChannel({ name: "messages", adapterKind: "http", urlPath: "/messages/v1/session" }),
makeChannel({
name: "messages",
adapterKind: "http",
method: "GET",
urlPath: "/messages/v1/session/:sessionId/stream",
}),
makeChannel({ name: "stripe", adapterKind: "stripe-webhook" }),
makeChannel({ name: "mystery" }),
{ kind: "disabled", logicalPath: "channels/disabled.ts", name: "disabled" },
Expand Down Expand Up @@ -279,46 +290,45 @@ describe("buildVercelAgentSummary", () => {
]);

// Disabled channels are dropped; remaining channels are normalized to
// the four-value display type.
// the four-value display type and grouped one entry per authored
// channel, with route-level detail nested under `routes`.
expect(summary.channels).toEqual([
{
adapterKind: "slack",
logicalPath: "channels/slack.ts",
method: "POST",
name: "slack",
routes: [{ method: "POST", urlPath: "/slack" }],
type: "slack",
urlPath: "/slack",
},
{
adapterKind: "weather-slack",
logicalPath: "channels/weather-bot.ts",
method: "POST",
name: "weather-bot",
routes: [{ method: "POST", urlPath: "/weather-bot" }],
type: "slack",
urlPath: "/weather-bot",
},
{
adapterKind: "http",
logicalPath: "channels/messages.ts",
method: "POST",
name: "messages",
routes: [
{ method: "POST", urlPath: "/messages/v1/session" },
{ method: "GET", urlPath: "/messages/v1/session/:sessionId/stream" },
],
type: "http",
urlPath: "/messages",
},
{
adapterKind: "stripe-webhook",
logicalPath: "channels/stripe.ts",
method: "POST",
name: "stripe",
routes: [{ method: "POST", urlPath: "/stripe" }],
type: "webhook",
urlPath: "/stripe",
},
{
logicalPath: "channels/mystery.ts",
method: "POST",
name: "mystery",
routes: [{ method: "POST", urlPath: "/mystery" }],
type: "unknown",
urlPath: "/mystery",
},
]);

Expand Down
45 changes: 32 additions & 13 deletions packages/eve/src/internal/nitro/host/build-vercel-agent-summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { resolveInstalledPackageInfo } from "#internal/application/package.js";
import {
type VercelEveAgentSummary,
type VercelEveChannelEntry,
type VercelEveChannelRoute,
type VercelEveConnectionEntry,
type VercelEveInstructionsEntry,
type VercelEveScheduleEntry,
Expand Down Expand Up @@ -54,7 +55,7 @@ export function buildVercelAgentSummary(input: {
tools: manifest.tools.map(toToolEntry),
skills: manifest.skills.map(toSkillEntry),
connections: manifest.connections.map(toConnectionEntry),
channels: manifest.channels.filter(isActiveChannel).map(toChannelEntry),
channels: groupChannelEntries(manifest.channels.filter(isActiveChannel)),
sandbox:
manifest.sandbox === null
? null
Expand Down Expand Up @@ -167,20 +168,38 @@ function toConnectionEntry(connection: CompiledConnectionDefinition): VercelEveC
return entry;
}

function toChannelEntry(channel: CompiledChannelDefinition): VercelEveChannelEntry {
const entry: VercelEveChannelEntry = {
name: channel.name,
method: channel.method,
urlPath: channel.urlPath,
type: normalizeChannelKindForDisplay(channel.adapterKind),
logicalPath: channel.logicalPath,
};

if (channel.adapterKind !== undefined) {
return { ...entry, adapterKind: channel.adapterKind };
/**
* Groups the manifest's route-level channel definitions into one summary
* entry per authored channel, preserving first-occurrence order of both
* channels and their routes. The manifest stores one definition per
* `(method, urlPath)` — the granularity the runtime mounts — but the
* summary's unit is the authored channel, with routes nested under it.
*/
function groupChannelEntries(
channels: readonly CompiledChannelDefinition[],
): VercelEveChannelEntry[] {
const grouped = new Map<string, VercelEveChannelEntry & { routes: VercelEveChannelRoute[] }>();

for (const channel of channels) {
const route: VercelEveChannelRoute = { method: channel.method, urlPath: channel.urlPath };
const existing = grouped.get(channel.name);
if (existing !== undefined) {
existing.routes.push(route);
continue;
}
const entry = {
name: channel.name,
type: normalizeChannelKindForDisplay(channel.adapterKind),
logicalPath: channel.logicalPath,
routes: [route],
};
grouped.set(
channel.name,
channel.adapterKind === undefined ? entry : { ...entry, adapterKind: channel.adapterKind },
);
}

return entry;
return [...grouped.values()];
}

function toSubagentEntry(subagent: CompiledSubagentNode): VercelEveSubagentEntry {
Expand Down
24 changes: 20 additions & 4 deletions packages/eve/src/internal/vercel-agent-summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export const VERCEL_EVE_AGENT_SUMMARY_KIND = "vercel-eve-agent-summary" as const
* making semantic changes consumers must opt into. Adding optional fields
* does not require a version bump.
*/
export const VERCEL_EVE_AGENT_SUMMARY_VERSION = 3;
export const VERCEL_EVE_AGENT_SUMMARY_VERSION = 4;

/**
* Output path (relative to the agent's `appRoot`) where eve writes the
Expand Down Expand Up @@ -140,18 +140,34 @@ export interface VercelEveConnectionEntry {
};
}

export interface VercelEveChannelEntry {
readonly name: string;
/**
* One HTTP route mounted by a channel.
*/
export interface VercelEveChannelRoute {
readonly method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "WEBSOCKET";
readonly urlPath: string;
}

/**
* One authored channel, carrying every route it mounts.
*
* Schema version 4 groups routes under their authored channel: a channel
* module like `channels/eve.ts` compiles to one route per `(method,
* urlPath)` internally, but reads as a single channel in product UI.
* Version 3 emitted one flat entry per route, which made consumers
* over-count channels unless they re-grouped by `name` themselves.
*/
export interface VercelEveChannelEntry {
readonly name: string;
readonly type: VercelEveChannelType;
/**
* The raw `ChannelAdapter.kind` reported by the route, when present.
* The raw `ChannelAdapter.kind` reported by the channel, when present.
* Useful when `type` is `"unknown"` and a consumer wants to render a
* disambiguating label.
*/
readonly adapterKind?: string;
readonly logicalPath: string;
readonly routes: readonly VercelEveChannelRoute[];
}

export interface VercelEveSandboxEntry {
Expand Down
Loading