mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-05 16:04:43 +00:00
feat(plugins): add live timeline rows and client slash commands
Plugins can transform live timeline source items at render time, append durable plugin rows from daemon handlers, and register client-only slash commands in the composer. Related to #3940 and #3952.
This commit is contained in:
@@ -339,6 +339,15 @@ export interface CompactionTimelineItem {
|
||||
preTokens?: number;
|
||||
}
|
||||
|
||||
export interface PluginTimelineItem {
|
||||
type: "plugin";
|
||||
id: string;
|
||||
pluginId: string;
|
||||
kind: string;
|
||||
version: number;
|
||||
data: JsonValue;
|
||||
}
|
||||
|
||||
export interface AgentTaskItem {
|
||||
text: string;
|
||||
completed: boolean;
|
||||
@@ -354,7 +363,8 @@ export type AgentTimelineItem =
|
||||
| ToolCallTimelineItem
|
||||
| { type: "todo"; items: AgentTaskItem[] }
|
||||
| { type: "error"; message: string }
|
||||
| CompactionTimelineItem;
|
||||
| CompactionTimelineItem
|
||||
| PluginTimelineItem;
|
||||
|
||||
export type AgentStreamEvent =
|
||||
| { type: "thread_started"; sessionId: string; provider: AgentProvider }
|
||||
|
||||
@@ -7,6 +7,50 @@ import {
|
||||
} from "./messages.js";
|
||||
|
||||
describe("plugin protocol compatibility", () => {
|
||||
it("parses plugin timeline append messages and advertises the capability", () => {
|
||||
expect(
|
||||
SessionInboundMessageSchema.parse({
|
||||
type: "agent.timeline.append.request",
|
||||
requestId: "request-append",
|
||||
agentId: "agent-1",
|
||||
item: {
|
||||
type: "plugin",
|
||||
id: "review-1",
|
||||
kind: "review",
|
||||
version: 1,
|
||||
data: { status: "running" },
|
||||
},
|
||||
}),
|
||||
).toMatchObject({ type: "agent.timeline.append.request" });
|
||||
expect(
|
||||
SessionOutboundMessageSchema.parse({
|
||||
type: "agent.timeline.append.response",
|
||||
payload: { requestId: "request-append", seq: 7, epoch: "epoch-1" },
|
||||
}),
|
||||
).toMatchObject({ type: "agent.timeline.append.response" });
|
||||
expect(
|
||||
StatusMessageSchema.parse({
|
||||
type: "status",
|
||||
payload: {
|
||||
status: "server_info",
|
||||
serverId: "server-1",
|
||||
features: { pluginTimelineItems: true },
|
||||
},
|
||||
}).payload,
|
||||
).toMatchObject({ features: { pluginTimelineItems: true } });
|
||||
});
|
||||
|
||||
it.each([0, -1, 1.5])("rejects plugin timeline version %s", (version) => {
|
||||
expect(() =>
|
||||
SessionInboundMessageSchema.parse({
|
||||
type: "agent.timeline.append.request",
|
||||
requestId: "append-1",
|
||||
agentId: "agent-1",
|
||||
item: { type: "plugin", id: "row-1", kind: "review", version, data: {} },
|
||||
}),
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it("keeps old directory plugin config valid when enabled is absent", () => {
|
||||
const config = MutableDaemonConfigSchema.parse({
|
||||
mcp: { injectIntoAgents: true },
|
||||
|
||||
@@ -286,8 +286,13 @@ import type {
|
||||
ToolCallDetail,
|
||||
ToolCallTimelineItem,
|
||||
AgentUsage,
|
||||
JsonValue,
|
||||
} from "./agent-types.js";
|
||||
|
||||
// WebSocket payloads have already crossed JSON serialization. Keeping this as
|
||||
// unknown avoids zod-aot's recursive z.json() object-codegen regression.
|
||||
const JsonWireValueSchema = z.unknown() as z.ZodType<JsonValue>;
|
||||
|
||||
export const AgentStatusSchema = z.enum(AGENT_LIFECYCLE_STATUSES);
|
||||
|
||||
const AgentModeSchema: z.ZodType<AgentMode> = z.object({
|
||||
@@ -733,6 +738,14 @@ export const AgentTimelineItemPayloadSchema: z.ZodType<AgentTimelineItem, unknow
|
||||
trigger: z.enum(["auto", "manual"]).optional(),
|
||||
preTokens: z.number().optional(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("plugin"),
|
||||
id: z.string(),
|
||||
pluginId: PluginIdSchema,
|
||||
kind: z.string(),
|
||||
version: z.number(),
|
||||
data: JsonWireValueSchema,
|
||||
}),
|
||||
]);
|
||||
|
||||
export const AgentStreamEventPayloadSchema = z.discriminatedUnion("type", [
|
||||
@@ -1498,6 +1511,19 @@ export const PluginRpcInvokeRequestSchema = z.object({
|
||||
input: z.unknown(),
|
||||
});
|
||||
|
||||
export const AgentTimelineAppendRequestSchema = z.object({
|
||||
type: z.literal("agent.timeline.append.request"),
|
||||
requestId: z.string(),
|
||||
agentId: z.string(),
|
||||
item: z.object({
|
||||
type: z.literal("plugin"),
|
||||
id: z.string(),
|
||||
kind: z.string(),
|
||||
version: z.number().int().positive(),
|
||||
data: JsonWireValueSchema,
|
||||
}),
|
||||
});
|
||||
|
||||
export const AgentSkillOperationSchema = z.discriminatedUnion("kind", [
|
||||
z.object({ kind: z.literal("add"), name: z.string() }).strict(),
|
||||
z.object({ kind: z.literal("update"), name: z.string() }).strict(),
|
||||
@@ -3048,6 +3074,7 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
|
||||
PluginDisableRequestSchema,
|
||||
PluginRemoveRequestSchema,
|
||||
PluginRpcInvokeRequestSchema,
|
||||
AgentTimelineAppendRequestSchema,
|
||||
AgentSkillsGetStatusRequestSchema,
|
||||
AgentSkillsReconcileRequestSchema,
|
||||
AgentSkillsUninstallRequestSchema,
|
||||
@@ -3403,6 +3430,7 @@ export const ServerInfoStatusPayloadSchema = z
|
||||
// A daemon that predates this flag keeps `addTheme` in the server bundle it compiles,
|
||||
// so a theme plugin cannot start there at all.
|
||||
pluginThemes: z.boolean().optional(),
|
||||
pluginTimelineItems: z.boolean().optional(),
|
||||
// COMPAT(skillManagement): added in v0.4.0, remove gate after 2027-08-16.
|
||||
skillManagement: z.boolean().optional(),
|
||||
// COMPAT(terminalRestoreModes): added in v0.1.81, remove gate after 2026-11-23.
|
||||
@@ -4318,7 +4346,7 @@ export const AgentTimelineEntryPayloadSchema = z.object({
|
||||
seqStart: z.number().int().nonnegative(),
|
||||
seqEnd: z.number().int().nonnegative(),
|
||||
sourceSeqRanges: z.array(AgentTimelineSeqRangeSchema),
|
||||
collapsed: z.array(z.enum(["assistant_merge", "reasoning_merge", "tool_lifecycle"])),
|
||||
collapsed: z.array(z.enum(["assistant_merge", "reasoning_merge", "tool_lifecycle", "identity"])),
|
||||
});
|
||||
|
||||
export const FetchAgentTimelineResponseMessageSchema = z.object({
|
||||
@@ -6269,6 +6297,15 @@ export const PluginRpcInvokeResponseSchema = z.object({
|
||||
}),
|
||||
});
|
||||
|
||||
export const AgentTimelineAppendResponseSchema = z.object({
|
||||
type: z.literal("agent.timeline.append.response"),
|
||||
payload: z.object({
|
||||
requestId: z.string(),
|
||||
seq: z.number().int().nonnegative(),
|
||||
epoch: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
function agentSkillsStatusResponse<const Type extends string>(type: Type) {
|
||||
return z.object({
|
||||
type: z.literal(type),
|
||||
@@ -6318,6 +6355,7 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [
|
||||
PluginDisableResponseSchema,
|
||||
PluginRemoveResponseSchema,
|
||||
PluginRpcInvokeResponseSchema,
|
||||
AgentTimelineAppendResponseSchema,
|
||||
AgentSkillsGetStatusResponseSchema,
|
||||
AgentSkillsReconcileResponseSchema,
|
||||
AgentSkillsUninstallResponseSchema,
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { timelineItemIdentity } from "./timeline-identity.js";
|
||||
|
||||
describe("timelineItemIdentity", () => {
|
||||
it("uses tool call ids and plugin-scoped ids", () => {
|
||||
expect(
|
||||
timelineItemIdentity({
|
||||
type: "tool_call",
|
||||
callId: "call-1",
|
||||
name: "read",
|
||||
detail: { type: "unknown", input: null, output: null },
|
||||
status: "running",
|
||||
error: null,
|
||||
}),
|
||||
).toBe("call-1");
|
||||
expect(
|
||||
timelineItemIdentity({
|
||||
type: "plugin",
|
||||
id: "row-1",
|
||||
pluginId: "review",
|
||||
kind: "review",
|
||||
version: 1,
|
||||
data: {},
|
||||
}),
|
||||
).toBe("review/row-1");
|
||||
expect(timelineItemIdentity({ type: "reasoning", text: "hi" })).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { AgentTimelineItem } from "./agent-types.js";
|
||||
|
||||
export function timelineItemIdentity(item: AgentTimelineItem): string | null {
|
||||
if (item.type === "tool_call") return item.callId;
|
||||
if (item.type === "plugin") return `${item.pluginId}/${item.id}`;
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user