From 4dfff5a7d8759375a44646a31edb641c27681955 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Tue, 23 Jun 2026 22:58:05 -0400 Subject: [PATCH 1/7] refactor(schema): extract public event definitions --- packages/core/src/event-manifest.ts | 9 + packages/core/src/event.ts | 106 +-- packages/core/src/public-event-manifest.ts | 47 ++ packages/core/src/session/event.ts | 470 +------------ packages/core/src/session/todo.ts | 24 +- packages/core/src/util/error.ts | 71 +- packages/core/src/v1/permission.ts | 98 +-- packages/core/src/v1/session.ts | 634 +----------------- packages/core/test/event.test.ts | 28 +- packages/opencode/package.json | 1 + packages/opencode/src/event-manifest.ts | 60 ++ packages/opencode/src/event-v2-bridge.ts | 10 +- packages/opencode/src/server/event.ts | 2 + .../src/server/routes/instance/httpapi/api.ts | 11 +- .../routes/instance/httpapi/groups/global.ts | 8 +- .../server/routes/instance/httpapi/server.ts | 5 +- packages/opencode/src/session/todo.ts | 25 +- packages/opencode/test/event-manifest.test.ts | 20 + packages/schema/src/event.ts | 119 ++++ packages/schema/src/index.ts | 4 + packages/schema/src/named-error.ts | 62 ++ packages/schema/src/permission-v1.ts | 96 +++ packages/schema/src/session-event.ts | 529 +++++++++++++++ packages/schema/src/session-v1.ts | 633 +++++++++++++++++ packages/schema/src/session.ts | 3 + packages/schema/src/todo.ts | 22 + packages/schema/test/event.test.ts | 71 ++ packages/sdk/js/src/v2/gen/types.gen.ts | 34 + packages/server/package.json | 1 + packages/server/src/api.ts | 63 +- packages/server/src/groups/event.ts | 65 +- 31 files changed, 1848 insertions(+), 1483 deletions(-) create mode 100644 packages/core/src/event-manifest.ts create mode 100644 packages/core/src/public-event-manifest.ts create mode 100644 packages/opencode/src/event-manifest.ts create mode 100644 packages/opencode/test/event-manifest.test.ts create mode 100644 packages/schema/src/event.ts create mode 100644 packages/schema/src/named-error.ts create mode 100644 packages/schema/src/permission-v1.ts create mode 100644 packages/schema/src/session-event.ts create mode 100644 packages/schema/src/session-v1.ts create mode 100644 packages/schema/src/todo.ts create mode 100644 packages/schema/test/event.test.ts diff --git a/packages/core/src/event-manifest.ts b/packages/core/src/event-manifest.ts new file mode 100644 index 000000000000..af4c8e0f7254 --- /dev/null +++ b/packages/core/src/event-manifest.ts @@ -0,0 +1,9 @@ +export * as EventManifest from "./event-manifest" + +import { Event } from "@opencode-ai/schema/event" +import { SessionEvent } from "@opencode-ai/schema/session-event" +import { SessionV1 } from "@opencode-ai/schema/session-v1" + +export const Definitions = Event.inventory(...SessionV1.Events, ...SessionEvent.Definitions) +export const Latest = Event.latest(Definitions) +export const Durable = Event.durable(Definitions) diff --git a/packages/core/src/event.ts b/packages/core/src/event.ts index 93d5afce53c8..6fd808002ff5 100644 --- a/packages/core/src/event.ts +++ b/packages/core/src/event.ts @@ -1,46 +1,19 @@ export * as EventV2 from "./event" 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 { Database } from "./database/database" import { EventSequenceTable, EventTable } from "./event/sql" import { Location } from "./location" -import { withStatics } from "./schema" -import { Identifier } from "./util/identifier" import { LayerNode } from "./effect/layer-node" import { isDeepStrictEqual } from "node:util" +import { EventManifest } from "./event-manifest" -export const ID = Schema.String.check(Schema.isStartsWith("evt_")).pipe( - Schema.brand("Event.ID"), - withStatics((schema) => ({ - create: () => schema.make("evt_" + Identifier.ascending()), - })), -) -export type ID = typeof ID.Type - -export type Definition = { - readonly type: Type - readonly durable?: { - readonly version: number - readonly aggregate: string - } - readonly data: DataSchema -} - -export type Data = Schema.Schema.Type - -export type Payload = { - readonly id: ID - readonly type: D["type"] - readonly data: Data - readonly durable?: { - readonly aggregateID: string - readonly seq: number - readonly version: number - } - readonly location?: Location.Ref - readonly metadata?: Record -} +export const ID = Event.ID +export type ID = import("@opencode-ai/schema/event").ID +export type { Data, Definition, Payload } from "@opencode-ai/schema/event" export type Subscriber = (event: Payload) => Effect.Effect export type Unsubscribe = Effect.Effect @@ -74,52 +47,8 @@ export class InvalidDurableEventError extends Schema.TaggedErrorClass() -const durableRegistry = new Map() - -export function define(input: { - readonly type: Type - readonly durable?: { - readonly version: number - readonly aggregate: string - } - readonly schema: Fields -}): Schema.Schema>>> & Definition> { - 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>>> & - Definition> -} - -export function definitions() { - return registry.values().toArray() -} +export const define = Event.define +export const versionedType = Event.versionedType export interface PublishOptions { readonly id?: ID @@ -157,18 +86,21 @@ export class Service extends Context.Service()("@opencode/Ev export interface LayerOptions { readonly beforeAggregateRead?: (aggregateID: string) => Effect.Effect + readonly definitions?: ReadonlyArray } export const layerWith = (options?: LayerOptions) => Layer.effect( Service, Effect.gen(function* () { + const durableDefinitions = Event.durable([...EventManifest.Definitions, ...(options?.definitions ?? [])]) const pubsub = { all: yield* PubSub.unbounded(), durable: new Map>>(), typed: new Map>(), } const projectors = new Map() + // Projectors intentionally retain type-only dispatch. Exact type+version dispatch is a separate replay design. const listeners = new Array() const { db } = yield* Database.Service @@ -194,6 +126,7 @@ export const layerWith = (options?: LayerOptions) => ) function commitDurableEvent( + definition: Definition, event: Payload, input?: { readonly seq: number @@ -204,7 +137,6 @@ export const layerWith = (options?: LayerOptions) => commit?: (seq: number) => Effect.Effect, ) { return Effect.gen(function* () { - const definition = registry.get(event.type) const durable = definition?.durable if (durable) { const aggregateID = (event.data as Record)[durable.aggregate] @@ -356,9 +288,8 @@ export const layerWith = (options?: LayerOptions) => }) } - function publishEvent(event: Payload, commit?: PublishOptions["commit"]) { + function publishEvent(definition: D, event: Payload, commit?: PublishOptions["commit"]) { return Effect.gen(function* () { - const definition = registry.get(event.type) if (!definition?.durable && commit) return yield* Effect.die( new InvalidDurableEventError({ @@ -367,7 +298,7 @@ export const layerWith = (options?: LayerOptions) => }), ) if (definition?.durable) { - const committed = yield* commitDurableEvent(event as Payload, undefined, commit) + const committed = yield* commitDurableEvent(definition, event as Payload, undefined, commit) if (committed) { event = { ...event, @@ -416,6 +347,7 @@ export const layerWith = (options?: LayerOptions) => ? { directory: serviceLocation.directory, workspaceID: serviceLocation.workspaceID } : undefined) return yield* publishEvent( + definition, { id: options?.id ?? ID.create(), ...(options?.metadata ? { metadata: options.metadata } : {}), @@ -433,7 +365,7 @@ export const layerWith = (options?: LayerOptions) => options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean }, ) { return Effect.gen(function* () { - const definition = durableRegistry.get(event.type) + const definition = durableDefinitions.get(event.type) if (!definition?.durable) { yield* Effect.die( new InvalidDurableEventError({ type: event.type, message: `Unknown durable event type ${event.type}` }), @@ -446,7 +378,7 @@ export const layerWith = (options?: LayerOptions) => event.data, ), } as Payload - const committed = yield* commitDurableEvent(payload, { + const committed = yield* commitDurableEvent(definition, payload, { seq: event.seq, aggregateID: event.aggregateID, ownerID: options?.ownerID, @@ -530,8 +462,8 @@ export const layerWith = (options?: LayerOptions) => const streamAll = (): Stream.Stream => Stream.fromPubSub(pubsub.all) - const decodeSerializedEvent = (event: SerializedEvent): Payload => { - const definition = durableRegistry.get(event.type) + const decodeSerializedEvent = (event: SerializedEvent) => { + const definition = durableDefinitions.get(event.type) if (!definition?.durable) { throw new InvalidDurableEventError({ type: event.type, message: `Unknown durable event type ${event.type}` }) } diff --git a/packages/core/src/public-event-manifest.ts b/packages/core/src/public-event-manifest.ts new file mode 100644 index 000000000000..4216487bc11c --- /dev/null +++ b/packages/core/src/public-event-manifest.ts @@ -0,0 +1,47 @@ +export * as PublicEventManifest from "./public-event-manifest" + +import { Event } from "@opencode-ai/schema/event" +import { Todo } from "@opencode-ai/schema/todo" +import { Catalog } from "./catalog" +import { FileSystem } from "./filesystem" +import { Watcher } from "./filesystem/watcher" +import { Integration } from "./integration" +import { ModelsDev } from "./models-dev" +import { PermissionV2 } from "./permission" +import { PluginV2 } from "./plugin" +import { ProjectCopy } from "./project/copy" +import { Pty } from "./pty" +import { QuestionV2 } from "./question" +import { Reference } from "./reference" +import { EventManifest } from "./event-manifest" + +export const FoundationDefinitions = Event.inventory( + ModelsDev.Event.Refreshed, + Integration.Event.Updated, + Integration.Event.ConnectionUpdated, + Catalog.Event.Updated, + ...EventManifest.Definitions, +) + +export const FeatureDefinitions = Event.inventory( + FileSystem.Event.Edited, + Reference.Event.Updated, + PermissionV2.Event.Asked, + PermissionV2.Event.Replied, + PluginV2.Event.Added, + ProjectCopy.Event.Updated, + Watcher.Event.Updated, + Pty.Event.Created, + Pty.Event.Updated, + Pty.Event.Exited, + Pty.Event.Deleted, + QuestionV2.Event.Asked, + QuestionV2.Event.Replied, + QuestionV2.Event.Rejected, + Todo.Event.Updated, +) + +export const Definitions = Event.inventory(...FoundationDefinitions, ...FeatureDefinitions) + +export const Latest = Event.latest(Definitions) +export const Durable = Event.durable(Definitions) diff --git a/packages/core/src/session/event.ts b/packages/core/src/session/event.ts index 25cf147f80bd..6d1b430357b6 100644 --- a/packages/core/src/session/event.ts +++ b/packages/core/src/session/event.ts @@ -1,468 +1,2 @@ -import { Schema } from "effect" -import { ProviderMetadata, ToolContent } from "@opencode-ai/schema/llm" -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" +export * from "@opencode-ai/schema/session-event" +export { SessionEvent } from "@opencode-ai/schema/session-event" diff --git a/packages/core/src/session/todo.ts b/packages/core/src/session/todo.ts index 7b3c3be3f69b..7f0411028469 100644 --- a/packages/core/src/session/todo.ts +++ b/packages/core/src/session/todo.ts @@ -1,30 +1,16 @@ export * as SessionTodo from "./todo" import { asc, eq } from "drizzle-orm" -import { Context, Effect, Layer, Schema } from "effect" +import { Context, Effect, Layer } from "effect" +import { Todo } from "@opencode-ai/schema/todo" import { Database } from "../database/database" import { EventV2 } from "../event" import { SessionSchema } from "./schema" import { TodoTable } from "./sql" -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: "SessionTodo.Info" }) -export type Info = typeof Info.Type - -export const Event = { - Updated: EventV2.define({ - type: "todo.updated", - schema: { - sessionID: SessionSchema.ID, - todos: Schema.Array(Info), - }, - }), -} +export const Info = Todo.Info +export type Info = Todo.Info +export const Event = Todo.Event export interface Interface { readonly update: (input: { diff --git a/packages/core/src/util/error.ts b/packages/core/src/util/error.ts index 5fe41e1cff99..779ffd2853ba 100644 --- a/packages/core/src/util/error.ts +++ b/packages/core/src/util/error.ts @@ -1,70 +1 @@ -import { Schema } from "effect" - -export abstract class NamedError extends Error { - abstract schema(): Schema.Top - abstract toObject(): { name: string; data: unknown } - - static hasName(error: unknown, name: string): boolean { - return ( - typeof error === "object" && error !== null && "name" in error && (error as Record).name === name - ) - } - - static create( - name: Name, - fields: Fields, - ): ReturnType>> - static create( - name: Name, - data: DataSchema, - ): ReturnType> - static create(name: Name, data: Schema.Top | Schema.Struct.Fields) { - return NamedError.createSchemaClass(name, Schema.isSchema(data) ? data : Schema.Struct(data)) - } - - private static createSchemaClass(name: Name, data: DataSchema) { - const schema = Schema.Struct({ - name: Schema.Literal(name), - data, - }).annotate({ identifier: name }) - type Data = Schema.Schema.Type - - const result = class extends NamedError { - public static readonly Schema = schema - public static readonly EffectSchema = schema - public static readonly tag = name - - public override readonly name = name - - constructor( - public readonly data: Data, - options?: ErrorOptions, - ) { - super(name, options) - this.name = name - } - - static isInstance(input: unknown): input is InstanceType { - return NamedError.hasName(input, name) - } - - schema() { - return schema - } - - toObject() { - return { - name: name, - data: this.data, - } - } - } - Object.defineProperty(result, "name", { value: name }) - return result - } - - public static readonly Unknown = NamedError.create("UnknownError", { - message: Schema.String, - ref: Schema.optional(Schema.String), - }) -} +export { NamedError } from "@opencode-ai/schema/named-error" diff --git a/packages/core/src/v1/permission.ts b/packages/core/src/v1/permission.ts index b241ccd9077b..78ba6d30afa7 100644 --- a/packages/core/src/v1/permission.ts +++ b/packages/core/src/v1/permission.ts @@ -1,96 +1,2 @@ -export * as PermissionV1 from "./permission" - -import { Schema } from "effect" -import { ProjectV2 } from "../project" -import { withStatics } from "../schema" -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()("PermissionRejectedError", {}) { - override get message() { - return "The user rejected permission to use this specific tool call." - } -} - -export class CorrectedError extends Schema.TaggedErrorClass()("PermissionCorrectedError", { - feedback: Schema.String, -}) { - override get message() { - return `The user rejected permission to use this specific tool call with the following feedback: ${this.feedback}` - } -} - -export class DeniedError extends Schema.TaggedErrorClass()("PermissionDeniedError", { - ruleset: Schema.Any, -}) { - override get message() { - return `The user has specified a rule which prevents you from using this specific tool call. Here are some of the relevant rules ${JSON.stringify(this.ruleset)}` - } -} - -export class NotFoundError extends Schema.TaggedErrorClass()("Permission.NotFoundError", { - requestID: ID, -}) {} - -export type Error = DeniedError | RejectedError | CorrectedError +export * from "@opencode-ai/schema/permission-v1" +export { PermissionV1 } from "@opencode-ai/schema/permission-v1" diff --git a/packages/core/src/v1/session.ts b/packages/core/src/v1/session.ts index 181ba9807d05..e01d3e8f81c2 100644 --- a/packages/core/src/v1/session.ts +++ b/packages/core/src/v1/session.ts @@ -1,632 +1,2 @@ -export * as SessionV1 from "./session" - -import { Effect, Schema, Types } 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 { NamedError } from "../util/error" -import { SessionSchema } from "../session/schema" -import { WorkspaceV2 } from "../workspace" - -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_" + Identifier.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_" + Identifier.ascending()) })), -) -export type PartID = typeof PartID.Type - -export const OutputLengthError = NamedError.create("MessageOutputLengthError", {}) - -export const AuthError = NamedError.create("ProviderAuthError", { - providerID: Schema.String, - message: Schema.String, -}) - -export const AbortedError = NamedError.create("MessageAbortedError", { message: Schema.String }) -export const StructuredOutputError = NamedError.create("StructuredOutputError", { - message: Schema.String, - retries: NonNegativeInt, -}) -export const APIError = NamedError.create("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 -export const ContextOverflowError = NamedError.create("ContextOverflowError", { - message: Schema.String, - responseBody: Schema.optional(Schema.String), -}) -export const ContentFilterError = NamedError.create("ContentFilterError", { - message: Schema.String, -}) - -export class OutputFormatText extends Schema.Class("OutputFormatText")({ - type: Schema.Literal("text"), -}) {} - -export class OutputFormatJsonSchema extends Schema.Class("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 - -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> - -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> - -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> - -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> - -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> - -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> - -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> - -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> - -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>, "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> - -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> - -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> - -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> - -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> - -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> - -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>, "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> - -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 - -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> - -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> - -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> - -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> - -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>, "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, - }, - }), -} +export * from "@opencode-ai/schema/session-v1" +export { SessionV1 } from "@opencode-ai/schema/session-v1" diff --git a/packages/core/test/event.test.ts b/packages/core/test/event.test.ts index bd2f2eee980c..dad0e8d576a6 100644 --- a/packages/core/test/event.test.ts +++ b/packages/core/test/event.test.ts @@ -1,6 +1,7 @@ import { describe, expect } from "bun:test" import { Cause, DateTime, Deferred, Effect, Exit, Fiber, Layer, Schema, Stream } from "effect" import { EventV2 } from "@opencode-ai/core/event" +import { Event } from "@opencode-ai/schema/event" import { Database } from "@opencode-ai/core/database/database" import { EventSequenceTable, EventTable } from "@opencode-ai/core/event/sql" import { Location } from "@opencode-ai/core/location" @@ -16,10 +17,6 @@ const locationLayer = Layer.succeed( 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({ type: "test.message", schema: { @@ -82,6 +79,15 @@ const SyncTimestamp = EventV2.define({ }, }) +const eventLayer = Layer.mergeAll( + EventV2.layerWith({ + definitions: [Message, SyncMessage, SyncSent, GlobalMessage, VersionedMessage, SyncTimestamp], + }).pipe(Layer.provide(Database.defaultLayer)), + Database.defaultLayer, +) +const it = testEffect(eventLayer.pipe(Layer.provideMerge(locationLayer))) +const itWithoutLocation = testEffect(eventLayer) + describe("EventV2", () => { it.effect("publishes events with the current location", () => Effect.gen(function* () { @@ -122,26 +128,21 @@ describe("EventV2", () => { }), ) - it.effect("stores definitions in the exported registry", () => - Effect.sync(() => { - expect(EventV2.registry.get(Message.type)).toBe(Message) - }), - ) - - it.effect("keeps the latest sync definition in the registry", () => + it.effect("selects the latest durable definition independent of declaration order", () => Effect.sync(() => { const latest = EventV2.define({ type: "test.out-of-order", durable: { version: 2, aggregate: "id" }, schema: { id: Schema.String }, }) - EventV2.define({ + const historical = EventV2.define({ type: "test.out-of-order", durable: { version: 1, aggregate: "id" }, 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) }), ) @@ -407,6 +408,7 @@ describe("EventV2", () => { const continueRead = yield* Deferred.make() let pause = true const eventLayer = EventV2.layerWith({ + definitions: [SyncMessage], beforeAggregateRead: () => pause ? Deferred.succeed(readStarted, undefined).pipe(Effect.andThen(Deferred.await(continueRead))) diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 139217e348a4..76e9ce5577e2 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -34,6 +34,7 @@ "@opencode-ai/core": "workspace:*", "@opencode-ai/http-recorder": "workspace:*", "@opencode-ai/script": "workspace:*", + "@opencode-ai/schema": "workspace:*", "@standard-schema/spec": "1.0.0", "@tsconfig/bun": "catalog:", "@types/babel__core": "7.20.5", diff --git a/packages/opencode/src/event-manifest.ts b/packages/opencode/src/event-manifest.ts new file mode 100644 index 000000000000..a6795f435b41 --- /dev/null +++ b/packages/opencode/src/event-manifest.ts @@ -0,0 +1,60 @@ +export * as EventManifest from "./event-manifest" + +import { Event } from "@opencode-ai/schema/event" +import { PublicEventManifest } from "@opencode-ai/core/public-event-manifest" +import { Command } from "@/command" +import { Workspace } from "@/control-plane/workspace" +import { Ide } from "@/ide" +import { Installation } from "@/installation" +import { LSP } from "@/lsp/lsp" +import { MCP } from "@/mcp" +import { Permission } from "@/permission" +import { Project } from "@/project/project" +import { Vcs } from "@/project/vcs" +import { Question } from "@/question" +import { ServerEvent } from "@/server/event" +import { TuiEvent } from "@/server/tui-event" +import { MessageV2 } from "@/session/message-v2" +import { Session } from "@/session/session" +import { SessionCompaction } from "@/session/compaction" +import { SessionStatus } from "@/session/status" +import { Worktree } from "@/worktree" + +export const Definitions = Event.inventory( + ...PublicEventManifest.FoundationDefinitions, + MessageV2.Event.PartDelta, + Session.Event.Diff, + Session.Event.Error, + Installation.Event.Updated, + Installation.Event.UpdateAvailable, + ...PublicEventManifest.FeatureDefinitions, + LSP.Event.Updated, + Permission.Event.Asked, + Permission.Event.Replied, + TuiEvent.PromptAppend, + TuiEvent.CommandExecute, + TuiEvent.ToastShow, + TuiEvent.SessionSelect, + MCP.ToolsChanged, + MCP.BrowserOpenFailed, + Command.Event.Executed, + Project.Event.Updated, + SessionStatus.Event.Status, + SessionStatus.Event.Idle, + Question.Event.Asked, + Question.Event.Replied, + Question.Event.Rejected, + SessionCompaction.Event.Compacted, + Vcs.Event.BranchUpdated, + Workspace.Event.Ready, + Workspace.Event.Failed, + Workspace.Event.Status, + Worktree.Event.Ready, + Worktree.Event.Failed, + Ide.Event.Installed, + ServerEvent.Event.Connected, + ServerEvent.Event.Disposed, +) + +export const Latest = Event.latest(Definitions) +export const Durable = Event.durable(Definitions) diff --git a/packages/opencode/src/event-v2-bridge.ts b/packages/opencode/src/event-v2-bridge.ts index 4cef7311dcbe..836f408e0f48 100644 --- a/packages/opencode/src/event-v2-bridge.ts +++ b/packages/opencode/src/event-v2-bridge.ts @@ -7,9 +7,6 @@ import { EventV2 } from "@opencode-ai/core/event" import { Location } from "@opencode-ai/core/location" import { Project } from "@opencode-ai/core/project" 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" export class Service extends Context.Service()("@opencode/EventV2Bridge") {} @@ -45,10 +42,7 @@ export const layer = Layer.effect( workspace: workspaceID, payload: { id: event.id, type: event.type, properties: event.data }, }) - const durable = EventV2.registry.get(event.type)?.durable - if (durable === undefined || event.durable === undefined) return - const aggregateID = (event.data as Record)[durable.aggregate] - if (typeof aggregateID !== "string") return + if (event.durable === undefined) return GlobalBus.emit("event", { directory: event.location?.directory ?? ctx?.directory, project: ctx?.project.id, @@ -59,7 +53,7 @@ export const layer = Layer.effect( id: event.id, type: EventV2.versionedType(event.type, event.durable.version), seq: event.durable.seq, - aggregateID, + aggregateID: event.durable.aggregateID, data: event.data, }, }, diff --git a/packages/opencode/src/server/event.ts b/packages/opencode/src/server/event.ts index a58131255587..aacd4167e1fc 100644 --- a/packages/opencode/src/server/event.ts +++ b/packages/opencode/src/server/event.ts @@ -11,3 +11,5 @@ export const InstanceDisposed = Schema.Struct({ type: Schema.Literal("server.instance.disposed"), properties: Schema.Struct({ directory: Schema.String }), }).annotate({ identifier: "Event.server.instance.disposed" }) + +export * as ServerEvent from "./event" diff --git a/packages/opencode/src/server/routes/instance/httpapi/api.ts b/packages/opencode/src/server/routes/instance/httpapi/api.ts index 63cc225ebe9c..7cf59ec2371e 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/api.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/api.ts @@ -1,6 +1,7 @@ import { Schema } from "effect" import { HttpApi } from "effect/unstable/httpapi" import { EventV2 } from "@opencode-ai/core/event" +import { EventManifest } from "@/event-manifest" import { Credential } from "@opencode-ai/core/credential" import { Integration } from "@opencode-ai/core/integration" import { SkillV2 } from "@opencode-ai/core/skill" @@ -24,15 +25,13 @@ import { SessionApi } from "./groups/session" import { SyncApi } from "./groups/sync" import { TuiApi } from "./groups/tui" import { WorkspaceApi } from "./groups/workspace" -import { Api } from "@opencode-ai/server/api" -// GlobalEventSchema snapshots the registry after event-producing groups register their variants. +import { makeApi } from "@opencode-ai/server/api" import { GlobalApi } from "./groups/global" import { Authorization } from "./middleware/authorization" import { SchemaErrorMiddleware } from "./middleware/schema-error" const EventSchema = Schema.Union([ - ...EventV2.registry - .values() + ...EventManifest.Latest.values() .map((definition) => Schema.Struct({ id: EventV2.ID, @@ -44,6 +43,8 @@ const EventSchema = Schema.Union([ InstanceDisposed, ]).annotate({ identifier: "Event" }) +export const ServerApi = makeApi(EventManifest.Latest.values().toArray()) + export const RootHttpApi = HttpApi.make("opencode-root") .addHttpApi(ControlApi) .addHttpApi(ControlPlaneApi) @@ -73,7 +74,7 @@ export const OpenCodeHttpApi = HttpApi.make("opencode") .addHttpApi(RootHttpApi) .addHttpApi(EventApi) .addHttpApi(InstanceHttpApi) - .addHttpApi(Api) + .addHttpApi(ServerApi) .addHttpApi(PtyConnectApi) .annotate(HttpApi.AdditionalSchemas, [ EventSchema, diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/global.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/global.ts index 3e13154867df..61daefe8a2d4 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/global.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/global.ts @@ -1,6 +1,6 @@ -import { Config } from "@/config/config" import { ConfigV1 } from "@opencode-ai/core/v1/config/config" import { EventV2 } from "@opencode-ai/core/event" +import { EventManifest } from "@/event-manifest" import { InstanceDisposed } from "@/server/event" import "@opencode-ai/core/account" import "@/server/event" @@ -13,8 +13,7 @@ const GlobalHealth = Schema.Struct({ version: Schema.String, }) -const SyncEventSchemas = EventV2.registry - .values() +const SyncEventSchemas = EventManifest.Latest.values() .flatMap((definition) => { if (!definition.durable) return [] return [ @@ -38,8 +37,7 @@ const GlobalEventSchema = Schema.Struct({ project: Schema.optional(Schema.String), workspace: Schema.optional(Schema.String), payload: Schema.Union([ - ...EventV2.registry - .values() + ...EventManifest.Latest.values() .map((definition) => Schema.Struct({ id: EventV2.ID, type: Schema.Literal(definition.type), properties: definition.data }), ) diff --git a/packages/opencode/src/server/routes/instance/httpapi/server.ts b/packages/opencode/src/server/routes/instance/httpapi/server.ts index b74df8deb83a..e1dbe579fbf2 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/server.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/server.ts @@ -65,8 +65,7 @@ import { lazy } from "@/util/lazy" import { CorsConfig, isAllowedCorsOrigin, type CorsOptions } from "@opencode-ai/server/cors" import { serveUIEffect } from "@/server/shared/ui" import { ServerAuth } from "@/server/auth" -import { InstanceHttpApi, RootHttpApi } from "./api" -import { Api } from "@opencode-ai/server/api" +import { InstanceHttpApi, RootHttpApi, ServerApi } from "./api" import { PublicApi } from "./public" import { authorizationLayer, @@ -165,7 +164,7 @@ const instanceApiRoutes = HttpApiBuilder.layer(InstanceHttpApi).pipe( const instanceRoutes = instanceApiRoutes.pipe( Layer.provide([httpApiAuthLayer, workspaceRoutingLive, instanceContextLayer, schemaErrorLayer]), ) -const serverRoutes = HttpApiBuilder.layer(Api).pipe( +const serverRoutes = HttpApiBuilder.layer(ServerApi).pipe( Layer.provide(handlers), Layer.provide(PluginPtyEnvironment.layer), Layer.provide([serverHttpApiAuthLayer, v2SchemaErrorLayer]), diff --git a/packages/opencode/src/session/todo.ts b/packages/opencode/src/session/todo.ts index 6e9eeba62b80..f953d54b92bf 100644 --- a/packages/opencode/src/session/todo.ts +++ b/packages/opencode/src/session/todo.ts @@ -1,31 +1,16 @@ import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { SessionID } from "./schema" -import { Effect, Layer, Context, Schema } from "effect" +import { Effect, Layer, Context } from "effect" +import { Todo as TodoSchema } from "@opencode-ai/schema/todo" import { Database } from "@opencode-ai/core/database/database" import { eq } from "drizzle-orm" import { asc } from "drizzle-orm" import { TodoTable } from "@opencode-ai/core/session/sql" import { EventV2Bridge } from "@/event-v2-bridge" -import { EventV2 } from "@opencode-ai/core/event" -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 = Schema.Schema.Type - -export const Event = { - Updated: EventV2.define({ - type: "todo.updated", - schema: { - sessionID: SessionID, - todos: Schema.Array(Info), - }, - }), -} +export const Info = TodoSchema.Info +export type Info = TodoSchema.Info +export const Event = TodoSchema.Event export interface Interface { readonly update: (input: { sessionID: SessionID; todos: Info[] }) => Effect.Effect diff --git a/packages/opencode/test/event-manifest.test.ts b/packages/opencode/test/event-manifest.test.ts new file mode 100644 index 000000000000..8465c5fdc49e --- /dev/null +++ b/packages/opencode/test/event-manifest.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, test } from "bun:test" +import { SessionEvent } from "@opencode-ai/schema/session-event" +import { EventManifest } from "@/event-manifest" + +describe("public event manifest", () => { + test("contains every latest public wire type once", () => { + expect(EventManifest.Latest.size).toBe(86) + expect(EventManifest.Latest.get("session.next.step.ended")).toBe(SessionEvent.Step.Ended) + expect(EventManifest.Latest.has("ide.installed")).toBe(true) + expect(EventManifest.Latest.has("server.connected")).toBe(true) + expect(EventManifest.Latest.has("global.disposed")).toBe(true) + }) + + test("keeps historical durable versions out of the latest manifest", () => { + expect(EventManifest.Latest.values().toArray()).not.toContain(SessionEvent.Step.EndedV1) + expect(EventManifest.Latest.values().toArray()).not.toContain(SessionEvent.Step.FailedV1) + expect(EventManifest.Durable.get("session.next.step.ended.1")).toBe(SessionEvent.Step.EndedV1) + expect(EventManifest.Durable.get("session.next.step.ended.2")).toBe(SessionEvent.Step.Ended) + }) +}) diff --git a/packages/schema/src/event.ts b/packages/schema/src/event.ts new file mode 100644 index 000000000000..1e341218c811 --- /dev/null +++ b/packages/schema/src/event.ts @@ -0,0 +1,119 @@ +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 = Schema.Top & { + readonly type: Type + readonly durable?: { + readonly version: number + readonly aggregate: string + } + readonly data: DataSchema +} + +export type Data = Schema.Schema.Type + +export type Payload = { + readonly id: ID + readonly type: D["type"] + readonly data: Data + readonly durable?: { + readonly aggregateID: string + readonly seq: number + readonly version: number + } + readonly location?: Location.Ref + readonly metadata?: Record +} + +export function define(input: { + readonly type: Type + readonly durable?: { + readonly version: number + readonly aggregate: string + } + readonly schema: Fields +}): Schema.Schema>>> & Definition> { + 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>>> & Definition> +} + +export function inventory>(...definitions: Definitions) { + return Object.freeze(definitions) +} + +export function latest(definitions: ReadonlyArray) { + 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()), + ) +} + +export function versionedType(type: string, version: number) { + return `${type}.${version}` +} + +export function durable(definitions: ReadonlyArray) { + 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()), + ) +} + +function readonlyMap(map: Map): ReadonlyMap { + const result: ReadonlyMap = Object.freeze({ + get size() { + return map.size + }, + entries: () => map.entries(), + forEach: (callback: (value: Value, key: Key, map: ReadonlyMap) => 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 +} diff --git a/packages/schema/src/index.ts b/packages/schema/src/index.ts index e68140d9eead..4f05c9b41ac9 100644 --- a/packages/schema/src/index.ts +++ b/packages/schema/src/index.ts @@ -2,19 +2,23 @@ export { Agent } from "./agent" export { Command } from "./command" export { Connection } from "./connection" export { Credential } from "./credential" +export { Event } from "./event" export { FileSystem } from "./filesystem" export { Integration } from "./integration" export { LLM } from "./llm" export { Location } from "./location" export { Model } from "./model" export { Permission } from "./permission" +export { PermissionV1 } from "./permission-v1" export { Project } from "./project" export { Provider } from "./provider" export { Reference } from "./reference" export { Session } from "./session" +export { SessionV1 } from "./session-v1" export { SessionInput } from "./session-input" export { SessionMessage } from "./session-message" export { Skill } from "./skill" +export { Todo } from "./todo" export { Workspace } from "./workspace" export { Prompt, Source, FileAttachment, AgentAttachment } from "./prompt" export * from "./schema" diff --git a/packages/schema/src/named-error.ts b/packages/schema/src/named-error.ts new file mode 100644 index 000000000000..95043d3c8f2a --- /dev/null +++ b/packages/schema/src/named-error.ts @@ -0,0 +1,62 @@ +import { Schema } from "effect" + +export abstract class NamedError extends Error { + abstract schema(): Schema.Top + abstract toObject(): { name: string; data: unknown } + + static hasName(error: unknown, name: string): boolean { + return ( + typeof error === "object" && error !== null && "name" in error && (error as Record).name === name + ) + } + + static create( + name: Name, + fields: Fields, + ): ReturnType>> + static create( + name: Name, + data: DataSchema, + ): ReturnType> + static create(name: Name, data: Schema.Top | Schema.Struct.Fields) { + return NamedError.createSchemaClass(name, Schema.isSchema(data) ? data : Schema.Struct(data)) + } + + private static createSchemaClass(name: Name, data: DataSchema) { + const schema = Schema.Struct({ name: Schema.Literal(name), data }).annotate({ identifier: name }) + type Data = Schema.Schema.Type + const result = class extends NamedError { + public static readonly Schema = schema + public static readonly EffectSchema = schema + public static readonly tag = name + public override readonly name = name + + constructor( + public readonly data: Data, + options?: ErrorOptions, + ) { + super(name, options) + this.name = name + } + + static isInstance(input: unknown): input is InstanceType { + return NamedError.hasName(input, name) + } + + schema() { + return schema + } + + toObject() { + return { name, data: this.data } + } + } + Object.defineProperty(result, "name", { value: name }) + return result + } + + public static readonly Unknown = NamedError.create("UnknownError", { + message: Schema.String, + ref: Schema.optional(Schema.String), + }) +} diff --git a/packages/schema/src/permission-v1.ts b/packages/schema/src/permission-v1.ts new file mode 100644 index 000000000000..1294101fd8be --- /dev/null +++ b/packages/schema/src/permission-v1.ts @@ -0,0 +1,96 @@ +export * as PermissionV1 from "./permission-v1" + +import { Schema } from "effect" +import { Project } from "./project" +import { withStatics } from "./schema" +import { SessionID } from "./session-id" +import { ascending } from "./identifier" + +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.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: Project.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()("PermissionRejectedError", {}) { + override get message() { + return "The user rejected permission to use this specific tool call." + } +} + +export class CorrectedError extends Schema.TaggedErrorClass()("PermissionCorrectedError", { + feedback: Schema.String, +}) { + override get message() { + return `The user rejected permission to use this specific tool call with the following feedback: ${this.feedback}` + } +} + +export class DeniedError extends Schema.TaggedErrorClass()("PermissionDeniedError", { + ruleset: Schema.Any, +}) { + override get message() { + return `The user has specified a rule which prevents you from using this specific tool call. Here are some of the relevant rules ${JSON.stringify(this.ruleset)}` + } +} + +export class NotFoundError extends Schema.TaggedErrorClass()("Permission.NotFoundError", { + requestID: ID, +}) {} + +export type Error = DeniedError | RejectedError | CorrectedError diff --git a/packages/schema/src/session-event.ts b/packages/schema/src/session-event.ts new file mode 100644 index 000000000000..c899a66f610e --- /dev/null +++ b/packages/schema/src/session-event.ts @@ -0,0 +1,529 @@ +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.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 = 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 EndedV1 = Event.define({ + type: "session.next.step.ended", + ...options, + schema: { + ...Base, + 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 EndedV1 = typeof EndedV1.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 FailedV1 = Event.define({ + type: "session.next.step.failed", + ...options, + schema: { + ...Base, + error: UnknownError, + }, + }) + export type FailedV1 = typeof FailedV1.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 CurrentDefinitions = 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 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 HistoricalDefinitions = Event.inventory(Step.EndedV1, Step.FailedV1) +export const Definitions = Event.inventory(...CurrentDefinitions, ...HistoricalDefinitions) + +export const Durable = Schema.Union(DurableDefinitions, { mode: "oneOf" }).pipe(Schema.toTaggedUnion("type")) +export type DurableEvent = typeof Durable.Type + +export const All = Schema.Union(CurrentDefinitions, { mode: "oneOf" }).pipe(Schema.toTaggedUnion("type")) +export type Event = typeof All.Type +export type Type = Event["type"] + +export * as SessionEvent from "./session-event" diff --git a/packages/schema/src/session-v1.ts b/packages/schema/src/session-v1.ts new file mode 100644 index 000000000000..aca58eb30865 --- /dev/null +++ b/packages/schema/src/session-v1.ts @@ -0,0 +1,633 @@ +export * as SessionV1 from "./session-v1" + +import { Effect, Schema, Types } from "effect" +import { define } from "./event" +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 { NamedError } from "./named-error" +import { SessionID } from "./session-id" +import { Workspace } from "./workspace" + +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 + +export const OutputLengthError = NamedError.create("MessageOutputLengthError", {}) + +export const AuthError = NamedError.create("ProviderAuthError", { + providerID: Schema.String, + message: Schema.String, +}) + +export const AbortedError = NamedError.create("MessageAbortedError", { message: Schema.String }) +export const StructuredOutputError = NamedError.create("StructuredOutputError", { + message: Schema.String, + retries: NonNegativeInt, +}) +export const APIError = NamedError.create("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 +export const ContextOverflowError = NamedError.create("ContextOverflowError", { + message: Schema.String, + responseBody: Schema.optional(Schema.String), +}) +export const ContentFilterError = NamedError.create("ContentFilterError", { + message: Schema.String, +}) + +export class OutputFormatText extends Schema.Class("OutputFormatText")({ + type: Schema.Literal("text"), +}) {} + +export class OutputFormatJsonSchema extends Schema.Class("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 + +const partBase = { + id: PartID, + sessionID: SessionID.ID, + messageID: MessageID, +} + +export const SnapshotPart = Schema.Struct({ + ...partBase, + type: Schema.Literal("snapshot"), + snapshot: Schema.String, +}).annotate({ identifier: "SnapshotPart" }) +export type SnapshotPart = Types.DeepMutable> + +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> + +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> + +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> + +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> + +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> + +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> + +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> + +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>, "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> + +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> + +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> + +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> + +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> + +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> + +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>, "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: 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> + +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 + +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> + +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> + +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> + +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> + +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>, "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: Model.ID, + providerID: Provider.ID, + variant: optionalOmitUndefined(Schema.String), +}) + +export const SessionInfo = Schema.Struct({ + id: SessionID.ID, + slug: Schema.String, + projectID: Project.ID, + workspaceID: optionalOmitUndefined(Workspace.ID), + directory: Schema.String, + path: optionalOmitUndefined(Schema.String), + parentID: optionalOmitUndefined(SessionID.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: define({ + type: "session.created", + ...options, + schema: { + sessionID: SessionID.ID, + info: SessionInfo, + }, + }), + Updated: define({ + type: "session.updated", + ...options, + schema: { + sessionID: SessionID.ID, + info: SessionInfo, + }, + }), + Deleted: define({ + type: "session.deleted", + ...options, + schema: { + sessionID: SessionID.ID, + info: SessionInfo, + }, + }), + MessageUpdated: define({ + type: "message.updated", + ...options, + schema: { + sessionID: SessionID.ID, + info: Info, + }, + }), + MessageRemoved: define({ + type: "message.removed", + ...options, + schema: { + sessionID: SessionID.ID, + messageID: MessageID, + }, + }), + PartUpdated: define({ + type: "message.part.updated", + ...options, + schema: { + sessionID: SessionID.ID, + part: Part, + time: Schema.Finite, + }, + }), + PartRemoved: define({ + type: "message.part.removed", + ...options, + schema: { + sessionID: SessionID.ID, + messageID: MessageID, + partID: PartID, + }, + }), +} + +export const Events = Object.freeze(Object.values(Event)) diff --git a/packages/schema/src/session.ts b/packages/schema/src/session.ts index 7e0e16c75506..2bae18972ad7 100644 --- a/packages/schema/src/session.ts +++ b/packages/schema/src/session.ts @@ -7,6 +7,7 @@ import { Model } from "./model" import { Project } from "./project" import { DateTimeUtcFromMillis, optionalOmitUndefined, RelativePath } from "./schema" import { SessionID } from "./session-id" +import { SessionEvent } from "./session-event" export const ID = SessionID.ID export type ID = SessionID.ID @@ -44,3 +45,5 @@ export const ListAnchor = Schema.Struct({ direction: Schema.Literals(["previous", "next"]), }) export type ListAnchor = typeof ListAnchor.Type + +export const Event = SessionEvent diff --git a/packages/schema/src/todo.ts b/packages/schema/src/todo.ts new file mode 100644 index 000000000000..84bf1a2d7aa8 --- /dev/null +++ b/packages/schema/src/todo.ts @@ -0,0 +1,22 @@ +export * as Todo from "./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 Event = { + Updated: define({ + type: "todo.updated", + schema: { sessionID: SessionID.ID, todos: Schema.Array(Info) }, + }), +} +export const Events = inventory(Event.Updated) diff --git a/packages/schema/test/event.test.ts b/packages/schema/test/event.test.ts new file mode 100644 index 000000000000..a2af4cce9a81 --- /dev/null +++ b/packages/schema/test/event.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, test } from "bun:test" +import { Schema } from "effect" +import { Event } from "../src/event" +import { SessionEvent } from "../src/session-event" +import { SessionV1 } from "../src/session-v1" +import { Todo } from "../src/todo" + +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("indexes every durable session type and version", () => { + const durable = Event.durable([...SessionV1.Events, ...SessionEvent.Definitions]) + expect(durable.size).toBe( + SessionV1.Events.length + SessionEvent.DurableDefinitions.length + SessionEvent.HistoricalDefinitions.length, + ) + for (const definition of [ + ...SessionV1.Events, + ...SessionEvent.DurableDefinitions, + ...SessionEvent.HistoricalDefinitions, + ]) { + expect(durable.get(Event.versionedType(definition.type, definition.durable!.version))).toBe(definition) + } + }) + + test("latest aggregate excludes historical versions", () => { + const latest = Event.latest(SessionEvent.Definitions) + expect(latest.get(SessionEvent.Step.Ended.type)).toBe(SessionEvent.Step.Ended) + expect(latest.get(SessionEvent.Step.Failed.type)).toBe(SessionEvent.Step.Failed) + expect(latest.values().toArray()).not.toContain(SessionEvent.Step.EndedV1) + expect(latest.values().toArray()).not.toContain(SessionEvent.Step.FailedV1) + }) + + test("historical and current step shapes decode incompatibly", () => { + const historical = { + timestamp: 0, + sessionID: "ses_test", + finish: "stop", + cost: 0, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + } + expect(Schema.decodeUnknownSync(SessionEvent.Step.EndedV1.data)(historical)).toBeDefined() + expect(() => Schema.decodeUnknownSync(SessionEvent.Step.Ended.data)(historical)).toThrow() + }) + + test("domain inventories are explicit and complete", () => { + expect(SessionEvent.Definitions.length).toBe(31) + expect(Todo.Events).toEqual([Todo.Event.Updated]) + expect(Object.isFrozen(SessionEvent.Definitions)).toBe(true) + expect(Object.isFrozen(Todo.Events)).toBe(true) + }) +}) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 3116c295f125..b48f0d4d28bc 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -88,6 +88,7 @@ export type Event = | EventWorkspaceStatus | EventWorktreeReady | EventWorktreeFailed + | EventIdeInstalled | EventServerConnected | EventGlobalDisposed | EventServerInstanceDisposed @@ -1597,6 +1598,13 @@ export type GlobalEvent = { message: string } } + | { + id: string + type: "ide.installed" + properties: { + ide: string + } + } | { id: string type: "server.connected" @@ -2833,6 +2841,7 @@ export type V2Event = | V2EventWorkspaceStatus | V2EventWorktreeReady | V2EventWorktreeFailed + | V2EventIdeInstalled | V2EventServerConnected | V2EventGlobalDisposed @@ -5902,6 +5911,23 @@ export type V2EventWorktreeFailed = { } } +export type V2EventIdeInstalled = { + id: string + metadata?: { + [key: string]: unknown + } + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + type: "ide.installed" + data: { + ide: string + } +} + export type V2EventServerConnected = { id: string metadata?: { @@ -6876,6 +6902,14 @@ export type EventWorktreeFailed = { } } +export type EventIdeInstalled = { + id: string + type: "ide.installed" + properties: { + ide: string + } +} + export type EventServerConnected = { id: string type: "server.connected" diff --git a/packages/server/package.json b/packages/server/package.json index 18cd2ab1f2a9..f929c5b9dad0 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "@opencode-ai/core": "workspace:*", + "@opencode-ai/schema": "workspace:*", "drizzle-orm": "catalog:", "effect": "catalog:" }, diff --git a/packages/server/src/api.ts b/packages/server/src/api.ts index 42573e06484f..a0655d9b93ab 100644 --- a/packages/server/src/api.ts +++ b/packages/server/src/api.ts @@ -8,7 +8,9 @@ import { PermissionGroup } from "./groups/permission" import { FileSystemGroup } from "./groups/fs" import { CommandGroup } from "./groups/command" import { SkillGroup } from "./groups/skill" -import { EventGroup } from "./groups/event" +import { makeEventGroup } from "./groups/event" +import type { Definition } from "@opencode-ai/schema/event" +import { PublicEventManifest } from "@opencode-ai/core/public-event-manifest" import { AgentGroup } from "./groups/agent" import { HealthGroup } from "./groups/health" import { PtyGroup } from "./groups/pty" @@ -20,31 +22,34 @@ import { IntegrationGroup } from "./groups/integration" import { CredentialGroup } from "./groups/credential" import { ProjectCopyGroup } from "./groups/project-copy" -export const Api = HttpApi.make("server") - .add(HealthGroup) - .add(LocationGroup) - .add(AgentGroup) - .add(SessionGroup) - .add(MessageGroup) - .add(ModelGroup) - .add(ProviderGroup) - .add(IntegrationGroup) - .add(CredentialGroup) - .add(PermissionGroup) - .add(FileSystemGroup) - .add(CommandGroup) - .add(SkillGroup) - .add(EventGroup) - .add(PtyGroup) - .add(QuestionGroup) - .add(ReferenceGroup) - .add(ProjectCopyGroup) - .annotateMerge( - OpenApi.annotations({ - title: "opencode HttpApi", - version: "0.0.1", - description: "Experimental HttpApi surface for selected instance routes.", - }), - ) - .middleware(Authorization) - .middleware(SchemaErrorMiddleware) +export const makeApi = (definitions: ReadonlyArray) => + HttpApi.make("server") + .add(HealthGroup) + .add(LocationGroup) + .add(AgentGroup) + .add(SessionGroup) + .add(MessageGroup) + .add(ModelGroup) + .add(ProviderGroup) + .add(IntegrationGroup) + .add(CredentialGroup) + .add(PermissionGroup) + .add(FileSystemGroup) + .add(CommandGroup) + .add(SkillGroup) + .add(makeEventGroup(definitions)) + .add(PtyGroup) + .add(QuestionGroup) + .add(ReferenceGroup) + .add(ProjectCopyGroup) + .annotateMerge( + OpenApi.annotations({ + title: "opencode HttpApi", + version: "0.0.1", + description: "Experimental HttpApi surface for selected instance routes.", + }), + ) + .middleware(Authorization) + .middleware(SchemaErrorMiddleware) + +export const Api = makeApi(PublicEventManifest.Latest.values().toArray()) diff --git a/packages/server/src/groups/event.ts b/packages/server/src/groups/event.ts index fffa0250d2ee..a70bf021275d 100644 --- a/packages/server/src/groups/event.ts +++ b/packages/server/src/groups/event.ts @@ -1,5 +1,7 @@ import { EventV2 } from "@opencode-ai/core/event" +import { PublicEventManifest } from "@opencode-ai/core/public-event-manifest" import { Location } from "@opencode-ai/core/location" +import type { Definition } from "@opencode-ai/schema/event" import { Schema } from "effect" import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" @@ -10,33 +12,40 @@ const fields = { location: Schema.optional(Location.Ref), } -const Event = Schema.Union([ - ...EventV2.definitions().map((definition) => - Schema.Struct({ - ...fields, - type: Schema.Literal(definition.type), - data: definition.data as Schema.Struct<{}>, - }).annotate({ identifier: `V2Event.${definition.type}` }), - ), - Schema.Struct({ - ...fields, - type: Schema.Literal("server.connected"), - data: Schema.Struct({}), - }).annotate({ identifier: "V2Event.server.connected" }), -]).annotate({ identifier: "V2Event" }) - -export const EventGroup = HttpApiGroup.make("server.event") - .add( - HttpApiEndpoint.get("event.subscribe", "/api/event", { - success: Event, - }).annotateMerge( - OpenApi.annotations({ - identifier: "v2.event.subscribe", - summary: "Subscribe to events", - description: "Subscribe to native event payloads for the server.", - }), +const schema = (definitions: ReadonlyArray) => + Schema.Union([ + ...definitions.map((definition) => + Schema.Struct({ + ...fields, + type: Schema.Literal(definition.type), + data: definition.data, + }).annotate({ identifier: `V2Event.${definition.type}` }), ), - ) - .annotateMerge(OpenApi.annotations({ title: "events", description: "Experimental event stream route." })) + ...(definitions.some((definition) => definition.type === "server.connected") + ? [] + : [ + Schema.Struct({ + ...fields, + type: Schema.Literal("server.connected"), + data: Schema.Struct({}), + }).annotate({ identifier: "V2Event.server.connected" }), + ]), + ]).annotate({ identifier: "V2Event" }) + +export const makeEventGroup = (definitions: ReadonlyArray) => + 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." })) -export type Event = typeof Event.Type +export const EventGroup = makeEventGroup(PublicEventManifest.Latest.values().toArray()) +export type Event = Schema.Schema.Type> From 152762933f5adcf2998c256a6ec6edcdabec2213 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Tue, 23 Jun 2026 23:01:24 -0400 Subject: [PATCH 2/7] fix(server): preserve event API requirements --- packages/core/src/event.ts | 13 ++-- packages/core/src/util/error.ts | 71 ++++++++++++++++++- .../server/routes/instance/httpapi/server.ts | 5 +- packages/schema/src/event.ts | 10 ++- packages/server/src/api.ts | 13 ++-- 5 files changed, 94 insertions(+), 18 deletions(-) diff --git a/packages/core/src/event.ts b/packages/core/src/event.ts index 6fd808002ff5..e00ff6e40bf5 100644 --- a/packages/core/src/event.ts +++ b/packages/core/src/event.ts @@ -170,9 +170,10 @@ export const layerWith = (options?: LayerOptions) => .get() .pipe(Effect.orDie) const latest = row?.seq ?? -1 - const encoded = Schema.encodeUnknownSync( - definition.data as Schema.Codec, - )(event.data) as Record + const encoded = Schema.encodeUnknownSync(definition.data)(event.data) as Record< + string, + unknown + > if (input?.strictOwner && row?.ownerID && row.ownerID !== input.ownerID) { yield* Effect.die( new InvalidDurableEventError({ @@ -374,9 +375,7 @@ export const layerWith = (options?: LayerOptions) => const payload = { id: event.id, type: definition.type, - data: Schema.decodeUnknownSync(definition.data as Schema.Codec)( - event.data, - ), + data: Schema.decodeUnknownSync(definition.data)(event.data), } as Payload const committed = yield* commitDurableEvent(definition, payload, { seq: event.seq, @@ -471,7 +470,7 @@ export const layerWith = (options?: LayerOptions) => id: event.id, type: definition.type, durable: { aggregateID: event.aggregateID, seq: event.seq, version: definition.durable.version }, - data: Schema.decodeUnknownSync(definition.data as Schema.Codec)(event.data), + data: Schema.decodeUnknownSync(definition.data)(event.data), } } diff --git a/packages/core/src/util/error.ts b/packages/core/src/util/error.ts index 779ffd2853ba..5fe41e1cff99 100644 --- a/packages/core/src/util/error.ts +++ b/packages/core/src/util/error.ts @@ -1 +1,70 @@ -export { NamedError } from "@opencode-ai/schema/named-error" +import { Schema } from "effect" + +export abstract class NamedError extends Error { + abstract schema(): Schema.Top + abstract toObject(): { name: string; data: unknown } + + static hasName(error: unknown, name: string): boolean { + return ( + typeof error === "object" && error !== null && "name" in error && (error as Record).name === name + ) + } + + static create( + name: Name, + fields: Fields, + ): ReturnType>> + static create( + name: Name, + data: DataSchema, + ): ReturnType> + static create(name: Name, data: Schema.Top | Schema.Struct.Fields) { + return NamedError.createSchemaClass(name, Schema.isSchema(data) ? data : Schema.Struct(data)) + } + + private static createSchemaClass(name: Name, data: DataSchema) { + const schema = Schema.Struct({ + name: Schema.Literal(name), + data, + }).annotate({ identifier: name }) + type Data = Schema.Schema.Type + + const result = class extends NamedError { + public static readonly Schema = schema + public static readonly EffectSchema = schema + public static readonly tag = name + + public override readonly name = name + + constructor( + public readonly data: Data, + options?: ErrorOptions, + ) { + super(name, options) + this.name = name + } + + static isInstance(input: unknown): input is InstanceType { + return NamedError.hasName(input, name) + } + + schema() { + return schema + } + + toObject() { + return { + name: name, + data: this.data, + } + } + } + Object.defineProperty(result, "name", { value: name }) + return result + } + + public static readonly Unknown = NamedError.create("UnknownError", { + message: Schema.String, + ref: Schema.optional(Schema.String), + }) +} diff --git a/packages/opencode/src/server/routes/instance/httpapi/server.ts b/packages/opencode/src/server/routes/instance/httpapi/server.ts index e1dbe579fbf2..b74df8deb83a 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/server.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/server.ts @@ -65,7 +65,8 @@ import { lazy } from "@/util/lazy" import { CorsConfig, isAllowedCorsOrigin, type CorsOptions } from "@opencode-ai/server/cors" import { serveUIEffect } from "@/server/shared/ui" import { ServerAuth } from "@/server/auth" -import { InstanceHttpApi, RootHttpApi, ServerApi } from "./api" +import { InstanceHttpApi, RootHttpApi } from "./api" +import { Api } from "@opencode-ai/server/api" import { PublicApi } from "./public" import { authorizationLayer, @@ -164,7 +165,7 @@ const instanceApiRoutes = HttpApiBuilder.layer(InstanceHttpApi).pipe( const instanceRoutes = instanceApiRoutes.pipe( Layer.provide([httpApiAuthLayer, workspaceRoutingLive, instanceContextLayer, schemaErrorLayer]), ) -const serverRoutes = HttpApiBuilder.layer(ServerApi).pipe( +const serverRoutes = HttpApiBuilder.layer(Api).pipe( Layer.provide(handlers), Layer.provide(PluginPtyEnvironment.layer), Layer.provide([serverHttpApiAuthLayer, v2SchemaErrorLayer]), diff --git a/packages/schema/src/event.ts b/packages/schema/src/event.ts index 1e341218c811..58bbd690e6cc 100644 --- a/packages/schema/src/event.ts +++ b/packages/schema/src/event.ts @@ -11,7 +11,10 @@ export const ID = Schema.String.check(Schema.isStartsWith("evt_")).pipe( ) export type ID = typeof ID.Type -export type Definition = Schema.Top & { +export type Definition< + Type extends string = string, + DataSchema extends Schema.Codec = Schema.Codec, +> = Schema.Top & { readonly type: Type readonly durable?: { readonly version: number @@ -35,7 +38,10 @@ export type Payload = { readonly metadata?: Record } -export function define(input: { +export function define< + const Type extends string, + Fields extends Readonly>>, +>(input: { readonly type: Type readonly durable?: { readonly version: number diff --git a/packages/server/src/api.ts b/packages/server/src/api.ts index a0655d9b93ab..4f5615779195 100644 --- a/packages/server/src/api.ts +++ b/packages/server/src/api.ts @@ -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 { MessageGroup } from "./groups/message" import { ModelGroup } from "./groups/model" @@ -8,9 +8,8 @@ import { PermissionGroup } from "./groups/permission" import { FileSystemGroup } from "./groups/fs" import { CommandGroup } from "./groups/command" import { SkillGroup } from "./groups/skill" -import { makeEventGroup } from "./groups/event" +import { EventGroup, makeEventGroup } from "./groups/event" import type { Definition } from "@opencode-ai/schema/event" -import { PublicEventManifest } from "@opencode-ai/core/public-event-manifest" import { AgentGroup } from "./groups/agent" import { HealthGroup } from "./groups/health" import { PtyGroup } from "./groups/pty" @@ -22,7 +21,7 @@ import { IntegrationGroup } from "./groups/integration" import { CredentialGroup } from "./groups/credential" import { ProjectCopyGroup } from "./groups/project-copy" -export const makeApi = (definitions: ReadonlyArray) => +const makeApiFromGroup = (eventGroup: Group) => HttpApi.make("server") .add(HealthGroup) .add(LocationGroup) @@ -37,7 +36,7 @@ export const makeApi = (definitions: ReadonlyArray) => .add(FileSystemGroup) .add(CommandGroup) .add(SkillGroup) - .add(makeEventGroup(definitions)) + .add(eventGroup) .add(PtyGroup) .add(QuestionGroup) .add(ReferenceGroup) @@ -52,4 +51,6 @@ export const makeApi = (definitions: ReadonlyArray) => .middleware(Authorization) .middleware(SchemaErrorMiddleware) -export const Api = makeApi(PublicEventManifest.Latest.values().toArray()) +export const makeApi = (definitions: ReadonlyArray) => makeApiFromGroup(makeEventGroup(definitions)) + +export const Api = makeApiFromGroup(EventGroup) From 2570e5d91dd7f5781a6e29ec3285fcf243307dd0 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 24 Jun 2026 13:31:38 -0400 Subject: [PATCH 3/7] refactor(schema): preserve event domain ownership --- packages/core/src/event-manifest.ts | 4 +- packages/core/src/public-event-manifest.ts | 7 +- packages/core/src/session/event.ts | 499 +++++++++++++- packages/core/src/session/todo.ts | 24 +- packages/core/src/v1/permission.ts | 98 ++- packages/core/src/v1/session.ts | 644 +++++++++++++++++- packages/opencode/package.json | 1 - packages/opencode/src/event-manifest.ts | 10 +- packages/opencode/src/server/event.ts | 2 - packages/opencode/src/session/todo.ts | 25 +- packages/opencode/test/event-manifest.test.ts | 14 +- packages/schema/src/index.ts | 3 - packages/schema/src/named-error.ts | 62 -- packages/schema/src/permission-v1.ts | 96 --- packages/schema/src/session-event.ts | 529 -------------- packages/schema/src/session-v1.ts | 633 ----------------- packages/schema/src/session.ts | 3 - packages/schema/src/todo.ts | 22 - packages/schema/test/event.test.ts | 48 +- packages/sdk/js/src/v2/gen/types.gen.ts | 34 - packages/server/package.json | 1 - packages/server/src/api.ts | 2 +- packages/server/src/groups/event.ts | 24 +- 23 files changed, 1319 insertions(+), 1466 deletions(-) delete mode 100644 packages/schema/src/named-error.ts delete mode 100644 packages/schema/src/permission-v1.ts delete mode 100644 packages/schema/src/session-event.ts delete mode 100644 packages/schema/src/session-v1.ts delete mode 100644 packages/schema/src/todo.ts diff --git a/packages/core/src/event-manifest.ts b/packages/core/src/event-manifest.ts index af4c8e0f7254..8488b0a022db 100644 --- a/packages/core/src/event-manifest.ts +++ b/packages/core/src/event-manifest.ts @@ -1,8 +1,8 @@ export * as EventManifest from "./event-manifest" import { Event } from "@opencode-ai/schema/event" -import { SessionEvent } from "@opencode-ai/schema/session-event" -import { SessionV1 } from "@opencode-ai/schema/session-v1" +import { SessionEvent } from "./session/event" +import { SessionV1 } from "./v1/session" export const Definitions = Event.inventory(...SessionV1.Events, ...SessionEvent.Definitions) export const Latest = Event.latest(Definitions) diff --git a/packages/core/src/public-event-manifest.ts b/packages/core/src/public-event-manifest.ts index 4216487bc11c..5e07b4e42cba 100644 --- a/packages/core/src/public-event-manifest.ts +++ b/packages/core/src/public-event-manifest.ts @@ -1,7 +1,6 @@ export * as PublicEventManifest from "./public-event-manifest" import { Event } from "@opencode-ai/schema/event" -import { Todo } from "@opencode-ai/schema/todo" import { Catalog } from "./catalog" import { FileSystem } from "./filesystem" import { Watcher } from "./filesystem/watcher" @@ -14,6 +13,7 @@ import { Pty } from "./pty" import { QuestionV2 } from "./question" import { Reference } from "./reference" import { EventManifest } from "./event-manifest" +import { SessionTodo } from "./session/todo" export const FoundationDefinitions = Event.inventory( ModelsDev.Event.Refreshed, @@ -38,10 +38,11 @@ export const FeatureDefinitions = Event.inventory( QuestionV2.Event.Asked, QuestionV2.Event.Replied, QuestionV2.Event.Rejected, - Todo.Event.Updated, ) -export const Definitions = Event.inventory(...FoundationDefinitions, ...FeatureDefinitions) +export const TodoDefinitions = Event.inventory(SessionTodo.Event.Updated) + +export const Definitions = Event.inventory(...FoundationDefinitions, ...FeatureDefinitions, ...TodoDefinitions) export const Latest = Event.latest(Definitions) export const Durable = Event.durable(Definitions) diff --git a/packages/core/src/session/event.ts b/packages/core/src/session/event.ts index 6d1b430357b6..ac58629b388e 100644 --- a/packages/core/src/session/event.ts +++ b/packages/core/src/session/event.ts @@ -1,2 +1,497 @@ -export * from "@opencode-ai/schema/session-event" -export { SessionEvent } from "@opencode-ai/schema/session-event" +import { Schema } from "effect" +import { Event } from "@opencode-ai/schema/event" +import { ProviderMetadata, ToolContent } from "@opencode-ai/schema/llm" +import { Delivery } from "@opencode-ai/schema/session-delivery" +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 = 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: ModelV2.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: ModelV2.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"] + +export * as SessionEvent from "./event" diff --git a/packages/core/src/session/todo.ts b/packages/core/src/session/todo.ts index 7f0411028469..7b3c3be3f69b 100644 --- a/packages/core/src/session/todo.ts +++ b/packages/core/src/session/todo.ts @@ -1,16 +1,30 @@ export * as SessionTodo from "./todo" import { asc, eq } from "drizzle-orm" -import { Context, Effect, Layer } from "effect" -import { Todo } from "@opencode-ai/schema/todo" +import { Context, Effect, Layer, Schema } from "effect" import { Database } from "../database/database" import { EventV2 } from "../event" import { SessionSchema } from "./schema" import { TodoTable } from "./sql" -export const Info = Todo.Info -export type Info = Todo.Info -export const Event = Todo.Event +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: "SessionTodo.Info" }) +export type Info = typeof Info.Type + +export const Event = { + Updated: EventV2.define({ + type: "todo.updated", + schema: { + sessionID: SessionSchema.ID, + todos: Schema.Array(Info), + }, + }), +} export interface Interface { readonly update: (input: { diff --git a/packages/core/src/v1/permission.ts b/packages/core/src/v1/permission.ts index 78ba6d30afa7..b241ccd9077b 100644 --- a/packages/core/src/v1/permission.ts +++ b/packages/core/src/v1/permission.ts @@ -1,2 +1,96 @@ -export * from "@opencode-ai/schema/permission-v1" -export { PermissionV1 } from "@opencode-ai/schema/permission-v1" +export * as PermissionV1 from "./permission" + +import { Schema } from "effect" +import { ProjectV2 } from "../project" +import { withStatics } from "../schema" +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()("PermissionRejectedError", {}) { + override get message() { + return "The user rejected permission to use this specific tool call." + } +} + +export class CorrectedError extends Schema.TaggedErrorClass()("PermissionCorrectedError", { + feedback: Schema.String, +}) { + override get message() { + return `The user rejected permission to use this specific tool call with the following feedback: ${this.feedback}` + } +} + +export class DeniedError extends Schema.TaggedErrorClass()("PermissionDeniedError", { + ruleset: Schema.Any, +}) { + override get message() { + return `The user has specified a rule which prevents you from using this specific tool call. Here are some of the relevant rules ${JSON.stringify(this.ruleset)}` + } +} + +export class NotFoundError extends Schema.TaggedErrorClass()("Permission.NotFoundError", { + requestID: ID, +}) {} + +export type Error = DeniedError | RejectedError | CorrectedError diff --git a/packages/core/src/v1/session.ts b/packages/core/src/v1/session.ts index e01d3e8f81c2..45121f0c2d58 100644 --- a/packages/core/src/v1/session.ts +++ b/packages/core/src/v1/session.ts @@ -1,2 +1,642 @@ -export * from "@opencode-ai/schema/session-v1" -export { SessionV1 } from "@opencode-ai/schema/session-v1" +export * as SessionV1 from "./session" + +import { Effect, Schema, Types } from "effect" +import { define, inventory } from "@opencode-ai/schema/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 { NamedError } from "../util/error" +import { SessionSchema } from "../session/schema" +import { WorkspaceV2 } from "../workspace" + +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_" + Identifier.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_" + Identifier.ascending()) })), +) +export type PartID = typeof PartID.Type + +export const OutputLengthError = NamedError.create("MessageOutputLengthError", {}) + +export const AuthError = NamedError.create("ProviderAuthError", { + providerID: Schema.String, + message: Schema.String, +}) + +export const AbortedError = NamedError.create("MessageAbortedError", { message: Schema.String }) +export const StructuredOutputError = NamedError.create("StructuredOutputError", { + message: Schema.String, + retries: NonNegativeInt, +}) +export const APIError = NamedError.create("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 +export const ContextOverflowError = NamedError.create("ContextOverflowError", { + message: Schema.String, + responseBody: Schema.optional(Schema.String), +}) +export const ContentFilterError = NamedError.create("ContentFilterError", { + message: Schema.String, +}) + +export class OutputFormatText extends Schema.Class("OutputFormatText")({ + type: Schema.Literal("text"), +}) {} + +export class OutputFormatJsonSchema extends Schema.Class("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 + +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> + +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> + +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> + +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> + +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> + +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> + +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> + +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> + +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>, "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> + +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> + +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> + +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> + +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> + +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> + +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>, "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> + +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 + +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> + +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> + +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> + +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> + +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>, "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: define({ + type: "session.created", + ...options, + schema: { + sessionID: SessionSchema.ID, + info: SessionInfo, + }, + }), + Updated: define({ + type: "session.updated", + ...options, + schema: { + sessionID: SessionSchema.ID, + info: SessionInfo, + }, + }), + Deleted: define({ + type: "session.deleted", + ...options, + schema: { + sessionID: SessionSchema.ID, + info: SessionInfo, + }, + }), + MessageUpdated: define({ + type: "message.updated", + ...options, + schema: { + sessionID: SessionSchema.ID, + info: Info, + }, + }), + MessageRemoved: define({ + type: "message.removed", + ...options, + schema: { + sessionID: SessionSchema.ID, + messageID: MessageID, + }, + }), + PartUpdated: define({ + type: "message.part.updated", + ...options, + schema: { + sessionID: SessionSchema.ID, + part: Part, + time: Schema.Finite, + }, + }), + PartRemoved: define({ + type: "message.part.removed", + ...options, + schema: { + sessionID: SessionSchema.ID, + messageID: MessageID, + partID: PartID, + }, + }), +} + +export const Events = inventory( + Event.Created, + Event.Updated, + Event.Deleted, + Event.MessageUpdated, + Event.MessageRemoved, + Event.PartUpdated, + Event.PartRemoved, +) diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 76e9ce5577e2..139217e348a4 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -34,7 +34,6 @@ "@opencode-ai/core": "workspace:*", "@opencode-ai/http-recorder": "workspace:*", "@opencode-ai/script": "workspace:*", - "@opencode-ai/schema": "workspace:*", "@standard-schema/spec": "1.0.0", "@tsconfig/bun": "catalog:", "@types/babel__core": "7.20.5", diff --git a/packages/opencode/src/event-manifest.ts b/packages/opencode/src/event-manifest.ts index a6795f435b41..0b564882d24a 100644 --- a/packages/opencode/src/event-manifest.ts +++ b/packages/opencode/src/event-manifest.ts @@ -4,7 +4,6 @@ import { Event } from "@opencode-ai/schema/event" import { PublicEventManifest } from "@opencode-ai/core/public-event-manifest" import { Command } from "@/command" import { Workspace } from "@/control-plane/workspace" -import { Ide } from "@/ide" import { Installation } from "@/installation" import { LSP } from "@/lsp/lsp" import { MCP } from "@/mcp" @@ -12,12 +11,13 @@ import { Permission } from "@/permission" import { Project } from "@/project/project" import { Vcs } from "@/project/vcs" import { Question } from "@/question" -import { ServerEvent } from "@/server/event" +import { Event as ServerEvent } from "@/server/event" import { TuiEvent } from "@/server/tui-event" import { MessageV2 } from "@/session/message-v2" import { Session } from "@/session/session" import { SessionCompaction } from "@/session/compaction" import { SessionStatus } from "@/session/status" +import { Todo } from "@/session/todo" import { Worktree } from "@/worktree" export const Definitions = Event.inventory( @@ -28,6 +28,7 @@ export const Definitions = Event.inventory( Installation.Event.Updated, Installation.Event.UpdateAvailable, ...PublicEventManifest.FeatureDefinitions, + Todo.Event.Updated, LSP.Event.Updated, Permission.Event.Asked, Permission.Event.Replied, @@ -51,9 +52,8 @@ export const Definitions = Event.inventory( Workspace.Event.Status, Worktree.Event.Ready, Worktree.Event.Failed, - Ide.Event.Installed, - ServerEvent.Event.Connected, - ServerEvent.Event.Disposed, + ServerEvent.Connected, + ServerEvent.Disposed, ) export const Latest = Event.latest(Definitions) diff --git a/packages/opencode/src/server/event.ts b/packages/opencode/src/server/event.ts index aacd4167e1fc..a58131255587 100644 --- a/packages/opencode/src/server/event.ts +++ b/packages/opencode/src/server/event.ts @@ -11,5 +11,3 @@ export const InstanceDisposed = Schema.Struct({ type: Schema.Literal("server.instance.disposed"), properties: Schema.Struct({ directory: Schema.String }), }).annotate({ identifier: "Event.server.instance.disposed" }) - -export * as ServerEvent from "./event" diff --git a/packages/opencode/src/session/todo.ts b/packages/opencode/src/session/todo.ts index f953d54b92bf..6e9eeba62b80 100644 --- a/packages/opencode/src/session/todo.ts +++ b/packages/opencode/src/session/todo.ts @@ -1,16 +1,31 @@ import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { SessionID } from "./schema" -import { Effect, Layer, Context } from "effect" -import { Todo as TodoSchema } from "@opencode-ai/schema/todo" +import { Effect, Layer, Context, Schema } from "effect" import { Database } from "@opencode-ai/core/database/database" import { eq } from "drizzle-orm" import { asc } from "drizzle-orm" import { TodoTable } from "@opencode-ai/core/session/sql" import { EventV2Bridge } from "@/event-v2-bridge" +import { EventV2 } from "@opencode-ai/core/event" -export const Info = TodoSchema.Info -export type Info = TodoSchema.Info -export const Event = TodoSchema.Event +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 = Schema.Schema.Type + +export const Event = { + Updated: EventV2.define({ + type: "todo.updated", + schema: { + sessionID: SessionID, + todos: Schema.Array(Info), + }, + }), +} export interface Interface { readonly update: (input: { sessionID: SessionID; todos: Info[] }) => Effect.Effect diff --git a/packages/opencode/test/event-manifest.test.ts b/packages/opencode/test/event-manifest.test.ts index 8465c5fdc49e..fabd35b060db 100644 --- a/packages/opencode/test/event-manifest.test.ts +++ b/packages/opencode/test/event-manifest.test.ts @@ -1,20 +1,20 @@ import { describe, expect, test } from "bun:test" -import { SessionEvent } from "@opencode-ai/schema/session-event" +import { SessionEvent } from "@opencode-ai/core/session/event" +import { Todo } from "@/session/todo" import { EventManifest } from "@/event-manifest" describe("public event manifest", () => { test("contains every latest public wire type once", () => { - expect(EventManifest.Latest.size).toBe(86) + expect(EventManifest.Latest.size).toBe(85) expect(EventManifest.Latest.get("session.next.step.ended")).toBe(SessionEvent.Step.Ended) - expect(EventManifest.Latest.has("ide.installed")).toBe(true) + 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("keeps historical durable versions out of the latest manifest", () => { - expect(EventManifest.Latest.values().toArray()).not.toContain(SessionEvent.Step.EndedV1) - expect(EventManifest.Latest.values().toArray()).not.toContain(SessionEvent.Step.FailedV1) - expect(EventManifest.Durable.get("session.next.step.ended.1")).toBe(SessionEvent.Step.EndedV1) + 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) }) }) diff --git a/packages/schema/src/index.ts b/packages/schema/src/index.ts index 4f05c9b41ac9..afeffe069028 100644 --- a/packages/schema/src/index.ts +++ b/packages/schema/src/index.ts @@ -9,16 +9,13 @@ export { LLM } from "./llm" export { Location } from "./location" export { Model } from "./model" export { Permission } from "./permission" -export { PermissionV1 } from "./permission-v1" export { Project } from "./project" export { Provider } from "./provider" export { Reference } from "./reference" export { Session } from "./session" -export { SessionV1 } from "./session-v1" export { SessionInput } from "./session-input" export { SessionMessage } from "./session-message" export { Skill } from "./skill" -export { Todo } from "./todo" export { Workspace } from "./workspace" export { Prompt, Source, FileAttachment, AgentAttachment } from "./prompt" export * from "./schema" diff --git a/packages/schema/src/named-error.ts b/packages/schema/src/named-error.ts deleted file mode 100644 index 95043d3c8f2a..000000000000 --- a/packages/schema/src/named-error.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { Schema } from "effect" - -export abstract class NamedError extends Error { - abstract schema(): Schema.Top - abstract toObject(): { name: string; data: unknown } - - static hasName(error: unknown, name: string): boolean { - return ( - typeof error === "object" && error !== null && "name" in error && (error as Record).name === name - ) - } - - static create( - name: Name, - fields: Fields, - ): ReturnType>> - static create( - name: Name, - data: DataSchema, - ): ReturnType> - static create(name: Name, data: Schema.Top | Schema.Struct.Fields) { - return NamedError.createSchemaClass(name, Schema.isSchema(data) ? data : Schema.Struct(data)) - } - - private static createSchemaClass(name: Name, data: DataSchema) { - const schema = Schema.Struct({ name: Schema.Literal(name), data }).annotate({ identifier: name }) - type Data = Schema.Schema.Type - const result = class extends NamedError { - public static readonly Schema = schema - public static readonly EffectSchema = schema - public static readonly tag = name - public override readonly name = name - - constructor( - public readonly data: Data, - options?: ErrorOptions, - ) { - super(name, options) - this.name = name - } - - static isInstance(input: unknown): input is InstanceType { - return NamedError.hasName(input, name) - } - - schema() { - return schema - } - - toObject() { - return { name, data: this.data } - } - } - Object.defineProperty(result, "name", { value: name }) - return result - } - - public static readonly Unknown = NamedError.create("UnknownError", { - message: Schema.String, - ref: Schema.optional(Schema.String), - }) -} diff --git a/packages/schema/src/permission-v1.ts b/packages/schema/src/permission-v1.ts deleted file mode 100644 index 1294101fd8be..000000000000 --- a/packages/schema/src/permission-v1.ts +++ /dev/null @@ -1,96 +0,0 @@ -export * as PermissionV1 from "./permission-v1" - -import { Schema } from "effect" -import { Project } from "./project" -import { withStatics } from "./schema" -import { SessionID } from "./session-id" -import { ascending } from "./identifier" - -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.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: Project.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()("PermissionRejectedError", {}) { - override get message() { - return "The user rejected permission to use this specific tool call." - } -} - -export class CorrectedError extends Schema.TaggedErrorClass()("PermissionCorrectedError", { - feedback: Schema.String, -}) { - override get message() { - return `The user rejected permission to use this specific tool call with the following feedback: ${this.feedback}` - } -} - -export class DeniedError extends Schema.TaggedErrorClass()("PermissionDeniedError", { - ruleset: Schema.Any, -}) { - override get message() { - return `The user has specified a rule which prevents you from using this specific tool call. Here are some of the relevant rules ${JSON.stringify(this.ruleset)}` - } -} - -export class NotFoundError extends Schema.TaggedErrorClass()("Permission.NotFoundError", { - requestID: ID, -}) {} - -export type Error = DeniedError | RejectedError | CorrectedError diff --git a/packages/schema/src/session-event.ts b/packages/schema/src/session-event.ts deleted file mode 100644 index c899a66f610e..000000000000 --- a/packages/schema/src/session-event.ts +++ /dev/null @@ -1,529 +0,0 @@ -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.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 = 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 EndedV1 = Event.define({ - type: "session.next.step.ended", - ...options, - schema: { - ...Base, - 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 EndedV1 = typeof EndedV1.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 FailedV1 = Event.define({ - type: "session.next.step.failed", - ...options, - schema: { - ...Base, - error: UnknownError, - }, - }) - export type FailedV1 = typeof FailedV1.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 CurrentDefinitions = 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 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 HistoricalDefinitions = Event.inventory(Step.EndedV1, Step.FailedV1) -export const Definitions = Event.inventory(...CurrentDefinitions, ...HistoricalDefinitions) - -export const Durable = Schema.Union(DurableDefinitions, { mode: "oneOf" }).pipe(Schema.toTaggedUnion("type")) -export type DurableEvent = typeof Durable.Type - -export const All = Schema.Union(CurrentDefinitions, { mode: "oneOf" }).pipe(Schema.toTaggedUnion("type")) -export type Event = typeof All.Type -export type Type = Event["type"] - -export * as SessionEvent from "./session-event" diff --git a/packages/schema/src/session-v1.ts b/packages/schema/src/session-v1.ts deleted file mode 100644 index aca58eb30865..000000000000 --- a/packages/schema/src/session-v1.ts +++ /dev/null @@ -1,633 +0,0 @@ -export * as SessionV1 from "./session-v1" - -import { Effect, Schema, Types } from "effect" -import { define } from "./event" -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 { NamedError } from "./named-error" -import { SessionID } from "./session-id" -import { Workspace } from "./workspace" - -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 - -export const OutputLengthError = NamedError.create("MessageOutputLengthError", {}) - -export const AuthError = NamedError.create("ProviderAuthError", { - providerID: Schema.String, - message: Schema.String, -}) - -export const AbortedError = NamedError.create("MessageAbortedError", { message: Schema.String }) -export const StructuredOutputError = NamedError.create("StructuredOutputError", { - message: Schema.String, - retries: NonNegativeInt, -}) -export const APIError = NamedError.create("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 -export const ContextOverflowError = NamedError.create("ContextOverflowError", { - message: Schema.String, - responseBody: Schema.optional(Schema.String), -}) -export const ContentFilterError = NamedError.create("ContentFilterError", { - message: Schema.String, -}) - -export class OutputFormatText extends Schema.Class("OutputFormatText")({ - type: Schema.Literal("text"), -}) {} - -export class OutputFormatJsonSchema extends Schema.Class("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 - -const partBase = { - id: PartID, - sessionID: SessionID.ID, - messageID: MessageID, -} - -export const SnapshotPart = Schema.Struct({ - ...partBase, - type: Schema.Literal("snapshot"), - snapshot: Schema.String, -}).annotate({ identifier: "SnapshotPart" }) -export type SnapshotPart = Types.DeepMutable> - -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> - -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> - -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> - -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> - -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> - -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> - -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> - -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>, "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> - -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> - -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> - -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> - -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> - -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> - -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>, "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: 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> - -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 - -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> - -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> - -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> - -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> - -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>, "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: Model.ID, - providerID: Provider.ID, - variant: optionalOmitUndefined(Schema.String), -}) - -export const SessionInfo = Schema.Struct({ - id: SessionID.ID, - slug: Schema.String, - projectID: Project.ID, - workspaceID: optionalOmitUndefined(Workspace.ID), - directory: Schema.String, - path: optionalOmitUndefined(Schema.String), - parentID: optionalOmitUndefined(SessionID.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: define({ - type: "session.created", - ...options, - schema: { - sessionID: SessionID.ID, - info: SessionInfo, - }, - }), - Updated: define({ - type: "session.updated", - ...options, - schema: { - sessionID: SessionID.ID, - info: SessionInfo, - }, - }), - Deleted: define({ - type: "session.deleted", - ...options, - schema: { - sessionID: SessionID.ID, - info: SessionInfo, - }, - }), - MessageUpdated: define({ - type: "message.updated", - ...options, - schema: { - sessionID: SessionID.ID, - info: Info, - }, - }), - MessageRemoved: define({ - type: "message.removed", - ...options, - schema: { - sessionID: SessionID.ID, - messageID: MessageID, - }, - }), - PartUpdated: define({ - type: "message.part.updated", - ...options, - schema: { - sessionID: SessionID.ID, - part: Part, - time: Schema.Finite, - }, - }), - PartRemoved: define({ - type: "message.part.removed", - ...options, - schema: { - sessionID: SessionID.ID, - messageID: MessageID, - partID: PartID, - }, - }), -} - -export const Events = Object.freeze(Object.values(Event)) diff --git a/packages/schema/src/session.ts b/packages/schema/src/session.ts index 2bae18972ad7..7e0e16c75506 100644 --- a/packages/schema/src/session.ts +++ b/packages/schema/src/session.ts @@ -7,7 +7,6 @@ import { Model } from "./model" import { Project } from "./project" import { DateTimeUtcFromMillis, optionalOmitUndefined, RelativePath } from "./schema" import { SessionID } from "./session-id" -import { SessionEvent } from "./session-event" export const ID = SessionID.ID export type ID = SessionID.ID @@ -45,5 +44,3 @@ export const ListAnchor = Schema.Struct({ direction: Schema.Literals(["previous", "next"]), }) export type ListAnchor = typeof ListAnchor.Type - -export const Event = SessionEvent diff --git a/packages/schema/src/todo.ts b/packages/schema/src/todo.ts deleted file mode 100644 index 84bf1a2d7aa8..000000000000 --- a/packages/schema/src/todo.ts +++ /dev/null @@ -1,22 +0,0 @@ -export * as Todo from "./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 Event = { - Updated: define({ - type: "todo.updated", - schema: { sessionID: SessionID.ID, todos: Schema.Array(Info) }, - }), -} -export const Events = inventory(Event.Updated) diff --git a/packages/schema/test/event.test.ts b/packages/schema/test/event.test.ts index a2af4cce9a81..380faa5a4abd 100644 --- a/packages/schema/test/event.test.ts +++ b/packages/schema/test/event.test.ts @@ -1,9 +1,6 @@ import { describe, expect, test } from "bun:test" import { Schema } from "effect" import { Event } from "../src/event" -import { SessionEvent } from "../src/session-event" -import { SessionV1 } from "../src/session-v1" -import { Todo } from "../src/todo" describe("public event schemas", () => { test("definition is pure", () => { @@ -28,44 +25,13 @@ describe("public event schemas", () => { expect(Event.latest([current, historical]).get(current.type)).toBe(current) }) - test("indexes every durable session type and version", () => { - const durable = Event.durable([...SessionV1.Events, ...SessionEvent.Definitions]) - expect(durable.size).toBe( - SessionV1.Events.length + SessionEvent.DurableDefinitions.length + SessionEvent.HistoricalDefinitions.length, - ) - for (const definition of [ - ...SessionV1.Events, - ...SessionEvent.DurableDefinitions, - ...SessionEvent.HistoricalDefinitions, - ]) { - expect(durable.get(Event.versionedType(definition.type, definition.durable!.version))).toBe(definition) - } - }) - - test("latest aggregate excludes historical versions", () => { - const latest = Event.latest(SessionEvent.Definitions) - expect(latest.get(SessionEvent.Step.Ended.type)).toBe(SessionEvent.Step.Ended) - expect(latest.get(SessionEvent.Step.Failed.type)).toBe(SessionEvent.Step.Failed) - expect(latest.values().toArray()).not.toContain(SessionEvent.Step.EndedV1) - expect(latest.values().toArray()).not.toContain(SessionEvent.Step.FailedV1) - }) - - test("historical and current step shapes decode incompatibly", () => { - const historical = { - timestamp: 0, - sessionID: "ses_test", - finish: "stop", - cost: 0, - tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, - } - expect(Schema.decodeUnknownSync(SessionEvent.Step.EndedV1.data)(historical)).toBeDefined() - expect(() => Schema.decodeUnknownSync(SessionEvent.Step.Ended.data)(historical)).toThrow() - }) + 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 }, + }) - test("domain inventories are explicit and complete", () => { - expect(SessionEvent.Definitions.length).toBe(31) - expect(Todo.Events).toEqual([Todo.Event.Updated]) - expect(Object.isFrozen(SessionEvent.Definitions)).toBe(true) - expect(Object.isFrozen(Todo.Events)).toBe(true) + expect(Event.durable([definition]).get("test.durable.1")).toBe(definition) }) }) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index b48f0d4d28bc..3116c295f125 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -88,7 +88,6 @@ export type Event = | EventWorkspaceStatus | EventWorktreeReady | EventWorktreeFailed - | EventIdeInstalled | EventServerConnected | EventGlobalDisposed | EventServerInstanceDisposed @@ -1598,13 +1597,6 @@ export type GlobalEvent = { message: string } } - | { - id: string - type: "ide.installed" - properties: { - ide: string - } - } | { id: string type: "server.connected" @@ -2841,7 +2833,6 @@ export type V2Event = | V2EventWorkspaceStatus | V2EventWorktreeReady | V2EventWorktreeFailed - | V2EventIdeInstalled | V2EventServerConnected | V2EventGlobalDisposed @@ -5911,23 +5902,6 @@ export type V2EventWorktreeFailed = { } } -export type V2EventIdeInstalled = { - id: string - metadata?: { - [key: string]: unknown - } - durable?: { - aggregateID: string - seq: number - version: number - } - location?: LocationRef - type: "ide.installed" - data: { - ide: string - } -} - export type V2EventServerConnected = { id: string metadata?: { @@ -6902,14 +6876,6 @@ export type EventWorktreeFailed = { } } -export type EventIdeInstalled = { - id: string - type: "ide.installed" - properties: { - ide: string - } -} - export type EventServerConnected = { id: string type: "server.connected" diff --git a/packages/server/package.json b/packages/server/package.json index f929c5b9dad0..18cd2ab1f2a9 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -13,7 +13,6 @@ }, "dependencies": { "@opencode-ai/core": "workspace:*", - "@opencode-ai/schema": "workspace:*", "drizzle-orm": "catalog:", "effect": "catalog:" }, diff --git a/packages/server/src/api.ts b/packages/server/src/api.ts index 4f5615779195..a404d53f687d 100644 --- a/packages/server/src/api.ts +++ b/packages/server/src/api.ts @@ -9,7 +9,7 @@ import { FileSystemGroup } from "./groups/fs" import { CommandGroup } from "./groups/command" import { SkillGroup } from "./groups/skill" import { EventGroup, makeEventGroup } from "./groups/event" -import type { Definition } from "@opencode-ai/schema/event" +import type { Definition } from "@opencode-ai/core/event" import { AgentGroup } from "./groups/agent" import { HealthGroup } from "./groups/health" import { PtyGroup } from "./groups/pty" diff --git a/packages/server/src/groups/event.ts b/packages/server/src/groups/event.ts index a70bf021275d..7e8c02902047 100644 --- a/packages/server/src/groups/event.ts +++ b/packages/server/src/groups/event.ts @@ -1,7 +1,7 @@ import { EventV2 } from "@opencode-ai/core/event" import { PublicEventManifest } from "@opencode-ai/core/public-event-manifest" import { Location } from "@opencode-ai/core/location" -import type { Definition } from "@opencode-ai/schema/event" +import type { Definition } from "@opencode-ai/core/event" import { Schema } from "effect" import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" @@ -12,7 +12,7 @@ const fields = { location: Schema.optional(Location.Ref), } -const schema = (definitions: ReadonlyArray) => +const schema = >(definitions: Definitions) => Schema.Union([ ...definitions.map((definition) => Schema.Struct({ @@ -32,7 +32,7 @@ const schema = (definitions: ReadonlyArray) => ]), ]).annotate({ identifier: "V2Event" }) -export const makeEventGroup = (definitions: ReadonlyArray) => +export const makeEventGroup = >(definitions: Definitions) => HttpApiGroup.make("server.event") .add( HttpApiEndpoint.get("event.subscribe", "/api/event", { @@ -47,5 +47,19 @@ export const makeEventGroup = (definitions: ReadonlyArray) => ) .annotateMerge(OpenApi.annotations({ title: "events", description: "Experimental event stream route." })) -export const EventGroup = makeEventGroup(PublicEventManifest.Latest.values().toArray()) -export type Event = Schema.Schema.Type> +const EventSchema = schema(PublicEventManifest.Definitions) + +export const EventGroup = HttpApiGroup.make("server.event") + .add( + HttpApiEndpoint.get("event.subscribe", "/api/event", { + success: EventSchema, + }).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." })) +export type Event = typeof EventSchema.Type From a574e0f8edb9746075eac4c6e4b6e8535ac058e2 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 24 Jun 2026 14:06:28 -0400 Subject: [PATCH 4/7] refactor(schema): own canonical event definitions --- packages/core/src/catalog.ts | 5 +- packages/core/src/event-manifest.ts | 12 +- packages/core/src/event.ts | 4 +- packages/core/src/filesystem.ts | 12 +- packages/core/src/filesystem/watcher.ts | 13 +- packages/core/src/integration.ts | 11 +- packages/core/src/models-dev.ts | 8 +- packages/core/src/permission.ts | 46 +- packages/core/src/plugin.ts | 14 +- packages/core/src/project/copy.ts | 8 +- packages/core/src/pty.ts | 23 +- packages/core/src/pty/schema.ts | 14 +- packages/core/src/public-event-manifest.ts | 53 +- packages/core/src/question.ts | 68 +- packages/core/src/reference.ts | 4 +- packages/core/src/session/event.ts | 499 +------------ packages/core/src/session/todo.ts | 21 +- packages/core/src/v1/permission.ts | 67 +- packages/core/src/v1/session.ts | 661 ++--------------- .../core/test/legacy-event-schema.test.ts | 16 + packages/opencode/src/command/index.ts | 13 +- .../opencode/src/control-plane/workspace.ts | 26 +- packages/opencode/src/event-manifest.ts | 59 +- packages/opencode/src/ide/index.ts | 11 +- packages/opencode/src/installation/index.ts | 17 +- packages/opencode/src/lsp/lsp.ts | 6 +- packages/opencode/src/mcp/index.ts | 17 +- packages/opencode/src/permission/index.ts | 16 +- packages/opencode/src/project/project.ts | 50 +- packages/opencode/src/project/vcs.ts | 12 +- packages/opencode/src/question/index.ts | 106 +-- packages/opencode/src/question/schema.ts | 12 +- packages/opencode/src/server/event.ts | 7 +- packages/opencode/src/server/tui-event.ts | 54 +- packages/opencode/src/session/compaction.ts | 11 +- packages/opencode/src/session/message-v2.ts | 17 +- packages/opencode/src/session/session.ts | 64 +- packages/opencode/src/session/status.ts | 49 +- packages/opencode/src/session/todo.ts | 24 +- packages/opencode/src/snapshot/index.ts | 12 +- packages/opencode/src/worktree/index.ts | 18 +- packages/opencode/test/event-manifest.test.ts | 4 + packages/schema/src/catalog.ts | 8 + packages/schema/src/event-manifest.ts | 105 +++ packages/schema/src/file-diff.ts | 13 + packages/schema/src/filesystem-watcher.ts | 14 + packages/schema/src/filesystem.ts | 9 + packages/schema/src/ide-event.ts | 11 + packages/schema/src/installation-event.ts | 18 + packages/schema/src/integration.ts | 12 + packages/schema/src/legacy-event.ts | 19 + packages/schema/src/lsp-event.ts | 5 + packages/schema/src/mcp-event.ts | 19 + packages/schema/src/models-dev.ts | 10 + packages/schema/src/permission-v1.ts | 68 ++ packages/schema/src/permission.ts | 49 ++ packages/schema/src/plugin.ts | 16 + packages/schema/src/project-directories.ts | 12 + packages/schema/src/project.ts | 31 +- packages/schema/src/pty.ts | 36 + packages/schema/src/question-v1.ts | 58 ++ packages/schema/src/question.ts | 80 +++ packages/schema/src/reference.ts | 5 + packages/schema/src/server-event.ts | 6 + .../schema/src/session-compaction-event.ts | 11 + packages/schema/src/session-event.ts | 497 +++++++++++++ packages/schema/src/session-status-event.ts | 48 ++ packages/schema/src/session-todo.ts | 26 + packages/schema/src/session-v1.ts | 673 ++++++++++++++++++ packages/schema/src/tui-event.ts | 56 ++ packages/schema/src/vcs-event.ts | 11 + packages/schema/src/workspace-event.ts | 30 + packages/schema/src/worktree-event.ts | 19 + packages/schema/test/event-manifest.test.ts | 22 + packages/schema/test/legacy-event.test.ts | 47 ++ 75 files changed, 2255 insertions(+), 1953 deletions(-) create mode 100644 packages/core/test/legacy-event-schema.test.ts create mode 100644 packages/schema/src/catalog.ts create mode 100644 packages/schema/src/event-manifest.ts create mode 100644 packages/schema/src/file-diff.ts create mode 100644 packages/schema/src/filesystem-watcher.ts create mode 100644 packages/schema/src/ide-event.ts create mode 100644 packages/schema/src/installation-event.ts create mode 100644 packages/schema/src/legacy-event.ts create mode 100644 packages/schema/src/lsp-event.ts create mode 100644 packages/schema/src/mcp-event.ts create mode 100644 packages/schema/src/models-dev.ts create mode 100644 packages/schema/src/permission-v1.ts create mode 100644 packages/schema/src/plugin.ts create mode 100644 packages/schema/src/project-directories.ts create mode 100644 packages/schema/src/pty.ts create mode 100644 packages/schema/src/question-v1.ts create mode 100644 packages/schema/src/question.ts create mode 100644 packages/schema/src/server-event.ts create mode 100644 packages/schema/src/session-compaction-event.ts create mode 100644 packages/schema/src/session-event.ts create mode 100644 packages/schema/src/session-status-event.ts create mode 100644 packages/schema/src/session-todo.ts create mode 100644 packages/schema/src/session-v1.ts create mode 100644 packages/schema/src/tui-event.ts create mode 100644 packages/schema/src/vcs-event.ts create mode 100644 packages/schema/src/workspace-event.ts create mode 100644 packages/schema/src/worktree-event.ts create mode 100644 packages/schema/test/event-manifest.test.ts create mode 100644 packages/schema/test/legacy-event.test.ts diff --git a/packages/core/src/catalog.ts b/packages/core/src/catalog.ts index 431aa004b311..884cc2b051c5 100644 --- a/packages/core/src/catalog.ts +++ b/packages/core/src/catalog.ts @@ -1,6 +1,7 @@ export * as Catalog from "./catalog" import { Array, Context, Effect, Layer, Option, Order, pipe, Schema } from "effect" +import { CatalogEvent } from "@opencode-ai/schema/catalog" import { ModelV2 } from "./model" import { ProviderV2 } from "./provider" 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 Event = { - Updated: EventV2.define({ type: "catalog.updated", schema: {} }), -} +export const Event = CatalogEvent type Data = { providers: Map diff --git a/packages/core/src/event-manifest.ts b/packages/core/src/event-manifest.ts index 8488b0a022db..43c55b4bcdce 100644 --- a/packages/core/src/event-manifest.ts +++ b/packages/core/src/event-manifest.ts @@ -1,9 +1,7 @@ export * as EventManifest from "./event-manifest" -import { Event } from "@opencode-ai/schema/event" -import { SessionEvent } from "./session/event" -import { SessionV1 } from "./v1/session" - -export const Definitions = Event.inventory(...SessionV1.Events, ...SessionEvent.Definitions) -export const Latest = Event.latest(Definitions) -export const Durable = Event.durable(Definitions) +export { + CoreDefinitions as Definitions, + CoreDurable as Durable, + CoreLatest as Latest, +} from "@opencode-ai/schema/event-manifest" diff --git a/packages/core/src/event.ts b/packages/core/src/event.ts index e00ff6e40bf5..6a75c9731960 100644 --- a/packages/core/src/event.ts +++ b/packages/core/src/event.ts @@ -93,7 +93,9 @@ export const layerWith = (options?: LayerOptions) => Layer.effect( Service, Effect.gen(function* () { - const durableDefinitions = Event.durable([...EventManifest.Definitions, ...(options?.definitions ?? [])]) + const durableDefinitions = options?.definitions + ? Event.durable([...EventManifest.Definitions, ...options.definitions]) + : EventManifest.Durable const pubsub = { all: yield* PubSub.unbounded(), durable: new Map>>(), diff --git a/packages/core/src/filesystem.ts b/packages/core/src/filesystem.ts index d7536ab62e98..6edea1be4a73 100644 --- a/packages/core/src/filesystem.ts +++ b/packages/core/src/filesystem.ts @@ -2,12 +2,11 @@ export * as FileSystem from "./filesystem" import path from "path" import { Context, Effect, Layer, Schema } from "effect" -import { EventV2 } from "./event" import { FSUtil } from "./fs-util" import { Location } from "./location" import { PositiveInt, RelativePath } from "./schema" import { FileSystemSearch } from "./filesystem/search" -import { Entry, Match } from "@opencode-ai/schema/filesystem" +import { Entry, FileSystemEvent, Match } from "@opencode-ai/schema/filesystem" export { Entry, Match, Submatch } from "@opencode-ai/schema/filesystem" export const ReadInput = Schema.Struct({ @@ -48,14 +47,7 @@ export class GrepInput extends Schema.Class("FileSystem.GrepInput")({ limit: PositiveInt.pipe(Schema.optional), }) {} -export const Event = { - Edited: EventV2.define({ - type: "file.edited", - schema: { - file: Schema.String, - }, - }), -} +export const Event = FileSystemEvent export interface Interface { readonly read: (input: ReadInput) => Effect.Effect<{ readonly content: Uint8Array; readonly mime: string }> diff --git a/packages/core/src/filesystem/watcher.ts b/packages/core/src/filesystem/watcher.ts index 65d85e048233..c87edb39dc78 100644 --- a/packages/core/src/filesystem/watcher.ts +++ b/packages/core/src/filesystem/watcher.ts @@ -3,7 +3,8 @@ export * as Watcher from "./watcher" // @ts-ignore import { createWrapper } from "@parcel/watcher/wrapper" 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 { Config } from "../config" import { EventV2 } from "../event" @@ -19,15 +20,7 @@ declare const OPENCODE_LIBC: string | undefined const SUBSCRIBE_TIMEOUT_MS = 10_000 -export const Event = { - Updated: EventV2.define({ - type: "file.watcher.updated", - schema: { - file: Schema.String, - event: Schema.Literals(["add", "change", "unlink"]), - }, - }), -} +export const Event = FileSystemWatcher.Event const watcher = lazy((): typeof import("@parcel/watcher") | undefined => { try { diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index bd56cc025d1e..19daef9ae19a 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -136,16 +136,7 @@ export class AuthorizationError extends Schema.TaggedErrorClass -export const Event = { - Refreshed: EventV2.define({ - type: "models-dev.refreshed", - schema: {}, - }), -} +export const Event = ModelsDev.Event declare const OPENCODE_MODELS_DEV: Record | undefined diff --git a/packages/core/src/permission.ts b/packages/core/src/permission.ts index f509cd1bee94..0ee64a07d7f0 100644 --- a/packages/core/src/permission.ts +++ b/packages/core/src/permission.ts @@ -7,45 +7,31 @@ import { Location } from "./location" import { AgentV2 } from "./agent" import { SessionV2 } from "./session" import { SessionStore } from "./session/store" -import { withStatics } from "./schema" -import { Identifier } from "./util/identifier" import { Wildcard } from "./util/wildcard" import { PermissionSaved } from "./permission/saved" export { Effect, Rule, Ruleset } from "@opencode-ai/schema/permission" const missingAgentPermissions: Permission.Ruleset = [{ action: "*", resource: "*", effect: "deny" }] -export const ID = Schema.String.check(Schema.isStartsWith("per")).pipe( - Schema.brand("PermissionV2.ID"), - withStatics((schema) => ({ create: (id?: string) => schema.make(id ?? "per_" + Identifier.ascending()) })), -) +export const ID = Permission.ID 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 const Source = Permission.Source export type Source = typeof Source.Type const RequestFields = { - sessionID: SessionV2.ID, - 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), + sessionID: Permission.Request.fields.sessionID, + action: Permission.Request.fields.action, + resources: Permission.Request.fields.resources, + save: Permission.Request.fields.save, + metadata: Permission.Request.fields.metadata, + source: Permission.Request.fields.source, } -export const Request = Schema.Struct({ - id: ID, - ...RequestFields, -}).annotate({ identifier: "PermissionV2.Request" }) +export const Request = Permission.Request 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 const AssertInput = Schema.Struct({ @@ -68,17 +54,7 @@ export const AskResult = Schema.Struct({ }).annotate({ identifier: "PermissionV2.AskResult" }) export type AskResult = typeof AskResult.Type -export const 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 const Event = Permission.Event export class RejectedError extends Schema.TaggedErrorClass()("PermissionV2.RejectedError", {}) {} diff --git a/packages/core/src/plugin.ts b/packages/core/src/plugin.ts index 4bb40934a21a..af3559189c0f 100644 --- a/packages/core/src/plugin.ts +++ b/packages/core/src/plugin.ts @@ -1,7 +1,8 @@ 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 { PluginEvent, PluginID } from "@opencode-ai/schema/plugin" import { AgentV2 } from "./agent" import { AISDK } from "./aisdk" import { Catalog } from "./catalog" @@ -14,17 +15,10 @@ import { Reference } from "./reference" import { SkillV2 } from "./skill" 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 const Event = { - Added: EventV2.define({ - type: "plugin.added", - schema: { - id: ID, - }, - }), -} +export const Event = PluginEvent export interface Interface { readonly add: (id: ID, effect: Plugin["effect"]) => Effect.Effect diff --git a/packages/core/src/project/copy.ts b/packages/core/src/project/copy.ts index 441c380d7c7a..8030a1dfea9e 100644 --- a/packages/core/src/project/copy.ts +++ b/packages/core/src/project/copy.ts @@ -13,6 +13,7 @@ import { Slug } from "../util/slug" import { EventV2 } from "../event" import { Database } from "../database/database" 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 type StrategyID = typeof StrategyID.Type @@ -106,12 +107,7 @@ export interface Strategy { readonly list: (directory: AbsolutePath) => Effect.Effect } -export const Event = { - Updated: EventV2.define({ - type: "project.directories.updated", - schema: { projectID: Project.ID }, - }), -} +export const Event = ProjectDirectoriesEvent export interface Interface { readonly register: (strategy: Strategy) => Effect.Effect diff --git a/packages/core/src/pty.ts b/packages/core/src/pty.ts index 49269e952116..16e09ab57977 100644 --- a/packages/core/src/pty.ts +++ b/packages/core/src/pty.ts @@ -2,10 +2,11 @@ export * as Pty from "./pty" import type { Disp, Proc } from "#pty" import { Context, Effect, Layer, Schema, Types } from "effect" +import { PtyEvent, PtyInfo } from "@opencode-ai/schema/pty" import { Config } from "./config" import { EventV2 } from "./event" import { Location } from "./location" -import { NonNegativeInt, PositiveInt } from "./schema" +import { PositiveInt } from "./schema" import { PtyID } from "./pty/schema" import { Shell } from "./shell" import { lazy } from "./util/lazy" @@ -35,18 +36,7 @@ type Active = { listeners: Disp[] } -export const Info = Schema.Struct({ - 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 const Info = PtyInfo export type Info = Types.DeepMutable @@ -100,12 +90,7 @@ export class ExitedError extends Schema.TaggedErrorClass()("Pty.Exi ptyID: PtyID, }) {} -export const Event = { - 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 const Event = PtyEvent export interface Interface { readonly list: () => Effect.Effect diff --git a/packages/core/src/pty/schema.ts b/packages/core/src/pty/schema.ts index b8c973862f98..ab0c40521b1f 100644 --- a/packages/core/src/pty/schema.ts +++ b/packages/core/src/pty/schema.ts @@ -1,13 +1 @@ -import { Schema } from "effect" -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)), - })), -) +export { ID as PtyID } from "@opencode-ai/schema/pty" diff --git a/packages/core/src/public-event-manifest.ts b/packages/core/src/public-event-manifest.ts index 5e07b4e42cba..886d5927defc 100644 --- a/packages/core/src/public-event-manifest.ts +++ b/packages/core/src/public-event-manifest.ts @@ -1,48 +1,9 @@ export * as PublicEventManifest from "./public-event-manifest" -import { Event } from "@opencode-ai/schema/event" -import { Catalog } from "./catalog" -import { FileSystem } from "./filesystem" -import { Watcher } from "./filesystem/watcher" -import { Integration } from "./integration" -import { ModelsDev } from "./models-dev" -import { PermissionV2 } from "./permission" -import { PluginV2 } from "./plugin" -import { ProjectCopy } from "./project/copy" -import { Pty } from "./pty" -import { QuestionV2 } from "./question" -import { Reference } from "./reference" -import { EventManifest } from "./event-manifest" -import { SessionTodo } from "./session/todo" - -export const FoundationDefinitions = Event.inventory( - ModelsDev.Event.Refreshed, - Integration.Event.Updated, - Integration.Event.ConnectionUpdated, - Catalog.Event.Updated, - ...EventManifest.Definitions, -) - -export const FeatureDefinitions = Event.inventory( - FileSystem.Event.Edited, - Reference.Event.Updated, - PermissionV2.Event.Asked, - PermissionV2.Event.Replied, - PluginV2.Event.Added, - ProjectCopy.Event.Updated, - Watcher.Event.Updated, - Pty.Event.Created, - Pty.Event.Updated, - Pty.Event.Exited, - Pty.Event.Deleted, - QuestionV2.Event.Asked, - QuestionV2.Event.Replied, - QuestionV2.Event.Rejected, -) - -export const TodoDefinitions = Event.inventory(SessionTodo.Event.Updated) - -export const Definitions = Event.inventory(...FoundationDefinitions, ...FeatureDefinitions, ...TodoDefinitions) - -export const Latest = Event.latest(Definitions) -export const Durable = Event.durable(Definitions) +export { + FeatureDefinitions, + FoundationDefinitions, + PublicDefinitions as Definitions, + PublicDurable as Durable, + PublicLatest as Latest, +} from "@opencode-ai/schema/event-manifest" diff --git a/packages/core/src/question.ts b/packages/core/src/question.ts index a489fb9aac2f..2f4942af1b03 100644 --- a/packages/core/src/question.ts +++ b/packages/core/src/question.ts @@ -1,83 +1,35 @@ export * as QuestionV2 from "./question" import { Context, Deferred, Effect, Layer, Schema } from "effect" +import { Question } from "@opencode-ai/schema/question" import { EventV2 } from "./event" -import { Identifier } from "./id/id" -import { withStatics } from "./schema" import { SessionSchema } from "./session/schema" -export const ID = Schema.String.check(Schema.isStartsWith("que")).pipe( - Schema.brand("QuestionV2.ID"), - withStatics((schema) => ({ ascending: (id?: string) => schema.make(Identifier.ascending("question", id)) })), -) +export const ID = Question.ID 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 const Option = Question.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 const Info = Question.Info 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 const Tool = Schema.Struct({ - messageID: Schema.String, - callID: Schema.String, -}).annotate({ identifier: "QuestionV2.Tool" }) +export const Tool = Question.Tool export type Tool = typeof Tool.Type -export const Request = Schema.Struct({ - id: ID, - sessionID: SessionSchema.ID, - questions: Schema.Array(Info).annotate({ description: "Questions to ask" }), - tool: Tool.pipe(Schema.optional), -}).annotate({ identifier: "QuestionV2.Request" }) +export const Request = Question.Request 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 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 const Reply = Question.Reply export type Reply = typeof Reply.Type -export const 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 const Event = Question.Event export class RejectedError extends Schema.TaggedErrorClass()("QuestionV2.RejectedError", {}) { override get message() { diff --git a/packages/core/src/reference.ts b/packages/core/src/reference.ts index 3edc84e98a5e..f03a69d49c2f 100644 --- a/packages/core/src/reference.ts +++ b/packages/core/src/reference.ts @@ -18,9 +18,7 @@ export type GitSource = Reference.GitSource export const Source = Reference.Source export type Source = Reference.Source -export const Event = { - Updated: EventV2.define({ type: "reference.updated", schema: {} }), -} +export const Event = Reference.Event export class Info extends Schema.Class("Reference.Info")({ name: Schema.String, diff --git a/packages/core/src/session/event.ts b/packages/core/src/session/event.ts index ac58629b388e..78e40a699416 100644 --- a/packages/core/src/session/event.ts +++ b/packages/core/src/session/event.ts @@ -1,497 +1,2 @@ -import { Schema } from "effect" -import { Event } from "@opencode-ai/schema/event" -import { ProviderMetadata, ToolContent } from "@opencode-ai/schema/llm" -import { Delivery } from "@opencode-ai/schema/session-delivery" -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 = 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: ModelV2.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: ModelV2.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"] - -export * as SessionEvent from "./event" +export * from "@opencode-ai/schema/session-event" +export * as SessionEvent from "@opencode-ai/schema/session-event" diff --git a/packages/core/src/session/todo.ts b/packages/core/src/session/todo.ts index 7b3c3be3f69b..22a62711c8bc 100644 --- a/packages/core/src/session/todo.ts +++ b/packages/core/src/session/todo.ts @@ -1,30 +1,17 @@ export * as SessionTodo from "./todo" import { asc, eq } from "drizzle-orm" -import { Context, Effect, Layer, Schema } from "effect" +import { Context, Effect, Layer } from "effect" +import { SessionTodoEvent, SessionTodoInfo } from "@opencode-ai/schema/session-todo" import { Database } from "../database/database" import { EventV2 } from "../event" import { SessionSchema } from "./schema" import { TodoTable } from "./sql" -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: "SessionTodo.Info" }) +export const Info = SessionTodoInfo export type Info = typeof Info.Type -export const Event = { - Updated: EventV2.define({ - type: "todo.updated", - schema: { - sessionID: SessionSchema.ID, - todos: Schema.Array(Info), - }, - }), -} +export const Event = SessionTodoEvent export interface Interface { readonly update: (input: { diff --git a/packages/core/src/v1/permission.ts b/packages/core/src/v1/permission.ts index b241ccd9077b..c289196c182b 100644 --- a/packages/core/src/v1/permission.ts +++ b/packages/core/src/v1/permission.ts @@ -1,71 +1,8 @@ export * as PermissionV1 from "./permission" import { Schema } from "effect" -import { ProjectV2 } from "../project" -import { withStatics } from "../schema" -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 * from "@opencode-ai/schema/permission-v1" +import { ID } from "@opencode-ai/schema/permission-v1" export class RejectedError extends Schema.TaggedErrorClass()("PermissionRejectedError", {}) { override get message() { diff --git a/packages/core/src/v1/session.ts b/packages/core/src/v1/session.ts index 45121f0c2d58..b675e2a7986e 100644 --- a/packages/core/src/v1/session.ts +++ b/packages/core/src/v1/session.ts @@ -1,39 +1,53 @@ export * as SessionV1 from "./session" -import { Effect, Schema, Types } from "effect" -import { define, inventory } from "@opencode-ai/schema/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 { Schema } from "effect" import { NonNegativeInt } from "../schema" import { NamedError } from "../util/error" -import { SessionSchema } from "../session/schema" -import { WorkspaceV2 } from "../workspace" -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_" + Identifier.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_" + Identifier.ascending()) })), -) -export type PartID = typeof PartID.Type +export { + AgentPart, + AgentPartInput, + Assistant, + CompactionPart, + Event, + Events, + FilePart, + FilePartInput, + FilePartSource, + FileSource, + Format, + Info, + 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 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 StructuredOutputError = NamedError.create("StructuredOutputError", { message: Schema.String, @@ -52,591 +66,4 @@ export const ContextOverflowError = NamedError.create("ContextOverflowError", { message: Schema.String, responseBody: Schema.optional(Schema.String), }) -export const ContentFilterError = NamedError.create("ContentFilterError", { - message: Schema.String, -}) - -export class OutputFormatText extends Schema.Class("OutputFormatText")({ - type: Schema.Literal("text"), -}) {} - -export class OutputFormatJsonSchema extends Schema.Class("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 - -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> - -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> - -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> - -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> - -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> - -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> - -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> - -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> - -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>, "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> - -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> - -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> - -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> - -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> - -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> - -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>, "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> - -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 - -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> - -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> - -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> - -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> - -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>, "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: define({ - type: "session.created", - ...options, - schema: { - sessionID: SessionSchema.ID, - info: SessionInfo, - }, - }), - Updated: define({ - type: "session.updated", - ...options, - schema: { - sessionID: SessionSchema.ID, - info: SessionInfo, - }, - }), - Deleted: define({ - type: "session.deleted", - ...options, - schema: { - sessionID: SessionSchema.ID, - info: SessionInfo, - }, - }), - MessageUpdated: define({ - type: "message.updated", - ...options, - schema: { - sessionID: SessionSchema.ID, - info: Info, - }, - }), - MessageRemoved: define({ - type: "message.removed", - ...options, - schema: { - sessionID: SessionSchema.ID, - messageID: MessageID, - }, - }), - PartUpdated: define({ - type: "message.part.updated", - ...options, - schema: { - sessionID: SessionSchema.ID, - part: Part, - time: Schema.Finite, - }, - }), - PartRemoved: define({ - type: "message.part.removed", - ...options, - schema: { - sessionID: SessionSchema.ID, - messageID: MessageID, - partID: PartID, - }, - }), -} - -export const Events = inventory( - Event.Created, - Event.Updated, - Event.Deleted, - Event.MessageUpdated, - Event.MessageRemoved, - Event.PartUpdated, - Event.PartRemoved, -) +export const ContentFilterError = NamedError.create("ContentFilterError", { message: Schema.String }) diff --git a/packages/core/test/legacy-event-schema.test.ts b/packages/core/test/legacy-event-schema.test.ts new file mode 100644 index 000000000000..d9a2833b6986 --- /dev/null +++ b/packages/core/test/legacy-event-schema.test.ts @@ -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 } }) + }) +}) diff --git a/packages/opencode/src/command/index.ts b/packages/opencode/src/command/index.ts index 0463e83f6bef..dacf41d0eeed 100644 --- a/packages/opencode/src/command/index.ts +++ b/packages/opencode/src/command/index.ts @@ -2,29 +2,20 @@ import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { InstanceState } from "@/effect/instance-state" import { EffectBridge } from "@/effect/bridge" import type { InstanceContext } from "@/project/instance-context" -import { SessionID, MessageID } from "@/session/schema" import { Effect, Layer, Context, Schema } from "effect" import { Config } from "@/config/config" import { MCP } from "../mcp" import { Skill } from "../skill" -import { EventV2 } from "@opencode-ai/core/event" import PROMPT_INITIALIZE from "./template/initialize.txt" import PROMPT_REVIEW from "./template/review.txt" +import { LegacyEvent } from "@opencode-ai/schema/legacy-event" type State = { commands: Record } export const Event = { - Executed: EventV2.define({ - type: "command.executed", - schema: { - name: Schema.String, - sessionID: SessionID, - arguments: Schema.String, - messageID: MessageID, - }, - }), + Executed: LegacyEvent.CommandExecuted, } export const Info = Schema.Struct({ diff --git a/packages/opencode/src/control-plane/workspace.ts b/packages/opencode/src/control-plane/workspace.ts index 0fdd6f0c7dc8..40195d480472 100644 --- a/packages/opencode/src/control-plane/workspace.ts +++ b/packages/opencode/src/control-plane/workspace.ts @@ -33,6 +33,7 @@ import { Vcs } from "@/project/vcs" import { InstanceStore } from "@/project/instance-store" import { InstanceBootstrap } from "@/project/bootstrap" import { WorkspaceAdapterRuntime } from "./workspace-adapter-runtime" +import { WorkspaceEvent } from "@opencode-ai/schema/workspace-event" export const Info = Schema.Struct({ ...WorkspaceInfoSchema.fields, @@ -40,27 +41,10 @@ export const Info = Schema.Struct({ }).annotate({ identifier: "Workspace" }) export type Info = WorkspaceInfo & { timeUsed: number } -export const ConnectionStatus = Schema.Struct({ - workspaceID: WorkspaceV2.ID, - status: Schema.Literals(["connected", "connecting", "disconnected", "error"]), -}) -export type ConnectionStatus = Schema.Schema.Type - -export const Event = { - 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 }), -} +export const ConnectionStatus = WorkspaceEvent.ConnectionStatus +export type ConnectionStatus = WorkspaceEvent.ConnectionStatus + +export const Event = WorkspaceEvent function fromRow(row: typeof WorkspaceTable.$inferSelect): Info { return { diff --git a/packages/opencode/src/event-manifest.ts b/packages/opencode/src/event-manifest.ts index 0b564882d24a..b84b3f29798b 100644 --- a/packages/opencode/src/event-manifest.ts +++ b/packages/opencode/src/event-manifest.ts @@ -1,60 +1,3 @@ export * as EventManifest from "./event-manifest" -import { Event } from "@opencode-ai/schema/event" -import { PublicEventManifest } from "@opencode-ai/core/public-event-manifest" -import { Command } from "@/command" -import { Workspace } from "@/control-plane/workspace" -import { Installation } from "@/installation" -import { LSP } from "@/lsp/lsp" -import { MCP } from "@/mcp" -import { Permission } from "@/permission" -import { Project } from "@/project/project" -import { Vcs } from "@/project/vcs" -import { Question } from "@/question" -import { Event as ServerEvent } from "@/server/event" -import { TuiEvent } from "@/server/tui-event" -import { MessageV2 } from "@/session/message-v2" -import { Session } from "@/session/session" -import { SessionCompaction } from "@/session/compaction" -import { SessionStatus } from "@/session/status" -import { Todo } from "@/session/todo" -import { Worktree } from "@/worktree" - -export const Definitions = Event.inventory( - ...PublicEventManifest.FoundationDefinitions, - MessageV2.Event.PartDelta, - Session.Event.Diff, - Session.Event.Error, - Installation.Event.Updated, - Installation.Event.UpdateAvailable, - ...PublicEventManifest.FeatureDefinitions, - Todo.Event.Updated, - LSP.Event.Updated, - Permission.Event.Asked, - Permission.Event.Replied, - TuiEvent.PromptAppend, - TuiEvent.CommandExecute, - TuiEvent.ToastShow, - TuiEvent.SessionSelect, - MCP.ToolsChanged, - MCP.BrowserOpenFailed, - Command.Event.Executed, - Project.Event.Updated, - SessionStatus.Event.Status, - SessionStatus.Event.Idle, - Question.Event.Asked, - Question.Event.Replied, - Question.Event.Rejected, - SessionCompaction.Event.Compacted, - Vcs.Event.BranchUpdated, - Workspace.Event.Ready, - Workspace.Event.Failed, - Workspace.Event.Status, - Worktree.Event.Ready, - Worktree.Event.Failed, - ServerEvent.Connected, - ServerEvent.Disposed, -) - -export const Latest = Event.latest(Definitions) -export const Durable = Event.durable(Definitions) +export { Definitions, Durable, Latest } from "@opencode-ai/schema/event-manifest" diff --git a/packages/opencode/src/ide/index.ts b/packages/opencode/src/ide/index.ts index fd8b8fc8cb6d..31b3add14b1d 100644 --- a/packages/opencode/src/ide/index.ts +++ b/packages/opencode/src/ide/index.ts @@ -1,7 +1,7 @@ -import { EventV2 } from "@opencode-ai/core/event" import { Schema } from "effect" import { NamedError } from "@opencode-ai/core/util/error" import { Process } from "@/util/process" +import { IdeEvent } from "@opencode-ai/schema/ide-event" const SUPPORTED_IDES = [ { name: "Windsurf" as const, cmd: "windsurf" }, @@ -11,14 +11,7 @@ const SUPPORTED_IDES = [ { name: "VSCodium" as const, cmd: "codium" }, ] -export const Event = { - Installed: EventV2.define({ - type: "ide.installed", - schema: { - ide: Schema.String, - }, - }), -} +export const Event = IdeEvent export const AlreadyInstalledError = NamedError.create("AlreadyInstalledError", {}) diff --git a/packages/opencode/src/installation/index.ts b/packages/opencode/src/installation/index.ts index 0ed10dc327dd..b4b888ed78d4 100644 --- a/packages/opencode/src/installation/index.ts +++ b/packages/opencode/src/installation/index.ts @@ -8,30 +8,17 @@ import { errorMessage } from "@/util/error" import { ChildProcess } from "effect/unstable/process" import { AppProcess } from "@opencode-ai/core/process" import path from "path" -import { EventV2 } from "@opencode-ai/core/event" import { makeRuntime } from "@opencode-ai/core/effect/runtime" import semver from "semver" import { InstallationChannel, InstallationVersion } from "@opencode-ai/core/installation/version" 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 ReleaseType = "patch" | "minor" | "major" -export const Event = { - Updated: EventV2.define({ - type: "installation.updated", - schema: { - version: Schema.String, - }, - }), - UpdateAvailable: EventV2.define({ - type: "installation.update-available", - schema: { - version: Schema.String, - }, - }), -} +export const Event = InstallationEvent export function getReleaseType(current: string, latest: string): ReleaseType { const currMajor = semver.major(current) diff --git a/packages/opencode/src/lsp/lsp.ts b/packages/opencode/src/lsp/lsp.ts index 0e7cf82d9d1e..bf19dda7c86d 100644 --- a/packages/opencode/src/lsp/lsp.ts +++ b/packages/opencode/src/lsp/lsp.ts @@ -1,7 +1,6 @@ import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { FSUtil } from "@opencode-ai/core/fs-util" import { EventV2Bridge } from "@/event-v2-bridge" -import { EventV2 } from "@opencode-ai/core/event" import * as LSPClient from "./client" import path from "path" import { pathToFileURL, fileURLToPath } from "url" @@ -14,10 +13,9 @@ import { InstanceState } from "@/effect/instance-state" import { containsPath } from "@/project/instance-context" import { NonNegativeInt } from "@opencode-ai/core/schema" import { RuntimeFlags } from "@/effect/runtime-flags" +import { LspEvent } from "@opencode-ai/schema/lsp-event" -export const Event = { - Updated: EventV2.define({ type: "lsp.updated", schema: {} }), -} +export const Event = LspEvent const Position = Schema.Struct({ line: NonNegativeInt, diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index bb24d4bd9e76..db673244b3d7 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -26,7 +26,6 @@ import { McpOAuthProvider, OAUTH_CALLBACK_PATH } from "./oauth-provider" import { McpOAuthCallback } from "./oauth-callback" import { McpAuth } from "./auth" import { EventV2Bridge } from "@/event-v2-bridge" -import { EventV2 } from "@opencode-ai/core/event" import { TuiEvent } from "@/server/tui-event" import open from "open" 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 { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { McpCatalog } from "./catalog" +import { McpEvent } from "@opencode-ai/schema/mcp-event" const DEFAULT_TIMEOUT = 30_000 const CLIENT_OPTIONS = { @@ -59,20 +59,9 @@ export const Resource = Schema.Struct({ }).annotate({ identifier: "McpResource" }) export type Resource = Schema.Schema.Type -export const ToolsChanged = EventV2.define({ - type: "mcp.tools.changed", - schema: { - server: Schema.String, - }, -}) +export const ToolsChanged = McpEvent.ToolsChanged -export const BrowserOpenFailed = EventV2.define({ - type: "mcp.browser.open.failed", - schema: { - mcpName: Schema.String, - url: Schema.String, - }, -}) +export const BrowserOpenFailed = McpEvent.BrowserOpenFailed export const Failed = NamedError.create("MCPFailed", { name: Schema.String, diff --git a/packages/opencode/src/permission/index.ts b/packages/opencode/src/permission/index.ts index 553b3e5b1f3d..92f236594c40 100644 --- a/packages/opencode/src/permission/index.ts +++ b/packages/opencode/src/permission/index.ts @@ -6,19 +6,9 @@ import { Deferred, Effect, Layer, Context } from "effect" import os from "os" import { PermissionV1 } from "@opencode-ai/core/v1/permission" import { EventV2Bridge } from "@/event-v2-bridge" -import { EventV2 } from "@opencode-ai/core/event" - -export const Event = { - 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, - }, - }), -} +import { PermissionV1Event } from "@opencode-ai/schema/permission-v1" + +export const Event = PermissionV1Event export interface Interface { readonly ask: (input: PermissionV1.AskInput) => Effect.Effect diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index 5f1b64743a74..9e290a86604e 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -16,46 +16,19 @@ import { FSUtil } from "@opencode-ai/core/fs-util" import { AppProcess } from "@opencode-ai/core/process" import { ProjectV2 } from "@opencode-ai/core/project" 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 { RuntimeFlags } from "@/effect/runtime-flags" import { EventV2Bridge } from "@/event-v2-bridge" import { EventV2 } from "@opencode-ai/core/event" +import { LegacyEvent } from "@opencode-ai/schema/legacy-event" +import { Project } from "@opencode-ai/schema/project" -const ProjectVcs = Schema.Literal("git") - -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 const Info = Project.Info export type Info = Types.DeepMutable> export const Event = { - Updated: EventV2.define({ type: "project.updated", schema: Info.fields }), + Updated: LegacyEvent.ProjectUpdated, } type Row = typeof ProjectTable.$inferSelect @@ -72,7 +45,7 @@ export function fromRow(row: Row): Info { return { id: row.id, 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, icon, time: { @@ -88,15 +61,15 @@ export function fromRow(row: Row): Info { export const UpdateInput = Schema.Struct({ projectID: ProjectV2.ID, name: Schema.optional(Schema.String), - icon: Schema.optional(ProjectIcon), - commands: Schema.optional(ProjectCommands), + icon: Schema.optional(Project.Icon), + commands: Schema.optional(Project.Commands), }) export type UpdateInput = Types.DeepMutable> export const UpdatePayload = Schema.Struct({ name: Schema.optional(Schema.String), - icon: Schema.optional(ProjectIcon), - commands: Schema.optional(ProjectCommands), + icon: Schema.optional(Project.Icon), + commands: Schema.optional(Project.Commands), }).annotate({ identifier: "ProjectUpdateInput" }) export type UpdatePayload = Types.DeepMutable> @@ -135,7 +108,6 @@ export const layer = Layer.effect( Service, Effect.gen(function* () { const fs = yield* FSUtil.Service - const proc = yield* AppProcess.Service const spawner = yield* ChildProcessSpawner.ChildProcessSpawner const projectV2 = yield* ProjectV2.Service const projectDirectories = yield* ProjectDirectories.Service @@ -168,7 +140,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 diff --git a/packages/opencode/src/project/vcs.ts b/packages/opencode/src/project/vcs.ts index decb3cfd989d..6f8c86b0b3fe 100644 --- a/packages/opencode/src/project/vcs.ts +++ b/packages/opencode/src/project/vcs.ts @@ -1,11 +1,12 @@ 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 { InstanceState } from "@/effect/instance-state" import { Watcher } from "@opencode-ai/core/filesystem/watcher" import { Git } from "@/git" import { EventV2Bridge } from "@/event-v2-bridge" import { EventV2 } from "@opencode-ai/core/event" +import { VcsEvent } from "@opencode-ai/schema/vcs-event" const PATCH_CONTEXT_LINES = 2_147_483_647 const MAX_PATCH_BYTES = 10_000_000 @@ -234,14 +235,7 @@ const track = Effect.fnUntraced(function* ( export const Mode = Schema.Literals(["git", "branch"]) export type Mode = Schema.Schema.Type -export const Event = { - BranchUpdated: EventV2.define({ - type: "vcs.branch.updated", - schema: { - branch: Schema.optional(Schema.String), - }, - }), -} +export const Event = VcsEvent export const Info = Schema.Struct({ branch: Schema.optional(Schema.String), diff --git a/packages/opencode/src/question/index.ts b/packages/opencode/src/question/index.ts index 61bdc40ee8f3..0517ec7ebe68 100644 --- a/packages/opencode/src/question/index.ts +++ b/packages/opencode/src/question/index.ts @@ -1,94 +1,28 @@ import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { Deferred, Effect, Layer, Schema, Context } from "effect" import { InstanceState } from "@/effect/instance-state" -import { SessionID, MessageID } from "@/session/schema" +import { SessionID } from "@/session/schema" import { QuestionID } from "./schema" import { EventV2Bridge } from "@/event-v2-bridge" -import { EventV2 } from "@opencode-ai/core/event" - -// Schemas — these are pure data; nothing checks class identity (see PR -// description) so they're plain `Schema.Struct` + type alias. That lets -// `Question.ask` and other internal sites trust the type contract without a -// re-decode to coerce nested class instances. - -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" }) -export type Option = Schema.Schema.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.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 - -export const Prompt = Schema.Struct(base).annotate({ identifier: "QuestionPrompt" }) -export type Prompt = Schema.Schema.Type - -export const Tool = Schema.Struct({ - messageID: MessageID, - callID: Schema.String, -}).annotate({ identifier: "QuestionTool" }) -export type Tool = Schema.Schema.Type - -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 - -export const Answer = Schema.Array(Schema.String).annotate({ identifier: "QuestionAnswer" }) -export type Answer = Schema.Schema.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: "QuestionReply" }) -export type Reply = Schema.Schema.Type - -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 }), -} +import { QuestionV1 } from "@opencode-ai/schema/question-v1" + +export const Option = QuestionV1.Option +export type Option = typeof Option.Type +export const Info = QuestionV1.Info +export type Info = typeof Info.Type +export const Prompt = QuestionV1.Prompt +export type Prompt = typeof Prompt.Type +export const Tool = QuestionV1.Tool +export type Tool = typeof Tool.Type +export const Request = QuestionV1.Request +export type Request = typeof Request.Type +export const Answer = QuestionV1.Answer +export type Answer = typeof Answer.Type +export const Reply = QuestionV1.Reply +export type Reply = typeof Reply.Type +export const Replied = QuestionV1.Replied +export const Rejected = QuestionV1.Rejected +export const Event = QuestionV1.Event export class RejectedError extends Schema.TaggedErrorClass()("QuestionRejectedError", {}) { override get message() { diff --git a/packages/opencode/src/question/schema.ts b/packages/opencode/src/question/schema.ts index 2574594a2301..ed7f1edee7fc 100644 --- a/packages/opencode/src/question/schema.ts +++ b/packages/opencode/src/question/schema.ts @@ -1,10 +1,4 @@ -import { Schema } from "effect" +import { QuestionV1 } from "@opencode-ai/schema/question-v1" -import { Identifier } from "@/id/id" -import { Newtype } from "@opencode-ai/core/schema" - -export class QuestionID extends Newtype()("QuestionID", Schema.String.check(Schema.isStartsWith("que"))) { - static ascending(id?: string): QuestionID { - return this.make(Identifier.ascending("question", id)) - } -} +export const QuestionID = QuestionV1.ID +export type QuestionID = typeof QuestionID.Type diff --git a/packages/opencode/src/server/event.ts b/packages/opencode/src/server/event.ts index a58131255587..9bb8a8c3b04f 100644 --- a/packages/opencode/src/server/event.ts +++ b/packages/opencode/src/server/event.ts @@ -1,10 +1,7 @@ -import { EventV2 } from "@opencode-ai/core/event" import { Schema } from "effect" +import { ServerEvent } from "@opencode-ai/schema/server-event" -export const Event = { - Connected: EventV2.define({ type: "server.connected", schema: {} }), - Disposed: EventV2.define({ type: "global.disposed", schema: {} }), -} +export const Event = ServerEvent export const InstanceDisposed = Schema.Struct({ id: Schema.String, diff --git a/packages/opencode/src/server/tui-event.ts b/packages/opencode/src/server/tui-event.ts index 73412b8778b9..3fb3576db009 100644 --- a/packages/opencode/src/server/tui-event.ts +++ b/packages/opencode/src/server/tui-event.ts @@ -1,53 +1 @@ -import { SessionID } from "@/session/schema" -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" }), - }, - }), -} +export { TuiEvent } from "@opencode-ai/schema/tui-event" diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index c7ac963c690e..fe4df3461912 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -23,17 +23,10 @@ import { SessionEvent } from "@opencode-ai/core/session/event" import { SessionMessage } from "@opencode-ai/core/session/message" import { ProviderV2 } from "@opencode-ai/core/provider" import { ModelV2 } from "@opencode-ai/core/model" -import { EventV2 } from "@opencode-ai/core/event" import { buildPrompt } from "@opencode-ai/core/session/compaction" +import { SessionCompactionEvent } from "@opencode-ai/schema/session-compaction-event" -export const Event = { - Compacted: EventV2.define({ - type: "session.compacted", - schema: { - sessionID: SessionID, - }, - }), -} +export const Event = SessionCompactionEvent export const PRUNE_MINIMUM = 20_000 export const PRUNE_PROTECT = 40_000 diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 813fe49f325d..7cd82da16d01 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -1,5 +1,4 @@ -import { EventV2 } from "@opencode-ai/core/event" -import { SessionID, MessageID, PartID } from "./schema" +import { SessionID, MessageID } from "./schema" import { SessionV1 } from "@opencode-ai/core/v1/session" import { ProviderV2 } from "@opencode-ai/core/provider" import { @@ -12,11 +11,9 @@ import { Info, OutputLengthError, Part, - StructuredOutputError, SubtaskPart, User, WithParts, - type ToolPart, } from "@opencode-ai/core/v1/session" import { NamedError } from "@opencode-ai/core/util/error" @@ -38,6 +35,7 @@ import { isMedia } from "@/util/media" import type { SystemError } from "bun" import type { Provider } from "@/provider/provider" import { Effect, Schema } from "effect" +import { SessionV1PublicEvent } from "@opencode-ai/schema/session-v1" export const node = LayerNode.group([Database.node]) @@ -61,16 +59,7 @@ export const Event = { Updated: SessionV1.Event.MessageUpdated, Removed: SessionV1.Event.MessageRemoved, PartUpdated: SessionV1.Event.PartUpdated, - PartDelta: EventV2.define({ - type: "message.part.delta", - schema: { - sessionID: SessionID, - messageID: MessageID, - partID: PartID, - field: Schema.String, - delta: Schema.String, - }, - }), + PartDelta: SessionV1PublicEvent.PartDelta, PartRemoved: SessionV1.Event.PartRemoved, } diff --git a/packages/opencode/src/session/session.ts b/packages/opencode/src/session/session.ts index 038ec9aa0d6e..a40fa24dd880 100644 --- a/packages/opencode/src/session/session.ts +++ b/packages/opencode/src/session/session.ts @@ -11,7 +11,6 @@ import { InstallationVersion } from "@opencode-ai/core/installation/version" import { Database } from "@opencode-ai/core/database/database" import { makeRuntime } from "@opencode-ai/core/effect/runtime" import { EventV2Bridge } from "@/event-v2-bridge" -import { EventV2 } from "@opencode-ai/core/event" import { SessionV2 } from "@opencode-ai/core/session" import { SessionExecution } from "@opencode-ai/core/session/execution" @@ -38,13 +37,13 @@ import { WorkspaceV2 } from "@opencode-ai/core/workspace" import { SessionID, MessageID, PartID } from "./schema" import type { Provider } from "@/provider/provider" -import { Permission } from "@/permission" import { Global } from "@opencode-ai/core/global" import { Effect, Layer, Option, Context, Schema, Types } from "effect" import { NonNegativeInt, optionalOmitUndefined } from "@opencode-ai/core/schema" import { RuntimeFlags } from "@/effect/runtime-flags" import { ProviderV2 } from "@opencode-ai/core/provider" import { ModelV2 } from "@opencode-ai/core/model" +import { SessionV1PublicEvent } from "@opencode-ai/schema/session-v1" const runtime = makeRuntime(Database.Service, Database.defaultLayer) @@ -309,69 +308,12 @@ export type GlobalListInput = { 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 = { Created: SessionV1.Event.Created, Updated: SessionV1.Event.Updated, Deleted: SessionV1.Event.Deleted, - Diff: EventV2.define({ - type: "session.diff", - 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, - }, - }), + Diff: SessionV1PublicEvent.Diff, + Error: SessionV1PublicEvent.Error, } export function plan(input: { slug: string; time: { created: number } }, instance: InstanceContext) { diff --git a/packages/opencode/src/session/status.ts b/packages/opencode/src/session/status.ts index 68758ea6a3a6..a5714c55d5a7 100644 --- a/packages/opencode/src/session/status.ts +++ b/packages/opencode/src/session/status.ts @@ -1,53 +1,14 @@ import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { InstanceState } from "@/effect/instance-state" import { SessionID } from "./schema" -import { NonNegativeInt } from "@opencode-ai/core/schema" -import { Effect, Layer, Context, Schema } from "effect" +import { Effect, Layer, Context } from "effect" 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([ - 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 +export const Info = SessionStatusEvent.Info +export type Info = SessionStatusEvent.Info -export const Event = { - Status: EventV2.define({ - type: "session.status", - schema: { - sessionID: SessionID, - status: Info, - }, - }), - // deprecated - Idle: EventV2.define({ - type: "session.idle", - schema: { - sessionID: SessionID, - }, - }), -} +export const Event = SessionStatusEvent export interface Interface { readonly get: (sessionID: SessionID) => Effect.Effect diff --git a/packages/opencode/src/session/todo.ts b/packages/opencode/src/session/todo.ts index 6e9eeba62b80..462613522dce 100644 --- a/packages/opencode/src/session/todo.ts +++ b/packages/opencode/src/session/todo.ts @@ -1,31 +1,17 @@ import { LayerNode } from "@opencode-ai/core/effect/layer-node" 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 { eq } from "drizzle-orm" import { asc } from "drizzle-orm" import { TodoTable } from "@opencode-ai/core/session/sql" 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({ - 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 = Schema.Schema.Type +export const Info = SessionTodo.Info +export type Info = SessionTodo.Info -export const Event = { - Updated: EventV2.define({ - type: "todo.updated", - schema: { - sessionID: SessionID, - todos: Schema.Array(Info), - }, - }), -} +export const Event = SessionTodo.Event export interface Interface { readonly update: (input: { sessionID: SessionID; todos: Info[] }) => Effect.Effect diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index 604e046d9547..bf39b6eacc13 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -9,6 +9,7 @@ import { FSUtil } from "@opencode-ai/core/fs-util" import { Hash } from "@opencode-ai/core/util/hash" import { Config } from "@/config/config" import { Global } from "@opencode-ai/core/global" +import { FileDiffInfo } from "@opencode-ai/schema/file-diff" export const Patch = Schema.Struct({ hash: Schema.String, @@ -16,16 +17,7 @@ export const Patch = Schema.Struct({ }) export type Patch = typeof Patch.Type -export const FileDiff = Schema.Struct({ - // 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 const FileDiff = FileDiffInfo export type FileDiff = typeof FileDiff.Type const prune = "7.days" diff --git a/packages/opencode/src/worktree/index.ts b/packages/opencode/src/worktree/index.ts index 8ee485b8131a..f21a42c6ac9c 100644 --- a/packages/opencode/src/worktree/index.ts +++ b/packages/opencode/src/worktree/index.ts @@ -10,7 +10,6 @@ import { ProjectTable } from "@opencode-ai/core/project/sql" import type { ProjectV2 } from "@opencode-ai/core/project" import { Slug } from "@opencode-ai/core/util/slug" import { errorMessage } from "../util/error" -import { EventV2 } from "@opencode-ai/core/event" import { GlobalBus } from "@/bus/global" import { Git } from "@/git" 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 { AppProcess } from "@opencode-ai/core/process" import { InstanceState } from "@/effect/instance-state" +import { WorktreeEvent } from "@opencode-ai/schema/worktree-event" -export const Event = { - 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 Event = WorktreeEvent export const Info = Schema.Struct({ name: Schema.String, diff --git a/packages/opencode/test/event-manifest.test.ts b/packages/opencode/test/event-manifest.test.ts index fabd35b060db..a655039a3d22 100644 --- a/packages/opencode/test/event-manifest.test.ts +++ b/packages/opencode/test/event-manifest.test.ts @@ -1,10 +1,14 @@ 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) diff --git a/packages/schema/src/catalog.ts b/packages/schema/src/catalog.ts new file mode 100644 index 000000000000..7c08fa13655a --- /dev/null +++ b/packages/schema/src/catalog.ts @@ -0,0 +1,8 @@ +export * as Catalog from "./catalog" + +import { define } from "./event" + +export const Event = { + Updated: define({ type: "catalog.updated", schema: {} }), +} +export const CatalogEvent = Event diff --git a/packages/schema/src/event-manifest.ts b/packages/schema/src/event-manifest.ts new file mode 100644 index 000000000000..34e8eab48598 --- /dev/null +++ b/packages/schema/src/event-manifest.ts @@ -0,0 +1,105 @@ +export * as EventManifest from "./event-manifest" + +import { Catalog } from "./catalog" +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 { 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" + +export const CoreDefinitions = Event.inventory(...SessionV1.Events, ...SessionEvent.Definitions) +export const CoreLatest = Event.latest(CoreDefinitions) +export const CoreDurable = Event.durable(CoreDefinitions) + +export const FoundationDefinitions = Event.inventory( + ModelsDev.Event.Refreshed, + Integration.Event.Updated, + Integration.Event.ConnectionUpdated, + Catalog.Event.Updated, + ...CoreDefinitions, +) + +export const FeatureDefinitions = Event.inventory( + FileSystem.Event.Edited, + Reference.Event.Updated, + Permission.Event.Asked, + Permission.Event.Replied, + Plugin.Event.Added, + ProjectDirectories.Event.Updated, + FileSystemWatcher.Event.Updated, + Pty.Event.Created, + Pty.Event.Updated, + Pty.Event.Exited, + Pty.Event.Deleted, + Question.Event.Asked, + Question.Event.Replied, + Question.Event.Rejected, +) + +export const PublicDefinitions = Event.inventory( + ...FoundationDefinitions, + ...FeatureDefinitions, + SessionTodo.Event.Updated, +) +export const PublicLatest = Event.latest(PublicDefinitions) +export const PublicDurable = Event.durable(PublicDefinitions) + +export const Definitions = Event.inventory( + ...FoundationDefinitions, + SessionV1.PartDelta, + SessionV1.Diff, + SessionV1.Error, + InstallationEvent.Updated, + InstallationEvent.UpdateAvailable, + ...FeatureDefinitions, + SessionTodo.Event.Updated, + LspEvent.Updated, + PermissionV1.Event.Asked, + PermissionV1.Event.Replied, + TuiEvent.PromptAppend, + TuiEvent.CommandExecute, + TuiEvent.ToastShow, + TuiEvent.SessionSelect, + McpEvent.ToolsChanged, + McpEvent.BrowserOpenFailed, + LegacyEvent.CommandExecuted, + LegacyEvent.ProjectUpdated, + SessionStatusEvent.Status, + SessionStatusEvent.Idle, + QuestionV1.Event.Asked, + QuestionV1.Event.Replied, + QuestionV1.Event.Rejected, + SessionCompactionEvent.Compacted, + VcsEvent.BranchUpdated, + WorkspaceEvent.Ready, + WorkspaceEvent.Failed, + WorkspaceEvent.Status, + WorktreeEvent.Ready, + WorktreeEvent.Failed, + ServerEvent.Connected, + ServerEvent.Disposed, +) +export const Latest = Event.latest(Definitions) +export const Durable = Event.durable(Definitions) diff --git a/packages/schema/src/file-diff.ts b/packages/schema/src/file-diff.ts new file mode 100644 index 000000000000..73d69c1a5604 --- /dev/null +++ b/packages/schema/src/file-diff.ts @@ -0,0 +1,13 @@ +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 +export const FileDiffInfo = Info diff --git a/packages/schema/src/filesystem-watcher.ts b/packages/schema/src/filesystem-watcher.ts new file mode 100644 index 000000000000..56fcecb7f25f --- /dev/null +++ b/packages/schema/src/filesystem-watcher.ts @@ -0,0 +1,14 @@ +export * as FileSystemWatcher from "./filesystem-watcher" + +import { Schema } from "effect" +import { define } from "./event" + +export const Event = { + Updated: define({ + type: "file.watcher.updated", + schema: { + file: Schema.String, + event: Schema.Literals(["add", "change", "unlink"]), + }, + }), +} diff --git a/packages/schema/src/filesystem.ts b/packages/schema/src/filesystem.ts index f638696e61bc..f6b353d0991c 100644 --- a/packages/schema/src/filesystem.ts +++ b/packages/schema/src/filesystem.ts @@ -1,8 +1,17 @@ export * as FileSystem from "./filesystem" import { Schema } from "effect" +import { define } from "./event" import { NonNegativeInt, PositiveInt, RelativePath } from "./schema" +export const Event = { + Edited: define({ + type: "file.edited", + schema: { file: Schema.String }, + }), +} +export const FileSystemEvent = Event + export interface Entry extends Schema.Schema.Type {} export const Entry = Schema.Struct({ path: RelativePath, diff --git a/packages/schema/src/ide-event.ts b/packages/schema/src/ide-event.ts new file mode 100644 index 000000000000..e572a7c2b539 --- /dev/null +++ b/packages/schema/src/ide-event.ts @@ -0,0 +1,11 @@ +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, + }, +}) diff --git a/packages/schema/src/installation-event.ts b/packages/schema/src/installation-event.ts new file mode 100644 index 000000000000..879e708891a0 --- /dev/null +++ b/packages/schema/src/installation-event.ts @@ -0,0 +1,18 @@ +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, + }, +}) diff --git a/packages/schema/src/integration.ts b/packages/schema/src/integration.ts index b30b333a4118..299be5bc2593 100644 --- a/packages/schema/src/integration.ts +++ b/packages/schema/src/integration.ts @@ -1,6 +1,7 @@ export * as Integration from "./integration" import { Schema } from "effect" +import { define } from "./event" export const ID = Schema.String.pipe(Schema.brand("Integration.ID")) export type ID = typeof ID.Type @@ -72,6 +73,17 @@ export type Method = typeof Method.Type export const Inputs = Schema.Record(Schema.String, Schema.String).annotate({ identifier: "Integration.Inputs" }) export type Inputs = typeof Inputs.Type +export const Event = { + Updated: define({ + type: "integration.updated", + schema: {}, + }), + ConnectionUpdated: define({ + type: "integration.connection.updated", + schema: { integrationID: ID }, + }), +} + export interface Ref extends Schema.Schema.Type {} export const Ref = Schema.Struct({ id: ID, diff --git a/packages/schema/src/legacy-event.ts b/packages/schema/src/legacy-event.ts new file mode 100644 index 000000000000..25bc6ee02ee4 --- /dev/null +++ b/packages/schema/src/legacy-event.ts @@ -0,0 +1,19 @@ +export * as LegacyEvent from "./legacy-event" + +import { Schema } from "effect" +import { define } from "./event" +import { Project } from "./project" +import { Session } from "./session" +import { SessionV1 } from "./session-v1" + +export const ProjectUpdated = define({ type: "project.updated", schema: Project.Info.fields }) + +export const CommandExecuted = define({ + type: "command.executed", + schema: { + name: Schema.String, + sessionID: Session.ID, + arguments: Schema.String, + messageID: SessionV1.MessageID, + }, +}) diff --git a/packages/schema/src/lsp-event.ts b/packages/schema/src/lsp-event.ts new file mode 100644 index 000000000000..a62b022b8bab --- /dev/null +++ b/packages/schema/src/lsp-event.ts @@ -0,0 +1,5 @@ +export * as LspEvent from "./lsp-event" + +import { Event } from "./event" + +export const Updated = Event.define({ type: "lsp.updated", schema: {} }) diff --git a/packages/schema/src/mcp-event.ts b/packages/schema/src/mcp-event.ts new file mode 100644 index 000000000000..a30a83253f4d --- /dev/null +++ b/packages/schema/src/mcp-event.ts @@ -0,0 +1,19 @@ +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, + }, +}) diff --git a/packages/schema/src/models-dev.ts b/packages/schema/src/models-dev.ts new file mode 100644 index 000000000000..8b7697574cda --- /dev/null +++ b/packages/schema/src/models-dev.ts @@ -0,0 +1,10 @@ +export * as ModelsDev from "./models-dev" + +import { define } from "./event" + +export const Event = { + Refreshed: define({ + type: "models-dev.refreshed", + schema: {}, + }), +} diff --git a/packages/schema/src/permission-v1.ts b/packages/schema/src/permission-v1.ts new file mode 100644 index 000000000000..a7f7c0b1a8f0 --- /dev/null +++ b/packages/schema/src/permission-v1.ts @@ -0,0 +1,68 @@ +export * as PermissionV1 from "./permission-v1" + +import { Schema } from "effect" +import { define } from "./event" +import { ascending } from "./identifier" +import { Project } from "./project" +import { withStatics } from "./schema" +import { Session } from "./session" + +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: Session.ID, + 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 + +export const Event = { + Asked: define({ type: "permission.asked", schema: Request.fields }), + Replied: define({ + type: "permission.replied", + schema: { sessionID: Session.ID, requestID: ID, reply: Reply }, + }), +} +export const PermissionV1Event = Event diff --git a/packages/schema/src/permission.ts b/packages/schema/src/permission.ts index 0c3227335ac8..3e7970f4d13c 100644 --- a/packages/schema/src/permission.ts +++ b/packages/schema/src/permission.ts @@ -1,6 +1,55 @@ export * as Permission from "./permission" import { Schema } from "effect" +import { define } 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.ID, + 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 + +export const Event = { + Asked: define({ type: "permission.v2.asked", schema: Request.fields }), + Replied: define({ + type: "permission.v2.replied", + schema: { + sessionID: SessionID.ID, + requestID: ID, + reply: Reply, + }, + }), +} export const Effect = Schema.Literals(["allow", "deny", "ask"]).annotate({ identifier: "PermissionV2.Effect" }) export type Effect = typeof Effect.Type diff --git a/packages/schema/src/plugin.ts b/packages/schema/src/plugin.ts new file mode 100644 index 000000000000..9c002da57c34 --- /dev/null +++ b/packages/schema/src/plugin.ts @@ -0,0 +1,16 @@ +export * as Plugin from "./plugin" + +import { Schema } from "effect" +import { define } from "./event" + +export const ID = Schema.String.pipe(Schema.brand("Plugin.ID")) +export type ID = typeof ID.Type +export const PluginID = ID + +export const Event = { + Added: define({ + type: "plugin.added", + schema: { id: ID }, + }), +} +export const PluginEvent = Event diff --git a/packages/schema/src/project-directories.ts b/packages/schema/src/project-directories.ts new file mode 100644 index 000000000000..fdcbd6535738 --- /dev/null +++ b/packages/schema/src/project-directories.ts @@ -0,0 +1,12 @@ +export * as ProjectDirectories from "./project-directories" + +import { define } from "./event" +import { Project } from "./project" + +export const Event = { + Updated: define({ + type: "project.directories.updated", + schema: { projectID: Project.ID }, + }), +} +export const ProjectDirectoriesEvent = Event diff --git a/packages/schema/src/project.ts b/packages/schema/src/project.ts index 3a957a034710..64ed38ce5a6b 100644 --- a/packages/schema/src/project.ts +++ b/packages/schema/src/project.ts @@ -1,10 +1,39 @@ export * as Project from "./project" import { Schema } from "effect" -import { withStatics } from "./schema" +import { NonNegativeInt, optionalOmitUndefined, withStatics } from "./schema" export const ID = Schema.String.pipe( Schema.brand("Project.ID"), withStatics((schema) => ({ global: schema.make("global") })), ) export type ID = typeof ID.Type + +export const Vcs = Schema.Literal("git") +export const Icon = Schema.Struct({ + url: optionalOmitUndefined(Schema.String), + override: optionalOmitUndefined(Schema.String), + color: optionalOmitUndefined(Schema.String), +}) +export const Commands = Schema.Struct({ + start: optionalOmitUndefined( + Schema.String.annotate({ description: "Startup script to run when creating a new workspace (worktree)" }), + ), +}) +export const Time = Schema.Struct({ + created: NonNegativeInt, + updated: NonNegativeInt, + initialized: optionalOmitUndefined(NonNegativeInt), +}) + +export const Info = Schema.Struct({ + id: ID, + worktree: Schema.String, + vcs: optionalOmitUndefined(Vcs), + name: optionalOmitUndefined(Schema.String), + icon: optionalOmitUndefined(Icon), + commands: optionalOmitUndefined(Commands), + time: Time, + sandboxes: Schema.Array(Schema.String), +}).annotate({ identifier: "Project" }) +export type Info = typeof Info.Type diff --git a/packages/schema/src/pty.ts b/packages/schema/src/pty.ts new file mode 100644 index 000000000000..7bafe87b4663 --- /dev/null +++ b/packages/schema/src/pty.ts @@ -0,0 +1,36 @@ +export * as Pty from "./pty" + +import { Schema } from "effect" +import { define } 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 + +export const Event = { + Created: define({ type: "pty.created", schema: { info: Info } }), + Updated: define({ type: "pty.updated", schema: { info: Info } }), + Exited: define({ type: "pty.exited", schema: { id: ID, exitCode: NonNegativeInt } }), + Deleted: define({ type: "pty.deleted", schema: { id: ID } }), +} +export const PtyEvent = Event diff --git a/packages/schema/src/question-v1.ts b/packages/schema/src/question-v1.ts new file mode 100644 index 000000000000..2bc79b0857bd --- /dev/null +++ b/packages/schema/src/question-v1.ts @@ -0,0 +1,58 @@ +export * as QuestionV1 from "./question-v1" + +import { Schema } from "effect" +import { define } from "./event" +import { ascending } from "./identifier" +import { withStatics } from "./schema" +import { Session } from "./session" +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: Session.ID, + 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: Session.ID, requestID: ID, answers: Schema.Array(Answer) }).annotate({ + identifier: "QuestionReplied", +}) +export const Rejected = Schema.Struct({ sessionID: Session.ID, requestID: ID }).annotate({ + identifier: "QuestionRejected", +}) + +export const Event = { + Asked: define({ type: "question.asked", schema: Request.fields }), + Replied: define({ type: "question.replied", schema: Replied.fields }), + Rejected: define({ type: "question.rejected", schema: Rejected.fields }), +} diff --git a/packages/schema/src/question.ts b/packages/schema/src/question.ts new file mode 100644 index 000000000000..fc7c5ea57f8a --- /dev/null +++ b/packages/schema/src/question.ts @@ -0,0 +1,80 @@ +export * as Question from "./question" + +import { Schema } from "effect" +import { define } 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.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 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 + +export const Event = { + Asked: define({ type: "question.v2.asked", schema: Request.fields }), + Replied: define({ + type: "question.v2.replied", + schema: { + sessionID: SessionID.ID, + requestID: ID, + answers: Schema.Array(Answer), + }, + }), + Rejected: define({ + type: "question.v2.rejected", + schema: { + sessionID: SessionID.ID, + requestID: ID, + }, + }), +} diff --git a/packages/schema/src/reference.ts b/packages/schema/src/reference.ts index b0a61cee42d8..ced0e9e46cf0 100644 --- a/packages/schema/src/reference.ts +++ b/packages/schema/src/reference.ts @@ -1,8 +1,13 @@ export * as Reference from "./reference" import { Schema } from "effect" +import { define } from "./event" import { AbsolutePath } from "./schema" +export const Event = { + Updated: define({ type: "reference.updated", schema: {} }), +} + export interface LocalSource extends Schema.Schema.Type {} export const LocalSource = Schema.Struct({ type: Schema.Literal("local"), diff --git a/packages/schema/src/server-event.ts b/packages/schema/src/server-event.ts new file mode 100644 index 000000000000..9959d477ef58 --- /dev/null +++ b/packages/schema/src/server-event.ts @@ -0,0 +1,6 @@ +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: {} }) diff --git a/packages/schema/src/session-compaction-event.ts b/packages/schema/src/session-compaction-event.ts new file mode 100644 index 000000000000..aed16fea75f5 --- /dev/null +++ b/packages/schema/src/session-compaction-event.ts @@ -0,0 +1,11 @@ +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.ID, + }, +}) diff --git a/packages/schema/src/session-event.ts b/packages/schema/src/session-event.ts new file mode 100644 index 000000000000..821482f1edcd --- /dev/null +++ b/packages/schema/src/session-event.ts @@ -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 { Session } from "./session" +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: Session.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 = 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"] diff --git a/packages/schema/src/session-status-event.ts b/packages/schema/src/session-status-event.ts new file mode 100644 index 000000000000..ef64fb4ed1ea --- /dev/null +++ b/packages/schema/src/session-status-event.ts @@ -0,0 +1,48 @@ +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 + +export const Status = Event.define({ + type: "session.status", + schema: { + sessionID: SessionID.ID, + status: Info, + }, +}) + +// deprecated +export const Idle = Event.define({ + type: "session.idle", + schema: { + sessionID: SessionID.ID, + }, +}) diff --git a/packages/schema/src/session-todo.ts b/packages/schema/src/session-todo.ts new file mode 100644 index 000000000000..7fd6a2bb9d6f --- /dev/null +++ b/packages/schema/src/session-todo.ts @@ -0,0 +1,26 @@ +export * as SessionTodo from "./session-todo" + +import { Schema } from "effect" +import { define } 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 + +export const Event = { + Updated: define({ + type: "todo.updated", + schema: { + sessionID: SessionID.ID, + todos: Schema.Array(Info), + }, + }), +} +export const SessionTodoEvent = Event diff --git a/packages/schema/src/session-v1.ts b/packages/schema/src/session-v1.ts new file mode 100644 index 000000000000..fb0bb355dd03 --- /dev/null +++ b/packages/schema/src/session-v1.ts @@ -0,0 +1,673 @@ +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 { Session } from "./session" +import { Workspace } from "./workspace" + +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: 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 +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")({ + type: Schema.Literal("text"), +}) {} + +export class OutputFormatJsonSchema extends Schema.Class("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 + +const partBase = { + id: PartID, + sessionID: Session.ID, + messageID: MessageID, +} + +export const SnapshotPart = Schema.Struct({ + ...partBase, + type: Schema.Literal("snapshot"), + snapshot: Schema.String, +}).annotate({ identifier: "SnapshotPart" }) +export type SnapshotPart = Types.DeepMutable> + +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> + +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> + +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> + +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> + +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> + +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> + +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> + +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>, "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> + +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> + +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> + +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> + +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> + +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> + +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>, "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> + +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 + +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> + +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> + +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> + +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> + +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>, "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: Session.ID, + slug: Schema.String, + projectID: Project.ID, + workspaceID: optionalOmitUndefined(Workspace.ID), + directory: Schema.String, + path: optionalOmitUndefined(Schema.String), + parentID: optionalOmitUndefined(Session.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: define({ + type: "session.created", + ...options, + schema: { + sessionID: Session.ID, + info: SessionInfo, + }, + }), + Updated: define({ + type: "session.updated", + ...options, + schema: { + sessionID: Session.ID, + info: SessionInfo, + }, + }), + Deleted: define({ + type: "session.deleted", + ...options, + schema: { + sessionID: Session.ID, + info: SessionInfo, + }, + }), + MessageUpdated: define({ + type: "message.updated", + ...options, + schema: { + sessionID: Session.ID, + info: Info, + }, + }), + MessageRemoved: define({ + type: "message.removed", + ...options, + schema: { + sessionID: Session.ID, + messageID: MessageID, + }, + }), + PartUpdated: define({ + type: "message.part.updated", + ...options, + schema: { + sessionID: Session.ID, + part: Part, + time: Schema.Finite, + }, + }), + PartRemoved: define({ + type: "message.part.removed", + ...options, + schema: { + sessionID: Session.ID, + messageID: MessageID, + partID: PartID, + }, + }), +} + +export const Events = inventory( + Event.Created, + Event.Updated, + Event.Deleted, + Event.MessageUpdated, + Event.MessageRemoved, + Event.PartUpdated, + Event.PartRemoved, +) + +export const PartDelta = define({ + type: "message.part.delta", + schema: { + sessionID: Session.ID, + messageID: MessageID, + partID: PartID, + field: Schema.String, + delta: Schema.String, + }, +}) + +export const Diff = define({ + type: "session.diff", + schema: { + sessionID: Session.ID, + diff: Schema.Array(FileDiff.Info), + }, +}) + +export const Error = define({ + type: "session.error", + schema: { + sessionID: Schema.optional(Session.ID), + error: Assistant.fields.error, + }, +}) + +export const SessionV1PublicEvent = { + PartDelta, + Diff, + Error, +} diff --git a/packages/schema/src/tui-event.ts b/packages/schema/src/tui-event.ts new file mode 100644 index 000000000000..a34a535c8e9e --- /dev/null +++ b/packages/schema/src/tui-event.ts @@ -0,0 +1,56 @@ +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.ID.annotate({ description: "Session ID to navigate to" }), + }, +}) diff --git a/packages/schema/src/vcs-event.ts b/packages/schema/src/vcs-event.ts new file mode 100644 index 000000000000..c64d999164d0 --- /dev/null +++ b/packages/schema/src/vcs-event.ts @@ -0,0 +1,11 @@ +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), + }, +}) diff --git a/packages/schema/src/workspace-event.ts b/packages/schema/src/workspace-event.ts new file mode 100644 index 000000000000..a429e7623f94 --- /dev/null +++ b/packages/schema/src/workspace-event.ts @@ -0,0 +1,30 @@ +export * as WorkspaceEvent from "./workspace-event" + +import { Schema } from "effect" +import { Event } from "./event" +import { Workspace } from "./workspace" + +export const ConnectionStatus = Schema.Struct({ + workspaceID: Workspace.ID, + 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, +}) diff --git a/packages/schema/src/worktree-event.ts b/packages/schema/src/worktree-event.ts new file mode 100644 index 000000000000..f5823c478816 --- /dev/null +++ b/packages/schema/src/worktree-event.ts @@ -0,0 +1,19 @@ +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, + }, +}) diff --git a/packages/schema/test/event-manifest.test.ts b/packages/schema/test/event-manifest.test.ts new file mode 100644 index 000000000000..5084d0de4a12 --- /dev/null +++ b/packages/schema/test/event-manifest.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, test } from "bun:test" +import { EventManifest } from "../src/event-manifest" +import { SessionEvent } from "../src/session-event" +import { SessionTodo } from "../src/session-todo" + +describe("public event manifest", () => { + test("owns the complete public event surface", () => { + expect(EventManifest.CoreDefinitions.length).toBe(36) + expect(EventManifest.PublicDefinitions.length).toBe(55) + expect(EventManifest.Definitions.length).toBe(85) + expect(EventManifest.Latest.size).toBe(85) + expect(EventManifest.Durable.size).toBe(32) + }) + + test("uses canonical definitions for current public events", () => { + 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.has("ide.installed")).toBe(false) + expect(EventManifest.Durable.has("session.next.step.ended.1")).toBe(false) + expect(EventManifest.Durable.get("session.next.step.ended.2")).toBe(SessionEvent.Step.Ended) + }) +}) diff --git a/packages/schema/test/legacy-event.test.ts b/packages/schema/test/legacy-event.test.ts new file mode 100644 index 000000000000..80524ddcb47c --- /dev/null +++ b/packages/schema/test/legacy-event.test.ts @@ -0,0 +1,47 @@ +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 { SessionV1 } from "../src/session-v1" + +describe("legacy public event schemas", () => { + test("keeps the seven durable SessionV1 definitions", () => { + expect(SessionV1.Events.map((event) => event.type)).toEqual([ + "session.created", + "session.updated", + "session.deleted", + "message.updated", + "message.removed", + "message.part.updated", + "message.part.removed", + ]) + expect(SessionV1.Events.every((event) => event.durable?.aggregate === "sessionID")).toBe(true) + expect(SessionV1.Events.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, + LegacyEvent.ProjectUpdated.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", + ]) + }) +}) From 84fb4496967be88f06f1bd86bfc36a8e39be4626 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 24 Jun 2026 16:10:26 -0400 Subject: [PATCH 5/7] refactor(schema): clarify event domain ownership --- bun.lock | 1 + packages/core/src/event-manifest.ts | 7 - packages/core/src/event.ts | 12 +- packages/core/src/public-event-manifest.ts | 8 +- packages/core/src/v1/session.ts | 1 - packages/core/test/event.test.ts | 250 +++++++++--------- packages/opencode/package.json | 1 + packages/opencode/src/project/project.ts | 3 +- packages/opencode/src/session/message-v2.ts | 3 +- packages/opencode/src/session/session.ts | 5 +- packages/schema/src/catalog.ts | 7 +- packages/schema/src/durable-event-manifest.ts | 10 + packages/schema/src/event-manifest.ts | 107 +++----- packages/schema/src/filesystem-watcher.ts | 19 +- packages/schema/src/filesystem.ts | 13 +- packages/schema/src/ide-event.ts | 2 + packages/schema/src/installation-event.ts | 2 + packages/schema/src/integration.ts | 21 +- packages/schema/src/legacy-event.ts | 11 +- packages/schema/src/location.ts | 4 +- packages/schema/src/lsp-event.ts | 2 + packages/schema/src/mcp-event.ts | 2 + packages/schema/src/models-dev.ts | 13 +- packages/schema/src/permission-v1.ts | 19 +- packages/schema/src/permission.ts | 25 +- packages/schema/src/plugin.ts | 13 +- packages/schema/src/project-directories.ts | 13 +- packages/schema/src/project.ts | 4 + packages/schema/src/pty.ts | 13 +- packages/schema/src/question-v1.ts | 24 +- packages/schema/src/question.ts | 39 ++- packages/schema/src/reference.ts | 7 +- packages/schema/src/server-event.ts | 2 + .../schema/src/session-compaction-event.ts | 4 +- packages/schema/src/session-event.ts | 4 +- packages/schema/src/session-id.ts | 6 +- packages/schema/src/session-input.ts | 2 +- packages/schema/src/session-message.ts | 2 +- packages/schema/src/session-status-event.ts | 6 +- packages/schema/src/session-todo.ts | 19 +- packages/schema/src/session-v1.ts | 59 +++-- packages/schema/src/session.ts | 7 +- packages/schema/src/tui-event.ts | 4 +- packages/schema/src/vcs-event.ts | 2 + packages/schema/src/workspace-event.ts | 6 +- packages/schema/src/workspace-id.ts | 19 ++ packages/schema/src/workspace.ts | 24 +- packages/schema/src/worktree-event.ts | 2 + packages/schema/test/event-manifest.test.ts | 35 ++- packages/schema/test/legacy-event.test.ts | 16 +- 50 files changed, 452 insertions(+), 428 deletions(-) delete mode 100644 packages/core/src/event-manifest.ts create mode 100644 packages/schema/src/durable-event-manifest.ts create mode 100644 packages/schema/src/workspace-id.ts diff --git a/bun.lock b/bun.lock index 411c09c8ffc4..0e40a5546c38 100644 --- a/bun.lock +++ b/bun.lock @@ -539,6 +539,7 @@ "@openauthjs/openauth": "catalog:", "@opencode-ai/llm": "workspace:*", "@opencode-ai/plugin": "workspace:*", + "@opencode-ai/schema": "workspace:*", "@opencode-ai/script": "workspace:*", "@opencode-ai/sdk": "workspace:*", "@opencode-ai/server": "workspace:*", diff --git a/packages/core/src/event-manifest.ts b/packages/core/src/event-manifest.ts deleted file mode 100644 index 43c55b4bcdce..000000000000 --- a/packages/core/src/event-manifest.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * as EventManifest from "./event-manifest" - -export { - CoreDefinitions as Definitions, - CoreDurable as Durable, - CoreLatest as Latest, -} from "@opencode-ai/schema/event-manifest" diff --git a/packages/core/src/event.ts b/packages/core/src/event.ts index 6a75c9731960..132a88b11125 100644 --- a/packages/core/src/event.ts +++ b/packages/core/src/event.ts @@ -9,7 +9,7 @@ import { EventSequenceTable, EventTable } from "./event/sql" import { Location } from "./location" import { LayerNode } from "./effect/layer-node" import { isDeepStrictEqual } from "node:util" -import { EventManifest } from "./event-manifest" +import { Durable } from "@opencode-ai/schema/durable-event-manifest" export const ID = Event.ID export type ID = import("@opencode-ai/schema/event").ID @@ -86,23 +86,19 @@ export class Service extends Context.Service()("@opencode/Ev export interface LayerOptions { readonly beforeAggregateRead?: (aggregateID: string) => Effect.Effect - readonly definitions?: ReadonlyArray } export const layerWith = (options?: LayerOptions) => Layer.effect( Service, Effect.gen(function* () { - const durableDefinitions = options?.definitions - ? Event.durable([...EventManifest.Definitions, ...options.definitions]) - : EventManifest.Durable const pubsub = { all: yield* PubSub.unbounded(), durable: new Map>>(), typed: new Map>(), } const projectors = new Map() - // Projectors intentionally retain type-only dispatch. Exact type+version dispatch is a separate replay design. + // TODO: Bind durable projectors to exact type+version before supporting incompatible historical payloads. const listeners = new Array() const { db } = yield* Database.Service @@ -368,7 +364,7 @@ export const layerWith = (options?: LayerOptions) => options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean }, ) { return Effect.gen(function* () { - const definition = durableDefinitions.get(event.type) + const definition = Durable.get(event.type) if (!definition?.durable) { yield* Effect.die( new InvalidDurableEventError({ type: event.type, message: `Unknown durable event type ${event.type}` }), @@ -464,7 +460,7 @@ export const layerWith = (options?: LayerOptions) => const streamAll = (): Stream.Stream => Stream.fromPubSub(pubsub.all) const decodeSerializedEvent = (event: SerializedEvent) => { - const definition = durableDefinitions.get(event.type) + const definition = Durable.get(event.type) if (!definition?.durable) { throw new InvalidDurableEventError({ type: event.type, message: `Unknown durable event type ${event.type}` }) } diff --git a/packages/core/src/public-event-manifest.ts b/packages/core/src/public-event-manifest.ts index 886d5927defc..99c208d9067b 100644 --- a/packages/core/src/public-event-manifest.ts +++ b/packages/core/src/public-event-manifest.ts @@ -1,9 +1,3 @@ export * as PublicEventManifest from "./public-event-manifest" -export { - FeatureDefinitions, - FoundationDefinitions, - PublicDefinitions as Definitions, - PublicDurable as Durable, - PublicLatest as Latest, -} from "@opencode-ai/schema/event-manifest" +export { ServerDefinitions as Definitions } from "@opencode-ai/schema/event-manifest" diff --git a/packages/core/src/v1/session.ts b/packages/core/src/v1/session.ts index b675e2a7986e..b4ce31e5f09a 100644 --- a/packages/core/src/v1/session.ts +++ b/packages/core/src/v1/session.ts @@ -10,7 +10,6 @@ export { Assistant, CompactionPart, Event, - Events, FilePart, FilePartInput, FilePartSource, diff --git a/packages/core/test/event.test.ts b/packages/core/test/event.test.ts index dad0e8d576a6..e2b2a5df046c 100644 --- a/packages/core/test/event.test.ts +++ b/packages/core/test/event.test.ts @@ -2,10 +2,13 @@ import { describe, expect } from "bun:test" import { Cause, DateTime, Deferred, Effect, Exit, Fiber, Layer, Schema, Stream } from "effect" 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 { EventSequenceTable, EventTable } from "@opencode-ai/core/event/sql" 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 { eq } from "drizzle-orm" import { location } from "./fixture/location" @@ -67,24 +70,13 @@ const VersionedMessage = EventV2.define({ }, }) -const SyncTimestamp = EventV2.define({ - type: "test.timestamp", - durable: { - version: 1, - aggregate: "id", - }, - schema: { - id: Schema.String, - timestamp: DateTimeUtcFromMillis, - }, +const DurableMessage = SessionV1.Event.MessageRemoved +const durableData = (sessionID: Session.ID, text: string) => ({ + sessionID, + messageID: SessionV1.MessageID.ascending(`msg_${text}`), }) -const eventLayer = Layer.mergeAll( - EventV2.layerWith({ - definitions: [Message, SyncMessage, SyncSent, GlobalMessage, VersionedMessage, SyncTimestamp], - }).pipe(Layer.provide(Database.defaultLayer)), - Database.defaultLayer, -) +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) @@ -364,19 +356,19 @@ describe("EventV2", () => { it.effect("replays durable aggregate events after a sequence and tails new events", () => Effect.gen(function* () { const events = yield* EventV2.Service - const aggregateID = EventV2.ID.create() - yield* events.publish(SyncMessage, { id: aggregateID, text: "zero" }) - yield* events.publish(SyncMessage, { id: aggregateID, text: "one" }) + const aggregateID = Session.ID.create() + yield* events.publish(DurableMessage, durableData(aggregateID, "zero")) + yield* events.publish(DurableMessage, durableData(aggregateID, "one")) const fiber = yield* events .durable({ aggregateID, after: 0 }) .pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped) 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([ - [1, { id: aggregateID, text: "one" }], - [2, { id: aggregateID, text: "two" }], + [1, durableData(aggregateID, "one")], + [2, durableData(aggregateID, "two")], ]) }), ) @@ -384,20 +376,15 @@ describe("EventV2", () => { it.effect("catches durable aggregate events published during replay handoff", () => Effect.gen(function* () { const events = yield* EventV2.Service - const aggregateID = EventV2.ID.create() - yield* events.publish(SyncMessage, { id: aggregateID, text: "zero" }) + const aggregateID = Session.ID.create() + yield* events.publish(DurableMessage, durableData(aggregateID, "zero")) 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( - Array.from(yield* Fiber.join(fiber)).map((event) => [ - event.durable?.seq, - (event.data as { text: string }).text, - ]), - ).toEqual([ - [0, "zero"], - [1, "one"], + expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.durable?.seq, event.data])).toEqual([ + [0, durableData(aggregateID, "zero")], + [1, durableData(aggregateID, "one")], ]) }), ) @@ -408,7 +395,6 @@ describe("EventV2", () => { const continueRead = yield* Deferred.make() let pause = true const eventLayer = EventV2.layerWith({ - definitions: [SyncMessage], beforeAggregateRead: () => pause ? Deferred.succeed(readStarted, undefined).pipe(Effect.andThen(Deferred.await(continueRead))) @@ -417,16 +403,16 @@ describe("EventV2", () => { yield* Effect.gen(function* () { 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) yield* Deferred.await(readStarted) pause = false - yield* events.publish(SyncMessage, { id: aggregateID, text: "during handoff" }) + yield* events.publish(DurableMessage, durableData(aggregateID, "during handoff")) yield* Deferred.succeed(continueRead, undefined) 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))) }), @@ -435,7 +421,7 @@ describe("EventV2", () => { it.effect("coalesces durable aggregate wakes while draining every committed event", () => Effect.gen(function* () { const events = yield* EventV2.Service - const aggregateID = EventV2.ID.create() + const aggregateID = Session.ID.create() const count = 64 const fiber = yield* events .durable({ aggregateID }) @@ -443,11 +429,11 @@ describe("EventV2", () => { yield* Effect.yieldNow 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( - Array.from({ length: count }, (_, index) => [index, { id: aggregateID, text: String(index) }]), + Array.from({ length: count }, (_, index) => [index, durableData(aggregateID, String(index))]), ) }), ) @@ -455,14 +441,14 @@ describe("EventV2", () => { it.effect("omits live-only events from durable aggregate streams", () => Effect.gen(function* () { 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) yield* Effect.yieldNow 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]) }), ) @@ -489,23 +475,23 @@ describe("EventV2", () => { Effect.gen(function* () { const events = yield* EventV2.Service const received = new Array() - yield* events.project(SyncMessage, (event) => + yield* events.project(DurableMessage, (event) => Effect.sync(() => { received.push(event) }), ) - const aggregateID = EventV2.ID.create() + const aggregateID = Session.ID.create() yield* events.replay({ id: EventV2.ID.create(), - type: EventV2.versionedType(SyncMessage.type, 1), + type: EventV2.versionedType(DurableMessage.type, 1), seq: 0, aggregateID, - data: { id: aggregateID, text: "hello" }, + data: durableData(aggregateID, "hello"), }) - expect(received[0]?.type).toBe(SyncMessage.type) - expect(received[0]?.data).toEqual({ id: aggregateID, text: "hello" }) + expect(received[0]?.type).toBe(DurableMessage.type) + expect(received[0]?.data).toEqual(durableData(aggregateID, "hello")) }), ) @@ -513,14 +499,14 @@ describe("EventV2", () => { Effect.gen(function* () { const events = yield* EventV2.Service const { db } = yield* Database.Service - const aggregateID = EventV2.ID.create() + const aggregateID = Session.ID.create() yield* events.replay({ id: EventV2.ID.create(), - type: EventV2.versionedType(SyncMessage.type, 1), + type: EventV2.versionedType(DurableMessage.type, 1), seq: 0, aggregateID, - data: { id: aggregateID, text: "replayed" }, + data: durableData(aggregateID, "replayed"), }) const rows = yield* db .select() @@ -540,11 +526,11 @@ describe("EventV2", () => { Effect.gen(function* () { const events = yield* EventV2.Service const { db } = yield* Database.Service - const envelopeAggregateID = EventV2.ID.create() - const payloadAggregateID = EventV2.ID.create() + const envelopeAggregateID = Session.ID.create() + const payloadAggregateID = Session.ID.create() const received = new Array() - yield* events.publish(SyncMessage, { id: payloadAggregateID, text: "seed" }) - yield* events.project(SyncMessage, (event) => + yield* events.publish(DurableMessage, durableData(payloadAggregateID, "seed")) + yield* events.project(DurableMessage, (event) => Effect.sync(() => { received.push(event) }), @@ -553,10 +539,10 @@ describe("EventV2", () => { const exit = yield* events .replay({ id: EventV2.ID.create(), - type: EventV2.versionedType(SyncMessage.type, 1), + type: EventV2.versionedType(DurableMessage.type, 1), seq: 1, aggregateID: envelopeAggregateID, - data: { id: payloadAggregateID, text: "replayed" }, + data: durableData(payloadAggregateID, "replayed"), }) .pipe(Effect.exit) const rows = yield* db @@ -582,22 +568,22 @@ describe("EventV2", () => { it.effect("replay defects on sequence mismatch", () => Effect.gen(function* () { const events = yield* EventV2.Service - const aggregateID = EventV2.ID.create() + const aggregateID = Session.ID.create() yield* events.replay({ id: EventV2.ID.create(), - type: EventV2.versionedType(SyncMessage.type, 1), + type: EventV2.versionedType(DurableMessage.type, 1), seq: 0, aggregateID, - data: { id: aggregateID, text: "first" }, + data: durableData(aggregateID, "first"), }) const exit = yield* events .replay({ id: EventV2.ID.create(), - type: EventV2.versionedType(SyncMessage.type, 1), + type: EventV2.versionedType(DurableMessage.type, 1), seq: 5, aggregateID, - data: { id: aggregateID, text: "bad" }, + data: durableData(aggregateID, "bad"), }) .pipe(Effect.exit) @@ -608,9 +594,9 @@ describe("EventV2", () => { it.effect("replay decodes synchronized transformed values before projection", () => Effect.gen(function* () { const events = yield* EventV2.Service - const aggregateID = EventV2.ID.create() - const received = new Array() - yield* events.project(SyncTimestamp, (event) => + const aggregateID = Session.ID.create() + const received = new Array() + yield* events.project(SessionEvent.ContextUpdated, (event) => Effect.sync(() => { received.push(event) }), @@ -618,10 +604,10 @@ describe("EventV2", () => { yield* events.replay({ id: EventV2.ID.create(), - type: EventV2.versionedType(SyncTimestamp.type, 1), + type: EventV2.versionedType(SessionEvent.ContextUpdated.type, 1), seq: 0, 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)) @@ -648,21 +634,21 @@ describe("EventV2", () => { it.effect("replayAll validates contiguous aggregate events", () => Effect.gen(function* () { const events = yield* EventV2.Service - const aggregateID = EventV2.ID.create() + const aggregateID = Session.ID.create() const source = yield* events.replayAll([ { id: EventV2.ID.create(), - type: EventV2.versionedType(SyncMessage.type, 1), + type: EventV2.versionedType(DurableMessage.type, 1), seq: 0, aggregateID, - data: { id: aggregateID, text: "one" }, + data: durableData(aggregateID, "one"), }, { id: EventV2.ID.create(), - type: EventV2.versionedType(SyncMessage.type, 1), + type: EventV2.versionedType(DurableMessage.type, 1), seq: 1, aggregateID, - data: { id: aggregateID, text: "two" }, + data: durableData(aggregateID, "two"), }, ]) @@ -674,38 +660,38 @@ describe("EventV2", () => { Effect.gen(function* () { const events = yield* EventV2.Service const { db } = yield* Database.Service - const aggregateID = EventV2.ID.create() + const aggregateID = Session.ID.create() const one = yield* events.replayAll([ { id: EventV2.ID.create(), - type: EventV2.versionedType(SyncMessage.type, 1), + type: EventV2.versionedType(DurableMessage.type, 1), seq: 0, aggregateID, - data: { id: aggregateID, text: "one" }, + data: durableData(aggregateID, "one"), }, { id: EventV2.ID.create(), - type: EventV2.versionedType(SyncMessage.type, 1), + type: EventV2.versionedType(DurableMessage.type, 1), seq: 1, aggregateID, - data: { id: aggregateID, text: "two" }, + data: durableData(aggregateID, "two"), }, ]) const two = yield* events.replayAll([ { id: EventV2.ID.create(), - type: EventV2.versionedType(SyncMessage.type, 1), + type: EventV2.versionedType(DurableMessage.type, 1), seq: 2, aggregateID, - data: { id: aggregateID, text: "three" }, + data: durableData(aggregateID, "three"), }, { id: EventV2.ID.create(), - type: EventV2.versionedType(SyncMessage.type, 1), + type: EventV2.versionedType(DurableMessage.type, 1), seq: 3, aggregateID, - data: { id: aggregateID, text: "four" }, + data: durableData(aggregateID, "four"), }, ]) const rows = yield* db @@ -725,10 +711,10 @@ describe("EventV2", () => { Effect.gen(function* () { const events = yield* EventV2.Service const received = new Array() - const aggregateID = EventV2.ID.create() - yield* events.publish(SyncMessage, { id: aggregateID, text: "seed" }) + const aggregateID = Session.ID.create() + yield* events.publish(DurableMessage, durableData(aggregateID, "seed")) yield* events.claim(aggregateID, "owner-a") - yield* events.project(SyncMessage, (event) => + yield* events.project(DurableMessage, (event) => Effect.sync(() => { received.push(event) }), @@ -737,10 +723,10 @@ describe("EventV2", () => { yield* events.replay( { id: EventV2.ID.create(), - type: EventV2.versionedType(SyncMessage.type, 1), + type: EventV2.versionedType(DurableMessage.type, 1), seq: 1, aggregateID, - data: { id: aggregateID, text: "ignored" }, + data: durableData(aggregateID, "ignored"), }, { ownerID: "owner-b" }, ) @@ -752,14 +738,14 @@ describe("EventV2", () => { it.effect("strict owner fences exact replay", () => Effect.gen(function* () { const events = yield* EventV2.Service - const aggregateID = EventV2.ID.create() + const aggregateID = Session.ID.create() const id = EventV2.ID.create() const replayed = { id, - type: EventV2.versionedType(SyncMessage.type, 1), + type: EventV2.versionedType(DurableMessage.type, 1), seq: 0, aggregateID, - data: { id: aggregateID, text: "owned" }, + data: durableData(aggregateID, "owned"), } yield* events.replay(replayed, { ownerID: "owner-a" }) @@ -773,11 +759,11 @@ describe("EventV2", () => { Effect.gen(function* () { const events = yield* EventV2.Service const { db } = yield* Database.Service - const aggregateID = EventV2.ID.create() - const published = yield* events.publish(SyncMessage, { id: aggregateID, text: "owned" }) + const aggregateID = Session.ID.create() + const published = yield* events.publish(DurableMessage, durableData(aggregateID, "owned")) const replayed = { id: published.id, - type: EventV2.versionedType(SyncMessage.type, 1), + type: EventV2.versionedType(DurableMessage.type, 1), seq: published.durable!.seq, aggregateID, data: published.data, @@ -794,7 +780,7 @@ describe("EventV2", () => { expect(row?.ownerID).toBe("owner-a") const exit = yield* events .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 }, ) .pipe(Effect.exit) @@ -806,15 +792,15 @@ describe("EventV2", () => { Effect.gen(function* () { const events = yield* EventV2.Service const { db } = yield* Database.Service - const aggregateID = EventV2.ID.create() + const aggregateID = Session.ID.create() yield* events.replay( { id: EventV2.ID.create(), - type: EventV2.versionedType(SyncMessage.type, 1), + type: EventV2.versionedType(DurableMessage.type, 1), seq: 0, aggregateID, - data: { id: aggregateID, text: "owned" }, + data: durableData(aggregateID, "owned"), }, { ownerID: "owner-1" }, ) @@ -833,26 +819,26 @@ describe("EventV2", () => { Effect.gen(function* () { const events = yield* EventV2.Service const { db } = yield* Database.Service - const aggregateID = EventV2.ID.create() - yield* events.publish(SyncMessage, { id: aggregateID, text: "local" }) + const aggregateID = Session.ID.create() + yield* events.publish(DurableMessage, durableData(aggregateID, "local")) yield* events.replay( { id: EventV2.ID.create(), - type: EventV2.versionedType(SyncMessage.type, 1), + type: EventV2.versionedType(DurableMessage.type, 1), seq: 1, aggregateID, - data: { id: aggregateID, text: "claimed" }, + data: durableData(aggregateID, "claimed"), }, { ownerID: "owner-1" }, ) yield* events.replay( { id: EventV2.ID.create(), - type: EventV2.versionedType(SyncMessage.type, 1), + type: EventV2.versionedType(DurableMessage.type, 1), seq: 2, aggregateID, - data: { id: aggregateID, text: "fenced" }, + data: durableData(aggregateID, "fenced"), }, { ownerID: "owner-2" }, ) @@ -877,14 +863,14 @@ describe("EventV2", () => { it.effect("strict replay rejects an owner conflict instead of silently skipping it", () => Effect.gen(function* () { const events = yield* EventV2.Service - const aggregateID = EventV2.ID.create() + const aggregateID = Session.ID.create() yield* events.replay( { id: EventV2.ID.create(), - type: EventV2.versionedType(SyncMessage.type, 1), + type: EventV2.versionedType(DurableMessage.type, 1), seq: 0, aggregateID, - data: { id: aggregateID, text: "claimed" }, + data: durableData(aggregateID, "claimed"), }, { ownerID: "owner-1" }, ) @@ -893,10 +879,10 @@ describe("EventV2", () => { .replay( { id: EventV2.ID.create(), - type: EventV2.versionedType(SyncMessage.type, 1), + type: EventV2.versionedType(DurableMessage.type, 1), seq: 1, aggregateID, - data: { id: aggregateID, text: "conflict" }, + data: durableData(aggregateID, "conflict"), }, { ownerID: "owner-2", strictOwner: true }, ) @@ -910,14 +896,14 @@ describe("EventV2", () => { Effect.gen(function* () { const events = yield* EventV2.Service const received = new Array() - const aggregateID = EventV2.ID.create() + const aggregateID = Session.ID.create() yield* events.listen((event) => Effect.sync(() => received.push(event))) const replayed = { id: EventV2.ID.create(), - type: EventV2.versionedType(SyncMessage.type, 1), + type: EventV2.versionedType(DurableMessage.type, 1), seq: 0, aggregateID, - data: { id: aggregateID, text: "replayed" }, + data: durableData(aggregateID, "replayed"), } yield* events.replay(replayed, { publish: true }) @@ -931,19 +917,19 @@ describe("EventV2", () => { Effect.gen(function* () { const events = yield* EventV2.Service const received = new Array() - const aggregateID = EventV2.ID.create() + const aggregateID = Session.ID.create() const replayed = { id: EventV2.ID.create(), - type: EventV2.versionedType(SyncMessage.type, 1), + type: EventV2.versionedType(DurableMessage.type, 1), seq: 0, aggregateID, - data: { id: aggregateID, text: "original" }, + data: durableData(aggregateID, "original"), } yield* events.listen((event) => Effect.sync(() => received.push(event))) yield* events.replay(replayed, { publish: true }) const exit = yield* events - .replay({ ...replayed, data: { id: aggregateID, text: "divergent" } }, { publish: true }) + .replay({ ...replayed, data: durableData(aggregateID, "divergent") }, { publish: true }) .pipe(Effect.exit) expect(String(exit)).toContain("Replay diverged") @@ -954,23 +940,23 @@ describe("EventV2", () => { it.effect("rejects an event ID reused at another aggregate position", () => Effect.gen(function* () { const events = yield* EventV2.Service - const aggregateID = EventV2.ID.create() + const aggregateID = Session.ID.create() const id = EventV2.ID.create() yield* events.replay({ id, - type: EventV2.versionedType(SyncMessage.type, 1), + type: EventV2.versionedType(DurableMessage.type, 1), seq: 0, aggregateID, - data: { id: aggregateID, text: "first" }, + data: durableData(aggregateID, "first"), }) const exit = yield* events .replay({ id, - type: EventV2.versionedType(SyncMessage.type, 1), + type: EventV2.versionedType(DurableMessage.type, 1), seq: 1, aggregateID, - data: { id: aggregateID, text: "second" }, + data: durableData(aggregateID, "second"), }) .pipe(Effect.exit) @@ -982,27 +968,27 @@ describe("EventV2", () => { Effect.gen(function* () { const events = yield* EventV2.Service const { db } = yield* Database.Service - const aggregateID = EventV2.ID.create() + const aggregateID = Session.ID.create() const received = new Array() yield* events.listen((event) => Effect.sync(() => received.push(event))) yield* events.replay( { id: EventV2.ID.create(), - type: EventV2.versionedType(SyncMessage.type, 1), + type: EventV2.versionedType(DurableMessage.type, 1), seq: 0, aggregateID, - data: { id: aggregateID, text: "first" }, + data: durableData(aggregateID, "first"), }, { ownerID: "owner-1" }, ) yield* events.replay( { id: EventV2.ID.create(), - type: EventV2.versionedType(SyncMessage.type, 1), + type: EventV2.versionedType(DurableMessage.type, 1), seq: 1, aggregateID, - data: { id: aggregateID, text: "ignored" }, + data: durableData(aggregateID, "ignored"), }, { ownerID: "owner-2", publish: true }, ) @@ -1049,10 +1035,10 @@ describe("EventV2", () => { Effect.gen(function* () { const events = yield* EventV2.Service const received = new Array() - const aggregateID = EventV2.ID.create() - yield* events.publish(SyncMessage, { id: aggregateID, text: "seed" }) + const aggregateID = Session.ID.create() + yield* events.publish(DurableMessage, durableData(aggregateID, "seed")) yield* events.remove(aggregateID) - yield* events.project(SyncMessage, (event) => + yield* events.project(DurableMessage, (event) => Effect.sync(() => { received.push(event) }), @@ -1060,13 +1046,13 @@ describe("EventV2", () => { yield* events.replay({ id: EventV2.ID.create(), - type: EventV2.versionedType(SyncMessage.type, 1), + type: EventV2.versionedType(DurableMessage.type, 1), seq: 0, 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")) }), ) }) diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 139217e348a4..87cf66bc1ab3 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -86,6 +86,7 @@ "@openauthjs/openauth": "catalog:", "@opencode-ai/llm": "workspace:*", "@opencode-ai/plugin": "workspace:*", + "@opencode-ai/schema": "workspace:*", "@opencode-ai/script": "workspace:*", "@opencode-ai/sdk": "workspace:*", "@opencode-ai/server": "workspace:*", diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index 9e290a86604e..82ae979ba3ce 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -21,14 +21,13 @@ import { serviceUse } from "@opencode-ai/core/effect/service-use" import { RuntimeFlags } from "@/effect/runtime-flags" import { EventV2Bridge } from "@/event-v2-bridge" import { EventV2 } from "@opencode-ai/core/event" -import { LegacyEvent } from "@opencode-ai/schema/legacy-event" import { Project } from "@opencode-ai/schema/project" export const Info = Project.Info export type Info = Types.DeepMutable> export const Event = { - Updated: LegacyEvent.ProjectUpdated, + Updated: Project.Event.Updated, } type Row = typeof ProjectTable.$inferSelect diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 7cd82da16d01..798518d0ba57 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -35,7 +35,6 @@ import { isMedia } from "@/util/media" import type { SystemError } from "bun" import type { Provider } from "@/provider/provider" import { Effect, Schema } from "effect" -import { SessionV1PublicEvent } from "@opencode-ai/schema/session-v1" export const node = LayerNode.group([Database.node]) @@ -59,7 +58,7 @@ export const Event = { Updated: SessionV1.Event.MessageUpdated, Removed: SessionV1.Event.MessageRemoved, PartUpdated: SessionV1.Event.PartUpdated, - PartDelta: SessionV1PublicEvent.PartDelta, + PartDelta: SessionV1.Event.PartDelta, PartRemoved: SessionV1.Event.PartRemoved, } diff --git a/packages/opencode/src/session/session.ts b/packages/opencode/src/session/session.ts index a40fa24dd880..64d4111088da 100644 --- a/packages/opencode/src/session/session.ts +++ b/packages/opencode/src/session/session.ts @@ -43,7 +43,6 @@ import { NonNegativeInt, optionalOmitUndefined } from "@opencode-ai/core/schema" import { RuntimeFlags } from "@/effect/runtime-flags" import { ProviderV2 } from "@opencode-ai/core/provider" import { ModelV2 } from "@opencode-ai/core/model" -import { SessionV1PublicEvent } from "@opencode-ai/schema/session-v1" const runtime = makeRuntime(Database.Service, Database.defaultLayer) @@ -312,8 +311,8 @@ export const Event = { Created: SessionV1.Event.Created, Updated: SessionV1.Event.Updated, Deleted: SessionV1.Event.Deleted, - Diff: SessionV1PublicEvent.Diff, - Error: SessionV1PublicEvent.Error, + Diff: SessionV1.Event.Diff, + Error: SessionV1.Event.Error, } export function plan(input: { slug: string; time: { created: number } }, instance: InstanceContext) { diff --git a/packages/schema/src/catalog.ts b/packages/schema/src/catalog.ts index 7c08fa13655a..93df6d973b0e 100644 --- a/packages/schema/src/catalog.ts +++ b/packages/schema/src/catalog.ts @@ -1,8 +1,7 @@ export * as Catalog from "./catalog" -import { define } from "./event" +import { define, inventory } from "./event" -export const Event = { - Updated: define({ type: "catalog.updated", schema: {} }), -} +const Updated = define({ type: "catalog.updated", schema: {} }) +export const Event = { Updated, Definitions: inventory(Updated) } export const CatalogEvent = Event diff --git a/packages/schema/src/durable-event-manifest.ts b/packages/schema/src/durable-event-manifest.ts new file mode 100644 index 000000000000..ed3fb98bb299 --- /dev/null +++ b/packages/schema/src/durable-event-manifest.ts @@ -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, +]) diff --git a/packages/schema/src/event-manifest.ts b/packages/schema/src/event-manifest.ts index 34e8eab48598..b681362e83be 100644 --- a/packages/schema/src/event-manifest.ts +++ b/packages/schema/src/event-manifest.ts @@ -1,6 +1,7 @@ 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" @@ -13,6 +14,7 @@ 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" @@ -29,77 +31,54 @@ import { VcsEvent } from "./vcs-event" import { WorkspaceEvent } from "./workspace-event" import { WorktreeEvent } from "./worktree-event" -export const CoreDefinitions = Event.inventory(...SessionV1.Events, ...SessionEvent.Definitions) -export const CoreLatest = Event.latest(CoreDefinitions) -export const CoreDurable = Event.durable(CoreDefinitions) +const sessionV1DurableDefinitions = SessionV1.Event.Definitions.filter((definition) => definition.durable !== undefined) +const sessionV1LiveDefinitions = SessionV1.Event.Definitions.filter((definition) => definition.durable === undefined) -export const FoundationDefinitions = Event.inventory( - ModelsDev.Event.Refreshed, - Integration.Event.Updated, - Integration.Event.ConnectionUpdated, - Catalog.Event.Updated, - ...CoreDefinitions, +const coreDefinitions = Event.inventory(...sessionV1DurableDefinitions, ...SessionEvent.Definitions) + +const foundationDefinitions = Event.inventory( + ...ModelsDev.Event.Definitions, + ...Integration.Event.Definitions, + ...Catalog.Event.Definitions, + ...coreDefinitions, ) -export const FeatureDefinitions = Event.inventory( - FileSystem.Event.Edited, - Reference.Event.Updated, - Permission.Event.Asked, - Permission.Event.Replied, - Plugin.Event.Added, - ProjectDirectories.Event.Updated, - FileSystemWatcher.Event.Updated, - Pty.Event.Created, - Pty.Event.Updated, - Pty.Event.Exited, - Pty.Event.Deleted, - Question.Event.Asked, - Question.Event.Replied, - Question.Event.Rejected, +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 PublicDefinitions = Event.inventory( - ...FoundationDefinitions, - ...FeatureDefinitions, - SessionTodo.Event.Updated, +export const ServerDefinitions = Event.inventory( + ...foundationDefinitions, + ...featureDefinitions, + ...SessionTodo.Event.Definitions, ) -export const PublicLatest = Event.latest(PublicDefinitions) -export const PublicDurable = Event.durable(PublicDefinitions) export const Definitions = Event.inventory( - ...FoundationDefinitions, - SessionV1.PartDelta, - SessionV1.Diff, - SessionV1.Error, - InstallationEvent.Updated, - InstallationEvent.UpdateAvailable, - ...FeatureDefinitions, - SessionTodo.Event.Updated, - LspEvent.Updated, - PermissionV1.Event.Asked, - PermissionV1.Event.Replied, - TuiEvent.PromptAppend, - TuiEvent.CommandExecute, - TuiEvent.ToastShow, - TuiEvent.SessionSelect, - McpEvent.ToolsChanged, - McpEvent.BrowserOpenFailed, - LegacyEvent.CommandExecuted, - LegacyEvent.ProjectUpdated, - SessionStatusEvent.Status, - SessionStatusEvent.Idle, - QuestionV1.Event.Asked, - QuestionV1.Event.Replied, - QuestionV1.Event.Rejected, - SessionCompactionEvent.Compacted, - VcsEvent.BranchUpdated, - WorkspaceEvent.Ready, - WorkspaceEvent.Failed, - WorkspaceEvent.Status, - WorktreeEvent.Ready, - WorktreeEvent.Failed, - ServerEvent.Connected, - ServerEvent.Disposed, + ...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 const Durable = Event.durable(Definitions) +export { Durable } diff --git a/packages/schema/src/filesystem-watcher.ts b/packages/schema/src/filesystem-watcher.ts index 56fcecb7f25f..5e4da777cae8 100644 --- a/packages/schema/src/filesystem-watcher.ts +++ b/packages/schema/src/filesystem-watcher.ts @@ -1,14 +1,13 @@ export * as FileSystemWatcher from "./filesystem-watcher" import { Schema } from "effect" -import { define } from "./event" +import { define, inventory } from "./event" -export const Event = { - Updated: define({ - type: "file.watcher.updated", - schema: { - file: Schema.String, - event: Schema.Literals(["add", "change", "unlink"]), - }, - }), -} +const Updated = define({ + type: "file.watcher.updated", + schema: { + file: Schema.String, + event: Schema.Literals(["add", "change", "unlink"]), + }, +}) +export const Event = { Updated, Definitions: inventory(Updated) } diff --git a/packages/schema/src/filesystem.ts b/packages/schema/src/filesystem.ts index f6b353d0991c..0d6b8deefa6a 100644 --- a/packages/schema/src/filesystem.ts +++ b/packages/schema/src/filesystem.ts @@ -1,15 +1,14 @@ export * as FileSystem from "./filesystem" import { Schema } from "effect" -import { define } from "./event" +import { define, inventory } from "./event" import { NonNegativeInt, PositiveInt, RelativePath } from "./schema" -export const Event = { - Edited: define({ - type: "file.edited", - schema: { file: Schema.String }, - }), -} +const Edited = define({ + type: "file.edited", + schema: { file: Schema.String }, +}) +export const Event = { Edited, Definitions: inventory(Edited) } export const FileSystemEvent = Event export interface Entry extends Schema.Schema.Type {} diff --git a/packages/schema/src/ide-event.ts b/packages/schema/src/ide-event.ts index e572a7c2b539..ca4218602143 100644 --- a/packages/schema/src/ide-event.ts +++ b/packages/schema/src/ide-event.ts @@ -9,3 +9,5 @@ export const Installed = Event.define({ ide: Schema.String, }, }) + +export const Definitions = Event.inventory(Installed) diff --git a/packages/schema/src/installation-event.ts b/packages/schema/src/installation-event.ts index 879e708891a0..69ecf6708468 100644 --- a/packages/schema/src/installation-event.ts +++ b/packages/schema/src/installation-event.ts @@ -16,3 +16,5 @@ export const UpdateAvailable = Event.define({ version: Schema.String, }, }) + +export const Definitions = Event.inventory(Updated, UpdateAvailable) diff --git a/packages/schema/src/integration.ts b/packages/schema/src/integration.ts index 299be5bc2593..78bbbe6602f7 100644 --- a/packages/schema/src/integration.ts +++ b/packages/schema/src/integration.ts @@ -1,7 +1,7 @@ export * as Integration from "./integration" import { Schema } from "effect" -import { define } from "./event" +import { define, inventory } from "./event" export const ID = Schema.String.pipe(Schema.brand("Integration.ID")) export type ID = typeof ID.Type @@ -73,16 +73,15 @@ export type Method = typeof Method.Type export const Inputs = Schema.Record(Schema.String, Schema.String).annotate({ identifier: "Integration.Inputs" }) export type Inputs = typeof Inputs.Type -export const Event = { - Updated: define({ - type: "integration.updated", - schema: {}, - }), - ConnectionUpdated: define({ - type: "integration.connection.updated", - schema: { integrationID: ID }, - }), -} +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 {} export const Ref = Schema.Struct({ diff --git a/packages/schema/src/legacy-event.ts b/packages/schema/src/legacy-event.ts index 25bc6ee02ee4..21c46df0e07a 100644 --- a/packages/schema/src/legacy-event.ts +++ b/packages/schema/src/legacy-event.ts @@ -1,19 +1,18 @@ export * as LegacyEvent from "./legacy-event" import { Schema } from "effect" -import { define } from "./event" -import { Project } from "./project" -import { Session } from "./session" +import { define, inventory } from "./event" +import { SessionID } from "./session-id" import { SessionV1 } from "./session-v1" -export const ProjectUpdated = define({ type: "project.updated", schema: Project.Info.fields }) - export const CommandExecuted = define({ type: "command.executed", schema: { name: Schema.String, - sessionID: Session.ID, + sessionID: SessionID, arguments: Schema.String, messageID: SessionV1.MessageID, }, }) + +export const Definitions = inventory(CommandExecuted) diff --git a/packages/schema/src/location.ts b/packages/schema/src/location.ts index 970ec7f0a3d3..02aa65017b2a 100644 --- a/packages/schema/src/location.ts +++ b/packages/schema/src/location.ts @@ -2,12 +2,12 @@ export * as Location from "./location" import { Effect, Schema } from "effect" import { AbsolutePath } from "./schema" -import { Workspace } from "./workspace" +import { WorkspaceID } from "./workspace-id" export interface Ref extends Schema.Schema.Type {} export const Ref = Schema.Struct({ directory: AbsolutePath, - workspaceID: Schema.optional(Workspace.ID).pipe( + workspaceID: Schema.optional(WorkspaceID).pipe( Schema.withDecodingDefault(Effect.succeed(undefined)), Schema.withConstructorDefault(Effect.succeed(undefined)), ), diff --git a/packages/schema/src/lsp-event.ts b/packages/schema/src/lsp-event.ts index a62b022b8bab..b6908469663a 100644 --- a/packages/schema/src/lsp-event.ts +++ b/packages/schema/src/lsp-event.ts @@ -3,3 +3,5 @@ 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) diff --git a/packages/schema/src/mcp-event.ts b/packages/schema/src/mcp-event.ts index a30a83253f4d..1d050df92738 100644 --- a/packages/schema/src/mcp-event.ts +++ b/packages/schema/src/mcp-event.ts @@ -17,3 +17,5 @@ export const BrowserOpenFailed = Event.define({ url: Schema.String, }, }) + +export const Definitions = Event.inventory(ToolsChanged, BrowserOpenFailed) diff --git a/packages/schema/src/models-dev.ts b/packages/schema/src/models-dev.ts index 8b7697574cda..4432bc4591ec 100644 --- a/packages/schema/src/models-dev.ts +++ b/packages/schema/src/models-dev.ts @@ -1,10 +1,9 @@ export * as ModelsDev from "./models-dev" -import { define } from "./event" +import { define, inventory } from "./event" -export const Event = { - Refreshed: define({ - type: "models-dev.refreshed", - schema: {}, - }), -} +const Refreshed = define({ + type: "models-dev.refreshed", + schema: {}, +}) +export const Event = { Refreshed, Definitions: inventory(Refreshed) } diff --git a/packages/schema/src/permission-v1.ts b/packages/schema/src/permission-v1.ts index a7f7c0b1a8f0..d0db4fa94036 100644 --- a/packages/schema/src/permission-v1.ts +++ b/packages/schema/src/permission-v1.ts @@ -1,11 +1,11 @@ export * as PermissionV1 from "./permission-v1" import { Schema } from "effect" -import { define } from "./event" +import { define, inventory } from "./event" import { ascending } from "./identifier" import { Project } from "./project" import { withStatics } from "./schema" -import { Session } from "./session" +import { SessionID } from "./session-id" export const ID = Schema.String.check(Schema.isStartsWith("per")).pipe( Schema.brand("PermissionID"), @@ -26,7 +26,7 @@ export type Ruleset = typeof Ruleset.Type export const Request = Schema.Struct({ id: ID, - sessionID: Session.ID, + sessionID: SessionID, permission: Schema.String, patterns: Schema.Array(Schema.String), metadata: Schema.Record(Schema.String, Schema.Unknown), @@ -58,11 +58,10 @@ export const ReplyInput = Schema.Struct({ requestID: ID, ...ReplyBody.fields }). }) export type ReplyInput = typeof ReplyInput.Type -export const Event = { - Asked: define({ type: "permission.asked", schema: Request.fields }), - Replied: define({ - type: "permission.replied", - schema: { sessionID: Session.ID, requestID: ID, reply: Reply }, - }), -} +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 diff --git a/packages/schema/src/permission.ts b/packages/schema/src/permission.ts index 3e7970f4d13c..fc4235a5305e 100644 --- a/packages/schema/src/permission.ts +++ b/packages/schema/src/permission.ts @@ -1,7 +1,7 @@ export * as Permission from "./permission" import { Schema } from "effect" -import { define } from "./event" +import { define, inventory } from "./event" import { ascending } from "./identifier" import { SessionID } from "./session-id" import { withStatics } from "./schema" @@ -22,7 +22,7 @@ export const Source = Schema.Union([ export type Source = typeof Source.Type const RequestFields = { - sessionID: SessionID.ID, + sessionID: SessionID, action: Schema.String, resources: Schema.Array(Schema.String), save: Schema.Array(Schema.String).pipe(Schema.optional), @@ -39,17 +39,16 @@ export type Request = typeof Request.Type export const Reply = Schema.Literals(["once", "always", "reject"]).annotate({ identifier: "PermissionV2.Reply" }) export type Reply = typeof Reply.Type -export const Event = { - Asked: define({ type: "permission.v2.asked", schema: Request.fields }), - Replied: define({ - type: "permission.v2.replied", - schema: { - sessionID: SessionID.ID, - requestID: ID, - reply: Reply, - }, - }), -} +const Asked = define({ type: "permission.v2.asked", schema: Request.fields }) +const Replied = define({ + type: "permission.v2.replied", + schema: { + sessionID: SessionID, + requestID: ID, + reply: Reply, + }, +}) +export const Event = { Asked, Replied, Definitions: inventory(Asked, Replied) } export const Effect = Schema.Literals(["allow", "deny", "ask"]).annotate({ identifier: "PermissionV2.Effect" }) export type Effect = typeof Effect.Type diff --git a/packages/schema/src/plugin.ts b/packages/schema/src/plugin.ts index 9c002da57c34..6202bcd64eb5 100644 --- a/packages/schema/src/plugin.ts +++ b/packages/schema/src/plugin.ts @@ -1,16 +1,15 @@ export * as Plugin from "./plugin" import { Schema } from "effect" -import { define } from "./event" +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 -export const Event = { - Added: define({ - type: "plugin.added", - schema: { id: ID }, - }), -} +const Added = define({ + type: "plugin.added", + schema: { id: ID }, +}) +export const Event = { Added, Definitions: inventory(Added) } export const PluginEvent = Event diff --git a/packages/schema/src/project-directories.ts b/packages/schema/src/project-directories.ts index fdcbd6535738..a589a6bd3cd6 100644 --- a/packages/schema/src/project-directories.ts +++ b/packages/schema/src/project-directories.ts @@ -1,12 +1,11 @@ export * as ProjectDirectories from "./project-directories" -import { define } from "./event" +import { define, inventory } from "./event" import { Project } from "./project" -export const Event = { - Updated: define({ - type: "project.directories.updated", - schema: { projectID: Project.ID }, - }), -} +const Updated = define({ + type: "project.directories.updated", + schema: { projectID: Project.ID }, +}) +export const Event = { Updated, Definitions: inventory(Updated) } export const ProjectDirectoriesEvent = Event diff --git a/packages/schema/src/project.ts b/packages/schema/src/project.ts index 64ed38ce5a6b..dd2ae6554132 100644 --- a/packages/schema/src/project.ts +++ b/packages/schema/src/project.ts @@ -1,6 +1,7 @@ export * as Project from "./project" import { Schema } from "effect" +import { define, inventory } from "./event" import { NonNegativeInt, optionalOmitUndefined, withStatics } from "./schema" export const ID = Schema.String.pipe( @@ -37,3 +38,6 @@ export const Info = Schema.Struct({ 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) } diff --git a/packages/schema/src/pty.ts b/packages/schema/src/pty.ts index 7bafe87b4663..1836d9da4460 100644 --- a/packages/schema/src/pty.ts +++ b/packages/schema/src/pty.ts @@ -1,7 +1,7 @@ export * as Pty from "./pty" import { Schema } from "effect" -import { define } from "./event" +import { define, inventory } from "./event" import { ascending } from "./identifier" import { NonNegativeInt } from "./schema" import { withStatics } from "./schema" @@ -27,10 +27,9 @@ export const Info = Schema.Struct({ }).annotate({ identifier: "Pty" }) export const PtyInfo = Info -export const Event = { - Created: define({ type: "pty.created", schema: { info: Info } }), - Updated: define({ type: "pty.updated", schema: { info: Info } }), - Exited: define({ type: "pty.exited", schema: { id: ID, exitCode: NonNegativeInt } }), - Deleted: define({ type: "pty.deleted", schema: { id: ID } }), -} +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 diff --git a/packages/schema/src/question-v1.ts b/packages/schema/src/question-v1.ts index 2bc79b0857bd..cea377917ee0 100644 --- a/packages/schema/src/question-v1.ts +++ b/packages/schema/src/question-v1.ts @@ -1,10 +1,10 @@ export * as QuestionV1 from "./question-v1" import { Schema } from "effect" -import { define } from "./event" +import { define, inventory } from "./event" import { ascending } from "./identifier" import { withStatics } from "./schema" -import { Session } from "./session" +import { SessionID } from "./session-id" import { SessionV1 } from "./session-v1" export const ID = Schema.String.check(Schema.isStartsWith("que")).pipe( @@ -34,7 +34,7 @@ export const Tool = Schema.Struct({ messageID: SessionV1.MessageID, callID: Sche }) export const Request = Schema.Struct({ id: ID, - sessionID: Session.ID, + sessionID: SessionID, questions: Schema.Array(Info).annotate({ description: "Questions to ask" }), tool: Schema.optional(Tool), }).annotate({ identifier: "QuestionRequest" }) @@ -44,15 +44,23 @@ export const Reply = Schema.Struct({ description: "User answers in order of questions (each answer is an array of selected labels)", }), }).annotate({ identifier: "QuestionReply" }) -export const Replied = Schema.Struct({ sessionID: Session.ID, requestID: ID, answers: Schema.Array(Answer) }).annotate({ +export const Replied = Schema.Struct({ + sessionID: SessionID, + requestID: ID, + answers: Schema.Array(Answer), +}).annotate({ identifier: "QuestionReplied", }) -export const Rejected = Schema.Struct({ sessionID: Session.ID, requestID: ID }).annotate({ +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: define({ type: "question.asked", schema: Request.fields }), - Replied: define({ type: "question.replied", schema: Replied.fields }), - Rejected: define({ type: "question.rejected", schema: Rejected.fields }), + Asked, + Replied: RepliedEvent, + Rejected: RejectedEvent, + Definitions: inventory(Asked, RepliedEvent, RejectedEvent), } diff --git a/packages/schema/src/question.ts b/packages/schema/src/question.ts index fc7c5ea57f8a..3794e811ef2a 100644 --- a/packages/schema/src/question.ts +++ b/packages/schema/src/question.ts @@ -1,7 +1,7 @@ export * as Question from "./question" import { Schema } from "effect" -import { define } from "./event" +import { define, inventory } from "./event" import { ascending } from "./identifier" import { SessionID } from "./session-id" import { withStatics } from "./schema" @@ -44,7 +44,7 @@ export type Tool = typeof Tool.Type export const Request = Schema.Struct({ id: ID, - sessionID: SessionID.ID, + sessionID: SessionID, questions: Schema.Array(Info).annotate({ description: "Questions to ask" }), tool: Tool.pipe(Schema.optional), }).annotate({ identifier: "QuestionV2.Request" }) @@ -60,21 +60,20 @@ export const Reply = Schema.Struct({ }).annotate({ identifier: "QuestionV2.Reply" }) export type Reply = typeof Reply.Type -export const Event = { - Asked: define({ type: "question.v2.asked", schema: Request.fields }), - Replied: define({ - type: "question.v2.replied", - schema: { - sessionID: SessionID.ID, - requestID: ID, - answers: Schema.Array(Answer), - }, - }), - Rejected: define({ - type: "question.v2.rejected", - schema: { - sessionID: SessionID.ID, - requestID: ID, - }, - }), -} +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) } diff --git a/packages/schema/src/reference.ts b/packages/schema/src/reference.ts index ced0e9e46cf0..054139101685 100644 --- a/packages/schema/src/reference.ts +++ b/packages/schema/src/reference.ts @@ -1,12 +1,11 @@ export * as Reference from "./reference" import { Schema } from "effect" -import { define } from "./event" +import { define, inventory } from "./event" import { AbsolutePath } from "./schema" -export const Event = { - Updated: define({ type: "reference.updated", schema: {} }), -} +const Updated = define({ type: "reference.updated", schema: {} }) +export const Event = { Updated, Definitions: inventory(Updated) } export interface LocalSource extends Schema.Schema.Type {} export const LocalSource = Schema.Struct({ diff --git a/packages/schema/src/server-event.ts b/packages/schema/src/server-event.ts index 9959d477ef58..9d8ac2d470ff 100644 --- a/packages/schema/src/server-event.ts +++ b/packages/schema/src/server-event.ts @@ -4,3 +4,5 @@ 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) diff --git a/packages/schema/src/session-compaction-event.ts b/packages/schema/src/session-compaction-event.ts index aed16fea75f5..ed1169ea6757 100644 --- a/packages/schema/src/session-compaction-event.ts +++ b/packages/schema/src/session-compaction-event.ts @@ -6,6 +6,8 @@ import { SessionID } from "./session-id" export const Compacted = Event.define({ type: "session.compacted", schema: { - sessionID: SessionID.ID, + sessionID: SessionID, }, }) + +export const Definitions = Event.inventory(Compacted) diff --git a/packages/schema/src/session-event.ts b/packages/schema/src/session-event.ts index 821482f1edcd..e7ae782f25ad 100644 --- a/packages/schema/src/session-event.ts +++ b/packages/schema/src/session-event.ts @@ -7,7 +7,7 @@ import { Delivery } from "./session-delivery" import { Model } from "./model" import { DateTimeUtcFromMillis, NonNegativeInt, RelativePath } from "./schema" import { FileAttachment, Prompt } from "./prompt" -import { Session } from "./session" +import { SessionID } from "./session-id" import { Location } from "./location" import { SessionMessageID } from "./session-message-id" import { SessionMessage } from "./session-message" @@ -25,7 +25,7 @@ export type Source = typeof Source.Type const Base = { timestamp: DateTimeUtcFromMillis, - sessionID: Session.ID, + sessionID: SessionID, } const PromptFields = { ...Base, diff --git a/packages/schema/src/session-id.ts b/packages/schema/src/session-id.ts index 8193f5728c8a..747413e03052 100644 --- a/packages/schema/src/session-id.ts +++ b/packages/schema/src/session-id.ts @@ -1,10 +1,8 @@ -export * as SessionID from "./session-id" - import { Schema } from "effect" import { descending } from "./identifier" import { withStatics } from "./schema" -export const ID = Schema.String.check(Schema.isStartsWith("ses")).pipe( +export const SessionID = Schema.String.check(Schema.isStartsWith("ses")).pipe( Schema.brand("SessionID"), withStatics((schema) => { const create = () => schema.make("ses_" + descending()) @@ -14,4 +12,4 @@ export const ID = Schema.String.check(Schema.isStartsWith("ses")).pipe( } }), ) -export type ID = typeof ID.Type +export type SessionID = typeof SessionID.Type diff --git a/packages/schema/src/session-input.ts b/packages/schema/src/session-input.ts index 13d795924988..55010c19161c 100644 --- a/packages/schema/src/session-input.ts +++ b/packages/schema/src/session-input.ts @@ -14,7 +14,7 @@ export interface Admitted extends Schema.Schema.Type {} export const Admitted = Schema.Struct({ admittedSeq: NonNegativeInt, id: SessionMessageID.ID, - sessionID: SessionID.ID, + sessionID: SessionID, prompt: Prompt, delivery: Delivery, timeCreated: DateTimeUtcFromMillis, diff --git a/packages/schema/src/session-message.ts b/packages/schema/src/session-message.ts index 806922332d4b..3631c3072cc3 100644 --- a/packages/schema/src/session-message.ts +++ b/packages/schema/src/session-message.ts @@ -49,7 +49,7 @@ export const User = Schema.Struct({ export interface Synthetic extends Schema.Schema.Type {} export const Synthetic = Schema.Struct({ ...Base, - sessionID: SessionID.ID, + sessionID: SessionID, text: Schema.String, type: Schema.Literal("synthetic"), }).annotate({ identifier: "Session.Message.Synthetic" }) diff --git a/packages/schema/src/session-status-event.ts b/packages/schema/src/session-status-event.ts index ef64fb4ed1ea..5233f97edb06 100644 --- a/packages/schema/src/session-status-event.ts +++ b/packages/schema/src/session-status-event.ts @@ -34,7 +34,7 @@ export type Info = Schema.Schema.Type export const Status = Event.define({ type: "session.status", schema: { - sessionID: SessionID.ID, + sessionID: SessionID, status: Info, }, }) @@ -43,6 +43,8 @@ export const Status = Event.define({ export const Idle = Event.define({ type: "session.idle", schema: { - sessionID: SessionID.ID, + sessionID: SessionID, }, }) + +export const Definitions = Event.inventory(Status, Idle) diff --git a/packages/schema/src/session-todo.ts b/packages/schema/src/session-todo.ts index 7fd6a2bb9d6f..d2757adf420e 100644 --- a/packages/schema/src/session-todo.ts +++ b/packages/schema/src/session-todo.ts @@ -1,7 +1,7 @@ export * as SessionTodo from "./session-todo" import { Schema } from "effect" -import { define } from "./event" +import { define, inventory } from "./event" import { SessionID } from "./session-id" export const Info = Schema.Struct({ @@ -14,13 +14,12 @@ export const Info = Schema.Struct({ export type Info = typeof Info.Type export const SessionTodoInfo = Info -export const Event = { - Updated: define({ - type: "todo.updated", - schema: { - sessionID: SessionID.ID, - todos: Schema.Array(Info), - }, - }), -} +const Updated = define({ + type: "todo.updated", + schema: { + sessionID: SessionID, + todos: Schema.Array(Info), + }, +}) +export const Event = { Updated, Definitions: inventory(Updated) } export const SessionTodoEvent = Event diff --git a/packages/schema/src/session-v1.ts b/packages/schema/src/session-v1.ts index fb0bb355dd03..6892041fc552 100644 --- a/packages/schema/src/session-v1.ts +++ b/packages/schema/src/session-v1.ts @@ -9,8 +9,8 @@ import { Provider } from "./provider" import { Model } from "./model" import { NonNegativeInt, optionalOmitUndefined, withStatics } from "./schema" import { ascending } from "./identifier" -import { Session } from "./session" -import { Workspace } from "./workspace" +import { SessionID } from "./session-id" +import { WorkspaceID } from "./workspace-id" const Timestamp = Schema.Finite.check(Schema.isGreaterThanOrEqualTo(0)) @@ -80,7 +80,7 @@ export type OutputFormat = Schema.Schema.Type const partBase = { id: PartID, - sessionID: Session.ID, + sessionID: SessionID, messageID: MessageID, } @@ -541,13 +541,13 @@ const SessionModel = Schema.Struct({ }) export const SessionInfo = Schema.Struct({ - id: Session.ID, + id: SessionID, slug: Schema.String, projectID: Project.ID, - workspaceID: optionalOmitUndefined(Workspace.ID), + workspaceID: optionalOmitUndefined(WorkspaceID), directory: Schema.String, path: optionalOmitUndefined(Schema.String), - parentID: optionalOmitUndefined(Session.ID), + parentID: optionalOmitUndefined(SessionID), summary: optionalOmitUndefined(SessionSummary), cost: optionalOmitUndefined(Schema.Finite), tokens: optionalOmitUndefined(SessionTokens), @@ -568,12 +568,12 @@ export const SessionInfo = Schema.Struct({ }).annotate({ identifier: "Session" }) export type SessionInfo = typeof SessionInfo.Type -export const Event = { +const events = { Created: define({ type: "session.created", ...options, schema: { - sessionID: Session.ID, + sessionID: SessionID, info: SessionInfo, }, }), @@ -581,7 +581,7 @@ export const Event = { type: "session.updated", ...options, schema: { - sessionID: Session.ID, + sessionID: SessionID, info: SessionInfo, }, }), @@ -589,7 +589,7 @@ export const Event = { type: "session.deleted", ...options, schema: { - sessionID: Session.ID, + sessionID: SessionID, info: SessionInfo, }, }), @@ -597,7 +597,7 @@ export const Event = { type: "message.updated", ...options, schema: { - sessionID: Session.ID, + sessionID: SessionID, info: Info, }, }), @@ -605,7 +605,7 @@ export const Event = { type: "message.removed", ...options, schema: { - sessionID: Session.ID, + sessionID: SessionID, messageID: MessageID, }, }), @@ -613,7 +613,7 @@ export const Event = { type: "message.part.updated", ...options, schema: { - sessionID: Session.ID, + sessionID: SessionID, part: Part, time: Schema.Finite, }, @@ -622,27 +622,17 @@ export const Event = { type: "message.part.removed", ...options, schema: { - sessionID: Session.ID, + sessionID: SessionID, messageID: MessageID, partID: PartID, }, }), } -export const Events = inventory( - Event.Created, - Event.Updated, - Event.Deleted, - Event.MessageUpdated, - Event.MessageRemoved, - Event.PartUpdated, - Event.PartRemoved, -) - export const PartDelta = define({ type: "message.part.delta", schema: { - sessionID: Session.ID, + sessionID: SessionID, messageID: MessageID, partID: PartID, field: Schema.String, @@ -653,7 +643,7 @@ export const PartDelta = define({ export const Diff = define({ type: "session.diff", schema: { - sessionID: Session.ID, + sessionID: SessionID, diff: Schema.Array(FileDiff.Info), }, }) @@ -661,13 +651,26 @@ export const Diff = define({ export const Error = define({ type: "session.error", schema: { - sessionID: Schema.optional(Session.ID), + sessionID: Schema.optional(SessionID), error: Assistant.fields.error, }, }) -export const SessionV1PublicEvent = { +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, + ), } diff --git a/packages/schema/src/session.ts b/packages/schema/src/session.ts index 7e0e16c75506..2bed421d2959 100644 --- a/packages/schema/src/session.ts +++ b/packages/schema/src/session.ts @@ -6,10 +6,13 @@ import { Location } from "./location" import { Model } from "./model" import { Project } from "./project" import { DateTimeUtcFromMillis, optionalOmitUndefined, RelativePath } from "./schema" +import { SessionEvent } from "./session-event" import { SessionID } from "./session-id" -export const ID = SessionID.ID -export type ID = SessionID.ID +export const ID = SessionID +export type ID = SessionID + +export const Event = SessionEvent export interface Info extends Schema.Schema.Type {} export const Info = Schema.Struct({ diff --git a/packages/schema/src/tui-event.ts b/packages/schema/src/tui-event.ts index a34a535c8e9e..41fa4dbeb6c0 100644 --- a/packages/schema/src/tui-event.ts +++ b/packages/schema/src/tui-event.ts @@ -51,6 +51,8 @@ export const ToastShow = Event.define({ export const SessionSelect = Event.define({ type: "tui.session.select", schema: { - sessionID: SessionID.ID.annotate({ description: "Session ID to navigate to" }), + sessionID: SessionID.annotate({ description: "Session ID to navigate to" }), }, }) + +export const Definitions = Event.inventory(PromptAppend, CommandExecute, ToastShow, SessionSelect) diff --git a/packages/schema/src/vcs-event.ts b/packages/schema/src/vcs-event.ts index c64d999164d0..ac1f74b98bea 100644 --- a/packages/schema/src/vcs-event.ts +++ b/packages/schema/src/vcs-event.ts @@ -9,3 +9,5 @@ export const BranchUpdated = Event.define({ branch: Schema.optional(Schema.String), }, }) + +export const Definitions = Event.inventory(BranchUpdated) diff --git a/packages/schema/src/workspace-event.ts b/packages/schema/src/workspace-event.ts index a429e7623f94..687af2cc7cb9 100644 --- a/packages/schema/src/workspace-event.ts +++ b/packages/schema/src/workspace-event.ts @@ -2,10 +2,10 @@ export * as WorkspaceEvent from "./workspace-event" import { Schema } from "effect" import { Event } from "./event" -import { Workspace } from "./workspace" +import { WorkspaceID } from "./workspace-id" export const ConnectionStatus = Schema.Struct({ - workspaceID: Workspace.ID, + workspaceID: WorkspaceID, status: Schema.Literals(["connected", "connecting", "disconnected", "error"]), }) export type ConnectionStatus = typeof ConnectionStatus.Type @@ -28,3 +28,5 @@ export const Status = Event.define({ type: "workspace.status", schema: ConnectionStatus.fields, }) + +export const Definitions = Event.inventory(Ready, Failed, Status) diff --git a/packages/schema/src/workspace-id.ts b/packages/schema/src/workspace-id.ts new file mode 100644 index 000000000000..e69a562ffccf --- /dev/null +++ b/packages/schema/src/workspace-id.ts @@ -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 diff --git a/packages/schema/src/workspace.ts b/packages/schema/src/workspace.ts index c2a7d1a6f943..ce35bf3b2452 100644 --- a/packages/schema/src/workspace.ts +++ b/packages/schema/src/workspace.ts @@ -1,21 +1,9 @@ export * as Workspace from "./workspace" -import { Schema } from "effect" -import { ascending } from "./identifier" -import { withStatics } from "./schema" +import { WorkspaceEvent } from "./workspace-event" +import { WorkspaceID } from "./workspace-id" -export const ID = Schema.String.check(Schema.isStartsWith("wrk")).pipe( - Schema.brand("WorkspaceV2.ID"), - withStatics((schema) => { - const create = () => schema.make("wrk_" + ascending()) - return { - ascending: (id?: string) => { - if (!id) return create() - if (!id.startsWith("wrk")) throw new Error(`ID ${id} does not start with wrk`) - return schema.make(id) - }, - create, - } - }), -) -export type ID = typeof ID.Type +export const ID = WorkspaceID +export type ID = WorkspaceID + +export const Event = WorkspaceEvent diff --git a/packages/schema/src/worktree-event.ts b/packages/schema/src/worktree-event.ts index f5823c478816..809fe8d9fe36 100644 --- a/packages/schema/src/worktree-event.ts +++ b/packages/schema/src/worktree-event.ts @@ -17,3 +17,5 @@ export const Failed = Event.define({ message: Schema.String, }, }) + +export const Definitions = Event.inventory(Ready, Failed) diff --git a/packages/schema/test/event-manifest.test.ts b/packages/schema/test/event-manifest.test.ts index 5084d0de4a12..5694afdd30df 100644 --- a/packages/schema/test/event-manifest.test.ts +++ b/packages/schema/test/event-manifest.test.ts @@ -1,21 +1,52 @@ 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.CoreDefinitions.length).toBe(36) - expect(EventManifest.PublicDefinitions.length).toBe(55) + 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) }) diff --git a/packages/schema/test/legacy-event.test.ts b/packages/schema/test/legacy-event.test.ts index 80524ddcb47c..e43c5681f201 100644 --- a/packages/schema/test/legacy-event.test.ts +++ b/packages/schema/test/legacy-event.test.ts @@ -2,11 +2,12 @@ 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("keeps the seven durable SessionV1 definitions", () => { - expect(SessionV1.Events.map((event) => event.type)).toEqual([ + test("owns all SessionV1 definitions", () => { + expect(SessionV1.Event.Definitions.map((event) => event.type)).toEqual([ "session.created", "session.updated", "session.deleted", @@ -14,9 +15,14 @@ describe("legacy public event schemas", () => { "message.removed", "message.part.updated", "message.part.removed", + "message.part.delta", + "session.diff", + "session.error", ]) - expect(SessionV1.Events.every((event) => event.durable?.aggregate === "sessionID")).toBe(true) - expect(SessionV1.Events.every((event) => event.durable?.version === 1)).toBe(true) + 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", () => { @@ -29,7 +35,7 @@ describe("legacy public event schemas", () => { QuestionV1.Event.Asked.type, QuestionV1.Event.Replied.type, QuestionV1.Event.Rejected.type, - LegacyEvent.ProjectUpdated.type, + Project.Event.Updated.type, LegacyEvent.CommandExecuted.type, ]).toEqual([ "message.part.delta", From 072690543f9ba4854d5e6a79c0d77aa99278404d Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 24 Jun 2026 16:18:14 -0400 Subject: [PATCH 6/7] refactor(schema): remove file diff alias --- packages/opencode/src/snapshot/index.ts | 4 ++-- packages/schema/src/file-diff.ts | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index bf39b6eacc13..c425d08dba7b 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -9,7 +9,7 @@ import { FSUtil } from "@opencode-ai/core/fs-util" import { Hash } from "@opencode-ai/core/util/hash" import { Config } from "@/config/config" import { Global } from "@opencode-ai/core/global" -import { FileDiffInfo } from "@opencode-ai/schema/file-diff" +import { Info } from "@opencode-ai/schema/file-diff" export const Patch = Schema.Struct({ hash: Schema.String, @@ -17,7 +17,7 @@ export const Patch = Schema.Struct({ }) export type Patch = typeof Patch.Type -export const FileDiff = FileDiffInfo +export const FileDiff = Info export type FileDiff = typeof FileDiff.Type const prune = "7.days" diff --git a/packages/schema/src/file-diff.ts b/packages/schema/src/file-diff.ts index 73d69c1a5604..26bfd4155b44 100644 --- a/packages/schema/src/file-diff.ts +++ b/packages/schema/src/file-diff.ts @@ -10,4 +10,3 @@ export const Info = Schema.Struct({ status: Schema.optional(Schema.Literals(["added", "deleted", "modified"])), }).annotate({ identifier: "SnapshotFileDiff" }) export type Info = typeof Info.Type -export const FileDiffInfo = Info From 39525460526038c841286d7f0a94d47693c86ebf Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 24 Jun 2026 16:34:17 -0400 Subject: [PATCH 7/7] refactor(schema): remove redundant event aliases --- packages/core/src/catalog.ts | 4 ++-- packages/core/src/filesystem.ts | 4 ++-- packages/core/src/session/todo.ts | 4 ++-- packages/schema/src/catalog.ts | 1 - packages/schema/src/filesystem.ts | 1 - packages/schema/src/session-todo.ts | 1 - 6 files changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/core/src/catalog.ts b/packages/core/src/catalog.ts index 884cc2b051c5..5f90b09f308c 100644 --- a/packages/core/src/catalog.ts +++ b/packages/core/src/catalog.ts @@ -1,7 +1,7 @@ export * as Catalog from "./catalog" import { Array, Context, Effect, Layer, Option, Order, pipe, Schema } from "effect" -import { CatalogEvent } from "@opencode-ai/schema/catalog" +import { Catalog } from "@opencode-ai/schema/catalog" import { ModelV2 } from "./model" import { ProviderV2 } from "./provider" import { EventV2 } from "./event" @@ -18,7 +18,7 @@ export type DefaultModel = { providerID: ProviderV2.ID; modelID: ModelV2.ID } export const PolicyActions = Schema.Literals(["provider.use"]) -export const Event = CatalogEvent +export const Event = Catalog.Event type Data = { providers: Map diff --git a/packages/core/src/filesystem.ts b/packages/core/src/filesystem.ts index 6edea1be4a73..e55ebbb66d37 100644 --- a/packages/core/src/filesystem.ts +++ b/packages/core/src/filesystem.ts @@ -6,7 +6,7 @@ import { FSUtil } from "./fs-util" import { Location } from "./location" import { PositiveInt, RelativePath } from "./schema" import { FileSystemSearch } from "./filesystem/search" -import { Entry, FileSystemEvent, 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 const ReadInput = Schema.Struct({ @@ -47,7 +47,7 @@ export class GrepInput extends Schema.Class("FileSystem.GrepInput")({ limit: PositiveInt.pipe(Schema.optional), }) {} -export const Event = FileSystemEvent +export const Event = FileSystem.Event export interface Interface { readonly read: (input: ReadInput) => Effect.Effect<{ readonly content: Uint8Array; readonly mime: string }> diff --git a/packages/core/src/session/todo.ts b/packages/core/src/session/todo.ts index 22a62711c8bc..95420f78f728 100644 --- a/packages/core/src/session/todo.ts +++ b/packages/core/src/session/todo.ts @@ -2,7 +2,7 @@ export * as SessionTodo from "./todo" import { asc, eq } from "drizzle-orm" import { Context, Effect, Layer } from "effect" -import { SessionTodoEvent, SessionTodoInfo } from "@opencode-ai/schema/session-todo" +import { SessionTodo, SessionTodoInfo } from "@opencode-ai/schema/session-todo" import { Database } from "../database/database" import { EventV2 } from "../event" import { SessionSchema } from "./schema" @@ -11,7 +11,7 @@ import { TodoTable } from "./sql" export const Info = SessionTodoInfo export type Info = typeof Info.Type -export const Event = SessionTodoEvent +export const Event = SessionTodo.Event export interface Interface { readonly update: (input: { diff --git a/packages/schema/src/catalog.ts b/packages/schema/src/catalog.ts index 93df6d973b0e..54abb5b1280b 100644 --- a/packages/schema/src/catalog.ts +++ b/packages/schema/src/catalog.ts @@ -4,4 +4,3 @@ import { define, inventory } from "./event" const Updated = define({ type: "catalog.updated", schema: {} }) export const Event = { Updated, Definitions: inventory(Updated) } -export const CatalogEvent = Event diff --git a/packages/schema/src/filesystem.ts b/packages/schema/src/filesystem.ts index 0d6b8deefa6a..045f7a7d2ebe 100644 --- a/packages/schema/src/filesystem.ts +++ b/packages/schema/src/filesystem.ts @@ -9,7 +9,6 @@ const Edited = define({ schema: { file: Schema.String }, }) export const Event = { Edited, Definitions: inventory(Edited) } -export const FileSystemEvent = Event export interface Entry extends Schema.Schema.Type {} export const Entry = Schema.Struct({ diff --git a/packages/schema/src/session-todo.ts b/packages/schema/src/session-todo.ts index d2757adf420e..cf62c5186cb9 100644 --- a/packages/schema/src/session-todo.ts +++ b/packages/schema/src/session-todo.ts @@ -22,4 +22,3 @@ const Updated = define({ }, }) export const Event = { Updated, Definitions: inventory(Updated) } -export const SessionTodoEvent = Event