refactor(schema): extract public event definitions (#33579)

This commit is contained in:
Kit Langton 2026-06-24 22:43:17 +02:00 committed by GitHub
commit 24b0132bc5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
93 changed files with 2743 additions and 2127 deletions

View file

@ -0,0 +1,6 @@
export * as Catalog from "./catalog"
import { define, inventory } from "./event"
const Updated = define({ type: "catalog.updated", schema: {} })
export const Event = { Updated, Definitions: inventory(Updated) }

View file

@ -0,0 +1,10 @@
export * as DurableEventManifest from "./durable-event-manifest"
import { Event } from "./event"
import { SessionEvent } from "./session-event"
import { SessionV1 } from "./session-v1"
export const Durable = Event.durable([
...SessionV1.Event.Definitions.filter((definition) => definition.durable !== undefined),
...SessionEvent.DurableDefinitions,
])

View file

@ -0,0 +1,84 @@
export * as EventManifest from "./event-manifest"
import { Catalog } from "./catalog"
import { Durable } from "./durable-event-manifest"
import { Event } from "./event"
import { FileSystem } from "./filesystem"
import { FileSystemWatcher } from "./filesystem-watcher"
import { InstallationEvent } from "./installation-event"
import { Integration } from "./integration"
import { LegacyEvent } from "./legacy-event"
import { LspEvent } from "./lsp-event"
import { McpEvent } from "./mcp-event"
import { ModelsDev } from "./models-dev"
import { Permission } from "./permission"
import { PermissionV1 } from "./permission-v1"
import { Plugin } from "./plugin"
import { Project } from "./project"
import { ProjectDirectories } from "./project-directories"
import { Pty } from "./pty"
import { Question } from "./question"
import { QuestionV1 } from "./question-v1"
import { Reference } from "./reference"
import { ServerEvent } from "./server-event"
import { SessionCompactionEvent } from "./session-compaction-event"
import { SessionEvent } from "./session-event"
import { SessionStatusEvent } from "./session-status-event"
import { SessionTodo } from "./session-todo"
import { SessionV1 } from "./session-v1"
import { TuiEvent } from "./tui-event"
import { VcsEvent } from "./vcs-event"
import { WorkspaceEvent } from "./workspace-event"
import { WorktreeEvent } from "./worktree-event"
const sessionV1DurableDefinitions = SessionV1.Event.Definitions.filter((definition) => definition.durable !== undefined)
const sessionV1LiveDefinitions = SessionV1.Event.Definitions.filter((definition) => definition.durable === undefined)
const coreDefinitions = Event.inventory(...sessionV1DurableDefinitions, ...SessionEvent.Definitions)
const foundationDefinitions = Event.inventory(
...ModelsDev.Event.Definitions,
...Integration.Event.Definitions,
...Catalog.Event.Definitions,
...coreDefinitions,
)
const featureDefinitions = Event.inventory(
...FileSystem.Event.Definitions,
...Reference.Event.Definitions,
...Permission.Event.Definitions,
...Plugin.Event.Definitions,
...ProjectDirectories.Event.Definitions,
...FileSystemWatcher.Event.Definitions,
...Pty.Event.Definitions,
...Question.Event.Definitions,
)
export const ServerDefinitions = Event.inventory(
...foundationDefinitions,
...featureDefinitions,
...SessionTodo.Event.Definitions,
)
export const Definitions = Event.inventory(
...foundationDefinitions,
...sessionV1LiveDefinitions,
...InstallationEvent.Definitions,
...featureDefinitions,
...SessionTodo.Event.Definitions,
...LspEvent.Definitions,
...PermissionV1.Event.Definitions,
...TuiEvent.Definitions,
...McpEvent.Definitions,
...LegacyEvent.Definitions,
...Project.Event.Definitions,
...SessionStatusEvent.Definitions,
...QuestionV1.Event.Definitions,
...SessionCompactionEvent.Definitions,
...VcsEvent.Definitions,
...WorkspaceEvent.Definitions,
...WorktreeEvent.Definitions,
...ServerEvent.Definitions,
)
export const Latest = Event.latest(Definitions)
export { Durable }

View file

@ -0,0 +1,125 @@
export * as Event from "./event"
import { Schema } from "effect"
import { ascending } from "./identifier"
import { Location } from "./location"
import { withStatics } from "./schema"
export const ID = Schema.String.check(Schema.isStartsWith("evt_")).pipe(
Schema.brand("Event.ID"),
withStatics((schema) => ({ create: () => schema.make("evt_" + ascending()) })),
)
export type ID = typeof ID.Type
export type Definition<
Type extends string = string,
DataSchema extends Schema.Codec<unknown, unknown> = Schema.Codec<unknown, unknown>,
> = Schema.Top & {
readonly type: Type
readonly durable?: {
readonly version: number
readonly aggregate: string
}
readonly data: DataSchema
}
export type Data<D extends Definition> = Schema.Schema.Type<D["data"]>
export type Payload<D extends Definition = Definition> = {
readonly id: ID
readonly type: D["type"]
readonly data: Data<D>
readonly durable?: {
readonly aggregateID: string
readonly seq: number
readonly version: number
}
readonly location?: Location.Ref
readonly metadata?: Record<string, unknown>
}
export function define<
const Type extends string,
Fields extends Readonly<Record<PropertyKey, Schema.Codec<unknown, unknown>>>,
>(input: {
readonly type: Type
readonly durable?: {
readonly version: number
readonly aggregate: string
}
readonly schema: Fields
}): Schema.Schema<Payload<Definition<Type, Schema.Struct<Fields>>>> & Definition<Type, Schema.Struct<Fields>> {
const data = Schema.Struct(input.schema)
return Object.assign(
Schema.Struct({
id: ID,
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
type: Schema.Literal(input.type),
durable: Schema.optional(
Schema.Struct({ aggregateID: Schema.String, seq: Schema.Number, version: Schema.Number }),
),
location: Schema.optional(Location.Ref),
data,
}).annotate({ identifier: input.type }),
{
type: input.type,
...(input.durable === undefined ? {} : { durable: input.durable }),
data,
},
) as Schema.Schema<Payload<Definition<Type, Schema.Struct<Fields>>>> & Definition<Type, Schema.Struct<Fields>>
}
export function inventory<const Definitions extends ReadonlyArray<Definition>>(...definitions: Definitions) {
return Object.freeze(definitions)
}
export function latest(definitions: ReadonlyArray<Definition>) {
return readonlyMap(
definitions.reduce((result, definition) => {
const existing = result.get(definition.type)
if (!existing) {
result.set(definition.type, definition)
return result
}
if (definition.durable && existing.durable && definition.durable.version !== existing.durable.version) {
if (definition.durable.version > existing.durable.version) result.set(definition.type, definition)
return result
}
if (definition !== existing) throw new Error(`Duplicate latest event definition for ${definition.type}`)
return result
}, new Map<string, Definition>()),
)
}
export function versionedType(type: string, version: number) {
return `${type}.${version}`
}
export function durable(definitions: ReadonlyArray<Definition>) {
return readonlyMap(
definitions.reduce((result, definition) => {
if (!definition.durable) return result
const key = versionedType(definition.type, definition.durable.version)
if (result.has(key)) throw new Error(`Duplicate durable event definition for ${key}`)
result.set(key, definition)
return result
}, new Map<string, Definition>()),
)
}
function readonlyMap<Key, Value>(map: Map<Key, Value>): ReadonlyMap<Key, Value> {
const result: ReadonlyMap<Key, Value> = Object.freeze({
get size() {
return map.size
},
entries: () => map.entries(),
forEach: (callback: (value: Value, key: Key, map: ReadonlyMap<Key, Value>) => void, thisArg?: unknown) =>
map.forEach((value, key) => callback.call(thisArg, value, key, result)),
get: (key: Key) => map.get(key),
has: (key: Key) => map.has(key),
keys: () => map.keys(),
values: () => map.values(),
[Symbol.iterator]: () => map[Symbol.iterator](),
})
return result
}

View file

@ -0,0 +1,12 @@
export * as FileDiff from "./file-diff"
import { Schema } from "effect"
export const Info = Schema.Struct({
file: Schema.optional(Schema.String),
patch: Schema.optional(Schema.String),
additions: Schema.Finite,
deletions: Schema.Finite,
status: Schema.optional(Schema.Literals(["added", "deleted", "modified"])),
}).annotate({ identifier: "SnapshotFileDiff" })
export type Info = typeof Info.Type

View file

@ -0,0 +1,13 @@
export * as FileSystemWatcher from "./filesystem-watcher"
import { Schema } from "effect"
import { define, inventory } from "./event"
const Updated = define({
type: "file.watcher.updated",
schema: {
file: Schema.String,
event: Schema.Literals(["add", "change", "unlink"]),
},
})
export const Event = { Updated, Definitions: inventory(Updated) }

View file

@ -1,8 +1,15 @@
export * as FileSystem from "./filesystem"
import { Schema } from "effect"
import { define, inventory } from "./event"
import { NonNegativeInt, PositiveInt, RelativePath } from "./schema"
const Edited = define({
type: "file.edited",
schema: { file: Schema.String },
})
export const Event = { Edited, Definitions: inventory(Edited) }
export interface Entry extends Schema.Schema.Type<typeof Entry> {}
export const Entry = Schema.Struct({
path: RelativePath,

View file

@ -0,0 +1,13 @@
export * as IdeEvent from "./ide-event"
import { Schema } from "effect"
import { Event } from "./event"
export const Installed = Event.define({
type: "ide.installed",
schema: {
ide: Schema.String,
},
})
export const Definitions = Event.inventory(Installed)

View file

@ -2,6 +2,7 @@ export { Agent } from "./agent"
export { Command } from "./command"
export { Connection } from "./connection"
export { Credential } from "./credential"
export { Event } from "./event"
export { FileSystem } from "./filesystem"
export { Integration } from "./integration"
export { LLM } from "./llm"

View file

@ -0,0 +1,20 @@
export * as InstallationEvent from "./installation-event"
import { Schema } from "effect"
import { Event } from "./event"
export const Updated = Event.define({
type: "installation.updated",
schema: {
version: Schema.String,
},
})
export const UpdateAvailable = Event.define({
type: "installation.update-available",
schema: {
version: Schema.String,
},
})
export const Definitions = Event.inventory(Updated, UpdateAvailable)

View file

@ -1,6 +1,7 @@
export * as Integration from "./integration"
import { Schema } from "effect"
import { define, inventory } from "./event"
export const ID = Schema.String.pipe(Schema.brand("Integration.ID"))
export type ID = typeof ID.Type
@ -72,6 +73,16 @@ export type Method = typeof Method.Type
export const Inputs = Schema.Record(Schema.String, Schema.String).annotate({ identifier: "Integration.Inputs" })
export type Inputs = typeof Inputs.Type
const Updated = define({
type: "integration.updated",
schema: {},
})
const ConnectionUpdated = define({
type: "integration.connection.updated",
schema: { integrationID: ID },
})
export const Event = { Updated, ConnectionUpdated, Definitions: inventory(Updated, ConnectionUpdated) }
export interface Ref extends Schema.Schema.Type<typeof Ref> {}
export const Ref = Schema.Struct({
id: ID,

View file

@ -0,0 +1,18 @@
export * as LegacyEvent from "./legacy-event"
import { Schema } from "effect"
import { define, inventory } from "./event"
import { SessionID } from "./session-id"
import { SessionV1 } from "./session-v1"
export const CommandExecuted = define({
type: "command.executed",
schema: {
name: Schema.String,
sessionID: SessionID,
arguments: Schema.String,
messageID: SessionV1.MessageID,
},
})
export const Definitions = inventory(CommandExecuted)

View file

@ -2,12 +2,12 @@ export * as Location from "./location"
import { Effect, Schema } from "effect"
import { AbsolutePath } from "./schema"
import { Workspace } from "./workspace"
import { WorkspaceID } from "./workspace-id"
export interface Ref extends Schema.Schema.Type<typeof Ref> {}
export const Ref = Schema.Struct({
directory: AbsolutePath,
workspaceID: Schema.optional(Workspace.ID).pipe(
workspaceID: Schema.optional(WorkspaceID).pipe(
Schema.withDecodingDefault(Effect.succeed(undefined)),
Schema.withConstructorDefault(Effect.succeed(undefined)),
),

View file

@ -0,0 +1,7 @@
export * as LspEvent from "./lsp-event"
import { Event } from "./event"
export const Updated = Event.define({ type: "lsp.updated", schema: {} })
export const Definitions = Event.inventory(Updated)

View file

@ -0,0 +1,21 @@
export * as McpEvent from "./mcp-event"
import { Schema } from "effect"
import { Event } from "./event"
export const ToolsChanged = Event.define({
type: "mcp.tools.changed",
schema: {
server: Schema.String,
},
})
export const BrowserOpenFailed = Event.define({
type: "mcp.browser.open.failed",
schema: {
mcpName: Schema.String,
url: Schema.String,
},
})
export const Definitions = Event.inventory(ToolsChanged, BrowserOpenFailed)

View file

@ -0,0 +1,9 @@
export * as ModelsDev from "./models-dev"
import { define, inventory } from "./event"
const Refreshed = define({
type: "models-dev.refreshed",
schema: {},
})
export const Event = { Refreshed, Definitions: inventory(Refreshed) }

View file

@ -0,0 +1,67 @@
export * as PermissionV1 from "./permission-v1"
import { Schema } from "effect"
import { define, inventory } from "./event"
import { ascending } from "./identifier"
import { Project } from "./project"
import { withStatics } from "./schema"
import { SessionID } from "./session-id"
export const ID = Schema.String.check(Schema.isStartsWith("per")).pipe(
Schema.brand("PermissionID"),
withStatics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "per_" + ascending()) })),
)
export type ID = typeof ID.Type
export const Action = Schema.Literals(["allow", "deny", "ask"]).annotate({ identifier: "PermissionAction" })
export type Action = typeof Action.Type
export const Rule = Schema.Struct({ permission: Schema.String, pattern: Schema.String, action: Action }).annotate({
identifier: "PermissionRule",
})
export type Rule = typeof Rule.Type
export const Ruleset = Schema.Array(Rule).annotate({ identifier: "PermissionRuleset" })
export type Ruleset = typeof Ruleset.Type
export const Request = Schema.Struct({
id: ID,
sessionID: SessionID,
permission: Schema.String,
patterns: Schema.Array(Schema.String),
metadata: Schema.Record(Schema.String, Schema.Unknown),
always: Schema.Array(Schema.String),
tool: Schema.optional(Schema.Struct({ messageID: Schema.String, callID: Schema.String })),
}).annotate({ identifier: "PermissionRequest" })
export type Request = typeof Request.Type
export const Reply = Schema.Literals(["once", "always", "reject"])
export type Reply = typeof Reply.Type
export const ReplyBody = Schema.Struct({ reply: Reply, message: Schema.optional(Schema.String) }).annotate({
identifier: "PermissionReplyBody",
})
export type ReplyBody = typeof ReplyBody.Type
export const Approval = Schema.Struct({ projectID: Project.ID, patterns: Schema.Array(Schema.String) }).annotate({
identifier: "PermissionApproval",
})
export type Approval = typeof Approval.Type
export const AskInput = Schema.Struct({ ...Request.fields, id: Schema.optional(ID), ruleset: Ruleset }).annotate({
identifier: "PermissionAskInput",
})
export type AskInput = typeof AskInput.Type
export const ReplyInput = Schema.Struct({ requestID: ID, ...ReplyBody.fields }).annotate({
identifier: "PermissionReplyInput",
})
export type ReplyInput = typeof ReplyInput.Type
const Asked = define({ type: "permission.asked", schema: Request.fields })
const Replied = define({
type: "permission.replied",
schema: { sessionID: SessionID, requestID: ID, reply: Reply },
})
export const Event = { Asked, Replied, Definitions: inventory(Asked, Replied) }
export const PermissionV1Event = Event

View file

@ -1,6 +1,54 @@
export * as Permission from "./permission"
import { Schema } from "effect"
import { define, inventory } from "./event"
import { ascending } from "./identifier"
import { SessionID } from "./session-id"
import { withStatics } from "./schema"
export const ID = Schema.String.check(Schema.isStartsWith("per")).pipe(
Schema.brand("PermissionV2.ID"),
withStatics((schema) => ({ create: (id?: string) => schema.make(id ?? "per_" + ascending()) })),
)
export type ID = typeof ID.Type
export const Source = Schema.Union([
Schema.Struct({
type: Schema.Literal("tool"),
messageID: Schema.String,
callID: Schema.String,
}),
]).annotate({ identifier: "PermissionV2.Source" })
export type Source = typeof Source.Type
const RequestFields = {
sessionID: SessionID,
action: Schema.String,
resources: Schema.Array(Schema.String),
save: Schema.Array(Schema.String).pipe(Schema.optional),
metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
source: Source.pipe(Schema.optional),
}
export const Request = Schema.Struct({
id: ID,
...RequestFields,
}).annotate({ identifier: "PermissionV2.Request" })
export type Request = typeof Request.Type
export const Reply = Schema.Literals(["once", "always", "reject"]).annotate({ identifier: "PermissionV2.Reply" })
export type Reply = typeof Reply.Type
const Asked = define({ type: "permission.v2.asked", schema: Request.fields })
const Replied = define({
type: "permission.v2.replied",
schema: {
sessionID: SessionID,
requestID: ID,
reply: Reply,
},
})
export const Event = { Asked, Replied, Definitions: inventory(Asked, Replied) }
export const Effect = Schema.Literals(["allow", "deny", "ask"]).annotate({ identifier: "PermissionV2.Effect" })
export type Effect = typeof Effect.Type

View file

@ -0,0 +1,15 @@
export * as Plugin from "./plugin"
import { Schema } from "effect"
import { define, inventory } from "./event"
export const ID = Schema.String.pipe(Schema.brand("Plugin.ID"))
export type ID = typeof ID.Type
export const PluginID = ID
const Added = define({
type: "plugin.added",
schema: { id: ID },
})
export const Event = { Added, Definitions: inventory(Added) }
export const PluginEvent = Event

View file

@ -0,0 +1,11 @@
export * as ProjectDirectories from "./project-directories"
import { define, inventory } from "./event"
import { Project } from "./project"
const Updated = define({
type: "project.directories.updated",
schema: { projectID: Project.ID },
})
export const Event = { Updated, Definitions: inventory(Updated) }
export const ProjectDirectoriesEvent = Event

View file

@ -1,10 +1,43 @@
export * as Project from "./project"
import { Schema } from "effect"
import { withStatics } from "./schema"
import { define, inventory } from "./event"
import { NonNegativeInt, optionalOmitUndefined, withStatics } from "./schema"
export const ID = Schema.String.pipe(
Schema.brand("Project.ID"),
withStatics((schema) => ({ global: schema.make("global") })),
)
export type ID = typeof ID.Type
export const Vcs = Schema.Literal("git")
export const Icon = Schema.Struct({
url: optionalOmitUndefined(Schema.String),
override: optionalOmitUndefined(Schema.String),
color: optionalOmitUndefined(Schema.String),
})
export const Commands = Schema.Struct({
start: optionalOmitUndefined(
Schema.String.annotate({ description: "Startup script to run when creating a new workspace (worktree)" }),
),
})
export const Time = Schema.Struct({
created: NonNegativeInt,
updated: NonNegativeInt,
initialized: optionalOmitUndefined(NonNegativeInt),
})
export const Info = Schema.Struct({
id: ID,
worktree: Schema.String,
vcs: optionalOmitUndefined(Vcs),
name: optionalOmitUndefined(Schema.String),
icon: optionalOmitUndefined(Icon),
commands: optionalOmitUndefined(Commands),
time: Time,
sandboxes: Schema.Array(Schema.String),
}).annotate({ identifier: "Project" })
export type Info = typeof Info.Type
const Updated = define({ type: "project.updated", schema: Info.fields })
export const Event = { Updated, Definitions: inventory(Updated) }

View file

@ -0,0 +1,35 @@
export * as Pty from "./pty"
import { Schema } from "effect"
import { define, inventory } from "./event"
import { ascending } from "./identifier"
import { NonNegativeInt } from "./schema"
import { withStatics } from "./schema"
const IDSchema = Schema.String.check(Schema.isStartsWith("pty")).pipe(Schema.brand("PtyID"))
export const ID = IDSchema.pipe(
withStatics((schema: typeof IDSchema) => ({
ascending: (id?: string) => schema.make(id ?? "pty_" + ascending()),
})),
)
export type ID = typeof ID.Type
export const Info = Schema.Struct({
id: ID,
title: Schema.String,
command: Schema.String,
args: Schema.Array(Schema.String),
cwd: Schema.String,
status: Schema.Literals(["running", "exited"]),
pid: NonNegativeInt,
exitCode: Schema.optional(NonNegativeInt),
}).annotate({ identifier: "Pty" })
export const PtyInfo = Info
const Created = define({ type: "pty.created", schema: { info: Info } })
const Updated = define({ type: "pty.updated", schema: { info: Info } })
const Exited = define({ type: "pty.exited", schema: { id: ID, exitCode: NonNegativeInt } })
const Deleted = define({ type: "pty.deleted", schema: { id: ID } })
export const Event = { Created, Updated, Exited, Deleted, Definitions: inventory(Created, Updated, Exited, Deleted) }
export const PtyEvent = Event

View file

@ -0,0 +1,66 @@
export * as QuestionV1 from "./question-v1"
import { Schema } from "effect"
import { define, inventory } from "./event"
import { ascending } from "./identifier"
import { withStatics } from "./schema"
import { SessionID } from "./session-id"
import { SessionV1 } from "./session-v1"
export const ID = Schema.String.check(Schema.isStartsWith("que")).pipe(
Schema.brand("QuestionID"),
withStatics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "que_" + ascending()) })),
)
export const Option = Schema.Struct({
label: Schema.String.annotate({ description: "Display text (1-5 words, concise)" }),
description: Schema.String.annotate({ description: "Explanation of choice" }),
}).annotate({ identifier: "QuestionOption" })
const base = {
question: Schema.String.annotate({ description: "Complete question" }),
header: Schema.String.annotate({ description: "Very short label (max 30 chars)" }),
options: Schema.Array(Option).annotate({ description: "Available choices" }),
multiple: Schema.optional(Schema.Boolean).annotate({ description: "Allow selecting multiple choices" }),
}
export const Info = Schema.Struct({
...base,
custom: Schema.optional(Schema.Boolean).annotate({ description: "Allow typing a custom answer (default: true)" }),
}).annotate({ identifier: "QuestionInfo" })
export const Prompt = Schema.Struct(base).annotate({ identifier: "QuestionPrompt" })
export const Tool = Schema.Struct({ messageID: SessionV1.MessageID, callID: Schema.String }).annotate({
identifier: "QuestionTool",
})
export const Request = Schema.Struct({
id: ID,
sessionID: SessionID,
questions: Schema.Array(Info).annotate({ description: "Questions to ask" }),
tool: Schema.optional(Tool),
}).annotate({ identifier: "QuestionRequest" })
export const Answer = Schema.Array(Schema.String).annotate({ identifier: "QuestionAnswer" })
export const Reply = Schema.Struct({
answers: Schema.Array(Answer).annotate({
description: "User answers in order of questions (each answer is an array of selected labels)",
}),
}).annotate({ identifier: "QuestionReply" })
export const Replied = Schema.Struct({
sessionID: SessionID,
requestID: ID,
answers: Schema.Array(Answer),
}).annotate({
identifier: "QuestionReplied",
})
export const Rejected = Schema.Struct({ sessionID: SessionID, requestID: ID }).annotate({
identifier: "QuestionRejected",
})
const Asked = define({ type: "question.asked", schema: Request.fields })
const RepliedEvent = define({ type: "question.replied", schema: Replied.fields })
const RejectedEvent = define({ type: "question.rejected", schema: Rejected.fields })
export const Event = {
Asked,
Replied: RepliedEvent,
Rejected: RejectedEvent,
Definitions: inventory(Asked, RepliedEvent, RejectedEvent),
}

View file

@ -0,0 +1,79 @@
export * as Question from "./question"
import { Schema } from "effect"
import { define, inventory } from "./event"
import { ascending } from "./identifier"
import { SessionID } from "./session-id"
import { withStatics } from "./schema"
export const ID = Schema.String.check(Schema.isStartsWith("que")).pipe(
Schema.brand("QuestionV2.ID"),
withStatics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "que_" + ascending()) })),
)
export type ID = typeof ID.Type
export const Option = Schema.Struct({
label: Schema.String.annotate({ description: "Display text (1-5 words, concise)" }),
description: Schema.String.annotate({ description: "Explanation of choice" }),
}).annotate({ identifier: "QuestionV2.Option" })
export type Option = typeof Option.Type
const base = {
question: Schema.String.annotate({ description: "Complete question" }),
header: Schema.String.annotate({ description: "Very short label (max 30 chars)" }),
options: Schema.Array(Option).annotate({ description: "Available choices" }),
multiple: Schema.Boolean.pipe(Schema.optional).annotate({ description: "Allow selecting multiple choices" }),
}
export const Info = Schema.Struct({
...base,
custom: Schema.Boolean.pipe(Schema.optional).annotate({
description: "Allow typing a custom answer (default: true)",
}),
}).annotate({ identifier: "QuestionV2.Info" })
export type Info = typeof Info.Type
export const Prompt = Schema.Struct(base).annotate({ identifier: "QuestionV2.Prompt" })
export type Prompt = typeof Prompt.Type
export const Tool = Schema.Struct({
messageID: Schema.String,
callID: Schema.String,
}).annotate({ identifier: "QuestionV2.Tool" })
export type Tool = typeof Tool.Type
export const Request = Schema.Struct({
id: ID,
sessionID: SessionID,
questions: Schema.Array(Info).annotate({ description: "Questions to ask" }),
tool: Tool.pipe(Schema.optional),
}).annotate({ identifier: "QuestionV2.Request" })
export type Request = typeof Request.Type
export const Answer = Schema.Array(Schema.String).annotate({ identifier: "QuestionV2.Answer" })
export type Answer = typeof Answer.Type
export const Reply = Schema.Struct({
answers: Schema.Array(Answer).annotate({
description: "User answers in order of questions (each answer is an array of selected labels)",
}),
}).annotate({ identifier: "QuestionV2.Reply" })
export type Reply = typeof Reply.Type
const Asked = define({ type: "question.v2.asked", schema: Request.fields })
const Replied = define({
type: "question.v2.replied",
schema: {
sessionID: SessionID,
requestID: ID,
answers: Schema.Array(Answer),
},
})
const Rejected = define({
type: "question.v2.rejected",
schema: {
sessionID: SessionID,
requestID: ID,
},
})
export const Event = { Asked, Replied, Rejected, Definitions: inventory(Asked, Replied, Rejected) }

View file

@ -1,8 +1,12 @@
export * as Reference from "./reference"
import { Schema } from "effect"
import { define, inventory } from "./event"
import { AbsolutePath } from "./schema"
const Updated = define({ type: "reference.updated", schema: {} })
export const Event = { Updated, Definitions: inventory(Updated) }
export interface LocalSource extends Schema.Schema.Type<typeof LocalSource> {}
export const LocalSource = Schema.Struct({
type: Schema.Literal("local"),

View file

@ -0,0 +1,8 @@
export * as ServerEvent from "./server-event"
import { Event } from "./event"
export const Connected = Event.define({ type: "server.connected", schema: {} })
export const Disposed = Event.define({ type: "global.disposed", schema: {} })
export const Definitions = Event.inventory(Connected, Disposed)

View file

@ -0,0 +1,13 @@
export * as SessionCompactionEvent from "./session-compaction-event"
import { Event } from "./event"
import { SessionID } from "./session-id"
export const Compacted = Event.define({
type: "session.compacted",
schema: {
sessionID: SessionID,
},
})
export const Definitions = Event.inventory(Compacted)

View file

@ -0,0 +1,497 @@
export * as SessionEvent from "./session-event"
import { Schema } from "effect"
import { Event } from "./event"
import { ProviderMetadata, ToolContent } from "./llm"
import { Delivery } from "./session-delivery"
import { Model } from "./model"
import { DateTimeUtcFromMillis, NonNegativeInt, RelativePath } from "./schema"
import { FileAttachment, Prompt } from "./prompt"
import { SessionID } from "./session-id"
import { Location } from "./location"
import { SessionMessageID } from "./session-message-id"
import { SessionMessage } from "./session-message"
export { FileAttachment }
export const Source = Schema.Struct({
start: NonNegativeInt,
end: NonNegativeInt,
text: Schema.String,
}).annotate({
identifier: "session.next.event.source",
})
export type Source = typeof Source.Type
const Base = {
timestamp: DateTimeUtcFromMillis,
sessionID: SessionID,
}
const PromptFields = {
...Base,
messageID: SessionMessageID.ID,
prompt: Prompt,
delivery: Delivery,
}
const options = {
durable: {
aggregate: "sessionID",
version: 1,
},
} as const
const stepSettlementOptions = {
durable: {
aggregate: "sessionID",
version: 2,
},
} as const
export const UnknownError = SessionMessage.UnknownError
export type UnknownError = SessionMessage.UnknownError
export const AgentSwitched = Event.define({
type: "session.next.agent.switched",
...options,
schema: {
...Base,
messageID: SessionMessageID.ID,
agent: Schema.String,
},
})
export type AgentSwitched = typeof AgentSwitched.Type
export const ModelSwitched = Event.define({
type: "session.next.model.switched",
...options,
schema: {
...Base,
messageID: SessionMessageID.ID,
model: Model.Ref,
},
})
export type ModelSwitched = typeof ModelSwitched.Type
export const Moved = Event.define({
type: "session.next.moved",
...options,
schema: {
...Base,
location: Location.Ref,
subdirectory: RelativePath.pipe(Schema.optional),
},
})
export type Moved = typeof Moved.Type
export const Prompted = Event.define({
type: "session.next.prompted",
...options,
schema: PromptFields,
})
export type Prompted = typeof Prompted.Type
export const PromptAdmitted = Event.define({
type: "session.next.prompt.admitted",
...options,
schema: PromptFields,
})
export type PromptAdmitted = typeof PromptAdmitted.Type
export const ContextUpdated = Event.define({
type: "session.next.context.updated",
...options,
schema: {
...Base,
messageID: SessionMessageID.ID,
text: Schema.String,
},
})
export type ContextUpdated = typeof ContextUpdated.Type
export const Synthetic = Event.define({
type: "session.next.synthetic",
...options,
schema: {
...Base,
messageID: SessionMessageID.ID,
text: Schema.String,
},
})
export type Synthetic = typeof Synthetic.Type
export namespace Shell {
export const Started = Event.define({
type: "session.next.shell.started",
...options,
schema: {
...Base,
messageID: SessionMessageID.ID,
callID: Schema.String,
command: Schema.String,
},
})
export type Started = typeof Started.Type
export const Ended = Event.define({
type: "session.next.shell.ended",
...options,
schema: {
...Base,
callID: Schema.String,
output: Schema.String,
},
})
export type Ended = typeof Ended.Type
}
export namespace Step {
export const Started = Event.define({
type: "session.next.step.started",
...options,
schema: {
...Base,
assistantMessageID: SessionMessageID.ID,
agent: Schema.String,
model: Model.Ref,
snapshot: Schema.String.pipe(Schema.optional),
},
})
export type Started = typeof Started.Type
export const Ended = Event.define({
type: "session.next.step.ended",
...stepSettlementOptions,
schema: {
...Base,
assistantMessageID: SessionMessageID.ID,
finish: Schema.String,
cost: Schema.Finite,
tokens: Schema.Struct({
input: Schema.Finite,
output: Schema.Finite,
reasoning: Schema.Finite,
cache: Schema.Struct({
read: Schema.Finite,
write: Schema.Finite,
}),
}),
snapshot: Schema.String.pipe(Schema.optional),
},
})
export type Ended = typeof Ended.Type
export const Failed = Event.define({
type: "session.next.step.failed",
...stepSettlementOptions,
schema: {
...Base,
assistantMessageID: SessionMessageID.ID,
error: UnknownError,
},
})
export type Failed = typeof Failed.Type
}
export namespace Text {
export const Started = Event.define({
type: "session.next.text.started",
...options,
schema: {
...Base,
assistantMessageID: SessionMessageID.ID,
textID: Schema.String,
},
})
export type Started = typeof Started.Type
// Stream fragments are live-only; Text.Ended is the replayable full-value boundary.
export const Delta = Event.define({
type: "session.next.text.delta",
schema: {
...Base,
assistantMessageID: SessionMessageID.ID,
textID: Schema.String,
delta: Schema.String,
},
})
export type Delta = typeof Delta.Type
export const Ended = Event.define({
type: "session.next.text.ended",
...options,
schema: {
...Base,
assistantMessageID: SessionMessageID.ID,
textID: Schema.String,
text: Schema.String,
},
})
export type Ended = typeof Ended.Type
}
export namespace Reasoning {
export const Started = Event.define({
type: "session.next.reasoning.started",
...options,
schema: {
...Base,
assistantMessageID: SessionMessageID.ID,
reasoningID: Schema.String,
providerMetadata: ProviderMetadata.pipe(Schema.optional),
},
})
export type Started = typeof Started.Type
// Stream fragments are live-only; Reasoning.Ended is the replayable full-value boundary.
export const Delta = Event.define({
type: "session.next.reasoning.delta",
schema: {
...Base,
assistantMessageID: SessionMessageID.ID,
reasoningID: Schema.String,
delta: Schema.String,
},
})
export type Delta = typeof Delta.Type
export const Ended = Event.define({
type: "session.next.reasoning.ended",
...options,
schema: {
...Base,
assistantMessageID: SessionMessageID.ID,
reasoningID: Schema.String,
text: Schema.String,
providerMetadata: ProviderMetadata.pipe(Schema.optional),
},
})
export type Ended = typeof Ended.Type
}
export namespace Tool {
const ToolBase = {
...Base,
assistantMessageID: SessionMessageID.ID,
callID: Schema.String,
}
export namespace Input {
export const Started = Event.define({
type: "session.next.tool.input.started",
...options,
schema: {
...ToolBase,
name: Schema.String,
},
})
export type Started = typeof Started.Type
// Stream fragments are live-only; Input.Ended is the replayable raw-input boundary.
export const Delta = Event.define({
type: "session.next.tool.input.delta",
schema: {
...ToolBase,
delta: Schema.String,
},
})
export type Delta = typeof Delta.Type
export const Ended = Event.define({
type: "session.next.tool.input.ended",
...options,
schema: {
...ToolBase,
text: Schema.String,
},
})
export type Ended = typeof Ended.Type
}
export const Called = Event.define({
type: "session.next.tool.called",
...options,
schema: {
...ToolBase,
tool: Schema.String,
input: Schema.Record(Schema.String, Schema.Unknown),
provider: Schema.Struct({
executed: Schema.Boolean,
metadata: ProviderMetadata.pipe(Schema.optional),
}),
},
})
export type Called = typeof Called.Type
/**
* Replayable bounded running-tool state. Tools should checkpoint semantic
* transitions or at a bounded cadence, not persist every stdout/stderr chunk.
*/
export const Progress = Event.define({
type: "session.next.tool.progress",
...options,
schema: {
...ToolBase,
structured: Schema.Record(Schema.String, Schema.Any),
content: Schema.Array(ToolContent),
},
})
export type Progress = typeof Progress.Type
export const Success = Event.define({
type: "session.next.tool.success",
...options,
schema: {
...ToolBase,
structured: Schema.Record(Schema.String, Schema.Any),
content: Schema.Array(ToolContent),
outputPaths: Schema.Array(Schema.String).pipe(Schema.optional),
result: Schema.Unknown.pipe(Schema.optional),
provider: Schema.Struct({
executed: Schema.Boolean,
metadata: ProviderMetadata.pipe(Schema.optional),
}),
},
})
export type Success = typeof Success.Type
export const Failed = Event.define({
type: "session.next.tool.failed",
...options,
schema: {
...ToolBase,
error: UnknownError,
result: Schema.Unknown.pipe(Schema.optional),
provider: Schema.Struct({
executed: Schema.Boolean,
metadata: ProviderMetadata.pipe(Schema.optional),
}),
},
})
export type Failed = typeof Failed.Type
}
export const RetryError = Schema.Struct({
message: Schema.String,
statusCode: Schema.Finite.pipe(Schema.optional),
isRetryable: Schema.Boolean,
responseHeaders: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
responseBody: Schema.String.pipe(Schema.optional),
metadata: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
}).annotate({
identifier: "session.next.retry_error",
})
export type RetryError = typeof RetryError.Type
export const Retried = Event.define({
type: "session.next.retried",
...options,
schema: {
...Base,
attempt: Schema.Finite,
error: RetryError,
},
})
export type Retried = typeof Retried.Type
export namespace Compaction {
export const Started = Event.define({
type: "session.next.compaction.started",
...options,
schema: {
...Base,
messageID: SessionMessageID.ID,
reason: Schema.Union([Schema.Literal("auto"), Schema.Literal("manual")]),
},
})
export type Started = typeof Started.Type
export const Delta = Event.define({
type: "session.next.compaction.delta",
schema: {
...Base,
messageID: SessionMessageID.ID,
text: Schema.String,
},
})
export type Delta = typeof Delta.Type
export const Ended = Event.define({
type: "session.next.compaction.ended",
...options,
schema: {
...Base,
messageID: SessionMessageID.ID,
reason: Started.data.fields.reason,
text: Schema.String,
recent: Schema.String,
},
})
export type Ended = typeof Ended.Type
}
export const DurableDefinitions = Event.inventory(
AgentSwitched,
ModelSwitched,
Moved,
Prompted,
PromptAdmitted,
ContextUpdated,
Synthetic,
Shell.Started,
Shell.Ended,
Step.Started,
Step.Ended,
Step.Failed,
Text.Started,
Text.Ended,
Tool.Input.Started,
Tool.Input.Ended,
Tool.Called,
Tool.Progress,
Tool.Success,
Tool.Failed,
Reasoning.Started,
Reasoning.Ended,
Retried,
Compaction.Started,
Compaction.Ended,
)
export const Definitions = Event.inventory(
AgentSwitched,
ModelSwitched,
Moved,
Prompted,
PromptAdmitted,
ContextUpdated,
Synthetic,
Shell.Started,
Shell.Ended,
Step.Started,
Step.Ended,
Step.Failed,
Text.Started,
Text.Delta,
Text.Ended,
Reasoning.Started,
Reasoning.Delta,
Reasoning.Ended,
Tool.Input.Started,
Tool.Input.Delta,
Tool.Input.Ended,
Tool.Called,
Tool.Progress,
Tool.Success,
Tool.Failed,
Retried,
Compaction.Started,
Compaction.Delta,
Compaction.Ended,
)
export const Durable = Schema.Union(DurableDefinitions, { mode: "oneOf" }).pipe(Schema.toTaggedUnion("type"))
export type DurableEvent = typeof Durable.Type
export const All = Schema.Union(Definitions, { mode: "oneOf" }).pipe(Schema.toTaggedUnion("type"))
export type Event = typeof All.Type
export type Type = Event["type"]

View file

@ -1,10 +1,8 @@
export * as SessionID from "./session-id"
import { Schema } from "effect"
import { descending } from "./identifier"
import { withStatics } from "./schema"
export const ID = Schema.String.check(Schema.isStartsWith("ses")).pipe(
export const SessionID = Schema.String.check(Schema.isStartsWith("ses")).pipe(
Schema.brand("SessionID"),
withStatics((schema) => {
const create = () => schema.make("ses_" + descending())
@ -14,4 +12,4 @@ export const ID = Schema.String.check(Schema.isStartsWith("ses")).pipe(
}
}),
)
export type ID = typeof ID.Type
export type SessionID = typeof SessionID.Type

View file

@ -14,7 +14,7 @@ export interface Admitted extends Schema.Schema.Type<typeof Admitted> {}
export const Admitted = Schema.Struct({
admittedSeq: NonNegativeInt,
id: SessionMessageID.ID,
sessionID: SessionID.ID,
sessionID: SessionID,
prompt: Prompt,
delivery: Delivery,
timeCreated: DateTimeUtcFromMillis,

View file

@ -49,7 +49,7 @@ export const User = Schema.Struct({
export interface Synthetic extends Schema.Schema.Type<typeof Synthetic> {}
export const Synthetic = Schema.Struct({
...Base,
sessionID: SessionID.ID,
sessionID: SessionID,
text: Schema.String,
type: Schema.Literal("synthetic"),
}).annotate({ identifier: "Session.Message.Synthetic" })

View file

@ -0,0 +1,50 @@
export * as SessionStatusEvent from "./session-status-event"
import { Schema } from "effect"
import { Event } from "./event"
import { NonNegativeInt } from "./schema"
import { SessionID } from "./session-id"
export const Info = Schema.Union([
Schema.Struct({
type: Schema.Literal("idle"),
}),
Schema.Struct({
type: Schema.Literal("retry"),
attempt: NonNegativeInt,
message: Schema.String,
action: Schema.optional(
Schema.Struct({
reason: Schema.String,
provider: Schema.String,
title: Schema.String,
message: Schema.String,
label: Schema.String,
link: Schema.optional(Schema.String),
}),
),
next: NonNegativeInt,
}),
Schema.Struct({
type: Schema.Literal("busy"),
}),
]).annotate({ identifier: "SessionStatus" })
export type Info = Schema.Schema.Type<typeof Info>
export const Status = Event.define({
type: "session.status",
schema: {
sessionID: SessionID,
status: Info,
},
})
// deprecated
export const Idle = Event.define({
type: "session.idle",
schema: {
sessionID: SessionID,
},
})
export const Definitions = Event.inventory(Status, Idle)

View file

@ -0,0 +1,24 @@
export * as SessionTodo from "./session-todo"
import { Schema } from "effect"
import { define, inventory } from "./event"
import { SessionID } from "./session-id"
export const Info = Schema.Struct({
content: Schema.String.annotate({ description: "Brief description of the task" }),
status: Schema.String.annotate({
description: "Current status of the task: pending, in_progress, completed, cancelled",
}),
priority: Schema.String.annotate({ description: "Priority level of the task: high, medium, low" }),
}).annotate({ identifier: "Todo" })
export type Info = typeof Info.Type
export const SessionTodoInfo = Info
const Updated = define({
type: "todo.updated",
schema: {
sessionID: SessionID,
todos: Schema.Array(Info),
},
})
export const Event = { Updated, Definitions: inventory(Updated) }

View file

@ -0,0 +1,676 @@
export * as SessionV1 from "./session-v1"
import { Effect, Schema, Types } from "effect"
import { define, inventory } from "./event"
import { FileDiff } from "./file-diff"
import { PermissionV1 } from "./permission-v1"
import { Project } from "./project"
import { Provider } from "./provider"
import { Model } from "./model"
import { NonNegativeInt, optionalOmitUndefined, withStatics } from "./schema"
import { ascending } from "./identifier"
import { SessionID } from "./session-id"
import { WorkspaceID } from "./workspace-id"
const Timestamp = Schema.Finite.check(Schema.isGreaterThanOrEqualTo(0))
export const MessageID = Schema.String.check(Schema.isStartsWith("msg")).pipe(
Schema.brand("MessageID"),
withStatics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "msg_" + ascending()) })),
)
export type MessageID = typeof MessageID.Type
export const PartID = Schema.String.check(Schema.isStartsWith("prt")).pipe(
Schema.brand("PartID"),
withStatics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "prt_" + ascending()) })),
)
export type PartID = typeof PartID.Type
const namedError = <Name extends string, Fields extends Schema.Struct.Fields>(name: Name, fields: Fields) => {
const schema = Schema.Struct({ name: Schema.Literal(name), data: Schema.Struct(fields) }).annotate({
identifier: name,
})
return { Schema: schema, EffectSchema: schema }
}
export const OutputLengthError = namedError("MessageOutputLengthError", {})
export const AuthError = namedError("ProviderAuthError", {
providerID: Schema.String,
message: Schema.String,
})
export const AbortedError = namedError("MessageAbortedError", { message: Schema.String })
export const StructuredOutputError = namedError("StructuredOutputError", {
message: Schema.String,
retries: NonNegativeInt,
})
export const APIError = namedError("APIError", {
message: Schema.String,
statusCode: Schema.optional(NonNegativeInt),
isRetryable: Schema.Boolean,
responseHeaders: Schema.optional(Schema.Record(Schema.String, Schema.String)),
responseBody: Schema.optional(Schema.String),
metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)),
})
export type APIError = Schema.Schema.Type<typeof APIError.Schema>
export const ContextOverflowError = namedError("ContextOverflowError", {
message: Schema.String,
responseBody: Schema.optional(Schema.String),
})
export const ContentFilterError = namedError("ContentFilterError", {
message: Schema.String,
})
export class OutputFormatText extends Schema.Class<OutputFormatText>("OutputFormatText")({
type: Schema.Literal("text"),
}) {}
export class OutputFormatJsonSchema extends Schema.Class<OutputFormatJsonSchema>("OutputFormatJsonSchema")({
type: Schema.Literal("json_schema"),
schema: Schema.Record(Schema.String, Schema.Any).annotate({ identifier: "JSONSchema" }),
retryCount: NonNegativeInt.pipe(Schema.optional, Schema.withDecodingDefault(Effect.succeed(2))),
}) {}
export const Format = Schema.Union([OutputFormatText, OutputFormatJsonSchema]).annotate({
discriminator: "type",
identifier: "OutputFormat",
})
export type OutputFormat = Schema.Schema.Type<typeof Format>
const partBase = {
id: PartID,
sessionID: SessionID,
messageID: MessageID,
}
export const SnapshotPart = Schema.Struct({
...partBase,
type: Schema.Literal("snapshot"),
snapshot: Schema.String,
}).annotate({ identifier: "SnapshotPart" })
export type SnapshotPart = Types.DeepMutable<Schema.Schema.Type<typeof SnapshotPart>>
export const PatchPart = Schema.Struct({
...partBase,
type: Schema.Literal("patch"),
hash: Schema.String,
files: Schema.Array(Schema.String),
}).annotate({ identifier: "PatchPart" })
export type PatchPart = Types.DeepMutable<Schema.Schema.Type<typeof PatchPart>>
export const TextPart = Schema.Struct({
...partBase,
type: Schema.Literal("text"),
text: Schema.String,
synthetic: Schema.optional(Schema.Boolean),
ignored: Schema.optional(Schema.Boolean),
time: Schema.optional(
Schema.Struct({
start: NonNegativeInt,
end: Schema.optional(NonNegativeInt),
}),
),
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
}).annotate({ identifier: "TextPart" })
export type TextPart = Types.DeepMutable<Schema.Schema.Type<typeof TextPart>>
export const ReasoningPart = Schema.Struct({
...partBase,
type: Schema.Literal("reasoning"),
text: Schema.String,
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
time: Schema.Struct({
start: NonNegativeInt,
end: Schema.optional(NonNegativeInt),
}),
}).annotate({ identifier: "ReasoningPart" })
export type ReasoningPart = Types.DeepMutable<Schema.Schema.Type<typeof ReasoningPart>>
const filePartSourceBase = {
text: Schema.Struct({
value: Schema.String,
start: Schema.Finite,
end: Schema.Finite,
}).annotate({ identifier: "FilePartSourceText" }),
}
export const Range = Schema.Struct({
start: Schema.Struct({ line: NonNegativeInt, character: NonNegativeInt }),
end: Schema.Struct({ line: NonNegativeInt, character: NonNegativeInt }),
}).annotate({ identifier: "Range" })
export type Range = typeof Range.Type
export const FileSource = Schema.Struct({
...filePartSourceBase,
type: Schema.Literal("file"),
path: Schema.String,
}).annotate({ identifier: "FileSource" })
export const SymbolSource = Schema.Struct({
...filePartSourceBase,
type: Schema.Literal("symbol"),
path: Schema.String,
range: Range,
name: Schema.String,
kind: NonNegativeInt,
}).annotate({ identifier: "SymbolSource" })
export const ResourceSource = Schema.Struct({
...filePartSourceBase,
type: Schema.Literal("resource"),
clientName: Schema.String,
uri: Schema.String,
}).annotate({ identifier: "ResourceSource" })
export const FilePartSource = Schema.Union([FileSource, SymbolSource, ResourceSource]).annotate({
discriminator: "type",
identifier: "FilePartSource",
})
export const FilePart = Schema.Struct({
...partBase,
type: Schema.Literal("file"),
mime: Schema.String,
filename: Schema.optional(Schema.String),
url: Schema.String,
source: Schema.optional(FilePartSource),
}).annotate({ identifier: "FilePart" })
export type FilePart = Types.DeepMutable<Schema.Schema.Type<typeof FilePart>>
export const AgentPart = Schema.Struct({
...partBase,
type: Schema.Literal("agent"),
name: Schema.String,
source: Schema.optional(
Schema.Struct({
value: Schema.String,
start: NonNegativeInt,
end: NonNegativeInt,
}),
),
}).annotate({ identifier: "AgentPart" })
export type AgentPart = Types.DeepMutable<Schema.Schema.Type<typeof AgentPart>>
export const CompactionPart = Schema.Struct({
...partBase,
type: Schema.Literal("compaction"),
auto: Schema.Boolean,
overflow: Schema.optional(Schema.Boolean),
tail_start_id: Schema.optional(MessageID),
}).annotate({ identifier: "CompactionPart" })
export type CompactionPart = Types.DeepMutable<Schema.Schema.Type<typeof CompactionPart>>
export const SubtaskPart = Schema.Struct({
...partBase,
type: Schema.Literal("subtask"),
prompt: Schema.String,
description: Schema.String,
agent: Schema.String,
model: Schema.optional(
Schema.Struct({
providerID: Provider.ID,
modelID: Model.ID,
}),
),
command: Schema.optional(Schema.String),
}).annotate({ identifier: "SubtaskPart" })
export type SubtaskPart = Types.DeepMutable<Schema.Schema.Type<typeof SubtaskPart>>
export const RetryPart = Schema.Struct({
...partBase,
type: Schema.Literal("retry"),
attempt: NonNegativeInt,
error: APIError.EffectSchema,
time: Schema.Struct({
created: NonNegativeInt,
}),
}).annotate({ identifier: "RetryPart" })
export type RetryPart = Omit<Types.DeepMutable<Schema.Schema.Type<typeof RetryPart>>, "error"> & {
error: APIError
}
export const StepStartPart = Schema.Struct({
...partBase,
type: Schema.Literal("step-start"),
snapshot: Schema.optional(Schema.String),
}).annotate({ identifier: "StepStartPart" })
export type StepStartPart = Types.DeepMutable<Schema.Schema.Type<typeof StepStartPart>>
export const StepFinishPart = Schema.Struct({
...partBase,
type: Schema.Literal("step-finish"),
reason: Schema.String,
snapshot: Schema.optional(Schema.String),
cost: Schema.Finite,
tokens: Schema.Struct({
total: Schema.optional(Schema.Finite),
input: Schema.Finite,
output: Schema.Finite,
reasoning: Schema.Finite,
cache: Schema.Struct({
read: Schema.Finite,
write: Schema.Finite,
}),
}),
}).annotate({ identifier: "StepFinishPart" })
export type StepFinishPart = Types.DeepMutable<Schema.Schema.Type<typeof StepFinishPart>>
export const ToolStatePending = Schema.Struct({
status: Schema.Literal("pending"),
input: Schema.Record(Schema.String, Schema.Any),
raw: Schema.String,
}).annotate({ identifier: "ToolStatePending" })
export type ToolStatePending = Types.DeepMutable<Schema.Schema.Type<typeof ToolStatePending>>
export const ToolStateRunning = Schema.Struct({
status: Schema.Literal("running"),
input: Schema.Record(Schema.String, Schema.Any),
title: Schema.optional(Schema.String),
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
time: Schema.Struct({
start: NonNegativeInt,
}),
}).annotate({ identifier: "ToolStateRunning" })
export type ToolStateRunning = Types.DeepMutable<Schema.Schema.Type<typeof ToolStateRunning>>
export const ToolStateCompleted = Schema.Struct({
status: Schema.Literal("completed"),
input: Schema.Record(Schema.String, Schema.Any),
output: Schema.String,
title: Schema.String,
metadata: Schema.Record(Schema.String, Schema.Any),
time: Schema.Struct({
start: NonNegativeInt,
end: NonNegativeInt,
compacted: Schema.optional(NonNegativeInt),
}),
attachments: Schema.optional(Schema.Array(FilePart)),
}).annotate({ identifier: "ToolStateCompleted" })
export type ToolStateCompleted = Types.DeepMutable<Schema.Schema.Type<typeof ToolStateCompleted>>
export const ToolStateError = Schema.Struct({
status: Schema.Literal("error"),
input: Schema.Record(Schema.String, Schema.Any),
error: Schema.String,
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
time: Schema.Struct({
start: NonNegativeInt,
end: NonNegativeInt,
}),
}).annotate({ identifier: "ToolStateError" })
export type ToolStateError = Types.DeepMutable<Schema.Schema.Type<typeof ToolStateError>>
export const ToolState = Schema.Union([
ToolStatePending,
ToolStateRunning,
ToolStateCompleted,
ToolStateError,
]).annotate({
discriminator: "status",
identifier: "ToolState",
})
export type ToolState = ToolStatePending | ToolStateRunning | ToolStateCompleted | ToolStateError
export const ToolPart = Schema.Struct({
...partBase,
type: Schema.Literal("tool"),
callID: Schema.String,
tool: Schema.String,
state: ToolState,
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
}).annotate({ identifier: "ToolPart" })
export type ToolPart = Omit<Types.DeepMutable<Schema.Schema.Type<typeof ToolPart>>, "state"> & {
state: ToolState
}
const messageBase = {
id: MessageID,
sessionID: partBase.sessionID,
}
export const User = Schema.Struct({
...messageBase,
role: Schema.Literal("user"),
time: Schema.Struct({
created: Timestamp,
}),
format: Schema.optional(Format),
summary: Schema.optional(
Schema.Struct({
title: Schema.optional(Schema.String),
body: Schema.optional(Schema.String),
diffs: Schema.Array(FileDiff.Info),
}),
),
agent: Schema.String,
model: Schema.Struct({
providerID: Provider.ID,
modelID: Model.ID,
variant: Schema.optional(Schema.String),
}),
system: Schema.optional(Schema.String),
tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)),
}).annotate({ identifier: "UserMessage" })
export type User = Types.DeepMutable<Schema.Schema.Type<typeof User>>
export const Part = Schema.Union([
TextPart,
SubtaskPart,
ReasoningPart,
FilePart,
ToolPart,
StepStartPart,
StepFinishPart,
SnapshotPart,
PatchPart,
AgentPart,
RetryPart,
CompactionPart,
]).annotate({ discriminator: "type", identifier: "Part" })
export type Part =
| TextPart
| SubtaskPart
| ReasoningPart
| FilePart
| ToolPart
| StepStartPart
| StepFinishPart
| SnapshotPart
| PatchPart
| AgentPart
| RetryPart
| CompactionPart
const AssistantErrorSchema = Schema.Union([
AuthError.EffectSchema,
namedError("UnknownError", { message: Schema.String, ref: Schema.optional(Schema.String) }).EffectSchema,
OutputLengthError.EffectSchema,
AbortedError.EffectSchema,
StructuredOutputError.EffectSchema,
ContextOverflowError.EffectSchema,
ContentFilterError.EffectSchema,
APIError.EffectSchema,
]).annotate({ discriminator: "name" })
type AssistantError = Schema.Schema.Type<typeof AssistantErrorSchema>
export const TextPartInput = Schema.Struct({
id: Schema.optional(PartID),
type: Schema.Literal("text"),
text: Schema.String,
synthetic: Schema.optional(Schema.Boolean),
ignored: Schema.optional(Schema.Boolean),
time: Schema.optional(
Schema.Struct({
start: NonNegativeInt,
end: Schema.optional(NonNegativeInt),
}),
),
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
}).annotate({ identifier: "TextPartInput" })
export type TextPartInput = Types.DeepMutable<Schema.Schema.Type<typeof TextPartInput>>
export const FilePartInput = Schema.Struct({
id: Schema.optional(PartID),
type: Schema.Literal("file"),
mime: Schema.String,
filename: Schema.optional(Schema.String),
url: Schema.String,
source: Schema.optional(FilePartSource),
}).annotate({ identifier: "FilePartInput" })
export type FilePartInput = Types.DeepMutable<Schema.Schema.Type<typeof FilePartInput>>
export const AgentPartInput = Schema.Struct({
id: Schema.optional(PartID),
type: Schema.Literal("agent"),
name: Schema.String,
source: Schema.optional(
Schema.Struct({
value: Schema.String,
start: NonNegativeInt,
end: NonNegativeInt,
}),
),
}).annotate({ identifier: "AgentPartInput" })
export type AgentPartInput = Types.DeepMutable<Schema.Schema.Type<typeof AgentPartInput>>
export const SubtaskPartInput = Schema.Struct({
id: Schema.optional(PartID),
type: Schema.Literal("subtask"),
prompt: Schema.String,
description: Schema.String,
agent: Schema.String,
model: Schema.optional(
Schema.Struct({
providerID: Provider.ID,
modelID: Model.ID,
}),
),
command: Schema.optional(Schema.String),
}).annotate({ identifier: "SubtaskPartInput" })
export type SubtaskPartInput = Types.DeepMutable<Schema.Schema.Type<typeof SubtaskPartInput>>
export const Assistant = Schema.Struct({
...messageBase,
role: Schema.Literal("assistant"),
time: Schema.Struct({
created: NonNegativeInt,
completed: Schema.optional(NonNegativeInt),
}),
error: Schema.optional(AssistantErrorSchema),
parentID: MessageID,
modelID: Model.ID,
providerID: Provider.ID,
mode: Schema.String,
agent: Schema.String,
path: Schema.Struct({
cwd: Schema.String,
root: Schema.String,
}),
summary: Schema.optional(Schema.Boolean),
cost: Schema.Finite,
tokens: Schema.Struct({
total: Schema.optional(Schema.Finite),
input: Schema.Finite,
output: Schema.Finite,
reasoning: Schema.Finite,
cache: Schema.Struct({
read: Schema.Finite,
write: Schema.Finite,
}),
}),
structured: Schema.optional(Schema.Any),
variant: Schema.optional(Schema.String),
finish: Schema.optional(Schema.String),
}).annotate({ identifier: "AssistantMessage" })
export type Assistant = Omit<Types.DeepMutable<Schema.Schema.Type<typeof Assistant>>, "error"> & {
error?: AssistantError
}
export const Info = Schema.Union([User, Assistant]).annotate({ discriminator: "role", identifier: "Message" })
export type Info = User | Assistant
export const WithParts = Schema.Struct({
info: Info,
parts: Schema.Array(Part),
})
export type WithParts = {
info: Info
parts: Part[]
}
const options = {
durable: {
aggregate: "sessionID",
version: 1,
},
} as const
const SessionSummary = Schema.Struct({
additions: Schema.Finite,
deletions: Schema.Finite,
files: Schema.Finite,
diffs: optionalOmitUndefined(Schema.Array(FileDiff.Info)),
})
const SessionTokens = Schema.Struct({
input: Schema.Finite,
output: Schema.Finite,
reasoning: Schema.Finite,
cache: Schema.Struct({
read: Schema.Finite,
write: Schema.Finite,
}),
})
const SessionShare = Schema.Struct({
url: Schema.String,
})
const SessionRevert = Schema.Struct({
messageID: MessageID,
partID: optionalOmitUndefined(PartID),
snapshot: optionalOmitUndefined(Schema.String),
diff: optionalOmitUndefined(Schema.String),
})
const SessionModel = Schema.Struct({
id: Model.ID,
providerID: Provider.ID,
variant: optionalOmitUndefined(Schema.String),
})
export const SessionInfo = Schema.Struct({
id: SessionID,
slug: Schema.String,
projectID: Project.ID,
workspaceID: optionalOmitUndefined(WorkspaceID),
directory: Schema.String,
path: optionalOmitUndefined(Schema.String),
parentID: optionalOmitUndefined(SessionID),
summary: optionalOmitUndefined(SessionSummary),
cost: optionalOmitUndefined(Schema.Finite),
tokens: optionalOmitUndefined(SessionTokens),
share: optionalOmitUndefined(SessionShare),
title: Schema.String,
agent: optionalOmitUndefined(Schema.String),
model: optionalOmitUndefined(SessionModel),
version: Schema.String,
metadata: optionalOmitUndefined(Schema.Record(Schema.String, Schema.Any)),
time: Schema.Struct({
created: NonNegativeInt,
updated: NonNegativeInt,
compacting: optionalOmitUndefined(NonNegativeInt),
archived: optionalOmitUndefined(Schema.Finite),
}),
permission: optionalOmitUndefined(PermissionV1.Ruleset),
revert: optionalOmitUndefined(SessionRevert),
}).annotate({ identifier: "Session" })
export type SessionInfo = typeof SessionInfo.Type
const events = {
Created: define({
type: "session.created",
...options,
schema: {
sessionID: SessionID,
info: SessionInfo,
},
}),
Updated: define({
type: "session.updated",
...options,
schema: {
sessionID: SessionID,
info: SessionInfo,
},
}),
Deleted: define({
type: "session.deleted",
...options,
schema: {
sessionID: SessionID,
info: SessionInfo,
},
}),
MessageUpdated: define({
type: "message.updated",
...options,
schema: {
sessionID: SessionID,
info: Info,
},
}),
MessageRemoved: define({
type: "message.removed",
...options,
schema: {
sessionID: SessionID,
messageID: MessageID,
},
}),
PartUpdated: define({
type: "message.part.updated",
...options,
schema: {
sessionID: SessionID,
part: Part,
time: Schema.Finite,
},
}),
PartRemoved: define({
type: "message.part.removed",
...options,
schema: {
sessionID: SessionID,
messageID: MessageID,
partID: PartID,
},
}),
}
export const PartDelta = define({
type: "message.part.delta",
schema: {
sessionID: SessionID,
messageID: MessageID,
partID: PartID,
field: Schema.String,
delta: Schema.String,
},
})
export const Diff = define({
type: "session.diff",
schema: {
sessionID: SessionID,
diff: Schema.Array(FileDiff.Info),
},
})
export const Error = define({
type: "session.error",
schema: {
sessionID: Schema.optional(SessionID),
error: Assistant.fields.error,
},
})
export const Event = {
...events,
PartDelta,
Diff,
Error,
Definitions: inventory(
events.Created,
events.Updated,
events.Deleted,
events.MessageUpdated,
events.MessageRemoved,
events.PartUpdated,
events.PartRemoved,
PartDelta,
Diff,
Error,
),
}

View file

@ -6,10 +6,13 @@ import { Location } from "./location"
import { Model } from "./model"
import { Project } from "./project"
import { DateTimeUtcFromMillis, optionalOmitUndefined, RelativePath } from "./schema"
import { SessionEvent } from "./session-event"
import { SessionID } from "./session-id"
export const ID = SessionID.ID
export type ID = SessionID.ID
export const ID = SessionID
export type ID = SessionID
export const Event = SessionEvent
export interface Info extends Schema.Schema.Type<typeof Info> {}
export const Info = Schema.Struct({

View file

@ -0,0 +1,58 @@
export * as TuiEvent from "./tui-event"
import { Effect, Schema } from "effect"
import { Event } from "./event"
import { PositiveInt } from "./schema"
import { SessionID } from "./session-id"
const DEFAULT_TOAST_DURATION = 5000
export const PromptAppend = Event.define({ type: "tui.prompt.append", schema: { text: Schema.String } })
export const CommandExecute = Event.define({
type: "tui.command.execute",
schema: {
command: Schema.Union([
Schema.Literals([
"session.list",
"session.new",
"session.share",
"session.interrupt",
"session.compact",
"session.page.up",
"session.page.down",
"session.line.up",
"session.line.down",
"session.half.page.up",
"session.half.page.down",
"session.first",
"session.last",
"prompt.clear",
"prompt.submit",
"agent.cycle",
]),
Schema.String,
]),
},
})
export const ToastShow = Event.define({
type: "tui.toast.show",
schema: {
title: Schema.optional(Schema.String),
message: Schema.String,
variant: Schema.Literals(["info", "success", "warning", "error"]),
duration: PositiveInt.pipe(Schema.withDecodingDefault(Effect.succeed(DEFAULT_TOAST_DURATION))).annotate({
description: "Duration in milliseconds",
}),
},
})
export const SessionSelect = Event.define({
type: "tui.session.select",
schema: {
sessionID: SessionID.annotate({ description: "Session ID to navigate to" }),
},
})
export const Definitions = Event.inventory(PromptAppend, CommandExecute, ToastShow, SessionSelect)

View file

@ -0,0 +1,13 @@
export * as VcsEvent from "./vcs-event"
import { Schema } from "effect"
import { Event } from "./event"
export const BranchUpdated = Event.define({
type: "vcs.branch.updated",
schema: {
branch: Schema.optional(Schema.String),
},
})
export const Definitions = Event.inventory(BranchUpdated)

View file

@ -0,0 +1,32 @@
export * as WorkspaceEvent from "./workspace-event"
import { Schema } from "effect"
import { Event } from "./event"
import { WorkspaceID } from "./workspace-id"
export const ConnectionStatus = Schema.Struct({
workspaceID: WorkspaceID,
status: Schema.Literals(["connected", "connecting", "disconnected", "error"]),
})
export type ConnectionStatus = typeof ConnectionStatus.Type
export const Ready = Event.define({
type: "workspace.ready",
schema: {
name: Schema.String,
},
})
export const Failed = Event.define({
type: "workspace.failed",
schema: {
message: Schema.String,
},
})
export const Status = Event.define({
type: "workspace.status",
schema: ConnectionStatus.fields,
})
export const Definitions = Event.inventory(Ready, Failed, Status)

View file

@ -0,0 +1,19 @@
import { Schema } from "effect"
import { ascending } from "./identifier"
import { withStatics } from "./schema"
export const WorkspaceID = Schema.String.check(Schema.isStartsWith("wrk")).pipe(
Schema.brand("WorkspaceV2.ID"),
withStatics((schema) => {
const create = () => schema.make("wrk_" + ascending())
return {
ascending: (id?: string) => {
if (!id) return create()
if (!id.startsWith("wrk")) throw new Error(`ID ${id} does not start with wrk`)
return schema.make(id)
},
create,
}
}),
)
export type WorkspaceID = typeof WorkspaceID.Type

View file

@ -1,21 +1,9 @@
export * as Workspace from "./workspace"
import { Schema } from "effect"
import { ascending } from "./identifier"
import { withStatics } from "./schema"
import { WorkspaceEvent } from "./workspace-event"
import { WorkspaceID } from "./workspace-id"
export const ID = Schema.String.check(Schema.isStartsWith("wrk")).pipe(
Schema.brand("WorkspaceV2.ID"),
withStatics((schema) => {
const create = () => schema.make("wrk_" + ascending())
return {
ascending: (id?: string) => {
if (!id) return create()
if (!id.startsWith("wrk")) throw new Error(`ID ${id} does not start with wrk`)
return schema.make(id)
},
create,
}
}),
)
export type ID = typeof ID.Type
export const ID = WorkspaceID
export type ID = WorkspaceID
export const Event = WorkspaceEvent

View file

@ -0,0 +1,21 @@
export * as WorktreeEvent from "./worktree-event"
import { Schema } from "effect"
import { Event } from "./event"
export const Ready = Event.define({
type: "worktree.ready",
schema: {
name: Schema.String,
branch: Schema.optional(Schema.String),
},
})
export const Failed = Event.define({
type: "worktree.failed",
schema: {
message: Schema.String,
},
})
export const Definitions = Event.inventory(Ready, Failed)