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

This commit is contained in:
Kit Langton
2026-06-24 16:43:17 -04:00
committed by GitHub
parent 858f35f1b3
commit 24b0132bc5
93 changed files with 2740 additions and 2124 deletions
+1
View File
@@ -539,6 +539,7 @@
"@openauthjs/openauth": "catalog:", "@openauthjs/openauth": "catalog:",
"@opencode-ai/llm": "workspace:*", "@opencode-ai/llm": "workspace:*",
"@opencode-ai/plugin": "workspace:*", "@opencode-ai/plugin": "workspace:*",
"@opencode-ai/schema": "workspace:*",
"@opencode-ai/script": "workspace:*", "@opencode-ai/script": "workspace:*",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"@opencode-ai/server": "workspace:*", "@opencode-ai/server": "workspace:*",
+2 -3
View File
@@ -1,6 +1,7 @@
export * as Catalog from "./catalog" export * as Catalog from "./catalog"
import { Array, Context, Effect, Layer, Option, Order, pipe, Schema } from "effect" import { Array, Context, Effect, Layer, Option, Order, pipe, Schema } from "effect"
import { Catalog } from "@opencode-ai/schema/catalog"
import { ModelV2 } from "./model" import { ModelV2 } from "./model"
import { ProviderV2 } from "./provider" import { ProviderV2 } from "./provider"
import { EventV2 } from "./event" import { EventV2 } from "./event"
@@ -17,9 +18,7 @@ export type DefaultModel = { providerID: ProviderV2.ID; modelID: ModelV2.ID }
export const PolicyActions = Schema.Literals(["provider.use"]) export const PolicyActions = Schema.Literals(["provider.use"])
export const Event = { export const Event = Catalog.Event
Updated: EventV2.define({ type: "catalog.updated", schema: {} }),
}
type Data = { type Data = {
providers: Map<ProviderV2.ID, ProviderRecord> providers: Map<ProviderV2.ID, ProviderRecord>
+23 -94
View File
@@ -1,46 +1,19 @@
export * as EventV2 from "./event" export * as EventV2 from "./event"
import { Cause, Context, Effect, Layer, Option, PubSub, Schema, Stream } from "effect" import { Cause, Context, Effect, Layer, Option, PubSub, Schema, Stream } from "effect"
import { Event } from "@opencode-ai/schema/event"
import type { Data, Definition, Payload } from "@opencode-ai/schema/event"
import { and, asc, eq, gt } from "drizzle-orm" import { and, asc, eq, gt } from "drizzle-orm"
import { Database } from "./database/database" import { Database } from "./database/database"
import { EventSequenceTable, EventTable } from "./event/sql" import { EventSequenceTable, EventTable } from "./event/sql"
import { Location } from "./location" import { Location } from "./location"
import { withStatics } from "./schema"
import { Identifier } from "./util/identifier"
import { LayerNode } from "./effect/layer-node" import { LayerNode } from "./effect/layer-node"
import { isDeepStrictEqual } from "node:util" import { isDeepStrictEqual } from "node:util"
import { Durable } from "@opencode-ai/schema/durable-event-manifest"
export const ID = Schema.String.check(Schema.isStartsWith("evt_")).pipe( export const ID = Event.ID
Schema.brand("Event.ID"), export type ID = import("@opencode-ai/schema/event").ID
withStatics((schema) => ({ export type { Data, Definition, Payload } from "@opencode-ai/schema/event"
create: () => schema.make("evt_" + Identifier.ascending()),
})),
)
export type ID = typeof ID.Type
export type Definition<Type extends string = string, DataSchema extends Schema.Top = 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 type Subscriber<D extends Definition = Definition> = (event: Payload<D>) => Effect.Effect<void> export type Subscriber<D extends Definition = Definition> = (event: Payload<D>) => Effect.Effect<void>
export type Unsubscribe = Effect.Effect<void> export type Unsubscribe = Effect.Effect<void>
@@ -74,52 +47,8 @@ export class InvalidDurableEventError extends Schema.TaggedErrorClass<InvalidDur
}, },
) {} ) {}
export function versionedType(type: string, version: number) { export const define = Event.define
return `${type}.${version}` export const versionedType = Event.versionedType
}
export const registry = new Map<string, Definition>()
const durableRegistry = new Map<string, Definition>()
export function define<const Type extends string, Fields extends Schema.Struct.Fields>(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)
const Payload = 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: Data,
}).annotate({ identifier: input.type })
const definition = Object.assign(Payload, {
type: input.type,
...(input.durable === undefined ? {} : { durable: input.durable }),
data: Data,
})
const existing = registry.get(input.type)
if (
input.durable === undefined ||
existing?.durable === undefined ||
input.durable.version >= existing.durable.version
) {
registry.set(input.type, definition)
}
if (input.durable) durableRegistry.set(versionedType(input.type, input.durable.version), definition)
return definition as Schema.Schema<Payload<Definition<Type, Schema.Struct<Fields>>>> &
Definition<Type, Schema.Struct<Fields>>
}
export function definitions() {
return registry.values().toArray()
}
export interface PublishOptions { export interface PublishOptions {
readonly id?: ID readonly id?: ID
@@ -169,6 +98,7 @@ export const layerWith = (options?: LayerOptions) =>
typed: new Map<string, PubSub.PubSub<Payload>>(), typed: new Map<string, PubSub.PubSub<Payload>>(),
} }
const projectors = new Map<string, Subscriber[]>() const projectors = new Map<string, Subscriber[]>()
// TODO: Bind durable projectors to exact type+version before supporting incompatible historical payloads.
const listeners = new Array<Subscriber>() const listeners = new Array<Subscriber>()
const { db } = yield* Database.Service const { db } = yield* Database.Service
@@ -194,6 +124,7 @@ export const layerWith = (options?: LayerOptions) =>
) )
function commitDurableEvent( function commitDurableEvent(
definition: Definition,
event: Payload, event: Payload,
input?: { input?: {
readonly seq: number readonly seq: number
@@ -204,7 +135,6 @@ export const layerWith = (options?: LayerOptions) =>
commit?: (seq: number) => Effect.Effect<void>, commit?: (seq: number) => Effect.Effect<void>,
) { ) {
return Effect.gen(function* () { return Effect.gen(function* () {
const definition = registry.get(event.type)
const durable = definition?.durable const durable = definition?.durable
if (durable) { if (durable) {
const aggregateID = (event.data as Record<string, unknown>)[durable.aggregate] const aggregateID = (event.data as Record<string, unknown>)[durable.aggregate]
@@ -238,9 +168,10 @@ export const layerWith = (options?: LayerOptions) =>
.get() .get()
.pipe(Effect.orDie) .pipe(Effect.orDie)
const latest = row?.seq ?? -1 const latest = row?.seq ?? -1
const encoded = Schema.encodeUnknownSync( const encoded = Schema.encodeUnknownSync(definition.data)(event.data) as Record<
definition.data as Schema.Codec<unknown, unknown, never, never>, string,
)(event.data) as Record<string, unknown> unknown
>
if (input?.strictOwner && row?.ownerID && row.ownerID !== input.ownerID) { if (input?.strictOwner && row?.ownerID && row.ownerID !== input.ownerID) {
yield* Effect.die( yield* Effect.die(
new InvalidDurableEventError({ new InvalidDurableEventError({
@@ -356,9 +287,8 @@ export const layerWith = (options?: LayerOptions) =>
}) })
} }
function publishEvent<D extends Definition>(event: Payload<D>, commit?: PublishOptions["commit"]) { function publishEvent<D extends Definition>(definition: D, event: Payload<D>, commit?: PublishOptions["commit"]) {
return Effect.gen(function* () { return Effect.gen(function* () {
const definition = registry.get(event.type)
if (!definition?.durable && commit) if (!definition?.durable && commit)
return yield* Effect.die( return yield* Effect.die(
new InvalidDurableEventError({ new InvalidDurableEventError({
@@ -367,7 +297,7 @@ export const layerWith = (options?: LayerOptions) =>
}), }),
) )
if (definition?.durable) { if (definition?.durable) {
const committed = yield* commitDurableEvent(event as Payload, undefined, commit) const committed = yield* commitDurableEvent(definition, event as Payload, undefined, commit)
if (committed) { if (committed) {
event = { event = {
...event, ...event,
@@ -416,6 +346,7 @@ export const layerWith = (options?: LayerOptions) =>
? { directory: serviceLocation.directory, workspaceID: serviceLocation.workspaceID } ? { directory: serviceLocation.directory, workspaceID: serviceLocation.workspaceID }
: undefined) : undefined)
return yield* publishEvent( return yield* publishEvent(
definition,
{ {
id: options?.id ?? ID.create(), id: options?.id ?? ID.create(),
...(options?.metadata ? { metadata: options.metadata } : {}), ...(options?.metadata ? { metadata: options.metadata } : {}),
@@ -433,7 +364,7 @@ export const layerWith = (options?: LayerOptions) =>
options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean }, options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean },
) { ) {
return Effect.gen(function* () { return Effect.gen(function* () {
const definition = durableRegistry.get(event.type) const definition = Durable.get(event.type)
if (!definition?.durable) { if (!definition?.durable) {
yield* Effect.die( yield* Effect.die(
new InvalidDurableEventError({ type: event.type, message: `Unknown durable event type ${event.type}` }), new InvalidDurableEventError({ type: event.type, message: `Unknown durable event type ${event.type}` }),
@@ -442,11 +373,9 @@ export const layerWith = (options?: LayerOptions) =>
const payload = { const payload = {
id: event.id, id: event.id,
type: definition.type, type: definition.type,
data: Schema.decodeUnknownSync(definition.data as Schema.Codec<unknown, unknown, never, never>)( data: Schema.decodeUnknownSync(definition.data)(event.data),
event.data,
),
} as Payload } as Payload
const committed = yield* commitDurableEvent(payload, { const committed = yield* commitDurableEvent(definition, payload, {
seq: event.seq, seq: event.seq,
aggregateID: event.aggregateID, aggregateID: event.aggregateID,
ownerID: options?.ownerID, ownerID: options?.ownerID,
@@ -530,8 +459,8 @@ export const layerWith = (options?: LayerOptions) =>
const streamAll = (): Stream.Stream<Payload> => Stream.fromPubSub(pubsub.all) const streamAll = (): Stream.Stream<Payload> => Stream.fromPubSub(pubsub.all)
const decodeSerializedEvent = (event: SerializedEvent): Payload => { const decodeSerializedEvent = (event: SerializedEvent) => {
const definition = durableRegistry.get(event.type) const definition = Durable.get(event.type)
if (!definition?.durable) { if (!definition?.durable) {
throw new InvalidDurableEventError({ type: event.type, message: `Unknown durable event type ${event.type}` }) throw new InvalidDurableEventError({ type: event.type, message: `Unknown durable event type ${event.type}` })
} }
@@ -539,7 +468,7 @@ export const layerWith = (options?: LayerOptions) =>
id: event.id, id: event.id,
type: definition.type, type: definition.type,
durable: { aggregateID: event.aggregateID, seq: event.seq, version: definition.durable.version }, durable: { aggregateID: event.aggregateID, seq: event.seq, version: definition.durable.version },
data: Schema.decodeUnknownSync(definition.data as Schema.Codec<unknown, unknown, never, never>)(event.data), data: Schema.decodeUnknownSync(definition.data)(event.data),
} }
} }
+2 -10
View File
@@ -2,12 +2,11 @@ export * as FileSystem from "./filesystem"
import path from "path" import path from "path"
import { Context, Effect, Layer, Schema } from "effect" import { Context, Effect, Layer, Schema } from "effect"
import { EventV2 } from "./event"
import { FSUtil } from "./fs-util" import { FSUtil } from "./fs-util"
import { Location } from "./location" import { Location } from "./location"
import { PositiveInt, RelativePath } from "./schema" import { PositiveInt, RelativePath } from "./schema"
import { FileSystemSearch } from "./filesystem/search" import { FileSystemSearch } from "./filesystem/search"
import { Entry, Match } from "@opencode-ai/schema/filesystem" import { Entry, FileSystem, Match } from "@opencode-ai/schema/filesystem"
export { Entry, Match, Submatch } from "@opencode-ai/schema/filesystem" export { Entry, Match, Submatch } from "@opencode-ai/schema/filesystem"
export const ReadInput = Schema.Struct({ export const ReadInput = Schema.Struct({
@@ -48,14 +47,7 @@ export class GrepInput extends Schema.Class<GrepInput>("FileSystem.GrepInput")({
limit: PositiveInt.pipe(Schema.optional), limit: PositiveInt.pipe(Schema.optional),
}) {} }) {}
export const Event = { export const Event = FileSystem.Event
Edited: EventV2.define({
type: "file.edited",
schema: {
file: Schema.String,
},
}),
}
export interface Interface { export interface Interface {
readonly read: (input: ReadInput) => Effect.Effect<{ readonly content: Uint8Array; readonly mime: string }> readonly read: (input: ReadInput) => Effect.Effect<{ readonly content: Uint8Array; readonly mime: string }>
+3 -10
View File
@@ -3,7 +3,8 @@ export * as Watcher from "./watcher"
// @ts-ignore // @ts-ignore
import { createWrapper } from "@parcel/watcher/wrapper" import { createWrapper } from "@parcel/watcher/wrapper"
import type ParcelWatcher from "@parcel/watcher" import type ParcelWatcher from "@parcel/watcher"
import { Cause, Context, Effect, Layer, Schema } from "effect" import { Cause, Context, Effect, Layer } from "effect"
import { FileSystemWatcher } from "@opencode-ai/schema/filesystem-watcher"
import path from "path" import path from "path"
import { Config } from "../config" import { Config } from "../config"
import { EventV2 } from "../event" import { EventV2 } from "../event"
@@ -19,15 +20,7 @@ declare const OPENCODE_LIBC: string | undefined
const SUBSCRIBE_TIMEOUT_MS = 10_000 const SUBSCRIBE_TIMEOUT_MS = 10_000
export const Event = { export const Event = FileSystemWatcher.Event
Updated: EventV2.define({
type: "file.watcher.updated",
schema: {
file: Schema.String,
event: Schema.Literals(["add", "change", "unlink"]),
},
}),
}
const watcher = lazy((): typeof import("@parcel/watcher") | undefined => { const watcher = lazy((): typeof import("@parcel/watcher") | undefined => {
try { try {
+1 -10
View File
@@ -136,16 +136,7 @@ export class AuthorizationError extends Schema.TaggedErrorClass<AuthorizationErr
export type Error = CodeRequiredError | AuthorizationError export type Error = CodeRequiredError | AuthorizationError
export const Event = { export const Event = Integration.Event
Updated: EventV2.define({
type: "integration.updated",
schema: {},
}),
ConnectionUpdated: EventV2.define({
type: "integration.connection.updated",
schema: { integrationID: ID },
}),
}
export const Ref = Integration.Ref export const Ref = Integration.Ref
export type Ref = Integration.Ref export type Ref = Integration.Ref
+2 -6
View File
@@ -1,6 +1,7 @@
import path from "path" import path from "path"
import { Context, Duration, Effect, Layer, Option, Schedule, Schema } from "effect" import { Context, Duration, Effect, Layer, Option, Schedule, Schema } from "effect"
import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http" import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http"
import { ModelsDev } from "@opencode-ai/schema/models-dev"
import { Global } from "./global" import { Global } from "./global"
import { Flag } from "./flag/flag" import { Flag } from "./flag/flag"
import { Flock } from "./util/flock" import { Flock } from "./util/flock"
@@ -108,12 +109,7 @@ export const Provider = Schema.Struct({
export type Provider = Schema.Schema.Type<typeof Provider> export type Provider = Schema.Schema.Type<typeof Provider>
export const Event = { export const Event = ModelsDev.Event
Refreshed: EventV2.define({
type: "models-dev.refreshed",
schema: {},
}),
}
declare const OPENCODE_MODELS_DEV: Record<string, Provider> | undefined declare const OPENCODE_MODELS_DEV: Record<string, Provider> | undefined
+11 -35
View File
@@ -7,45 +7,31 @@ import { Location } from "./location"
import { AgentV2 } from "./agent" import { AgentV2 } from "./agent"
import { SessionV2 } from "./session" import { SessionV2 } from "./session"
import { SessionStore } from "./session/store" import { SessionStore } from "./session/store"
import { withStatics } from "./schema"
import { Identifier } from "./util/identifier"
import { Wildcard } from "./util/wildcard" import { Wildcard } from "./util/wildcard"
import { PermissionSaved } from "./permission/saved" import { PermissionSaved } from "./permission/saved"
export { Effect, Rule, Ruleset } from "@opencode-ai/schema/permission" export { Effect, Rule, Ruleset } from "@opencode-ai/schema/permission"
const missingAgentPermissions: Permission.Ruleset = [{ action: "*", resource: "*", effect: "deny" }] const missingAgentPermissions: Permission.Ruleset = [{ action: "*", resource: "*", effect: "deny" }]
export const ID = Schema.String.check(Schema.isStartsWith("per")).pipe( export const ID = Permission.ID
Schema.brand("PermissionV2.ID"),
withStatics((schema) => ({ create: (id?: string) => schema.make(id ?? "per_" + Identifier.ascending()) })),
)
export type ID = typeof ID.Type export type ID = typeof ID.Type
export const Source = Schema.Union([ export const Source = Permission.Source
Schema.Struct({
type: Schema.Literal("tool"),
messageID: Schema.String,
callID: Schema.String,
}),
]).annotate({ identifier: "PermissionV2.Source" })
export type Source = typeof Source.Type export type Source = typeof Source.Type
const RequestFields = { const RequestFields = {
sessionID: SessionV2.ID, sessionID: Permission.Request.fields.sessionID,
action: Schema.String, action: Permission.Request.fields.action,
resources: Schema.Array(Schema.String), resources: Permission.Request.fields.resources,
save: Schema.Array(Schema.String).pipe(Schema.optional), save: Permission.Request.fields.save,
metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), metadata: Permission.Request.fields.metadata,
source: Source.pipe(Schema.optional), source: Permission.Request.fields.source,
} }
export const Request = Schema.Struct({ export const Request = Permission.Request
id: ID,
...RequestFields,
}).annotate({ identifier: "PermissionV2.Request" })
export type Request = typeof Request.Type export type Request = typeof Request.Type
export const Reply = Schema.Literals(["once", "always", "reject"]).annotate({ identifier: "PermissionV2.Reply" }) export const Reply = Permission.Reply
export type Reply = typeof Reply.Type export type Reply = typeof Reply.Type
export const AssertInput = Schema.Struct({ export const AssertInput = Schema.Struct({
@@ -68,17 +54,7 @@ export const AskResult = Schema.Struct({
}).annotate({ identifier: "PermissionV2.AskResult" }) }).annotate({ identifier: "PermissionV2.AskResult" })
export type AskResult = typeof AskResult.Type export type AskResult = typeof AskResult.Type
export const Event = { export const Event = Permission.Event
Asked: EventV2.define({ type: "permission.v2.asked", schema: Request.fields }),
Replied: EventV2.define({
type: "permission.v2.replied",
schema: {
sessionID: SessionV2.ID,
requestID: ID,
reply: Reply,
},
}),
}
export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("PermissionV2.RejectedError", {}) {} export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("PermissionV2.RejectedError", {}) {}
+4 -10
View File
@@ -1,7 +1,8 @@
export * as PluginV2 from "./plugin" export * as PluginV2 from "./plugin"
import { Context, Deferred, Effect, Exit, Layer, Schema, Scope } from "effect" import { Context, Deferred, Effect, Exit, Layer, Scope } from "effect"
import type { Plugin } from "@opencode-ai/plugin/v2/effect" import type { Plugin } from "@opencode-ai/plugin/v2/effect"
import { PluginEvent, PluginID } from "@opencode-ai/schema/plugin"
import { AgentV2 } from "./agent" import { AgentV2 } from "./agent"
import { AISDK } from "./aisdk" import { AISDK } from "./aisdk"
import { Catalog } from "./catalog" import { Catalog } from "./catalog"
@@ -14,17 +15,10 @@ import { Reference } from "./reference"
import { SkillV2 } from "./skill" import { SkillV2 } from "./skill"
import { State } from "./state" import { State } from "./state"
export const ID = Schema.String.pipe(Schema.brand("Plugin.ID")) export const ID = PluginID
export type ID = typeof ID.Type export type ID = typeof ID.Type
export const Event = { export const Event = PluginEvent
Added: EventV2.define({
type: "plugin.added",
schema: {
id: ID,
},
}),
}
export interface Interface { export interface Interface {
readonly add: (id: ID, effect: Plugin["effect"]) => Effect.Effect<void> readonly add: (id: ID, effect: Plugin["effect"]) => Effect.Effect<void>
+2 -6
View File
@@ -13,6 +13,7 @@ import { Slug } from "../util/slug"
import { EventV2 } from "../event" import { EventV2 } from "../event"
import { Database } from "../database/database" import { Database } from "../database/database"
import { Location } from "../location" import { Location } from "../location"
import { ProjectDirectoriesEvent } from "@opencode-ai/schema/project-directories"
export const StrategyID = Schema.Trim.pipe(Schema.check(Schema.isNonEmpty()), Schema.brand("ProjectCopy.StrategyID")) export const StrategyID = Schema.Trim.pipe(Schema.check(Schema.isNonEmpty()), Schema.brand("ProjectCopy.StrategyID"))
export type StrategyID = typeof StrategyID.Type export type StrategyID = typeof StrategyID.Type
@@ -106,12 +107,7 @@ export interface Strategy {
readonly list: (directory: AbsolutePath) => Effect.Effect<ListEntry[], Git.WorktreeError | DirectoryUnavailableError> readonly list: (directory: AbsolutePath) => Effect.Effect<ListEntry[], Git.WorktreeError | DirectoryUnavailableError>
} }
export const Event = { export const Event = ProjectDirectoriesEvent
Updated: EventV2.define({
type: "project.directories.updated",
schema: { projectID: Project.ID },
}),
}
export interface Interface { export interface Interface {
readonly register: (strategy: Strategy) => Effect.Effect<void, DuplicateStrategyError> readonly register: (strategy: Strategy) => Effect.Effect<void, DuplicateStrategyError>
+4 -19
View File
@@ -2,10 +2,11 @@ export * as Pty from "./pty"
import type { Disp, Proc } from "#pty" import type { Disp, Proc } from "#pty"
import { Context, Effect, Layer, Schema, Types } from "effect" import { Context, Effect, Layer, Schema, Types } from "effect"
import { PtyEvent, PtyInfo } from "@opencode-ai/schema/pty"
import { Config } from "./config" import { Config } from "./config"
import { EventV2 } from "./event" import { EventV2 } from "./event"
import { Location } from "./location" import { Location } from "./location"
import { NonNegativeInt, PositiveInt } from "./schema" import { PositiveInt } from "./schema"
import { PtyID } from "./pty/schema" import { PtyID } from "./pty/schema"
import { Shell } from "./shell" import { Shell } from "./shell"
import { lazy } from "./util/lazy" import { lazy } from "./util/lazy"
@@ -35,18 +36,7 @@ type Active = {
listeners: Disp[] listeners: Disp[]
} }
export const Info = Schema.Struct({ export const Info = PtyInfo
id: PtyID,
title: Schema.String,
command: Schema.String,
args: Schema.Array(Schema.String),
cwd: Schema.String,
status: Schema.Literals(["running", "exited"]),
// Windows ConPTY assigns the child pid asynchronously, so 0 is valid at spawn time.
pid: NonNegativeInt,
// Present once status is "exited".
exitCode: Schema.optional(NonNegativeInt),
}).annotate({ identifier: "Pty" })
export type Info = Types.DeepMutable<typeof Info.Type> export type Info = Types.DeepMutable<typeof Info.Type>
@@ -100,12 +90,7 @@ export class ExitedError extends Schema.TaggedErrorClass<ExitedError>()("Pty.Exi
ptyID: PtyID, ptyID: PtyID,
}) {} }) {}
export const Event = { export const Event = PtyEvent
Created: EventV2.define({ type: "pty.created", schema: { info: Info } }),
Updated: EventV2.define({ type: "pty.updated", schema: { info: Info } }),
Exited: EventV2.define({ type: "pty.exited", schema: { id: PtyID, exitCode: NonNegativeInt } }),
Deleted: EventV2.define({ type: "pty.deleted", schema: { id: PtyID } }),
}
export interface Interface { export interface Interface {
readonly list: () => Effect.Effect<Info[]> readonly list: () => Effect.Effect<Info[]>
+1 -13
View File
@@ -1,13 +1 @@
import { Schema } from "effect" export { ID as PtyID } from "@opencode-ai/schema/pty"
import { Identifier } from "../id/id"
import { withStatics } from "../schema"
const ptyIdSchema = Schema.String.check(Schema.isStartsWith("pty")).pipe(Schema.brand("PtyID"))
export type PtyID = typeof ptyIdSchema.Type
export const PtyID = ptyIdSchema.pipe(
withStatics((schema: typeof ptyIdSchema) => ({
ascending: (id?: string) => schema.make(Identifier.ascending("pty", id)),
})),
)
@@ -0,0 +1,3 @@
export * as PublicEventManifest from "./public-event-manifest"
export { ServerDefinitions as Definitions } from "@opencode-ai/schema/event-manifest"
+10 -58
View File
@@ -1,83 +1,35 @@
export * as QuestionV2 from "./question" export * as QuestionV2 from "./question"
import { Context, Deferred, Effect, Layer, Schema } from "effect" import { Context, Deferred, Effect, Layer, Schema } from "effect"
import { Question } from "@opencode-ai/schema/question"
import { EventV2 } from "./event" import { EventV2 } from "./event"
import { Identifier } from "./id/id"
import { withStatics } from "./schema"
import { SessionSchema } from "./session/schema" import { SessionSchema } from "./session/schema"
export const ID = Schema.String.check(Schema.isStartsWith("que")).pipe( export const ID = Question.ID
Schema.brand("QuestionV2.ID"),
withStatics((schema) => ({ ascending: (id?: string) => schema.make(Identifier.ascending("question", id)) })),
)
export type ID = typeof ID.Type export type ID = typeof ID.Type
export const Option = Schema.Struct({ export const Option = Question.Option
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 export type Option = typeof Option.Type
const base = { export const Info = Question.Info
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 type Info = typeof Info.Type
export const Prompt = Schema.Struct(base).annotate({ identifier: "QuestionV2.Prompt" }) export const Prompt = Question.Prompt
export type Prompt = typeof Prompt.Type export type Prompt = typeof Prompt.Type
export const Tool = Schema.Struct({ export const Tool = Question.Tool
messageID: Schema.String,
callID: Schema.String,
}).annotate({ identifier: "QuestionV2.Tool" })
export type Tool = typeof Tool.Type export type Tool = typeof Tool.Type
export const Request = Schema.Struct({ export const Request = Question.Request
id: ID,
sessionID: SessionSchema.ID,
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 type Request = typeof Request.Type
export const Answer = Schema.Array(Schema.String).annotate({ identifier: "QuestionV2.Answer" }) export const Answer = Question.Answer
export type Answer = typeof Answer.Type export type Answer = typeof Answer.Type
export const Reply = Schema.Struct({ export const Reply = Question.Reply
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 export type Reply = typeof Reply.Type
export const Event = { export const Event = Question.Event
Asked: EventV2.define({ type: "question.v2.asked", schema: Request.fields }),
Replied: EventV2.define({
type: "question.v2.replied",
schema: {
sessionID: SessionSchema.ID,
requestID: ID,
answers: Schema.Array(Answer),
},
}),
Rejected: EventV2.define({
type: "question.v2.rejected",
schema: {
sessionID: SessionSchema.ID,
requestID: ID,
},
}),
}
export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("QuestionV2.RejectedError", {}) { export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("QuestionV2.RejectedError", {}) {
override get message() { override get message() {
+1 -3
View File
@@ -18,9 +18,7 @@ export type GitSource = Reference.GitSource
export const Source = Reference.Source export const Source = Reference.Source
export type Source = Reference.Source export type Source = Reference.Source
export const Event = { export const Event = Reference.Event
Updated: EventV2.define({ type: "reference.updated", schema: {} }),
}
export class Info extends Schema.Class<Info>("Reference.Info")({ export class Info extends Schema.Class<Info>("Reference.Info")({
name: Schema.String, name: Schema.String,
+2 -468
View File
@@ -1,468 +1,2 @@
import { Schema } from "effect" export * from "@opencode-ai/schema/session-event"
import { ProviderMetadata, ToolContent } from "@opencode-ai/schema/llm" export * as SessionEvent from "@opencode-ai/schema/session-event"
import { Delivery } from "@opencode-ai/schema/session-delivery"
import { EventV2 } from "../event"
import { ModelV2 } from "../model"
import { DateTimeUtcFromMillis, NonNegativeInt, RelativePath } from "../schema"
import { FileAttachment, Prompt } from "./prompt"
import { SessionSchema } from "./schema"
import { Location } from "../location"
import { SessionMessageID } from "./message-id"
import { SessionMessage } from "./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: SessionSchema.ID,
}
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 = EventV2.define({
type: "session.next.agent.switched",
...options,
schema: {
...Base,
messageID: SessionMessageID.ID,
agent: Schema.String,
},
})
export type AgentSwitched = typeof AgentSwitched.Type
export const ModelSwitched = EventV2.define({
type: "session.next.model.switched",
...options,
schema: {
...Base,
messageID: SessionMessageID.ID,
model: ModelV2.Ref,
},
})
export type ModelSwitched = typeof ModelSwitched.Type
export const Moved = EventV2.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 = EventV2.define({
type: "session.next.prompted",
...options,
schema: PromptFields,
})
export type Prompted = typeof Prompted.Type
export const PromptAdmitted = EventV2.define({
type: "session.next.prompt.admitted",
...options,
schema: PromptFields,
})
export type PromptAdmitted = typeof PromptAdmitted.Type
export const ContextUpdated = EventV2.define({
type: "session.next.context.updated",
...options,
schema: {
...Base,
messageID: SessionMessageID.ID,
text: Schema.String,
},
})
export type ContextUpdated = typeof ContextUpdated.Type
export const Synthetic = EventV2.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 = EventV2.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 = EventV2.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 = EventV2.define({
type: "session.next.step.started",
...options,
schema: {
...Base,
assistantMessageID: SessionMessageID.ID,
agent: Schema.String,
model: ModelV2.Ref,
snapshot: Schema.String.pipe(Schema.optional),
},
})
export type Started = typeof Started.Type
export const Ended = EventV2.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 = EventV2.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 = EventV2.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 = EventV2.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 = EventV2.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 = EventV2.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 = EventV2.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 = EventV2.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 = EventV2.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 = EventV2.define({
type: "session.next.tool.input.delta",
schema: {
...ToolBase,
delta: Schema.String,
},
})
export type Delta = typeof Delta.Type
export const Ended = EventV2.define({
type: "session.next.tool.input.ended",
...options,
schema: {
...ToolBase,
text: Schema.String,
},
})
export type Ended = typeof Ended.Type
}
export const Called = EventV2.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 = EventV2.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 = EventV2.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 = EventV2.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 = EventV2.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 = EventV2.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 = EventV2.define({
type: "session.next.compaction.delta",
schema: {
...Base,
messageID: SessionMessageID.ID,
text: Schema.String,
},
})
export type Delta = typeof Delta.Type
export const Ended = EventV2.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
}
const DurableDefinitions = [
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,
] as const
const EphemeralDefinitions = [Text.Delta, Tool.Input.Delta, Reasoning.Delta, Compaction.Delta] as const
export const Durable = Schema.Union(DurableDefinitions, { mode: "oneOf" }).pipe(Schema.toTaggedUnion("type"))
export type DurableEvent = typeof Durable.Type
export const All = Schema.Union([...DurableDefinitions, ...EphemeralDefinitions], { mode: "oneOf" }).pipe(
Schema.toTaggedUnion("type"),
)
export type Event = typeof All.Type
export type Type = Event["type"]
export * as SessionEvent from "./event"
+4 -17
View File
@@ -1,30 +1,17 @@
export * as SessionTodo from "./todo" export * as SessionTodo from "./todo"
import { asc, eq } from "drizzle-orm" import { asc, eq } from "drizzle-orm"
import { Context, Effect, Layer, Schema } from "effect" import { Context, Effect, Layer } from "effect"
import { SessionTodo, SessionTodoInfo } from "@opencode-ai/schema/session-todo"
import { Database } from "../database/database" import { Database } from "../database/database"
import { EventV2 } from "../event" import { EventV2 } from "../event"
import { SessionSchema } from "./schema" import { SessionSchema } from "./schema"
import { TodoTable } from "./sql" import { TodoTable } from "./sql"
export const Info = Schema.Struct({ export const Info = SessionTodoInfo
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: "SessionTodo.Info" })
export type Info = typeof Info.Type export type Info = typeof Info.Type
export const Event = { export const Event = SessionTodo.Event
Updated: EventV2.define({
type: "todo.updated",
schema: {
sessionID: SessionSchema.ID,
todos: Schema.Array(Info),
},
}),
}
export interface Interface { export interface Interface {
readonly update: (input: { readonly update: (input: {
+2 -65
View File
@@ -1,71 +1,8 @@
export * as PermissionV1 from "./permission" export * as PermissionV1 from "./permission"
import { Schema } from "effect" import { Schema } from "effect"
import { ProjectV2 } from "../project" export * from "@opencode-ai/schema/permission-v1"
import { withStatics } from "../schema" import { ID } from "@opencode-ai/schema/permission-v1"
import { SessionSchema } from "../session/schema"
import { Identifier } from "../util/identifier"
export const ID = Schema.String.check(Schema.isStartsWith("per")).pipe(
Schema.brand("PermissionID"),
withStatics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "per_" + Identifier.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: SessionSchema.ID,
permission: Schema.String,
patterns: Schema.Array(Schema.String),
metadata: Schema.Record(Schema.String, Schema.Unknown),
always: Schema.Array(Schema.String),
tool: Schema.Struct({
messageID: Schema.String,
callID: Schema.String,
}).pipe(Schema.optional),
}).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.String.pipe(Schema.optional),
}).annotate({ identifier: "PermissionReplyBody" })
export type ReplyBody = typeof ReplyBody.Type
export const Approval = Schema.Struct({
projectID: ProjectV2.ID,
patterns: Schema.Array(Schema.String),
}).annotate({ identifier: "PermissionApproval" })
export type Approval = typeof Approval.Type
export const AskInput = Schema.Struct({
...Request.fields,
id: ID.pipe(Schema.optional),
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
export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("PermissionRejectedError", {}) { export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("PermissionRejectedError", {}) {
override get message() { override get message() {
+43 -607
View File
@@ -1,39 +1,52 @@
export * as SessionV1 from "./session" export * as SessionV1 from "./session"
import { Effect, Schema, Types } from "effect" import { Schema } from "effect"
import { EventV2 } from "../event"
import { PermissionV1 } from "./permission"
import { ProjectV2 } from "../project"
import { ProviderV2 } from "../provider"
import { ModelV2 } from "../model"
import { optionalOmitUndefined, withStatics } from "../schema"
import { Identifier } from "../util/identifier"
import { NonNegativeInt } from "../schema" import { NonNegativeInt } from "../schema"
import { NamedError } from "../util/error" import { NamedError } from "../util/error"
import { SessionSchema } from "../session/schema"
import { WorkspaceV2 } from "../workspace"
const Timestamp = Schema.Finite.check(Schema.isGreaterThanOrEqualTo(0)) export {
AgentPart,
export const MessageID = Schema.String.check(Schema.isStartsWith("msg")).pipe( AgentPartInput,
Schema.brand("MessageID"), Assistant,
withStatics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "msg_" + Identifier.ascending()) })), CompactionPart,
) Event,
export type MessageID = typeof MessageID.Type FilePart,
FilePartInput,
export const PartID = Schema.String.check(Schema.isStartsWith("prt")).pipe( FilePartSource,
Schema.brand("PartID"), FileSource,
withStatics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "prt_" + Identifier.ascending()) })), Format,
) Info,
export type PartID = typeof PartID.Type MessageID,
OutputFormatJsonSchema,
OutputFormatText,
Part,
PartID,
PatchPart,
Range,
ReasoningPart,
ResourceSource,
RetryPart,
SessionInfo,
SnapshotPart,
StepFinishPart,
StepStartPart,
SubtaskPart,
SubtaskPartInput,
SymbolSource,
TextPart,
TextPartInput,
ToolPart,
ToolState,
ToolStateCompleted,
ToolStateError,
ToolStatePending,
ToolStateRunning,
User,
WithParts,
} from "@opencode-ai/schema/session-v1"
export const OutputLengthError = NamedError.create("MessageOutputLengthError", {}) export const OutputLengthError = NamedError.create("MessageOutputLengthError", {})
export const AuthError = NamedError.create("ProviderAuthError", { providerID: Schema.String, message: Schema.String })
export const AuthError = NamedError.create("ProviderAuthError", {
providerID: Schema.String,
message: Schema.String,
})
export const AbortedError = NamedError.create("MessageAbortedError", { message: Schema.String }) export const AbortedError = NamedError.create("MessageAbortedError", { message: Schema.String })
export const StructuredOutputError = NamedError.create("StructuredOutputError", { export const StructuredOutputError = NamedError.create("StructuredOutputError", {
message: Schema.String, message: Schema.String,
@@ -52,581 +65,4 @@ export const ContextOverflowError = NamedError.create("ContextOverflowError", {
message: Schema.String, message: Schema.String,
responseBody: Schema.optional(Schema.String), responseBody: Schema.optional(Schema.String),
}) })
export const ContentFilterError = NamedError.create("ContentFilterError", { export const ContentFilterError = NamedError.create("ContentFilterError", { message: Schema.String })
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: SessionSchema.ID,
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: ProviderV2.ID,
modelID: ModelV2.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,
}
const FileDiff = 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 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),
}),
),
agent: Schema.String,
model: Schema.Struct({
providerID: ProviderV2.ID,
modelID: ModelV2.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.Unknown.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: ProviderV2.ID,
modelID: ModelV2.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: ModelV2.ID,
providerID: ProviderV2.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)),
})
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: ModelV2.ID,
providerID: ProviderV2.ID,
variant: optionalOmitUndefined(Schema.String),
})
export const SessionInfo = Schema.Struct({
id: SessionSchema.ID,
slug: Schema.String,
projectID: ProjectV2.ID,
workspaceID: optionalOmitUndefined(WorkspaceV2.ID),
directory: Schema.String,
path: optionalOmitUndefined(Schema.String),
parentID: optionalOmitUndefined(SessionSchema.ID),
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
export const Event = {
Created: EventV2.define({
type: "session.created",
...options,
schema: {
sessionID: SessionSchema.ID,
info: SessionInfo,
},
}),
Updated: EventV2.define({
type: "session.updated",
...options,
schema: {
sessionID: SessionSchema.ID,
info: SessionInfo,
},
}),
Deleted: EventV2.define({
type: "session.deleted",
...options,
schema: {
sessionID: SessionSchema.ID,
info: SessionInfo,
},
}),
MessageUpdated: EventV2.define({
type: "message.updated",
...options,
schema: {
sessionID: SessionSchema.ID,
info: Info,
},
}),
MessageRemoved: EventV2.define({
type: "message.removed",
...options,
schema: {
sessionID: SessionSchema.ID,
messageID: MessageID,
},
}),
PartUpdated: EventV2.define({
type: "message.part.updated",
...options,
schema: {
sessionID: SessionSchema.ID,
part: Part,
time: Schema.Finite,
},
}),
PartRemoved: EventV2.define({
type: "message.part.removed",
...options,
schema: {
sessionID: SessionSchema.ID,
messageID: MessageID,
partID: PartID,
},
}),
}
+126 -138
View File
@@ -1,10 +1,14 @@
import { describe, expect } from "bun:test" import { describe, expect } from "bun:test"
import { Cause, DateTime, Deferred, Effect, Exit, Fiber, Layer, Schema, Stream } from "effect" import { Cause, DateTime, Deferred, Effect, Exit, Fiber, Layer, Schema, Stream } from "effect"
import { EventV2 } from "@opencode-ai/core/event" import { EventV2 } from "@opencode-ai/core/event"
import { Event } from "@opencode-ai/schema/event"
import { Session } from "@opencode-ai/schema/session"
import { SessionEvent } from "@opencode-ai/schema/session-event"
import { SessionV1 } from "@opencode-ai/schema/session-v1"
import { Database } from "@opencode-ai/core/database/database" import { Database } from "@opencode-ai/core/database/database"
import { EventSequenceTable, EventTable } from "@opencode-ai/core/event/sql" import { EventSequenceTable, EventTable } from "@opencode-ai/core/event/sql"
import { Location } from "@opencode-ai/core/location" import { Location } from "@opencode-ai/core/location"
import { AbsolutePath, DateTimeUtcFromMillis } from "@opencode-ai/core/schema" import { AbsolutePath } from "@opencode-ai/core/schema"
import { WorkspaceV2 } from "@opencode-ai/core/workspace" import { WorkspaceV2 } from "@opencode-ai/core/workspace"
import { eq } from "drizzle-orm" import { eq } from "drizzle-orm"
import { location } from "./fixture/location" import { location } from "./fixture/location"
@@ -16,10 +20,6 @@ const locationLayer = Layer.succeed(
location({ directory: AbsolutePath.make("project"), workspaceID: WorkspaceV2.ID.make("wrk_test") }), location({ directory: AbsolutePath.make("project"), workspaceID: WorkspaceV2.ID.make("wrk_test") }),
), ),
) )
const eventLayer = Layer.mergeAll(EventV2.defaultLayer, Database.defaultLayer)
const it = testEffect(eventLayer.pipe(Layer.provideMerge(locationLayer)))
const itWithoutLocation = testEffect(eventLayer)
const Message = EventV2.define({ const Message = EventV2.define({
type: "test.message", type: "test.message",
schema: { schema: {
@@ -70,18 +70,16 @@ const VersionedMessage = EventV2.define({
}, },
}) })
const SyncTimestamp = EventV2.define({ const DurableMessage = SessionV1.Event.MessageRemoved
type: "test.timestamp", const durableData = (sessionID: Session.ID, text: string) => ({
durable: { sessionID,
version: 1, messageID: SessionV1.MessageID.ascending(`msg_${text}`),
aggregate: "id",
},
schema: {
id: Schema.String,
timestamp: DateTimeUtcFromMillis,
},
}) })
const eventLayer = Layer.mergeAll(EventV2.layerWith().pipe(Layer.provide(Database.defaultLayer)), Database.defaultLayer)
const it = testEffect(eventLayer.pipe(Layer.provideMerge(locationLayer)))
const itWithoutLocation = testEffect(eventLayer)
describe("EventV2", () => { describe("EventV2", () => {
it.effect("publishes events with the current location", () => it.effect("publishes events with the current location", () =>
Effect.gen(function* () { Effect.gen(function* () {
@@ -122,26 +120,21 @@ describe("EventV2", () => {
}), }),
) )
it.effect("stores definitions in the exported registry", () => it.effect("selects the latest durable definition independent of declaration order", () =>
Effect.sync(() => {
expect(EventV2.registry.get(Message.type)).toBe(Message)
}),
)
it.effect("keeps the latest sync definition in the registry", () =>
Effect.sync(() => { Effect.sync(() => {
const latest = EventV2.define({ const latest = EventV2.define({
type: "test.out-of-order", type: "test.out-of-order",
durable: { version: 2, aggregate: "id" }, durable: { version: 2, aggregate: "id" },
schema: { id: Schema.String }, schema: { id: Schema.String },
}) })
EventV2.define({ const historical = EventV2.define({
type: "test.out-of-order", type: "test.out-of-order",
durable: { version: 1, aggregate: "id" }, durable: { version: 1, aggregate: "id" },
schema: { id: Schema.String }, schema: { id: Schema.String },
}) })
expect(EventV2.registry.get("test.out-of-order")).toBe(latest) expect(Event.latest([latest, historical]).get("test.out-of-order")).toBe(latest)
expect(Event.latest([historical, latest]).get("test.out-of-order")).toBe(latest)
}), }),
) )
@@ -363,19 +356,19 @@ describe("EventV2", () => {
it.effect("replays durable aggregate events after a sequence and tails new events", () => it.effect("replays durable aggregate events after a sequence and tails new events", () =>
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const aggregateID = EventV2.ID.create() const aggregateID = Session.ID.create()
yield* events.publish(SyncMessage, { id: aggregateID, text: "zero" }) yield* events.publish(DurableMessage, durableData(aggregateID, "zero"))
yield* events.publish(SyncMessage, { id: aggregateID, text: "one" }) yield* events.publish(DurableMessage, durableData(aggregateID, "one"))
const fiber = yield* events const fiber = yield* events
.durable({ aggregateID, after: 0 }) .durable({ aggregateID, after: 0 })
.pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped) .pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped)
yield* Effect.yieldNow yield* Effect.yieldNow
yield* events.publish(SyncMessage, { id: aggregateID, text: "two" }) yield* events.publish(DurableMessage, durableData(aggregateID, "two"))
expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.durable?.seq, event.data])).toEqual([ expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.durable?.seq, event.data])).toEqual([
[1, { id: aggregateID, text: "one" }], [1, durableData(aggregateID, "one")],
[2, { id: aggregateID, text: "two" }], [2, durableData(aggregateID, "two")],
]) ])
}), }),
) )
@@ -383,20 +376,15 @@ describe("EventV2", () => {
it.effect("catches durable aggregate events published during replay handoff", () => it.effect("catches durable aggregate events published during replay handoff", () =>
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const aggregateID = EventV2.ID.create() const aggregateID = Session.ID.create()
yield* events.publish(SyncMessage, { id: aggregateID, text: "zero" }) yield* events.publish(DurableMessage, durableData(aggregateID, "zero"))
const fiber = yield* events.durable({ aggregateID }).pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped) const fiber = yield* events.durable({ aggregateID }).pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped)
yield* events.publish(SyncMessage, { id: aggregateID, text: "one" }) yield* events.publish(DurableMessage, durableData(aggregateID, "one"))
expect( expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.durable?.seq, event.data])).toEqual([
Array.from(yield* Fiber.join(fiber)).map((event) => [ [0, durableData(aggregateID, "zero")],
event.durable?.seq, [1, durableData(aggregateID, "one")],
(event.data as { text: string }).text,
]),
).toEqual([
[0, "zero"],
[1, "one"],
]) ])
}), }),
) )
@@ -415,16 +403,16 @@ describe("EventV2", () => {
yield* Effect.gen(function* () { yield* Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const aggregateID = EventV2.ID.create() const aggregateID = Session.ID.create()
const fiber = yield* events.durable({ aggregateID }).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) const fiber = yield* events.durable({ aggregateID }).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
yield* Deferred.await(readStarted) yield* Deferred.await(readStarted)
pause = false pause = false
yield* events.publish(SyncMessage, { id: aggregateID, text: "during handoff" }) yield* events.publish(DurableMessage, durableData(aggregateID, "during handoff"))
yield* Deferred.succeed(continueRead, undefined) yield* Deferred.succeed(continueRead, undefined)
expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.durable?.seq, event.data])).toEqual([ expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.durable?.seq, event.data])).toEqual([
[0, { id: aggregateID, text: "during handoff" }], [0, durableData(aggregateID, "during handoff")],
]) ])
}).pipe(Effect.provide(Layer.mergeAll(Database.defaultLayer, eventLayer))) }).pipe(Effect.provide(Layer.mergeAll(Database.defaultLayer, eventLayer)))
}), }),
@@ -433,7 +421,7 @@ describe("EventV2", () => {
it.effect("coalesces durable aggregate wakes while draining every committed event", () => it.effect("coalesces durable aggregate wakes while draining every committed event", () =>
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const aggregateID = EventV2.ID.create() const aggregateID = Session.ID.create()
const count = 64 const count = 64
const fiber = yield* events const fiber = yield* events
.durable({ aggregateID }) .durable({ aggregateID })
@@ -441,11 +429,11 @@ describe("EventV2", () => {
yield* Effect.yieldNow yield* Effect.yieldNow
for (let index = 0; index < count; index++) { for (let index = 0; index < count; index++) {
yield* events.publish(SyncMessage, { id: aggregateID, text: String(index) }) yield* events.publish(DurableMessage, durableData(aggregateID, String(index)))
} }
expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.durable?.seq, event.data])).toEqual( expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.durable?.seq, event.data])).toEqual(
Array.from({ length: count }, (_, index) => [index, { id: aggregateID, text: String(index) }]), Array.from({ length: count }, (_, index) => [index, durableData(aggregateID, String(index))]),
) )
}), }),
) )
@@ -453,14 +441,14 @@ describe("EventV2", () => {
it.effect("omits live-only events from durable aggregate streams", () => it.effect("omits live-only events from durable aggregate streams", () =>
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const aggregateID = EventV2.ID.create() const aggregateID = Session.ID.create()
const fiber = yield* events.durable({ aggregateID }).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) const fiber = yield* events.durable({ aggregateID }).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
yield* Effect.yieldNow yield* Effect.yieldNow
yield* events.publish(Message, { text: "live only" }) yield* events.publish(Message, { text: "live only" })
yield* events.publish(SyncMessage, { id: aggregateID, text: "durable" }) yield* events.publish(DurableMessage, durableData(aggregateID, "durable"))
expect(Array.from(yield* Fiber.join(fiber)).map((event) => event.type)).toEqual([SyncMessage.type]) expect(Array.from(yield* Fiber.join(fiber)).map((event) => event.type)).toEqual([DurableMessage.type])
}), }),
) )
@@ -487,23 +475,23 @@ describe("EventV2", () => {
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const received = new Array<EventV2.Payload>() const received = new Array<EventV2.Payload>()
yield* events.project(SyncMessage, (event) => yield* events.project(DurableMessage, (event) =>
Effect.sync(() => { Effect.sync(() => {
received.push(event) received.push(event)
}), }),
) )
const aggregateID = EventV2.ID.create() const aggregateID = Session.ID.create()
yield* events.replay({ yield* events.replay({
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(SyncMessage.type, 1), type: EventV2.versionedType(DurableMessage.type, 1),
seq: 0, seq: 0,
aggregateID, aggregateID,
data: { id: aggregateID, text: "hello" }, data: durableData(aggregateID, "hello"),
}) })
expect(received[0]?.type).toBe(SyncMessage.type) expect(received[0]?.type).toBe(DurableMessage.type)
expect(received[0]?.data).toEqual({ id: aggregateID, text: "hello" }) expect(received[0]?.data).toEqual(durableData(aggregateID, "hello"))
}), }),
) )
@@ -511,14 +499,14 @@ describe("EventV2", () => {
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const { db } = yield* Database.Service const { db } = yield* Database.Service
const aggregateID = EventV2.ID.create() const aggregateID = Session.ID.create()
yield* events.replay({ yield* events.replay({
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(SyncMessage.type, 1), type: EventV2.versionedType(DurableMessage.type, 1),
seq: 0, seq: 0,
aggregateID, aggregateID,
data: { id: aggregateID, text: "replayed" }, data: durableData(aggregateID, "replayed"),
}) })
const rows = yield* db const rows = yield* db
.select() .select()
@@ -538,11 +526,11 @@ describe("EventV2", () => {
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const { db } = yield* Database.Service const { db } = yield* Database.Service
const envelopeAggregateID = EventV2.ID.create() const envelopeAggregateID = Session.ID.create()
const payloadAggregateID = EventV2.ID.create() const payloadAggregateID = Session.ID.create()
const received = new Array<EventV2.Payload>() const received = new Array<EventV2.Payload>()
yield* events.publish(SyncMessage, { id: payloadAggregateID, text: "seed" }) yield* events.publish(DurableMessage, durableData(payloadAggregateID, "seed"))
yield* events.project(SyncMessage, (event) => yield* events.project(DurableMessage, (event) =>
Effect.sync(() => { Effect.sync(() => {
received.push(event) received.push(event)
}), }),
@@ -551,10 +539,10 @@ describe("EventV2", () => {
const exit = yield* events const exit = yield* events
.replay({ .replay({
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(SyncMessage.type, 1), type: EventV2.versionedType(DurableMessage.type, 1),
seq: 1, seq: 1,
aggregateID: envelopeAggregateID, aggregateID: envelopeAggregateID,
data: { id: payloadAggregateID, text: "replayed" }, data: durableData(payloadAggregateID, "replayed"),
}) })
.pipe(Effect.exit) .pipe(Effect.exit)
const rows = yield* db const rows = yield* db
@@ -580,22 +568,22 @@ describe("EventV2", () => {
it.effect("replay defects on sequence mismatch", () => it.effect("replay defects on sequence mismatch", () =>
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const aggregateID = EventV2.ID.create() const aggregateID = Session.ID.create()
yield* events.replay({ yield* events.replay({
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(SyncMessage.type, 1), type: EventV2.versionedType(DurableMessage.type, 1),
seq: 0, seq: 0,
aggregateID, aggregateID,
data: { id: aggregateID, text: "first" }, data: durableData(aggregateID, "first"),
}) })
const exit = yield* events const exit = yield* events
.replay({ .replay({
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(SyncMessage.type, 1), type: EventV2.versionedType(DurableMessage.type, 1),
seq: 5, seq: 5,
aggregateID, aggregateID,
data: { id: aggregateID, text: "bad" }, data: durableData(aggregateID, "bad"),
}) })
.pipe(Effect.exit) .pipe(Effect.exit)
@@ -606,9 +594,9 @@ describe("EventV2", () => {
it.effect("replay decodes synchronized transformed values before projection", () => it.effect("replay decodes synchronized transformed values before projection", () =>
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const aggregateID = EventV2.ID.create() const aggregateID = Session.ID.create()
const received = new Array<typeof SyncTimestamp.Type>() const received = new Array<typeof SessionEvent.ContextUpdated.Type>()
yield* events.project(SyncTimestamp, (event) => yield* events.project(SessionEvent.ContextUpdated, (event) =>
Effect.sync(() => { Effect.sync(() => {
received.push(event) received.push(event)
}), }),
@@ -616,10 +604,10 @@ describe("EventV2", () => {
yield* events.replay({ yield* events.replay({
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(SyncTimestamp.type, 1), type: EventV2.versionedType(SessionEvent.ContextUpdated.type, 1),
seq: 0, seq: 0,
aggregateID, aggregateID,
data: { id: aggregateID, timestamp: 0 }, data: { sessionID: aggregateID, messageID: "msg_context", timestamp: 0, text: "context" },
}) })
expect(received[0]?.data.timestamp).toEqual(DateTime.makeUnsafe(0)) expect(received[0]?.data.timestamp).toEqual(DateTime.makeUnsafe(0))
@@ -646,21 +634,21 @@ describe("EventV2", () => {
it.effect("replayAll validates contiguous aggregate events", () => it.effect("replayAll validates contiguous aggregate events", () =>
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const aggregateID = EventV2.ID.create() const aggregateID = Session.ID.create()
const source = yield* events.replayAll([ const source = yield* events.replayAll([
{ {
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(SyncMessage.type, 1), type: EventV2.versionedType(DurableMessage.type, 1),
seq: 0, seq: 0,
aggregateID, aggregateID,
data: { id: aggregateID, text: "one" }, data: durableData(aggregateID, "one"),
}, },
{ {
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(SyncMessage.type, 1), type: EventV2.versionedType(DurableMessage.type, 1),
seq: 1, seq: 1,
aggregateID, aggregateID,
data: { id: aggregateID, text: "two" }, data: durableData(aggregateID, "two"),
}, },
]) ])
@@ -672,38 +660,38 @@ describe("EventV2", () => {
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const { db } = yield* Database.Service const { db } = yield* Database.Service
const aggregateID = EventV2.ID.create() const aggregateID = Session.ID.create()
const one = yield* events.replayAll([ const one = yield* events.replayAll([
{ {
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(SyncMessage.type, 1), type: EventV2.versionedType(DurableMessage.type, 1),
seq: 0, seq: 0,
aggregateID, aggregateID,
data: { id: aggregateID, text: "one" }, data: durableData(aggregateID, "one"),
}, },
{ {
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(SyncMessage.type, 1), type: EventV2.versionedType(DurableMessage.type, 1),
seq: 1, seq: 1,
aggregateID, aggregateID,
data: { id: aggregateID, text: "two" }, data: durableData(aggregateID, "two"),
}, },
]) ])
const two = yield* events.replayAll([ const two = yield* events.replayAll([
{ {
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(SyncMessage.type, 1), type: EventV2.versionedType(DurableMessage.type, 1),
seq: 2, seq: 2,
aggregateID, aggregateID,
data: { id: aggregateID, text: "three" }, data: durableData(aggregateID, "three"),
}, },
{ {
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(SyncMessage.type, 1), type: EventV2.versionedType(DurableMessage.type, 1),
seq: 3, seq: 3,
aggregateID, aggregateID,
data: { id: aggregateID, text: "four" }, data: durableData(aggregateID, "four"),
}, },
]) ])
const rows = yield* db const rows = yield* db
@@ -723,10 +711,10 @@ describe("EventV2", () => {
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const received = new Array<EventV2.Payload>() const received = new Array<EventV2.Payload>()
const aggregateID = EventV2.ID.create() const aggregateID = Session.ID.create()
yield* events.publish(SyncMessage, { id: aggregateID, text: "seed" }) yield* events.publish(DurableMessage, durableData(aggregateID, "seed"))
yield* events.claim(aggregateID, "owner-a") yield* events.claim(aggregateID, "owner-a")
yield* events.project(SyncMessage, (event) => yield* events.project(DurableMessage, (event) =>
Effect.sync(() => { Effect.sync(() => {
received.push(event) received.push(event)
}), }),
@@ -735,10 +723,10 @@ describe("EventV2", () => {
yield* events.replay( yield* events.replay(
{ {
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(SyncMessage.type, 1), type: EventV2.versionedType(DurableMessage.type, 1),
seq: 1, seq: 1,
aggregateID, aggregateID,
data: { id: aggregateID, text: "ignored" }, data: durableData(aggregateID, "ignored"),
}, },
{ ownerID: "owner-b" }, { ownerID: "owner-b" },
) )
@@ -750,14 +738,14 @@ describe("EventV2", () => {
it.effect("strict owner fences exact replay", () => it.effect("strict owner fences exact replay", () =>
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const aggregateID = EventV2.ID.create() const aggregateID = Session.ID.create()
const id = EventV2.ID.create() const id = EventV2.ID.create()
const replayed = { const replayed = {
id, id,
type: EventV2.versionedType(SyncMessage.type, 1), type: EventV2.versionedType(DurableMessage.type, 1),
seq: 0, seq: 0,
aggregateID, aggregateID,
data: { id: aggregateID, text: "owned" }, data: durableData(aggregateID, "owned"),
} }
yield* events.replay(replayed, { ownerID: "owner-a" }) yield* events.replay(replayed, { ownerID: "owner-a" })
@@ -771,11 +759,11 @@ describe("EventV2", () => {
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const { db } = yield* Database.Service const { db } = yield* Database.Service
const aggregateID = EventV2.ID.create() const aggregateID = Session.ID.create()
const published = yield* events.publish(SyncMessage, { id: aggregateID, text: "owned" }) const published = yield* events.publish(DurableMessage, durableData(aggregateID, "owned"))
const replayed = { const replayed = {
id: published.id, id: published.id,
type: EventV2.versionedType(SyncMessage.type, 1), type: EventV2.versionedType(DurableMessage.type, 1),
seq: published.durable!.seq, seq: published.durable!.seq,
aggregateID, aggregateID,
data: published.data, data: published.data,
@@ -792,7 +780,7 @@ describe("EventV2", () => {
expect(row?.ownerID).toBe("owner-a") expect(row?.ownerID).toBe("owner-a")
const exit = yield* events const exit = yield* events
.replay( .replay(
{ ...replayed, id: EventV2.ID.create(), seq: 1, data: { id: aggregateID, text: "conflict" } }, { ...replayed, id: EventV2.ID.create(), seq: 1, data: durableData(aggregateID, "conflict") },
{ ownerID: "owner-b", strictOwner: true }, { ownerID: "owner-b", strictOwner: true },
) )
.pipe(Effect.exit) .pipe(Effect.exit)
@@ -804,15 +792,15 @@ describe("EventV2", () => {
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const { db } = yield* Database.Service const { db } = yield* Database.Service
const aggregateID = EventV2.ID.create() const aggregateID = Session.ID.create()
yield* events.replay( yield* events.replay(
{ {
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(SyncMessage.type, 1), type: EventV2.versionedType(DurableMessage.type, 1),
seq: 0, seq: 0,
aggregateID, aggregateID,
data: { id: aggregateID, text: "owned" }, data: durableData(aggregateID, "owned"),
}, },
{ ownerID: "owner-1" }, { ownerID: "owner-1" },
) )
@@ -831,26 +819,26 @@ describe("EventV2", () => {
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const { db } = yield* Database.Service const { db } = yield* Database.Service
const aggregateID = EventV2.ID.create() const aggregateID = Session.ID.create()
yield* events.publish(SyncMessage, { id: aggregateID, text: "local" }) yield* events.publish(DurableMessage, durableData(aggregateID, "local"))
yield* events.replay( yield* events.replay(
{ {
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(SyncMessage.type, 1), type: EventV2.versionedType(DurableMessage.type, 1),
seq: 1, seq: 1,
aggregateID, aggregateID,
data: { id: aggregateID, text: "claimed" }, data: durableData(aggregateID, "claimed"),
}, },
{ ownerID: "owner-1" }, { ownerID: "owner-1" },
) )
yield* events.replay( yield* events.replay(
{ {
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(SyncMessage.type, 1), type: EventV2.versionedType(DurableMessage.type, 1),
seq: 2, seq: 2,
aggregateID, aggregateID,
data: { id: aggregateID, text: "fenced" }, data: durableData(aggregateID, "fenced"),
}, },
{ ownerID: "owner-2" }, { ownerID: "owner-2" },
) )
@@ -875,14 +863,14 @@ describe("EventV2", () => {
it.effect("strict replay rejects an owner conflict instead of silently skipping it", () => it.effect("strict replay rejects an owner conflict instead of silently skipping it", () =>
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const aggregateID = EventV2.ID.create() const aggregateID = Session.ID.create()
yield* events.replay( yield* events.replay(
{ {
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(SyncMessage.type, 1), type: EventV2.versionedType(DurableMessage.type, 1),
seq: 0, seq: 0,
aggregateID, aggregateID,
data: { id: aggregateID, text: "claimed" }, data: durableData(aggregateID, "claimed"),
}, },
{ ownerID: "owner-1" }, { ownerID: "owner-1" },
) )
@@ -891,10 +879,10 @@ describe("EventV2", () => {
.replay( .replay(
{ {
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(SyncMessage.type, 1), type: EventV2.versionedType(DurableMessage.type, 1),
seq: 1, seq: 1,
aggregateID, aggregateID,
data: { id: aggregateID, text: "conflict" }, data: durableData(aggregateID, "conflict"),
}, },
{ ownerID: "owner-2", strictOwner: true }, { ownerID: "owner-2", strictOwner: true },
) )
@@ -908,14 +896,14 @@ describe("EventV2", () => {
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const received = new Array<EventV2.Payload>() const received = new Array<EventV2.Payload>()
const aggregateID = EventV2.ID.create() const aggregateID = Session.ID.create()
yield* events.listen((event) => Effect.sync(() => received.push(event))) yield* events.listen((event) => Effect.sync(() => received.push(event)))
const replayed = { const replayed = {
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(SyncMessage.type, 1), type: EventV2.versionedType(DurableMessage.type, 1),
seq: 0, seq: 0,
aggregateID, aggregateID,
data: { id: aggregateID, text: "replayed" }, data: durableData(aggregateID, "replayed"),
} }
yield* events.replay(replayed, { publish: true }) yield* events.replay(replayed, { publish: true })
@@ -929,19 +917,19 @@ describe("EventV2", () => {
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const received = new Array<EventV2.Payload>() const received = new Array<EventV2.Payload>()
const aggregateID = EventV2.ID.create() const aggregateID = Session.ID.create()
const replayed = { const replayed = {
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(SyncMessage.type, 1), type: EventV2.versionedType(DurableMessage.type, 1),
seq: 0, seq: 0,
aggregateID, aggregateID,
data: { id: aggregateID, text: "original" }, data: durableData(aggregateID, "original"),
} }
yield* events.listen((event) => Effect.sync(() => received.push(event))) yield* events.listen((event) => Effect.sync(() => received.push(event)))
yield* events.replay(replayed, { publish: true }) yield* events.replay(replayed, { publish: true })
const exit = yield* events const exit = yield* events
.replay({ ...replayed, data: { id: aggregateID, text: "divergent" } }, { publish: true }) .replay({ ...replayed, data: durableData(aggregateID, "divergent") }, { publish: true })
.pipe(Effect.exit) .pipe(Effect.exit)
expect(String(exit)).toContain("Replay diverged") expect(String(exit)).toContain("Replay diverged")
@@ -952,23 +940,23 @@ describe("EventV2", () => {
it.effect("rejects an event ID reused at another aggregate position", () => it.effect("rejects an event ID reused at another aggregate position", () =>
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const aggregateID = EventV2.ID.create() const aggregateID = Session.ID.create()
const id = EventV2.ID.create() const id = EventV2.ID.create()
yield* events.replay({ yield* events.replay({
id, id,
type: EventV2.versionedType(SyncMessage.type, 1), type: EventV2.versionedType(DurableMessage.type, 1),
seq: 0, seq: 0,
aggregateID, aggregateID,
data: { id: aggregateID, text: "first" }, data: durableData(aggregateID, "first"),
}) })
const exit = yield* events const exit = yield* events
.replay({ .replay({
id, id,
type: EventV2.versionedType(SyncMessage.type, 1), type: EventV2.versionedType(DurableMessage.type, 1),
seq: 1, seq: 1,
aggregateID, aggregateID,
data: { id: aggregateID, text: "second" }, data: durableData(aggregateID, "second"),
}) })
.pipe(Effect.exit) .pipe(Effect.exit)
@@ -980,27 +968,27 @@ describe("EventV2", () => {
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const { db } = yield* Database.Service const { db } = yield* Database.Service
const aggregateID = EventV2.ID.create() const aggregateID = Session.ID.create()
const received = new Array<EventV2.Payload>() const received = new Array<EventV2.Payload>()
yield* events.listen((event) => Effect.sync(() => received.push(event))) yield* events.listen((event) => Effect.sync(() => received.push(event)))
yield* events.replay( yield* events.replay(
{ {
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(SyncMessage.type, 1), type: EventV2.versionedType(DurableMessage.type, 1),
seq: 0, seq: 0,
aggregateID, aggregateID,
data: { id: aggregateID, text: "first" }, data: durableData(aggregateID, "first"),
}, },
{ ownerID: "owner-1" }, { ownerID: "owner-1" },
) )
yield* events.replay( yield* events.replay(
{ {
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(SyncMessage.type, 1), type: EventV2.versionedType(DurableMessage.type, 1),
seq: 1, seq: 1,
aggregateID, aggregateID,
data: { id: aggregateID, text: "ignored" }, data: durableData(aggregateID, "ignored"),
}, },
{ ownerID: "owner-2", publish: true }, { ownerID: "owner-2", publish: true },
) )
@@ -1047,10 +1035,10 @@ describe("EventV2", () => {
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const received = new Array<EventV2.Payload>() const received = new Array<EventV2.Payload>()
const aggregateID = EventV2.ID.create() const aggregateID = Session.ID.create()
yield* events.publish(SyncMessage, { id: aggregateID, text: "seed" }) yield* events.publish(DurableMessage, durableData(aggregateID, "seed"))
yield* events.remove(aggregateID) yield* events.remove(aggregateID)
yield* events.project(SyncMessage, (event) => yield* events.project(DurableMessage, (event) =>
Effect.sync(() => { Effect.sync(() => {
received.push(event) received.push(event)
}), }),
@@ -1058,13 +1046,13 @@ describe("EventV2", () => {
yield* events.replay({ yield* events.replay({
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(SyncMessage.type, 1), type: EventV2.versionedType(DurableMessage.type, 1),
seq: 0, seq: 0,
aggregateID, aggregateID,
data: { id: aggregateID, text: "replayed" }, data: durableData(aggregateID, "replayed"),
}) })
expect(received[0]?.data).toEqual({ id: aggregateID, text: "replayed" }) expect(received[0]?.data).toEqual(durableData(aggregateID, "replayed"))
}), }),
) )
}) })
@@ -0,0 +1,16 @@
import { describe, expect, test } from "bun:test"
import { SessionV1 as Wire } from "@opencode-ai/schema/session-v1"
import { SessionV1 } from "../src/v1/session"
describe("legacy event schema compatibility", () => {
test("Core references canonical SessionV1 definitions", () => {
expect(SessionV1.Event.Created).toBe(Wire.Event.Created)
expect(SessionV1.Event.PartUpdated).toBe(Wire.Event.PartUpdated)
})
test("Core retains NamedError constructor identity", () => {
const error = new SessionV1.APIError({ message: "failed", isRetryable: false })
expect(error).toBeInstanceOf(SessionV1.APIError)
expect(error.toObject()).toEqual({ name: "APIError", data: { message: "failed", isRetryable: false } })
})
})
+1
View File
@@ -86,6 +86,7 @@
"@openauthjs/openauth": "catalog:", "@openauthjs/openauth": "catalog:",
"@opencode-ai/llm": "workspace:*", "@opencode-ai/llm": "workspace:*",
"@opencode-ai/plugin": "workspace:*", "@opencode-ai/plugin": "workspace:*",
"@opencode-ai/schema": "workspace:*",
"@opencode-ai/script": "workspace:*", "@opencode-ai/script": "workspace:*",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"@opencode-ai/server": "workspace:*", "@opencode-ai/server": "workspace:*",
+2 -11
View File
@@ -2,29 +2,20 @@ import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { EffectBridge } from "@/effect/bridge" import { EffectBridge } from "@/effect/bridge"
import type { InstanceContext } from "@/project/instance-context" import type { InstanceContext } from "@/project/instance-context"
import { SessionID, MessageID } from "@/session/schema"
import { Effect, Layer, Context, Schema } from "effect" import { Effect, Layer, Context, Schema } from "effect"
import { Config } from "@/config/config" import { Config } from "@/config/config"
import { MCP } from "../mcp" import { MCP } from "../mcp"
import { Skill } from "../skill" import { Skill } from "../skill"
import { EventV2 } from "@opencode-ai/core/event"
import PROMPT_INITIALIZE from "./template/initialize.txt" import PROMPT_INITIALIZE from "./template/initialize.txt"
import PROMPT_REVIEW from "./template/review.txt" import PROMPT_REVIEW from "./template/review.txt"
import { LegacyEvent } from "@opencode-ai/schema/legacy-event"
type State = { type State = {
commands: Record<string, Info> commands: Record<string, Info>
} }
export const Event = { export const Event = {
Executed: EventV2.define({ Executed: LegacyEvent.CommandExecuted,
type: "command.executed",
schema: {
name: Schema.String,
sessionID: SessionID,
arguments: Schema.String,
messageID: MessageID,
},
}),
} }
export const Info = Schema.Struct({ export const Info = Schema.Struct({
@@ -33,6 +33,7 @@ import { Vcs } from "@/project/vcs"
import { InstanceStore } from "@/project/instance-store" import { InstanceStore } from "@/project/instance-store"
import { InstanceBootstrap } from "@/project/bootstrap" import { InstanceBootstrap } from "@/project/bootstrap"
import { WorkspaceAdapterRuntime } from "./workspace-adapter-runtime" import { WorkspaceAdapterRuntime } from "./workspace-adapter-runtime"
import { WorkspaceEvent } from "@opencode-ai/schema/workspace-event"
export const Info = Schema.Struct({ export const Info = Schema.Struct({
...WorkspaceInfoSchema.fields, ...WorkspaceInfoSchema.fields,
@@ -40,27 +41,10 @@ export const Info = Schema.Struct({
}).annotate({ identifier: "Workspace" }) }).annotate({ identifier: "Workspace" })
export type Info = WorkspaceInfo & { timeUsed: number } export type Info = WorkspaceInfo & { timeUsed: number }
export const ConnectionStatus = Schema.Struct({ export const ConnectionStatus = WorkspaceEvent.ConnectionStatus
workspaceID: WorkspaceV2.ID, export type ConnectionStatus = WorkspaceEvent.ConnectionStatus
status: Schema.Literals(["connected", "connecting", "disconnected", "error"]),
})
export type ConnectionStatus = Schema.Schema.Type<typeof ConnectionStatus>
export const Event = { export const Event = WorkspaceEvent
Ready: EventV2.define({
type: "workspace.ready",
schema: {
name: Schema.String,
},
}),
Failed: EventV2.define({
type: "workspace.failed",
schema: {
message: Schema.String,
},
}),
Status: EventV2.define({ type: "workspace.status", schema: ConnectionStatus.fields }),
}
function fromRow(row: typeof WorkspaceTable.$inferSelect): Info { function fromRow(row: typeof WorkspaceTable.$inferSelect): Info {
return { return {
+3
View File
@@ -0,0 +1,3 @@
export * as EventManifest from "./event-manifest"
export { Definitions, Durable, Latest } from "@opencode-ai/schema/event-manifest"
+2 -8
View File
@@ -7,9 +7,6 @@ import { EventV2 } from "@opencode-ai/core/event"
import { Location } from "@opencode-ai/core/location" import { Location } from "@opencode-ai/core/location"
import { Project } from "@opencode-ai/core/project" import { Project } from "@opencode-ai/core/project"
import { AbsolutePath } from "@opencode-ai/core/schema" import { AbsolutePath } from "@opencode-ai/core/schema"
import "@opencode-ai/core/account"
import "@opencode-ai/core/catalog"
import "@opencode-ai/core/session/event"
import { Context, Effect, Layer } from "effect" import { Context, Effect, Layer } from "effect"
export class Service extends Context.Service<Service, EventV2.Interface>()("@opencode/EventV2Bridge") {} export class Service extends Context.Service<Service, EventV2.Interface>()("@opencode/EventV2Bridge") {}
@@ -45,10 +42,7 @@ export const layer = Layer.effect(
workspace: workspaceID, workspace: workspaceID,
payload: { id: event.id, type: event.type, properties: event.data }, payload: { id: event.id, type: event.type, properties: event.data },
}) })
const durable = EventV2.registry.get(event.type)?.durable if (event.durable === undefined) return
if (durable === undefined || event.durable === undefined) return
const aggregateID = (event.data as Record<string, unknown>)[durable.aggregate]
if (typeof aggregateID !== "string") return
GlobalBus.emit("event", { GlobalBus.emit("event", {
directory: event.location?.directory ?? ctx?.directory, directory: event.location?.directory ?? ctx?.directory,
project: ctx?.project.id, project: ctx?.project.id,
@@ -59,7 +53,7 @@ export const layer = Layer.effect(
id: event.id, id: event.id,
type: EventV2.versionedType(event.type, event.durable.version), type: EventV2.versionedType(event.type, event.durable.version),
seq: event.durable.seq, seq: event.durable.seq,
aggregateID, aggregateID: event.durable.aggregateID,
data: event.data, data: event.data,
}, },
}, },
+2 -9
View File
@@ -1,7 +1,7 @@
import { EventV2 } from "@opencode-ai/core/event"
import { Schema } from "effect" import { Schema } from "effect"
import { NamedError } from "@opencode-ai/core/util/error" import { NamedError } from "@opencode-ai/core/util/error"
import { Process } from "@/util/process" import { Process } from "@/util/process"
import { IdeEvent } from "@opencode-ai/schema/ide-event"
const SUPPORTED_IDES = [ const SUPPORTED_IDES = [
{ name: "Windsurf" as const, cmd: "windsurf" }, { name: "Windsurf" as const, cmd: "windsurf" },
@@ -11,14 +11,7 @@ const SUPPORTED_IDES = [
{ name: "VSCodium" as const, cmd: "codium" }, { name: "VSCodium" as const, cmd: "codium" },
] ]
export const Event = { export const Event = IdeEvent
Installed: EventV2.define({
type: "ide.installed",
schema: {
ide: Schema.String,
},
}),
}
export const AlreadyInstalledError = NamedError.create("AlreadyInstalledError", {}) export const AlreadyInstalledError = NamedError.create("AlreadyInstalledError", {})
+2 -15
View File
@@ -8,30 +8,17 @@ import { errorMessage } from "@/util/error"
import { ChildProcess } from "effect/unstable/process" import { ChildProcess } from "effect/unstable/process"
import { AppProcess } from "@opencode-ai/core/process" import { AppProcess } from "@opencode-ai/core/process"
import path from "path" import path from "path"
import { EventV2 } from "@opencode-ai/core/event"
import { makeRuntime } from "@opencode-ai/core/effect/runtime" import { makeRuntime } from "@opencode-ai/core/effect/runtime"
import semver from "semver" import semver from "semver"
import { InstallationChannel, InstallationVersion } from "@opencode-ai/core/installation/version" import { InstallationChannel, InstallationVersion } from "@opencode-ai/core/installation/version"
import { NpmConfig } from "@opencode-ai/core/npm-config" import { NpmConfig } from "@opencode-ai/core/npm-config"
import { InstallationEvent } from "@opencode-ai/schema/installation-event"
export type Method = "curl" | "npm" | "yarn" | "pnpm" | "bun" | "brew" | "scoop" | "choco" | "unknown" export type Method = "curl" | "npm" | "yarn" | "pnpm" | "bun" | "brew" | "scoop" | "choco" | "unknown"
export type ReleaseType = "patch" | "minor" | "major" export type ReleaseType = "patch" | "minor" | "major"
export const Event = { export const Event = InstallationEvent
Updated: EventV2.define({
type: "installation.updated",
schema: {
version: Schema.String,
},
}),
UpdateAvailable: EventV2.define({
type: "installation.update-available",
schema: {
version: Schema.String,
},
}),
}
export function getReleaseType(current: string, latest: string): ReleaseType { export function getReleaseType(current: string, latest: string): ReleaseType {
const currMajor = semver.major(current) const currMajor = semver.major(current)
+2 -4
View File
@@ -1,7 +1,6 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { FSUtil } from "@opencode-ai/core/fs-util" import { FSUtil } from "@opencode-ai/core/fs-util"
import { EventV2Bridge } from "@/event-v2-bridge" import { EventV2Bridge } from "@/event-v2-bridge"
import { EventV2 } from "@opencode-ai/core/event"
import * as LSPClient from "./client" import * as LSPClient from "./client"
import path from "path" import path from "path"
import { pathToFileURL, fileURLToPath } from "url" import { pathToFileURL, fileURLToPath } from "url"
@@ -14,10 +13,9 @@ import { InstanceState } from "@/effect/instance-state"
import { containsPath } from "@/project/instance-context" import { containsPath } from "@/project/instance-context"
import { NonNegativeInt } from "@opencode-ai/core/schema" import { NonNegativeInt } from "@opencode-ai/core/schema"
import { RuntimeFlags } from "@/effect/runtime-flags" import { RuntimeFlags } from "@/effect/runtime-flags"
import { LspEvent } from "@opencode-ai/schema/lsp-event"
export const Event = { export const Event = LspEvent
Updated: EventV2.define({ type: "lsp.updated", schema: {} }),
}
const Position = Schema.Struct({ const Position = Schema.Struct({
line: NonNegativeInt, line: NonNegativeInt,
+3 -14
View File
@@ -26,7 +26,6 @@ import { McpOAuthProvider, OAUTH_CALLBACK_PATH } from "./oauth-provider"
import { McpOAuthCallback } from "./oauth-callback" import { McpOAuthCallback } from "./oauth-callback"
import { McpAuth } from "./auth" import { McpAuth } from "./auth"
import { EventV2Bridge } from "@/event-v2-bridge" import { EventV2Bridge } from "@/event-v2-bridge"
import { EventV2 } from "@opencode-ai/core/event"
import { TuiEvent } from "@/server/tui-event" import { TuiEvent } from "@/server/tui-event"
import open from "open" import open from "open"
import { Cause, Effect, Exit, Layer, Option, Context, Schema, Stream } from "effect" import { Cause, Effect, Exit, Layer, Option, Context, Schema, Stream } from "effect"
@@ -35,6 +34,7 @@ import { InstanceState } from "@/effect/instance-state"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { McpCatalog } from "./catalog" import { McpCatalog } from "./catalog"
import { McpEvent } from "@opencode-ai/schema/mcp-event"
const DEFAULT_TIMEOUT = 30_000 const DEFAULT_TIMEOUT = 30_000
const CLIENT_OPTIONS = { const CLIENT_OPTIONS = {
@@ -59,20 +59,9 @@ export const Resource = Schema.Struct({
}).annotate({ identifier: "McpResource" }) }).annotate({ identifier: "McpResource" })
export type Resource = Schema.Schema.Type<typeof Resource> export type Resource = Schema.Schema.Type<typeof Resource>
export const ToolsChanged = EventV2.define({ export const ToolsChanged = McpEvent.ToolsChanged
type: "mcp.tools.changed",
schema: {
server: Schema.String,
},
})
export const BrowserOpenFailed = EventV2.define({ export const BrowserOpenFailed = McpEvent.BrowserOpenFailed
type: "mcp.browser.open.failed",
schema: {
mcpName: Schema.String,
url: Schema.String,
},
})
export const Failed = NamedError.create("MCPFailed", { export const Failed = NamedError.create("MCPFailed", {
name: Schema.String, name: Schema.String,
+2 -12
View File
@@ -6,19 +6,9 @@ import { Deferred, Effect, Layer, Context } from "effect"
import os from "os" import os from "os"
import { PermissionV1 } from "@opencode-ai/core/v1/permission" import { PermissionV1 } from "@opencode-ai/core/v1/permission"
import { EventV2Bridge } from "@/event-v2-bridge" import { EventV2Bridge } from "@/event-v2-bridge"
import { EventV2 } from "@opencode-ai/core/event" import { PermissionV1Event } from "@opencode-ai/schema/permission-v1"
export const Event = { export const Event = PermissionV1Event
Asked: EventV2.define({ type: "permission.asked", schema: PermissionV1.Request.fields }),
Replied: EventV2.define({
type: "permission.replied",
schema: {
sessionID: PermissionV1.Request.fields.sessionID,
requestID: PermissionV1.ID,
reply: PermissionV1.Reply,
},
}),
}
export interface Interface { export interface Interface {
readonly ask: (input: PermissionV1.AskInput) => Effect.Effect<void, PermissionV1.Error> readonly ask: (input: PermissionV1.AskInput) => Effect.Effect<void, PermissionV1.Error>
+10 -39
View File
@@ -16,46 +16,18 @@ import { FSUtil } from "@opencode-ai/core/fs-util"
import { AppProcess } from "@opencode-ai/core/process" import { AppProcess } from "@opencode-ai/core/process"
import { ProjectV2 } from "@opencode-ai/core/project" import { ProjectV2 } from "@opencode-ai/core/project"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { AbsolutePath, NonNegativeInt, optionalOmitUndefined } from "@opencode-ai/core/schema" import { AbsolutePath } from "@opencode-ai/core/schema"
import { serviceUse } from "@opencode-ai/core/effect/service-use" import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { RuntimeFlags } from "@/effect/runtime-flags" import { RuntimeFlags } from "@/effect/runtime-flags"
import { EventV2Bridge } from "@/event-v2-bridge" import { EventV2Bridge } from "@/event-v2-bridge"
import { EventV2 } from "@opencode-ai/core/event" import { EventV2 } from "@opencode-ai/core/event"
import { Project } from "@opencode-ai/schema/project"
const ProjectVcs = Schema.Literal("git") export const Info = Project.Info
const ProjectIcon = Schema.Struct({
url: optionalOmitUndefined(Schema.String),
override: optionalOmitUndefined(Schema.String),
color: optionalOmitUndefined(Schema.String),
})
const ProjectCommands = Schema.Struct({
start: optionalOmitUndefined(
Schema.String.annotate({ description: "Startup script to run when creating a new workspace (worktree)" }),
),
})
const ProjectTime = Schema.Struct({
created: NonNegativeInt,
updated: NonNegativeInt,
initialized: optionalOmitUndefined(NonNegativeInt),
})
export const Info = Schema.Struct({
id: ProjectV2.ID,
worktree: Schema.String,
vcs: optionalOmitUndefined(ProjectVcs),
name: optionalOmitUndefined(Schema.String),
icon: optionalOmitUndefined(ProjectIcon),
commands: optionalOmitUndefined(ProjectCommands),
time: ProjectTime,
sandboxes: Schema.Array(Schema.String),
}).annotate({ identifier: "Project" })
export type Info = Types.DeepMutable<Schema.Schema.Type<typeof Info>> export type Info = Types.DeepMutable<Schema.Schema.Type<typeof Info>>
export const Event = { export const Event = {
Updated: EventV2.define({ type: "project.updated", schema: Info.fields }), Updated: Project.Event.Updated,
} }
type Row = typeof ProjectTable.$inferSelect type Row = typeof ProjectTable.$inferSelect
@@ -72,7 +44,7 @@ export function fromRow(row: Row): Info {
return { return {
id: row.id, id: row.id,
worktree: row.worktree, worktree: row.worktree,
vcs: row.vcs ? Schema.decodeUnknownSync(ProjectVcs)(row.vcs) : undefined, vcs: row.vcs ? Schema.decodeUnknownSync(Project.Vcs)(row.vcs) : undefined,
name: row.name ?? undefined, name: row.name ?? undefined,
icon, icon,
time: { time: {
@@ -88,15 +60,15 @@ export function fromRow(row: Row): Info {
export const UpdateInput = Schema.Struct({ export const UpdateInput = Schema.Struct({
projectID: ProjectV2.ID, projectID: ProjectV2.ID,
name: Schema.optional(Schema.String), name: Schema.optional(Schema.String),
icon: Schema.optional(ProjectIcon), icon: Schema.optional(Project.Icon),
commands: Schema.optional(ProjectCommands), commands: Schema.optional(Project.Commands),
}) })
export type UpdateInput = Types.DeepMutable<Schema.Schema.Type<typeof UpdateInput>> export type UpdateInput = Types.DeepMutable<Schema.Schema.Type<typeof UpdateInput>>
export const UpdatePayload = Schema.Struct({ export const UpdatePayload = Schema.Struct({
name: Schema.optional(Schema.String), name: Schema.optional(Schema.String),
icon: Schema.optional(ProjectIcon), icon: Schema.optional(Project.Icon),
commands: Schema.optional(ProjectCommands), commands: Schema.optional(Project.Commands),
}).annotate({ identifier: "ProjectUpdateInput" }) }).annotate({ identifier: "ProjectUpdateInput" })
export type UpdatePayload = Types.DeepMutable<Schema.Schema.Type<typeof UpdatePayload>> export type UpdatePayload = Types.DeepMutable<Schema.Schema.Type<typeof UpdatePayload>>
@@ -135,7 +107,6 @@ export const layer = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
const fs = yield* FSUtil.Service const fs = yield* FSUtil.Service
const proc = yield* AppProcess.Service
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
const projectV2 = yield* ProjectV2.Service const projectV2 = yield* ProjectV2.Service
const projectDirectories = yield* ProjectDirectories.Service const projectDirectories = yield* ProjectDirectories.Service
@@ -168,7 +139,7 @@ export const layer = Layer.effect(
}), }),
) )
const fakeVcs = Schema.decodeUnknownSync(Schema.optional(ProjectVcs))(Flag.OPENCODE_FAKE_VCS) const fakeVcs = Schema.decodeUnknownSync(Schema.optional(Project.Vcs))(Flag.OPENCODE_FAKE_VCS)
const scope = yield* Scope.Scope const scope = yield* Scope.Scope
+3 -9
View File
@@ -1,11 +1,12 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Effect, Layer, Context, Schema, Stream, Scope } from "effect" import { Effect, Layer, Context, Schema, Scope } from "effect"
import { formatPatch, structuredPatch } from "diff" import { formatPatch, structuredPatch } from "diff"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { Watcher } from "@opencode-ai/core/filesystem/watcher" import { Watcher } from "@opencode-ai/core/filesystem/watcher"
import { Git } from "@/git" import { Git } from "@/git"
import { EventV2Bridge } from "@/event-v2-bridge" import { EventV2Bridge } from "@/event-v2-bridge"
import { EventV2 } from "@opencode-ai/core/event" import { EventV2 } from "@opencode-ai/core/event"
import { VcsEvent } from "@opencode-ai/schema/vcs-event"
const PATCH_CONTEXT_LINES = 2_147_483_647 const PATCH_CONTEXT_LINES = 2_147_483_647
const MAX_PATCH_BYTES = 10_000_000 const MAX_PATCH_BYTES = 10_000_000
@@ -234,14 +235,7 @@ const track = Effect.fnUntraced(function* (
export const Mode = Schema.Literals(["git", "branch"]) export const Mode = Schema.Literals(["git", "branch"])
export type Mode = Schema.Schema.Type<typeof Mode> export type Mode = Schema.Schema.Type<typeof Mode>
export const Event = { export const Event = VcsEvent
BranchUpdated: EventV2.define({
type: "vcs.branch.updated",
schema: {
branch: Schema.optional(Schema.String),
},
}),
}
export const Info = Schema.Struct({ export const Info = Schema.Struct({
branch: Schema.optional(Schema.String), branch: Schema.optional(Schema.String),
+19 -85
View File
@@ -1,94 +1,28 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Deferred, Effect, Layer, Schema, Context } from "effect" import { Deferred, Effect, Layer, Schema, Context } from "effect"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { SessionID, MessageID } from "@/session/schema" import { SessionID } from "@/session/schema"
import { QuestionID } from "./schema" import { QuestionID } from "./schema"
import { EventV2Bridge } from "@/event-v2-bridge" import { EventV2Bridge } from "@/event-v2-bridge"
import { EventV2 } from "@opencode-ai/core/event" import { QuestionV1 } from "@opencode-ai/schema/question-v1"
// Schemas — these are pure data; nothing checks class identity (see PR export const Option = QuestionV1.Option
// description) so they're plain `Schema.Struct` + type alias. That lets export type Option = typeof Option.Type
// `Question.ask` and other internal sites trust the type contract without a export const Info = QuestionV1.Info
// re-decode to coerce nested class instances. export type Info = typeof Info.Type
export const Prompt = QuestionV1.Prompt
export const Option = Schema.Struct({ export type Prompt = typeof Prompt.Type
label: Schema.String.annotate({ export const Tool = QuestionV1.Tool
description: "Display text (1-5 words, concise)", export type Tool = typeof Tool.Type
}), export const Request = QuestionV1.Request
description: Schema.String.annotate({ export type Request = typeof Request.Type
description: "Explanation of choice", export const Answer = QuestionV1.Answer
}), export type Answer = typeof Answer.Type
}).annotate({ identifier: "QuestionOption" }) export const Reply = QuestionV1.Reply
export type Option = Schema.Schema.Type<typeof Option> export type Reply = typeof Reply.Type
export const Replied = QuestionV1.Replied
const base = { export const Rejected = QuestionV1.Rejected
question: Schema.String.annotate({ export const Event = QuestionV1.Event
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 type Info = Schema.Schema.Type<typeof Info>
export const Prompt = Schema.Struct(base).annotate({ identifier: "QuestionPrompt" })
export type Prompt = Schema.Schema.Type<typeof Prompt>
export const Tool = Schema.Struct({
messageID: MessageID,
callID: Schema.String,
}).annotate({ identifier: "QuestionTool" })
export type Tool = Schema.Schema.Type<typeof Tool>
export const Request = Schema.Struct({
id: QuestionID,
sessionID: SessionID,
questions: Schema.Array(Info).annotate({
description: "Questions to ask",
}),
tool: Schema.optional(Tool),
}).annotate({ identifier: "QuestionRequest" })
export type Request = Schema.Schema.Type<typeof Request>
export const Answer = Schema.Array(Schema.String).annotate({ identifier: "QuestionAnswer" })
export type Answer = Schema.Schema.Type<typeof Answer>
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 type Reply = Schema.Schema.Type<typeof Reply>
export const Replied = Schema.Struct({
sessionID: SessionID,
requestID: QuestionID,
answers: Schema.Array(Answer),
}).annotate({ identifier: "QuestionReplied" })
export const Rejected = Schema.Struct({
sessionID: SessionID,
requestID: QuestionID,
}).annotate({ identifier: "QuestionRejected" })
export const Event = {
Asked: EventV2.define({ type: "question.asked", schema: Request.fields }),
Replied: EventV2.define({ type: "question.replied", schema: Replied.fields }),
Rejected: EventV2.define({ type: "question.rejected", schema: Rejected.fields }),
}
export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("QuestionRejectedError", {}) { export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("QuestionRejectedError", {}) {
override get message() { override get message() {
+3 -9
View File
@@ -1,10 +1,4 @@
import { Schema } from "effect" import { QuestionV1 } from "@opencode-ai/schema/question-v1"
import { Identifier } from "@/id/id" export const QuestionID = QuestionV1.ID
import { Newtype } from "@opencode-ai/core/schema" export type QuestionID = typeof QuestionID.Type
export class QuestionID extends Newtype<QuestionID>()("QuestionID", Schema.String.check(Schema.isStartsWith("que"))) {
static ascending(id?: string): QuestionID {
return this.make(Identifier.ascending("question", id))
}
}
+2 -5
View File
@@ -1,10 +1,7 @@
import { EventV2 } from "@opencode-ai/core/event"
import { Schema } from "effect" import { Schema } from "effect"
import { ServerEvent } from "@opencode-ai/schema/server-event"
export const Event = { export const Event = ServerEvent
Connected: EventV2.define({ type: "server.connected", schema: {} }),
Disposed: EventV2.define({ type: "global.disposed", schema: {} }),
}
export const InstanceDisposed = Schema.Struct({ export const InstanceDisposed = Schema.Struct({
id: Schema.String, id: Schema.String,
@@ -1,6 +1,7 @@
import { Schema } from "effect" import { Schema } from "effect"
import { HttpApi } from "effect/unstable/httpapi" import { HttpApi } from "effect/unstable/httpapi"
import { EventV2 } from "@opencode-ai/core/event" import { EventV2 } from "@opencode-ai/core/event"
import { EventManifest } from "@/event-manifest"
import { Credential } from "@opencode-ai/core/credential" import { Credential } from "@opencode-ai/core/credential"
import { Integration } from "@opencode-ai/core/integration" import { Integration } from "@opencode-ai/core/integration"
import { SkillV2 } from "@opencode-ai/core/skill" import { SkillV2 } from "@opencode-ai/core/skill"
@@ -24,15 +25,13 @@ import { SessionApi } from "./groups/session"
import { SyncApi } from "./groups/sync" import { SyncApi } from "./groups/sync"
import { TuiApi } from "./groups/tui" import { TuiApi } from "./groups/tui"
import { WorkspaceApi } from "./groups/workspace" import { WorkspaceApi } from "./groups/workspace"
import { Api } from "@opencode-ai/server/api" import { makeApi } from "@opencode-ai/server/api"
// GlobalEventSchema snapshots the registry after event-producing groups register their variants.
import { GlobalApi } from "./groups/global" import { GlobalApi } from "./groups/global"
import { Authorization } from "./middleware/authorization" import { Authorization } from "./middleware/authorization"
import { SchemaErrorMiddleware } from "./middleware/schema-error" import { SchemaErrorMiddleware } from "./middleware/schema-error"
const EventSchema = Schema.Union([ const EventSchema = Schema.Union([
...EventV2.registry ...EventManifest.Latest.values()
.values()
.map((definition) => .map((definition) =>
Schema.Struct({ Schema.Struct({
id: EventV2.ID, id: EventV2.ID,
@@ -44,6 +43,8 @@ const EventSchema = Schema.Union([
InstanceDisposed, InstanceDisposed,
]).annotate({ identifier: "Event" }) ]).annotate({ identifier: "Event" })
export const ServerApi = makeApi(EventManifest.Latest.values().toArray())
export const RootHttpApi = HttpApi.make("opencode-root") export const RootHttpApi = HttpApi.make("opencode-root")
.addHttpApi(ControlApi) .addHttpApi(ControlApi)
.addHttpApi(ControlPlaneApi) .addHttpApi(ControlPlaneApi)
@@ -73,7 +74,7 @@ export const OpenCodeHttpApi = HttpApi.make("opencode")
.addHttpApi(RootHttpApi) .addHttpApi(RootHttpApi)
.addHttpApi(EventApi) .addHttpApi(EventApi)
.addHttpApi(InstanceHttpApi) .addHttpApi(InstanceHttpApi)
.addHttpApi(Api) .addHttpApi(ServerApi)
.addHttpApi(PtyConnectApi) .addHttpApi(PtyConnectApi)
.annotate(HttpApi.AdditionalSchemas, [ .annotate(HttpApi.AdditionalSchemas, [
EventSchema, EventSchema,
@@ -1,6 +1,6 @@
import { Config } from "@/config/config"
import { ConfigV1 } from "@opencode-ai/core/v1/config/config" import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import { EventV2 } from "@opencode-ai/core/event" import { EventV2 } from "@opencode-ai/core/event"
import { EventManifest } from "@/event-manifest"
import { InstanceDisposed } from "@/server/event" import { InstanceDisposed } from "@/server/event"
import "@opencode-ai/core/account" import "@opencode-ai/core/account"
import "@/server/event" import "@/server/event"
@@ -13,8 +13,7 @@ const GlobalHealth = Schema.Struct({
version: Schema.String, version: Schema.String,
}) })
const SyncEventSchemas = EventV2.registry const SyncEventSchemas = EventManifest.Latest.values()
.values()
.flatMap((definition) => { .flatMap((definition) => {
if (!definition.durable) return [] if (!definition.durable) return []
return [ return [
@@ -38,8 +37,7 @@ const GlobalEventSchema = Schema.Struct({
project: Schema.optional(Schema.String), project: Schema.optional(Schema.String),
workspace: Schema.optional(Schema.String), workspace: Schema.optional(Schema.String),
payload: Schema.Union([ payload: Schema.Union([
...EventV2.registry ...EventManifest.Latest.values()
.values()
.map((definition) => .map((definition) =>
Schema.Struct({ id: EventV2.ID, type: Schema.Literal(definition.type), properties: definition.data }), Schema.Struct({ id: EventV2.ID, type: Schema.Literal(definition.type), properties: definition.data }),
) )
+1 -53
View File
@@ -1,53 +1 @@
import { SessionID } from "@/session/schema" export { TuiEvent } from "@opencode-ai/schema/tui-event"
import { PositiveInt } from "@opencode-ai/core/schema"
import { EventV2 } from "@opencode-ai/core/event"
import { Effect, Schema } from "effect"
const DEFAULT_TOAST_DURATION = 5000
export const TuiEvent = {
PromptAppend: EventV2.define({ type: "tui.prompt.append", schema: { text: Schema.String } }),
CommandExecute: EventV2.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,
]),
},
}),
ToastShow: EventV2.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",
}),
},
}),
SessionSelect: EventV2.define({
type: "tui.session.select",
schema: {
sessionID: SessionID.annotate({ description: "Session ID to navigate to" }),
},
}),
}
+2 -9
View File
@@ -23,17 +23,10 @@ import { SessionEvent } from "@opencode-ai/core/session/event"
import { SessionMessage } from "@opencode-ai/core/session/message" import { SessionMessage } from "@opencode-ai/core/session/message"
import { ProviderV2 } from "@opencode-ai/core/provider" import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model" import { ModelV2 } from "@opencode-ai/core/model"
import { EventV2 } from "@opencode-ai/core/event"
import { buildPrompt } from "@opencode-ai/core/session/compaction" import { buildPrompt } from "@opencode-ai/core/session/compaction"
import { SessionCompactionEvent } from "@opencode-ai/schema/session-compaction-event"
export const Event = { export const Event = SessionCompactionEvent
Compacted: EventV2.define({
type: "session.compacted",
schema: {
sessionID: SessionID,
},
}),
}
export const PRUNE_MINIMUM = 20_000 export const PRUNE_MINIMUM = 20_000
export const PRUNE_PROTECT = 40_000 export const PRUNE_PROTECT = 40_000
+2 -14
View File
@@ -1,5 +1,4 @@
import { EventV2 } from "@opencode-ai/core/event" import { SessionID, MessageID } from "./schema"
import { SessionID, MessageID, PartID } from "./schema"
import { SessionV1 } from "@opencode-ai/core/v1/session" import { SessionV1 } from "@opencode-ai/core/v1/session"
import { ProviderV2 } from "@opencode-ai/core/provider" import { ProviderV2 } from "@opencode-ai/core/provider"
import { import {
@@ -12,11 +11,9 @@ import {
Info, Info,
OutputLengthError, OutputLengthError,
Part, Part,
StructuredOutputError,
SubtaskPart, SubtaskPart,
User, User,
WithParts, WithParts,
type ToolPart,
} from "@opencode-ai/core/v1/session" } from "@opencode-ai/core/v1/session"
import { NamedError } from "@opencode-ai/core/util/error" import { NamedError } from "@opencode-ai/core/util/error"
@@ -61,16 +58,7 @@ export const Event = {
Updated: SessionV1.Event.MessageUpdated, Updated: SessionV1.Event.MessageUpdated,
Removed: SessionV1.Event.MessageRemoved, Removed: SessionV1.Event.MessageRemoved,
PartUpdated: SessionV1.Event.PartUpdated, PartUpdated: SessionV1.Event.PartUpdated,
PartDelta: EventV2.define({ PartDelta: SessionV1.Event.PartDelta,
type: "message.part.delta",
schema: {
sessionID: SessionID,
messageID: MessageID,
partID: PartID,
field: Schema.String,
delta: Schema.String,
},
}),
PartRemoved: SessionV1.Event.PartRemoved, PartRemoved: SessionV1.Event.PartRemoved,
} }
+2 -61
View File
@@ -11,7 +11,6 @@ import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { Database } from "@opencode-ai/core/database/database" import { Database } from "@opencode-ai/core/database/database"
import { makeRuntime } from "@opencode-ai/core/effect/runtime" import { makeRuntime } from "@opencode-ai/core/effect/runtime"
import { EventV2Bridge } from "@/event-v2-bridge" import { EventV2Bridge } from "@/event-v2-bridge"
import { EventV2 } from "@opencode-ai/core/event"
import { SessionV2 } from "@opencode-ai/core/session" import { SessionV2 } from "@opencode-ai/core/session"
import { SessionExecution } from "@opencode-ai/core/session/execution" import { SessionExecution } from "@opencode-ai/core/session/execution"
@@ -38,7 +37,6 @@ import { WorkspaceV2 } from "@opencode-ai/core/workspace"
import { SessionID, MessageID, PartID } from "./schema" import { SessionID, MessageID, PartID } from "./schema"
import type { Provider } from "@/provider/provider" import type { Provider } from "@/provider/provider"
import { Permission } from "@/permission"
import { Global } from "@opencode-ai/core/global" import { Global } from "@opencode-ai/core/global"
import { Effect, Layer, Option, Context, Schema, Types } from "effect" import { Effect, Layer, Option, Context, Schema, Types } from "effect"
import { NonNegativeInt, optionalOmitUndefined } from "@opencode-ai/core/schema" import { NonNegativeInt, optionalOmitUndefined } from "@opencode-ai/core/schema"
@@ -309,69 +307,12 @@ export type GlobalListInput = {
archived?: boolean archived?: boolean
} }
const CreatedEventSchema = Schema.Struct({
sessionID: SessionID,
info: Info,
})
const UpdatedShare = Schema.Struct({
url: Schema.optional(Schema.NullOr(Schema.String)),
})
const UpdatedTime = Schema.Struct({
created: Schema.optional(Schema.NullOr(NonNegativeInt)),
updated: Schema.optional(Schema.NullOr(NonNegativeInt)),
compacting: Schema.optional(Schema.NullOr(NonNegativeInt)),
archived: Schema.optional(Schema.NullOr(ArchivedTimestamp)),
})
const UpdatedInfo = Schema.Struct({
id: Schema.optional(Schema.NullOr(SessionID)),
slug: Schema.optional(Schema.NullOr(Schema.String)),
projectID: Schema.optional(Schema.NullOr(ProjectV2.ID)),
workspaceID: Schema.optional(Schema.NullOr(WorkspaceV2.ID)),
directory: Schema.optional(Schema.NullOr(Schema.String)),
path: Schema.optional(Schema.NullOr(Schema.String)),
parentID: Schema.optional(Schema.NullOr(SessionID)),
summary: Schema.optional(Schema.NullOr(Summary)),
cost: Schema.optional(Schema.Finite),
tokens: Schema.optional(Tokens),
share: Schema.optional(UpdatedShare),
title: Schema.optional(Schema.NullOr(Schema.String)),
agent: Schema.optional(Schema.NullOr(Schema.String)),
model: Schema.optional(Schema.NullOr(Model)),
version: Schema.optional(Schema.NullOr(Schema.String)),
metadata: Schema.optional(Schema.NullOr(Metadata)),
time: Schema.optional(UpdatedTime),
permission: Schema.optional(Schema.NullOr(PermissionV1.Ruleset)),
revert: Schema.optional(Schema.NullOr(Revert)),
})
const UpdatedEventSchema = Schema.Struct({
sessionID: SessionID,
info: UpdatedInfo,
})
export const Event = { export const Event = {
Created: SessionV1.Event.Created, Created: SessionV1.Event.Created,
Updated: SessionV1.Event.Updated, Updated: SessionV1.Event.Updated,
Deleted: SessionV1.Event.Deleted, Deleted: SessionV1.Event.Deleted,
Diff: EventV2.define({ Diff: SessionV1.Event.Diff,
type: "session.diff", Error: SessionV1.Event.Error,
schema: {
sessionID: SessionID,
diff: Schema.Array(Snapshot.FileDiff),
},
}),
Error: EventV2.define({
type: "session.error",
schema: {
sessionID: Schema.optional(SessionID),
// Reuses SessionV1.Assistant.fields.error (already Schema.optional) so
// the derived schema keeps the same discriminated-union shape on the event stream.
error: SessionV1.Assistant.fields.error,
},
}),
} }
export function plan(input: { slug: string; time: { created: number } }, instance: InstanceContext) { export function plan(input: { slug: string; time: { created: number } }, instance: InstanceContext) {
+5 -44
View File
@@ -1,53 +1,14 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { SessionID } from "./schema" import { SessionID } from "./schema"
import { NonNegativeInt } from "@opencode-ai/core/schema" import { Effect, Layer, Context } from "effect"
import { Effect, Layer, Context, Schema } from "effect"
import { EventV2Bridge } from "@/event-v2-bridge" import { EventV2Bridge } from "@/event-v2-bridge"
import { EventV2 } from "@opencode-ai/core/event" import { SessionStatusEvent } from "@opencode-ai/schema/session-status-event"
export const Info = Schema.Union([ export const Info = SessionStatusEvent.Info
Schema.Struct({ export type Info = SessionStatusEvent.Info
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 Event = { export const Event = SessionStatusEvent
Status: EventV2.define({
type: "session.status",
schema: {
sessionID: SessionID,
status: Info,
},
}),
// deprecated
Idle: EventV2.define({
type: "session.idle",
schema: {
sessionID: SessionID,
},
}),
}
export interface Interface { export interface Interface {
readonly get: (sessionID: SessionID) => Effect.Effect<Info> readonly get: (sessionID: SessionID) => Effect.Effect<Info>
+5 -19
View File
@@ -1,31 +1,17 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { SessionID } from "./schema" import { SessionID } from "./schema"
import { Effect, Layer, Context, Schema } from "effect" import { Effect, Layer, Context } from "effect"
import { Database } from "@opencode-ai/core/database/database" import { Database } from "@opencode-ai/core/database/database"
import { eq } from "drizzle-orm" import { eq } from "drizzle-orm"
import { asc } from "drizzle-orm" import { asc } from "drizzle-orm"
import { TodoTable } from "@opencode-ai/core/session/sql" import { TodoTable } from "@opencode-ai/core/session/sql"
import { EventV2Bridge } from "@/event-v2-bridge" import { EventV2Bridge } from "@/event-v2-bridge"
import { EventV2 } from "@opencode-ai/core/event" import { SessionTodo } from "@opencode-ai/schema/session-todo"
export const Info = Schema.Struct({ export const Info = SessionTodo.Info
content: Schema.String.annotate({ description: "Brief description of the task" }), export type Info = SessionTodo.Info
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 = Schema.Schema.Type<typeof Info>
export const Event = { export const Event = SessionTodo.Event
Updated: EventV2.define({
type: "todo.updated",
schema: {
sessionID: SessionID,
todos: Schema.Array(Info),
},
}),
}
export interface Interface { export interface Interface {
readonly update: (input: { sessionID: SessionID; todos: Info[] }) => Effect.Effect<void> readonly update: (input: { sessionID: SessionID; todos: Info[] }) => Effect.Effect<void>
+2 -10
View File
@@ -9,6 +9,7 @@ import { FSUtil } from "@opencode-ai/core/fs-util"
import { Hash } from "@opencode-ai/core/util/hash" import { Hash } from "@opencode-ai/core/util/hash"
import { Config } from "@/config/config" import { Config } from "@/config/config"
import { Global } from "@opencode-ai/core/global" import { Global } from "@opencode-ai/core/global"
import { Info } from "@opencode-ai/schema/file-diff"
export const Patch = Schema.Struct({ export const Patch = Schema.Struct({
hash: Schema.String, hash: Schema.String,
@@ -16,16 +17,7 @@ export const Patch = Schema.Struct({
}) })
export type Patch = typeof Patch.Type export type Patch = typeof Patch.Type
export const FileDiff = Schema.Struct({ export const FileDiff = Info
// Optional because legacy/imported `summary_diffs` on disk may omit
// file details and patch text. Required Schema rejected the whole
// session response and broke session loading on Desktop.
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 FileDiff = typeof FileDiff.Type export type FileDiff = typeof FileDiff.Type
const prune = "7.days" const prune = "7.days"
+2 -16
View File
@@ -10,7 +10,6 @@ import { ProjectTable } from "@opencode-ai/core/project/sql"
import type { ProjectV2 } from "@opencode-ai/core/project" import type { ProjectV2 } from "@opencode-ai/core/project"
import { Slug } from "@opencode-ai/core/util/slug" import { Slug } from "@opencode-ai/core/util/slug"
import { errorMessage } from "../util/error" import { errorMessage } from "../util/error"
import { EventV2 } from "@opencode-ai/core/event"
import { GlobalBus } from "@/bus/global" import { GlobalBus } from "@/bus/global"
import { Git } from "@/git" import { Git } from "@/git"
import { Effect, Layer, Path, Schema, Scope, Context } from "effect" import { Effect, Layer, Path, Schema, Scope, Context } from "effect"
@@ -19,22 +18,9 @@ import { NodePath } from "@effect/platform-node"
import { FSUtil } from "@opencode-ai/core/fs-util" import { FSUtil } from "@opencode-ai/core/fs-util"
import { AppProcess } from "@opencode-ai/core/process" import { AppProcess } from "@opencode-ai/core/process"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { WorktreeEvent } from "@opencode-ai/schema/worktree-event"
export const Event = { export const Event = WorktreeEvent
Ready: EventV2.define({
type: "worktree.ready",
schema: {
name: Schema.String,
branch: Schema.optional(Schema.String),
},
}),
Failed: EventV2.define({
type: "worktree.failed",
schema: {
message: Schema.String,
},
}),
}
export const Info = Schema.Struct({ export const Info = Schema.Struct({
name: Schema.String, name: Schema.String,
@@ -0,0 +1,24 @@
import { describe, expect, test } from "bun:test"
import { SessionEvent } from "@opencode-ai/core/session/event"
import { EventManifest as SchemaEventManifest } from "@opencode-ai/schema/event-manifest"
import { Todo } from "@/session/todo"
import { EventManifest } from "@/event-manifest"
describe("public event manifest", () => {
test("contains every latest public wire type once", () => {
expect(EventManifest.Definitions).toBe(SchemaEventManifest.Definitions)
expect(EventManifest.Latest).toBe(SchemaEventManifest.Latest)
expect(EventManifest.Durable).toBe(SchemaEventManifest.Durable)
expect(EventManifest.Latest.size).toBe(85)
expect(EventManifest.Latest.get("session.next.step.ended")).toBe(SessionEvent.Step.Ended)
expect(EventManifest.Latest.get("todo.updated")).toBe(Todo.Event.Updated)
expect(EventManifest.Latest.has("ide.installed")).toBe(false)
expect(EventManifest.Latest.has("server.connected")).toBe(true)
expect(EventManifest.Latest.has("global.disposed")).toBe(true)
})
test("contains only the current step settlement versions", () => {
expect(EventManifest.Durable.has("session.next.step.ended.1")).toBe(false)
expect(EventManifest.Durable.get("session.next.step.ended.2")).toBe(SessionEvent.Step.Ended)
})
})
+6
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) }
@@ -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,
])
+84
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 }
+125
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
}
+12
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
+13
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) }
+7
View File
@@ -1,8 +1,15 @@
export * as FileSystem from "./filesystem" export * as FileSystem from "./filesystem"
import { Schema } from "effect" import { Schema } from "effect"
import { define, inventory } from "./event"
import { NonNegativeInt, PositiveInt, RelativePath } from "./schema" 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 interface Entry extends Schema.Schema.Type<typeof Entry> {}
export const Entry = Schema.Struct({ export const Entry = Schema.Struct({
path: RelativePath, path: RelativePath,
+13
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)
+1
View File
@@ -2,6 +2,7 @@ export { Agent } from "./agent"
export { Command } from "./command" export { Command } from "./command"
export { Connection } from "./connection" export { Connection } from "./connection"
export { Credential } from "./credential" export { Credential } from "./credential"
export { Event } from "./event"
export { FileSystem } from "./filesystem" export { FileSystem } from "./filesystem"
export { Integration } from "./integration" export { Integration } from "./integration"
export { LLM } from "./llm" export { LLM } from "./llm"
+20
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)
+11
View File
@@ -1,6 +1,7 @@
export * as Integration from "./integration" export * as Integration from "./integration"
import { Schema } from "effect" import { Schema } from "effect"
import { define, inventory } from "./event"
export const ID = Schema.String.pipe(Schema.brand("Integration.ID")) export const ID = Schema.String.pipe(Schema.brand("Integration.ID"))
export type ID = typeof ID.Type 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 const Inputs = Schema.Record(Schema.String, Schema.String).annotate({ identifier: "Integration.Inputs" })
export type Inputs = typeof Inputs.Type 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 interface Ref extends Schema.Schema.Type<typeof Ref> {}
export const Ref = Schema.Struct({ export const Ref = Schema.Struct({
id: ID, id: ID,
+18
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)
+2 -2
View File
@@ -2,12 +2,12 @@ export * as Location from "./location"
import { Effect, Schema } from "effect" import { Effect, Schema } from "effect"
import { AbsolutePath } from "./schema" import { AbsolutePath } from "./schema"
import { Workspace } from "./workspace" import { WorkspaceID } from "./workspace-id"
export interface Ref extends Schema.Schema.Type<typeof Ref> {} export interface Ref extends Schema.Schema.Type<typeof Ref> {}
export const Ref = Schema.Struct({ export const Ref = Schema.Struct({
directory: AbsolutePath, directory: AbsolutePath,
workspaceID: Schema.optional(Workspace.ID).pipe( workspaceID: Schema.optional(WorkspaceID).pipe(
Schema.withDecodingDefault(Effect.succeed(undefined)), Schema.withDecodingDefault(Effect.succeed(undefined)),
Schema.withConstructorDefault(Effect.succeed(undefined)), Schema.withConstructorDefault(Effect.succeed(undefined)),
), ),
+7
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)
+21
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)
+9
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) }
+67
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
+48
View File
@@ -1,6 +1,54 @@
export * as Permission from "./permission" export * as Permission from "./permission"
import { Schema } from "effect" 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 const Effect = Schema.Literals(["allow", "deny", "ask"]).annotate({ identifier: "PermissionV2.Effect" })
export type Effect = typeof Effect.Type export type Effect = typeof Effect.Type
+15
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
@@ -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
+34 -1
View File
@@ -1,10 +1,43 @@
export * as Project from "./project" export * as Project from "./project"
import { Schema } from "effect" 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( export const ID = Schema.String.pipe(
Schema.brand("Project.ID"), Schema.brand("Project.ID"),
withStatics((schema) => ({ global: schema.make("global") })), withStatics((schema) => ({ global: schema.make("global") })),
) )
export type ID = typeof ID.Type 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) }
+35
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
+66
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),
}
+79
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) }
+4
View File
@@ -1,8 +1,12 @@
export * as Reference from "./reference" export * as Reference from "./reference"
import { Schema } from "effect" import { Schema } from "effect"
import { define, inventory } from "./event"
import { AbsolutePath } from "./schema" 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 interface LocalSource extends Schema.Schema.Type<typeof LocalSource> {}
export const LocalSource = Schema.Struct({ export const LocalSource = Schema.Struct({
type: Schema.Literal("local"), type: Schema.Literal("local"),
+8
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)
@@ -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)
+497
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"]
+2 -4
View File
@@ -1,10 +1,8 @@
export * as SessionID from "./session-id"
import { Schema } from "effect" import { Schema } from "effect"
import { descending } from "./identifier" import { descending } from "./identifier"
import { withStatics } from "./schema" 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"), Schema.brand("SessionID"),
withStatics((schema) => { withStatics((schema) => {
const create = () => schema.make("ses_" + descending()) 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
+1 -1
View File
@@ -14,7 +14,7 @@ export interface Admitted extends Schema.Schema.Type<typeof Admitted> {}
export const Admitted = Schema.Struct({ export const Admitted = Schema.Struct({
admittedSeq: NonNegativeInt, admittedSeq: NonNegativeInt,
id: SessionMessageID.ID, id: SessionMessageID.ID,
sessionID: SessionID.ID, sessionID: SessionID,
prompt: Prompt, prompt: Prompt,
delivery: Delivery, delivery: Delivery,
timeCreated: DateTimeUtcFromMillis, timeCreated: DateTimeUtcFromMillis,
+1 -1
View File
@@ -49,7 +49,7 @@ export const User = Schema.Struct({
export interface Synthetic extends Schema.Schema.Type<typeof Synthetic> {} export interface Synthetic extends Schema.Schema.Type<typeof Synthetic> {}
export const Synthetic = Schema.Struct({ export const Synthetic = Schema.Struct({
...Base, ...Base,
sessionID: SessionID.ID, sessionID: SessionID,
text: Schema.String, text: Schema.String,
type: Schema.Literal("synthetic"), type: Schema.Literal("synthetic"),
}).annotate({ identifier: "Session.Message.Synthetic" }) }).annotate({ identifier: "Session.Message.Synthetic" })
@@ -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)
+24
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) }
+676
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,
),
}
+5 -2
View File
@@ -6,10 +6,13 @@ import { Location } from "./location"
import { Model } from "./model" import { Model } from "./model"
import { Project } from "./project" import { Project } from "./project"
import { DateTimeUtcFromMillis, optionalOmitUndefined, RelativePath } from "./schema" import { DateTimeUtcFromMillis, optionalOmitUndefined, RelativePath } from "./schema"
import { SessionEvent } from "./session-event"
import { SessionID } from "./session-id" import { SessionID } from "./session-id"
export const ID = SessionID.ID export const ID = SessionID
export type ID = SessionID.ID export type ID = SessionID
export const Event = SessionEvent
export interface Info extends Schema.Schema.Type<typeof Info> {} export interface Info extends Schema.Schema.Type<typeof Info> {}
export const Info = Schema.Struct({ export const Info = Schema.Struct({
+58
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)
+13
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)
+32
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)
+19
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
+6 -18
View File
@@ -1,21 +1,9 @@
export * as Workspace from "./workspace" export * as Workspace from "./workspace"
import { Schema } from "effect" import { WorkspaceEvent } from "./workspace-event"
import { ascending } from "./identifier" import { WorkspaceID } from "./workspace-id"
import { withStatics } from "./schema"
export const ID = Schema.String.check(Schema.isStartsWith("wrk")).pipe( export const ID = WorkspaceID
Schema.brand("WorkspaceV2.ID"), export type ID = WorkspaceID
withStatics((schema) => {
const create = () => schema.make("wrk_" + ascending()) export const Event = WorkspaceEvent
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
+21
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)
@@ -0,0 +1,53 @@
import { describe, expect, test } from "bun:test"
import { FileSystem, Integration, Permission, Project, Reference, Session, Workspace } from "../src"
import { EventManifest } from "../src/event-manifest"
import { IdeEvent } from "../src/ide-event"
import { SessionEvent } from "../src/session-event"
import { SessionTodo } from "../src/session-todo"
import { SessionV1 } from "../src/session-v1"
import { WorkspaceEvent } from "../src/workspace-event"
describe("public event manifest", () => {
test("owns the complete public event surface", () => {
expect(EventManifest.ServerDefinitions.length).toBe(55)
expect(EventManifest.Definitions.length).toBe(85)
expect(SessionV1.Event.Definitions).toEqual([
SessionV1.Event.Created,
SessionV1.Event.Updated,
SessionV1.Event.Deleted,
SessionV1.Event.MessageUpdated,
SessionV1.Event.MessageRemoved,
SessionV1.Event.PartUpdated,
SessionV1.Event.PartRemoved,
SessionV1.Event.PartDelta,
SessionV1.Event.Diff,
SessionV1.Event.Error,
])
expect(EventManifest.Latest.size).toBe(85)
expect(EventManifest.Durable.size).toBe(32)
})
test("uses canonical definitions for current public events", () => {
expect(Session.Event).toBe(SessionEvent)
expect(Session.Event.Definitions).toBe(SessionEvent.Definitions)
expect(Workspace.Event).toBe(WorkspaceEvent)
expect(Workspace.Event.Definitions).toBe(WorkspaceEvent.Definitions)
expect(EventManifest.Latest.get("session.next.step.ended")).toBe(SessionEvent.Step.Ended)
expect(EventManifest.Latest.get("todo.updated")).toBe(SessionTodo.Event.Updated)
expect(EventManifest.Latest.get("project.updated")).toBe(Project.Event.Updated)
expect(Project.Event.Definitions).toEqual([Project.Event.Updated])
expect(FileSystem.Event.Definitions).toEqual([FileSystem.Event.Edited])
expect(Integration.Event.Definitions).toEqual([Integration.Event.Updated, Integration.Event.ConnectionUpdated])
expect(Permission.Event.Definitions).toEqual([Permission.Event.Asked, Permission.Event.Replied])
expect(Reference.Event.Definitions).toEqual([Reference.Event.Updated])
expect(EventManifest.Latest.has("ide.installed")).toBe(false)
expect(IdeEvent.Definitions).toEqual([IdeEvent.Installed])
expect(EventManifest.Definitions.slice(40, 43)).toEqual([
SessionV1.Event.PartDelta,
SessionV1.Event.Diff,
SessionV1.Event.Error,
])
expect(EventManifest.Durable.has("session.next.step.ended.1")).toBe(false)
expect(EventManifest.Durable.get("session.next.step.ended.2")).toBe(SessionEvent.Step.Ended)
})
})
+37
View File
@@ -0,0 +1,37 @@
import { describe, expect, test } from "bun:test"
import { Schema } from "effect"
import { Event } from "../src/event"
describe("public event schemas", () => {
test("definition is pure", () => {
const definitions = Event.inventory()
Event.define({ type: "test.pure", schema: { value: Schema.String } })
expect(definitions).toEqual([])
})
test("latest selection is independent of declaration order", () => {
const historical = Event.define({
type: "test.versioned",
durable: { aggregate: "id", version: 1 },
schema: { id: Schema.String },
})
const current = Event.define({
type: "test.versioned",
durable: { aggregate: "id", version: 2 },
schema: { id: Schema.String, value: Schema.String },
})
expect(Event.latest([historical, current]).get(current.type)).toBe(current)
expect(Event.latest([current, historical]).get(current.type)).toBe(current)
})
test("durable definitions are indexed by type and version", () => {
const definition = Event.define({
type: "test.durable",
durable: { aggregate: "id", version: 1 },
schema: { id: Schema.String },
})
expect(Event.durable([definition]).get("test.durable.1")).toBe(definition)
})
})
+53
View File
@@ -0,0 +1,53 @@
import { describe, expect, test } from "bun:test"
import { LegacyEvent } from "../src/legacy-event"
import { PermissionV1 } from "../src/permission-v1"
import { QuestionV1 } from "../src/question-v1"
import { Project } from "../src/project"
import { SessionV1 } from "../src/session-v1"
describe("legacy public event schemas", () => {
test("owns all SessionV1 definitions", () => {
expect(SessionV1.Event.Definitions.map((event) => event.type)).toEqual([
"session.created",
"session.updated",
"session.deleted",
"message.updated",
"message.removed",
"message.part.updated",
"message.part.removed",
"message.part.delta",
"session.diff",
"session.error",
])
const durable = SessionV1.Event.Definitions.filter((event) => event.durable !== undefined)
expect(durable).toHaveLength(7)
expect(durable.every((event) => event.durable?.aggregate === "sessionID")).toBe(true)
expect(durable.every((event) => event.durable?.version === 1)).toBe(true)
})
test("owns the legacy transient public definitions", () => {
expect([
SessionV1.PartDelta.type,
SessionV1.Diff.type,
SessionV1.Error.type,
PermissionV1.Event.Asked.type,
PermissionV1.Event.Replied.type,
QuestionV1.Event.Asked.type,
QuestionV1.Event.Replied.type,
QuestionV1.Event.Rejected.type,
Project.Event.Updated.type,
LegacyEvent.CommandExecuted.type,
]).toEqual([
"message.part.delta",
"session.diff",
"session.error",
"permission.asked",
"permission.replied",
"question.asked",
"question.replied",
"question.rejected",
"project.updated",
"command.executed",
])
})
})
+36 -30
View File
@@ -1,4 +1,4 @@
import { HttpApi, OpenApi } from "effect/unstable/httpapi" import { HttpApi, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { SchemaErrorMiddleware } from "./middleware/schema-error" import { SchemaErrorMiddleware } from "./middleware/schema-error"
import { MessageGroup } from "./groups/message" import { MessageGroup } from "./groups/message"
import { ModelGroup } from "./groups/model" import { ModelGroup } from "./groups/model"
@@ -8,7 +8,8 @@ import { PermissionGroup } from "./groups/permission"
import { FileSystemGroup } from "./groups/fs" import { FileSystemGroup } from "./groups/fs"
import { CommandGroup } from "./groups/command" import { CommandGroup } from "./groups/command"
import { SkillGroup } from "./groups/skill" import { SkillGroup } from "./groups/skill"
import { EventGroup } from "./groups/event" import { EventGroup, makeEventGroup } from "./groups/event"
import type { Definition } from "@opencode-ai/core/event"
import { AgentGroup } from "./groups/agent" import { AgentGroup } from "./groups/agent"
import { HealthGroup } from "./groups/health" import { HealthGroup } from "./groups/health"
import { PtyGroup } from "./groups/pty" import { PtyGroup } from "./groups/pty"
@@ -20,31 +21,36 @@ import { IntegrationGroup } from "./groups/integration"
import { CredentialGroup } from "./groups/credential" import { CredentialGroup } from "./groups/credential"
import { ProjectCopyGroup } from "./groups/project-copy" import { ProjectCopyGroup } from "./groups/project-copy"
export const Api = HttpApi.make("server") const makeApiFromGroup = <const Group extends HttpApiGroup.Any>(eventGroup: Group) =>
.add(HealthGroup) HttpApi.make("server")
.add(LocationGroup) .add(HealthGroup)
.add(AgentGroup) .add(LocationGroup)
.add(SessionGroup) .add(AgentGroup)
.add(MessageGroup) .add(SessionGroup)
.add(ModelGroup) .add(MessageGroup)
.add(ProviderGroup) .add(ModelGroup)
.add(IntegrationGroup) .add(ProviderGroup)
.add(CredentialGroup) .add(IntegrationGroup)
.add(PermissionGroup) .add(CredentialGroup)
.add(FileSystemGroup) .add(PermissionGroup)
.add(CommandGroup) .add(FileSystemGroup)
.add(SkillGroup) .add(CommandGroup)
.add(EventGroup) .add(SkillGroup)
.add(PtyGroup) .add(eventGroup)
.add(QuestionGroup) .add(PtyGroup)
.add(ReferenceGroup) .add(QuestionGroup)
.add(ProjectCopyGroup) .add(ReferenceGroup)
.annotateMerge( .add(ProjectCopyGroup)
OpenApi.annotations({ .annotateMerge(
title: "opencode HttpApi", OpenApi.annotations({
version: "0.0.1", title: "opencode HttpApi",
description: "Experimental HttpApi surface for selected instance routes.", version: "0.0.1",
}), description: "Experimental HttpApi surface for selected instance routes.",
) }),
.middleware(Authorization) )
.middleware(SchemaErrorMiddleware) .middleware(Authorization)
.middleware(SchemaErrorMiddleware)
export const makeApi = (definitions: ReadonlyArray<Definition>) => makeApiFromGroup(makeEventGroup(definitions))
export const Api = makeApiFromGroup(EventGroup)
+40 -17
View File
@@ -1,5 +1,7 @@
import { EventV2 } from "@opencode-ai/core/event" import { EventV2 } from "@opencode-ai/core/event"
import { PublicEventManifest } from "@opencode-ai/core/public-event-manifest"
import { Location } from "@opencode-ai/core/location" import { Location } from "@opencode-ai/core/location"
import type { Definition } from "@opencode-ai/core/event"
import { Schema } from "effect" import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
@@ -10,25 +12,47 @@ const fields = {
location: Schema.optional(Location.Ref), location: Schema.optional(Location.Ref),
} }
const Event = Schema.Union([ const schema = <const Definitions extends ReadonlyArray<Definition>>(definitions: Definitions) =>
...EventV2.definitions().map((definition) => Schema.Union([
Schema.Struct({ ...definitions.map((definition) =>
...fields, Schema.Struct({
type: Schema.Literal(definition.type), ...fields,
data: definition.data as Schema.Struct<{}>, type: Schema.Literal(definition.type),
}).annotate({ identifier: `V2Event.${definition.type}` }), data: definition.data,
), }).annotate({ identifier: `V2Event.${definition.type}` }),
Schema.Struct({ ),
...fields, ...(definitions.some((definition) => definition.type === "server.connected")
type: Schema.Literal("server.connected"), ? []
data: Schema.Struct({}), : [
}).annotate({ identifier: "V2Event.server.connected" }), Schema.Struct({
]).annotate({ identifier: "V2Event" }) ...fields,
type: Schema.Literal("server.connected"),
data: Schema.Struct({}),
}).annotate({ identifier: "V2Event.server.connected" }),
]),
]).annotate({ identifier: "V2Event" })
export const makeEventGroup = <const Definitions extends ReadonlyArray<Definition>>(definitions: Definitions) =>
HttpApiGroup.make("server.event")
.add(
HttpApiEndpoint.get("event.subscribe", "/api/event", {
success: schema(definitions),
}).annotateMerge(
OpenApi.annotations({
identifier: "v2.event.subscribe",
summary: "Subscribe to events",
description: "Subscribe to native event payloads for the server.",
}),
),
)
.annotateMerge(OpenApi.annotations({ title: "events", description: "Experimental event stream route." }))
const EventSchema = schema(PublicEventManifest.Definitions)
export const EventGroup = HttpApiGroup.make("server.event") export const EventGroup = HttpApiGroup.make("server.event")
.add( .add(
HttpApiEndpoint.get("event.subscribe", "/api/event", { HttpApiEndpoint.get("event.subscribe", "/api/event", {
success: Event, success: EventSchema,
}).annotateMerge( }).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
identifier: "v2.event.subscribe", identifier: "v2.event.subscribe",
@@ -38,5 +62,4 @@ export const EventGroup = HttpApiGroup.make("server.event")
), ),
) )
.annotateMerge(OpenApi.annotations({ title: "events", description: "Experimental event stream route." })) .annotateMerge(OpenApi.annotations({ title: "events", description: "Experimental event stream route." }))
export type Event = typeof EventSchema.Type
export type Event = typeof Event.Type