mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-09 23:08:29 +00:00
refactor(schema): extract public event definitions (#33579)
This commit is contained in:
parent
858f35f1b3
commit
24b0132bc5
93 changed files with 2740 additions and 2124 deletions
1
bun.lock
1
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:*",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
export * as Catalog from "./catalog"
|
||||
|
||||
import { Array, Context, Effect, Layer, Option, Order, pipe, Schema } from "effect"
|
||||
import { Catalog } 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 = Catalog.Event
|
||||
|
||||
type Data = {
|
||||
providers: Map<ProviderV2.ID, ProviderRecord>
|
||||
|
|
|
|||
|
|
@ -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 { Durable } from "@opencode-ai/schema/durable-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<Type extends string = string, DataSchema extends Schema.Top = Schema.Top> = {
|
||||
readonly type: Type
|
||||
readonly durable?: {
|
||||
readonly version: number
|
||||
readonly aggregate: string
|
||||
}
|
||||
readonly data: DataSchema
|
||||
}
|
||||
|
||||
export type Data<D extends Definition> = Schema.Schema.Type<D["data"]>
|
||||
|
||||
export type Payload<D extends Definition = Definition> = {
|
||||
readonly id: ID
|
||||
readonly type: D["type"]
|
||||
readonly data: Data<D>
|
||||
readonly durable?: {
|
||||
readonly aggregateID: string
|
||||
readonly seq: number
|
||||
readonly version: number
|
||||
}
|
||||
readonly location?: Location.Ref
|
||||
readonly metadata?: Record<string, unknown>
|
||||
}
|
||||
export 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<D extends Definition = Definition> = (event: Payload<D>) => Effect.Effect<void>
|
||||
export type Unsubscribe = Effect.Effect<void>
|
||||
|
|
@ -74,52 +47,8 @@ export class InvalidDurableEventError extends Schema.TaggedErrorClass<InvalidDur
|
|||
},
|
||||
) {}
|
||||
|
||||
export function versionedType(type: string, version: number) {
|
||||
return `${type}.${version}`
|
||||
}
|
||||
|
||||
export const registry = new Map<string, Definition>()
|
||||
const durableRegistry = new Map<string, Definition>()
|
||||
|
||||
export function define<const Type extends string, Fields extends Schema.Struct.Fields>(input: {
|
||||
readonly type: Type
|
||||
readonly durable?: {
|
||||
readonly version: number
|
||||
readonly aggregate: string
|
||||
}
|
||||
readonly schema: Fields
|
||||
}): Schema.Schema<Payload<Definition<Type, Schema.Struct<Fields>>>> & Definition<Type, Schema.Struct<Fields>> {
|
||||
const Data = Schema.Struct(input.schema)
|
||||
const Payload = Schema.Struct({
|
||||
id: ID,
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
||||
type: Schema.Literal(input.type),
|
||||
durable: Schema.optional(Schema.Struct({ aggregateID: Schema.String, seq: Schema.Number, version: Schema.Number })),
|
||||
location: Schema.optional(Location.Ref),
|
||||
data: Data,
|
||||
}).annotate({ identifier: input.type })
|
||||
|
||||
const definition = Object.assign(Payload, {
|
||||
type: input.type,
|
||||
...(input.durable === undefined ? {} : { durable: input.durable }),
|
||||
data: Data,
|
||||
})
|
||||
const existing = registry.get(input.type)
|
||||
if (
|
||||
input.durable === undefined ||
|
||||
existing?.durable === undefined ||
|
||||
input.durable.version >= existing.durable.version
|
||||
) {
|
||||
registry.set(input.type, definition)
|
||||
}
|
||||
if (input.durable) durableRegistry.set(versionedType(input.type, input.durable.version), definition)
|
||||
return definition as Schema.Schema<Payload<Definition<Type, Schema.Struct<Fields>>>> &
|
||||
Definition<Type, Schema.Struct<Fields>>
|
||||
}
|
||||
|
||||
export function definitions() {
|
||||
return registry.values().toArray()
|
||||
}
|
||||
export const define = Event.define
|
||||
export const versionedType = Event.versionedType
|
||||
|
||||
export interface PublishOptions {
|
||||
readonly id?: ID
|
||||
|
|
@ -169,6 +98,7 @@ export const layerWith = (options?: LayerOptions) =>
|
|||
typed: new Map<string, PubSub.PubSub<Payload>>(),
|
||||
}
|
||||
const projectors = new Map<string, Subscriber[]>()
|
||||
// TODO: Bind durable projectors to exact type+version before supporting incompatible historical payloads.
|
||||
const listeners = new Array<Subscriber>()
|
||||
const { db } = yield* Database.Service
|
||||
|
||||
|
|
@ -194,6 +124,7 @@ export const layerWith = (options?: LayerOptions) =>
|
|||
)
|
||||
|
||||
function commitDurableEvent(
|
||||
definition: Definition,
|
||||
event: Payload,
|
||||
input?: {
|
||||
readonly seq: number
|
||||
|
|
@ -204,7 +135,6 @@ export const layerWith = (options?: LayerOptions) =>
|
|||
commit?: (seq: number) => Effect.Effect<void>,
|
||||
) {
|
||||
return Effect.gen(function* () {
|
||||
const definition = registry.get(event.type)
|
||||
const durable = definition?.durable
|
||||
if (durable) {
|
||||
const aggregateID = (event.data as Record<string, unknown>)[durable.aggregate]
|
||||
|
|
@ -238,9 +168,10 @@ export const layerWith = (options?: LayerOptions) =>
|
|||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
const latest = row?.seq ?? -1
|
||||
const encoded = Schema.encodeUnknownSync(
|
||||
definition.data as Schema.Codec<unknown, unknown, never, never>,
|
||||
)(event.data) as Record<string, unknown>
|
||||
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({
|
||||
|
|
@ -356,9 +287,8 @@ export const layerWith = (options?: LayerOptions) =>
|
|||
})
|
||||
}
|
||||
|
||||
function publishEvent<D extends Definition>(event: Payload<D>, commit?: PublishOptions["commit"]) {
|
||||
function publishEvent<D extends Definition>(definition: D, event: Payload<D>, commit?: PublishOptions["commit"]) {
|
||||
return Effect.gen(function* () {
|
||||
const definition = registry.get(event.type)
|
||||
if (!definition?.durable && commit)
|
||||
return yield* Effect.die(
|
||||
new InvalidDurableEventError({
|
||||
|
|
@ -367,7 +297,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 +346,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 +364,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 = Durable.get(event.type)
|
||||
if (!definition?.durable) {
|
||||
yield* Effect.die(
|
||||
new InvalidDurableEventError({ type: event.type, message: `Unknown durable event type ${event.type}` }),
|
||||
|
|
@ -442,11 +373,9 @@ export const layerWith = (options?: LayerOptions) =>
|
|||
const payload = {
|
||||
id: event.id,
|
||||
type: definition.type,
|
||||
data: Schema.decodeUnknownSync(definition.data as Schema.Codec<unknown, unknown, never, never>)(
|
||||
event.data,
|
||||
),
|
||||
data: Schema.decodeUnknownSync(definition.data)(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 +459,8 @@ export const layerWith = (options?: LayerOptions) =>
|
|||
|
||||
const streamAll = (): Stream.Stream<Payload> => Stream.fromPubSub(pubsub.all)
|
||||
|
||||
const decodeSerializedEvent = (event: SerializedEvent): Payload => {
|
||||
const definition = durableRegistry.get(event.type)
|
||||
const decodeSerializedEvent = (event: SerializedEvent) => {
|
||||
const definition = Durable.get(event.type)
|
||||
if (!definition?.durable) {
|
||||
throw new InvalidDurableEventError({ type: event.type, message: `Unknown durable event type ${event.type}` })
|
||||
}
|
||||
|
|
@ -539,7 +468,7 @@ export const layerWith = (options?: LayerOptions) =>
|
|||
id: event.id,
|
||||
type: definition.type,
|
||||
durable: { aggregateID: event.aggregateID, seq: event.seq, version: definition.durable.version },
|
||||
data: Schema.decodeUnknownSync(definition.data as Schema.Codec<unknown, unknown, never, never>)(event.data),
|
||||
data: Schema.decodeUnknownSync(definition.data)(event.data),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,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, FileSystem, 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<GrepInput>("FileSystem.GrepInput")({
|
|||
limit: PositiveInt.pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
export const Event = {
|
||||
Edited: EventV2.define({
|
||||
type: "file.edited",
|
||||
schema: {
|
||||
file: Schema.String,
|
||||
},
|
||||
}),
|
||||
}
|
||||
export const Event = FileSystem.Event
|
||||
|
||||
export interface Interface {
|
||||
readonly read: (input: ReadInput) => Effect.Effect<{ readonly content: Uint8Array; readonly mime: string }>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -136,16 +136,7 @@ export class AuthorizationError extends Schema.TaggedErrorClass<AuthorizationErr
|
|||
|
||||
export type Error = CodeRequiredError | AuthorizationError
|
||||
|
||||
export const Event = {
|
||||
Updated: EventV2.define({
|
||||
type: "integration.updated",
|
||||
schema: {},
|
||||
}),
|
||||
ConnectionUpdated: EventV2.define({
|
||||
type: "integration.connection.updated",
|
||||
schema: { integrationID: ID },
|
||||
}),
|
||||
}
|
||||
export const Event = Integration.Event
|
||||
|
||||
export const Ref = Integration.Ref
|
||||
export type Ref = Integration.Ref
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import path from "path"
|
||||
import { Context, Duration, Effect, Layer, Option, Schedule, Schema } from "effect"
|
||||
import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http"
|
||||
import { ModelsDev } from "@opencode-ai/schema/models-dev"
|
||||
import { Global } from "./global"
|
||||
import { Flag } from "./flag/flag"
|
||||
import { Flock } from "./util/flock"
|
||||
|
|
@ -108,12 +109,7 @@ export const Provider = Schema.Struct({
|
|||
|
||||
export type Provider = Schema.Schema.Type<typeof Provider>
|
||||
|
||||
export const Event = {
|
||||
Refreshed: EventV2.define({
|
||||
type: "models-dev.refreshed",
|
||||
schema: {},
|
||||
}),
|
||||
}
|
||||
export const Event = ModelsDev.Event
|
||||
|
||||
declare const OPENCODE_MODELS_DEV: Record<string, Provider> | undefined
|
||||
|
||||
|
|
|
|||
|
|
@ -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<RejectedError>()("PermissionV2.RejectedError", {}) {}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<void>
|
||||
|
|
|
|||
|
|
@ -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<ListEntry[], Git.WorktreeError | DirectoryUnavailableError>
|
||||
}
|
||||
|
||||
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<void, DuplicateStrategyError>
|
||||
|
|
|
|||
|
|
@ -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<typeof Info.Type>
|
||||
|
||||
|
|
@ -100,12 +90,7 @@ export class ExitedError extends Schema.TaggedErrorClass<ExitedError>()("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<Info[]>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
3
packages/core/src/public-event-manifest.ts
Normal file
3
packages/core/src/public-event-manifest.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export * as PublicEventManifest from "./public-event-manifest"
|
||||
|
||||
export { ServerDefinitions as Definitions } from "@opencode-ai/schema/event-manifest"
|
||||
|
|
@ -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<RejectedError>()("QuestionV2.RejectedError", {}) {
|
||||
override get message() {
|
||||
|
|
|
|||
|
|
@ -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<Info>("Reference.Info")({
|
||||
name: Schema.String,
|
||||
|
|
|
|||
|
|
@ -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 * as SessionEvent from "@opencode-ai/schema/session-event"
|
||||
|
|
|
|||
|
|
@ -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 { SessionTodo, 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 = SessionTodo.Event
|
||||
|
||||
export interface Interface {
|
||||
readonly update: (input: {
|
||||
|
|
|
|||
|
|
@ -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<RejectedError>()("PermissionRejectedError", {}) {
|
||||
override get message() {
|
||||
|
|
|
|||
|
|
@ -1,39 +1,52 @@
|
|||
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 { 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,
|
||||
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,581 +65,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>("OutputFormatText")({
|
||||
type: Schema.Literal("text"),
|
||||
}) {}
|
||||
|
||||
export class OutputFormatJsonSchema extends Schema.Class<OutputFormatJsonSchema>("OutputFormatJsonSchema")({
|
||||
type: Schema.Literal("json_schema"),
|
||||
schema: Schema.Record(Schema.String, Schema.Any).annotate({ identifier: "JSONSchema" }),
|
||||
retryCount: NonNegativeInt.pipe(Schema.optional, Schema.withDecodingDefault(Effect.succeed(2))),
|
||||
}) {}
|
||||
|
||||
export const Format = Schema.Union([OutputFormatText, OutputFormatJsonSchema]).annotate({
|
||||
discriminator: "type",
|
||||
identifier: "OutputFormat",
|
||||
})
|
||||
export type OutputFormat = Schema.Schema.Type<typeof Format>
|
||||
|
||||
const partBase = {
|
||||
id: PartID,
|
||||
sessionID: SessionSchema.ID,
|
||||
messageID: MessageID,
|
||||
}
|
||||
|
||||
export const SnapshotPart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("snapshot"),
|
||||
snapshot: Schema.String,
|
||||
}).annotate({ identifier: "SnapshotPart" })
|
||||
export type SnapshotPart = Types.DeepMutable<Schema.Schema.Type<typeof SnapshotPart>>
|
||||
|
||||
export const PatchPart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("patch"),
|
||||
hash: Schema.String,
|
||||
files: Schema.Array(Schema.String),
|
||||
}).annotate({ identifier: "PatchPart" })
|
||||
export type PatchPart = Types.DeepMutable<Schema.Schema.Type<typeof PatchPart>>
|
||||
|
||||
export const TextPart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("text"),
|
||||
text: Schema.String,
|
||||
synthetic: Schema.optional(Schema.Boolean),
|
||||
ignored: Schema.optional(Schema.Boolean),
|
||||
time: Schema.optional(
|
||||
Schema.Struct({
|
||||
start: NonNegativeInt,
|
||||
end: Schema.optional(NonNegativeInt),
|
||||
}),
|
||||
),
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
|
||||
}).annotate({ identifier: "TextPart" })
|
||||
export type TextPart = Types.DeepMutable<Schema.Schema.Type<typeof TextPart>>
|
||||
|
||||
export const ReasoningPart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("reasoning"),
|
||||
text: Schema.String,
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
|
||||
time: Schema.Struct({
|
||||
start: NonNegativeInt,
|
||||
end: Schema.optional(NonNegativeInt),
|
||||
}),
|
||||
}).annotate({ identifier: "ReasoningPart" })
|
||||
export type ReasoningPart = Types.DeepMutable<Schema.Schema.Type<typeof ReasoningPart>>
|
||||
|
||||
const filePartSourceBase = {
|
||||
text: Schema.Struct({
|
||||
value: Schema.String,
|
||||
start: Schema.Finite,
|
||||
end: Schema.Finite,
|
||||
}).annotate({ identifier: "FilePartSourceText" }),
|
||||
}
|
||||
|
||||
export const Range = Schema.Struct({
|
||||
start: Schema.Struct({ line: NonNegativeInt, character: NonNegativeInt }),
|
||||
end: Schema.Struct({ line: NonNegativeInt, character: NonNegativeInt }),
|
||||
}).annotate({ identifier: "Range" })
|
||||
export type Range = typeof Range.Type
|
||||
|
||||
export const FileSource = Schema.Struct({
|
||||
...filePartSourceBase,
|
||||
type: Schema.Literal("file"),
|
||||
path: Schema.String,
|
||||
}).annotate({ identifier: "FileSource" })
|
||||
|
||||
export const SymbolSource = Schema.Struct({
|
||||
...filePartSourceBase,
|
||||
type: Schema.Literal("symbol"),
|
||||
path: Schema.String,
|
||||
range: Range,
|
||||
name: Schema.String,
|
||||
kind: NonNegativeInt,
|
||||
}).annotate({ identifier: "SymbolSource" })
|
||||
|
||||
export const ResourceSource = Schema.Struct({
|
||||
...filePartSourceBase,
|
||||
type: Schema.Literal("resource"),
|
||||
clientName: Schema.String,
|
||||
uri: Schema.String,
|
||||
}).annotate({ identifier: "ResourceSource" })
|
||||
|
||||
export const FilePartSource = Schema.Union([FileSource, SymbolSource, ResourceSource]).annotate({
|
||||
discriminator: "type",
|
||||
identifier: "FilePartSource",
|
||||
})
|
||||
|
||||
export const FilePart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("file"),
|
||||
mime: Schema.String,
|
||||
filename: Schema.optional(Schema.String),
|
||||
url: Schema.String,
|
||||
source: Schema.optional(FilePartSource),
|
||||
}).annotate({ identifier: "FilePart" })
|
||||
export type FilePart = Types.DeepMutable<Schema.Schema.Type<typeof FilePart>>
|
||||
|
||||
export const AgentPart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("agent"),
|
||||
name: Schema.String,
|
||||
source: Schema.optional(
|
||||
Schema.Struct({
|
||||
value: Schema.String,
|
||||
start: NonNegativeInt,
|
||||
end: NonNegativeInt,
|
||||
}),
|
||||
),
|
||||
}).annotate({ identifier: "AgentPart" })
|
||||
export type AgentPart = Types.DeepMutable<Schema.Schema.Type<typeof AgentPart>>
|
||||
|
||||
export const CompactionPart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("compaction"),
|
||||
auto: Schema.Boolean,
|
||||
overflow: Schema.optional(Schema.Boolean),
|
||||
tail_start_id: Schema.optional(MessageID),
|
||||
}).annotate({ identifier: "CompactionPart" })
|
||||
export type CompactionPart = Types.DeepMutable<Schema.Schema.Type<typeof CompactionPart>>
|
||||
|
||||
export const SubtaskPart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("subtask"),
|
||||
prompt: Schema.String,
|
||||
description: Schema.String,
|
||||
agent: Schema.String,
|
||||
model: Schema.optional(
|
||||
Schema.Struct({
|
||||
providerID: ProviderV2.ID,
|
||||
modelID: ModelV2.ID,
|
||||
}),
|
||||
),
|
||||
command: Schema.optional(Schema.String),
|
||||
}).annotate({ identifier: "SubtaskPart" })
|
||||
export type SubtaskPart = Types.DeepMutable<Schema.Schema.Type<typeof SubtaskPart>>
|
||||
|
||||
export const RetryPart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("retry"),
|
||||
attempt: NonNegativeInt,
|
||||
error: APIError.EffectSchema,
|
||||
time: Schema.Struct({
|
||||
created: NonNegativeInt,
|
||||
}),
|
||||
}).annotate({ identifier: "RetryPart" })
|
||||
export type RetryPart = Omit<Types.DeepMutable<Schema.Schema.Type<typeof RetryPart>>, "error"> & {
|
||||
error: APIError
|
||||
}
|
||||
|
||||
export const StepStartPart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("step-start"),
|
||||
snapshot: Schema.optional(Schema.String),
|
||||
}).annotate({ identifier: "StepStartPart" })
|
||||
export type StepStartPart = Types.DeepMutable<Schema.Schema.Type<typeof StepStartPart>>
|
||||
|
||||
export const StepFinishPart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("step-finish"),
|
||||
reason: Schema.String,
|
||||
snapshot: Schema.optional(Schema.String),
|
||||
cost: Schema.Finite,
|
||||
tokens: Schema.Struct({
|
||||
total: Schema.optional(Schema.Finite),
|
||||
input: Schema.Finite,
|
||||
output: Schema.Finite,
|
||||
reasoning: Schema.Finite,
|
||||
cache: Schema.Struct({
|
||||
read: Schema.Finite,
|
||||
write: Schema.Finite,
|
||||
}),
|
||||
}),
|
||||
}).annotate({ identifier: "StepFinishPart" })
|
||||
export type StepFinishPart = Types.DeepMutable<Schema.Schema.Type<typeof StepFinishPart>>
|
||||
|
||||
export const ToolStatePending = Schema.Struct({
|
||||
status: Schema.Literal("pending"),
|
||||
input: Schema.Record(Schema.String, Schema.Any),
|
||||
raw: Schema.String,
|
||||
}).annotate({ identifier: "ToolStatePending" })
|
||||
export type ToolStatePending = Types.DeepMutable<Schema.Schema.Type<typeof ToolStatePending>>
|
||||
|
||||
export const ToolStateRunning = Schema.Struct({
|
||||
status: Schema.Literal("running"),
|
||||
input: Schema.Record(Schema.String, Schema.Any),
|
||||
title: Schema.optional(Schema.String),
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
|
||||
time: Schema.Struct({
|
||||
start: NonNegativeInt,
|
||||
}),
|
||||
}).annotate({ identifier: "ToolStateRunning" })
|
||||
export type ToolStateRunning = Types.DeepMutable<Schema.Schema.Type<typeof ToolStateRunning>>
|
||||
|
||||
export const ToolStateCompleted = Schema.Struct({
|
||||
status: Schema.Literal("completed"),
|
||||
input: Schema.Record(Schema.String, Schema.Any),
|
||||
output: Schema.String,
|
||||
title: Schema.String,
|
||||
metadata: Schema.Record(Schema.String, Schema.Any),
|
||||
time: Schema.Struct({
|
||||
start: NonNegativeInt,
|
||||
end: NonNegativeInt,
|
||||
compacted: Schema.optional(NonNegativeInt),
|
||||
}),
|
||||
attachments: Schema.optional(Schema.Array(FilePart)),
|
||||
}).annotate({ identifier: "ToolStateCompleted" })
|
||||
export type ToolStateCompleted = Types.DeepMutable<Schema.Schema.Type<typeof ToolStateCompleted>>
|
||||
|
||||
export const ToolStateError = Schema.Struct({
|
||||
status: Schema.Literal("error"),
|
||||
input: Schema.Record(Schema.String, Schema.Any),
|
||||
error: Schema.String,
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
|
||||
time: Schema.Struct({
|
||||
start: NonNegativeInt,
|
||||
end: NonNegativeInt,
|
||||
}),
|
||||
}).annotate({ identifier: "ToolStateError" })
|
||||
export type ToolStateError = Types.DeepMutable<Schema.Schema.Type<typeof ToolStateError>>
|
||||
|
||||
export const ToolState = Schema.Union([
|
||||
ToolStatePending,
|
||||
ToolStateRunning,
|
||||
ToolStateCompleted,
|
||||
ToolStateError,
|
||||
]).annotate({
|
||||
discriminator: "status",
|
||||
identifier: "ToolState",
|
||||
})
|
||||
export type ToolState = ToolStatePending | ToolStateRunning | ToolStateCompleted | ToolStateError
|
||||
|
||||
export const ToolPart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("tool"),
|
||||
callID: Schema.String,
|
||||
tool: Schema.String,
|
||||
state: ToolState,
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
|
||||
}).annotate({ identifier: "ToolPart" })
|
||||
export type ToolPart = Omit<Types.DeepMutable<Schema.Schema.Type<typeof ToolPart>>, "state"> & {
|
||||
state: ToolState
|
||||
}
|
||||
|
||||
const messageBase = {
|
||||
id: MessageID,
|
||||
sessionID: partBase.sessionID,
|
||||
}
|
||||
|
||||
const FileDiff = Schema.Struct({
|
||||
file: Schema.optional(Schema.String),
|
||||
patch: Schema.optional(Schema.String),
|
||||
additions: Schema.Finite,
|
||||
deletions: Schema.Finite,
|
||||
status: Schema.optional(Schema.Literals(["added", "deleted", "modified"])),
|
||||
}).annotate({ identifier: "SnapshotFileDiff" })
|
||||
|
||||
export const User = Schema.Struct({
|
||||
...messageBase,
|
||||
role: Schema.Literal("user"),
|
||||
time: Schema.Struct({
|
||||
created: Timestamp,
|
||||
}),
|
||||
format: Schema.optional(Format),
|
||||
summary: Schema.optional(
|
||||
Schema.Struct({
|
||||
title: Schema.optional(Schema.String),
|
||||
body: Schema.optional(Schema.String),
|
||||
diffs: Schema.Array(FileDiff),
|
||||
}),
|
||||
),
|
||||
agent: Schema.String,
|
||||
model: Schema.Struct({
|
||||
providerID: ProviderV2.ID,
|
||||
modelID: ModelV2.ID,
|
||||
variant: Schema.optional(Schema.String),
|
||||
}),
|
||||
system: Schema.optional(Schema.String),
|
||||
tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)),
|
||||
}).annotate({ identifier: "UserMessage" })
|
||||
export type User = Types.DeepMutable<Schema.Schema.Type<typeof User>>
|
||||
|
||||
export const Part = Schema.Union([
|
||||
TextPart,
|
||||
SubtaskPart,
|
||||
ReasoningPart,
|
||||
FilePart,
|
||||
ToolPart,
|
||||
StepStartPart,
|
||||
StepFinishPart,
|
||||
SnapshotPart,
|
||||
PatchPart,
|
||||
AgentPart,
|
||||
RetryPart,
|
||||
CompactionPart,
|
||||
]).annotate({ discriminator: "type", identifier: "Part" })
|
||||
export type Part =
|
||||
| TextPart
|
||||
| SubtaskPart
|
||||
| ReasoningPart
|
||||
| FilePart
|
||||
| ToolPart
|
||||
| StepStartPart
|
||||
| StepFinishPart
|
||||
| SnapshotPart
|
||||
| PatchPart
|
||||
| AgentPart
|
||||
| RetryPart
|
||||
| CompactionPart
|
||||
|
||||
const AssistantErrorSchema = Schema.Union([
|
||||
AuthError.EffectSchema,
|
||||
NamedError.Unknown.EffectSchema,
|
||||
OutputLengthError.EffectSchema,
|
||||
AbortedError.EffectSchema,
|
||||
StructuredOutputError.EffectSchema,
|
||||
ContextOverflowError.EffectSchema,
|
||||
ContentFilterError.EffectSchema,
|
||||
APIError.EffectSchema,
|
||||
]).annotate({ discriminator: "name" })
|
||||
type AssistantError = Schema.Schema.Type<typeof AssistantErrorSchema>
|
||||
|
||||
export const TextPartInput = Schema.Struct({
|
||||
id: Schema.optional(PartID),
|
||||
type: Schema.Literal("text"),
|
||||
text: Schema.String,
|
||||
synthetic: Schema.optional(Schema.Boolean),
|
||||
ignored: Schema.optional(Schema.Boolean),
|
||||
time: Schema.optional(
|
||||
Schema.Struct({
|
||||
start: NonNegativeInt,
|
||||
end: Schema.optional(NonNegativeInt),
|
||||
}),
|
||||
),
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
|
||||
}).annotate({ identifier: "TextPartInput" })
|
||||
export type TextPartInput = Types.DeepMutable<Schema.Schema.Type<typeof TextPartInput>>
|
||||
|
||||
export const FilePartInput = Schema.Struct({
|
||||
id: Schema.optional(PartID),
|
||||
type: Schema.Literal("file"),
|
||||
mime: Schema.String,
|
||||
filename: Schema.optional(Schema.String),
|
||||
url: Schema.String,
|
||||
source: Schema.optional(FilePartSource),
|
||||
}).annotate({ identifier: "FilePartInput" })
|
||||
export type FilePartInput = Types.DeepMutable<Schema.Schema.Type<typeof FilePartInput>>
|
||||
|
||||
export const AgentPartInput = Schema.Struct({
|
||||
id: Schema.optional(PartID),
|
||||
type: Schema.Literal("agent"),
|
||||
name: Schema.String,
|
||||
source: Schema.optional(
|
||||
Schema.Struct({
|
||||
value: Schema.String,
|
||||
start: NonNegativeInt,
|
||||
end: NonNegativeInt,
|
||||
}),
|
||||
),
|
||||
}).annotate({ identifier: "AgentPartInput" })
|
||||
export type AgentPartInput = Types.DeepMutable<Schema.Schema.Type<typeof AgentPartInput>>
|
||||
|
||||
export const SubtaskPartInput = Schema.Struct({
|
||||
id: Schema.optional(PartID),
|
||||
type: Schema.Literal("subtask"),
|
||||
prompt: Schema.String,
|
||||
description: Schema.String,
|
||||
agent: Schema.String,
|
||||
model: Schema.optional(
|
||||
Schema.Struct({
|
||||
providerID: ProviderV2.ID,
|
||||
modelID: ModelV2.ID,
|
||||
}),
|
||||
),
|
||||
command: Schema.optional(Schema.String),
|
||||
}).annotate({ identifier: "SubtaskPartInput" })
|
||||
export type SubtaskPartInput = Types.DeepMutable<Schema.Schema.Type<typeof SubtaskPartInput>>
|
||||
|
||||
export const Assistant = Schema.Struct({
|
||||
...messageBase,
|
||||
role: Schema.Literal("assistant"),
|
||||
time: Schema.Struct({
|
||||
created: NonNegativeInt,
|
||||
completed: Schema.optional(NonNegativeInt),
|
||||
}),
|
||||
error: Schema.optional(AssistantErrorSchema),
|
||||
parentID: MessageID,
|
||||
modelID: ModelV2.ID,
|
||||
providerID: ProviderV2.ID,
|
||||
mode: Schema.String,
|
||||
agent: Schema.String,
|
||||
path: Schema.Struct({
|
||||
cwd: Schema.String,
|
||||
root: Schema.String,
|
||||
}),
|
||||
summary: Schema.optional(Schema.Boolean),
|
||||
cost: Schema.Finite,
|
||||
tokens: Schema.Struct({
|
||||
total: Schema.optional(Schema.Finite),
|
||||
input: Schema.Finite,
|
||||
output: Schema.Finite,
|
||||
reasoning: Schema.Finite,
|
||||
cache: Schema.Struct({
|
||||
read: Schema.Finite,
|
||||
write: Schema.Finite,
|
||||
}),
|
||||
}),
|
||||
structured: Schema.optional(Schema.Any),
|
||||
variant: Schema.optional(Schema.String),
|
||||
finish: Schema.optional(Schema.String),
|
||||
}).annotate({ identifier: "AssistantMessage" })
|
||||
export type Assistant = Omit<Types.DeepMutable<Schema.Schema.Type<typeof Assistant>>, "error"> & {
|
||||
error?: AssistantError
|
||||
}
|
||||
|
||||
export const Info = Schema.Union([User, Assistant]).annotate({ discriminator: "role", identifier: "Message" })
|
||||
export type Info = User | Assistant
|
||||
|
||||
export const WithParts = Schema.Struct({
|
||||
info: Info,
|
||||
parts: Schema.Array(Part),
|
||||
})
|
||||
export type WithParts = {
|
||||
info: Info
|
||||
parts: Part[]
|
||||
}
|
||||
|
||||
const options = {
|
||||
durable: {
|
||||
aggregate: "sessionID",
|
||||
version: 1,
|
||||
},
|
||||
} as const
|
||||
|
||||
const SessionSummary = Schema.Struct({
|
||||
additions: Schema.Finite,
|
||||
deletions: Schema.Finite,
|
||||
files: Schema.Finite,
|
||||
diffs: optionalOmitUndefined(Schema.Array(FileDiff)),
|
||||
})
|
||||
|
||||
const SessionTokens = Schema.Struct({
|
||||
input: Schema.Finite,
|
||||
output: Schema.Finite,
|
||||
reasoning: Schema.Finite,
|
||||
cache: Schema.Struct({
|
||||
read: Schema.Finite,
|
||||
write: Schema.Finite,
|
||||
}),
|
||||
})
|
||||
|
||||
const SessionShare = Schema.Struct({
|
||||
url: Schema.String,
|
||||
})
|
||||
|
||||
const SessionRevert = Schema.Struct({
|
||||
messageID: MessageID,
|
||||
partID: optionalOmitUndefined(PartID),
|
||||
snapshot: optionalOmitUndefined(Schema.String),
|
||||
diff: optionalOmitUndefined(Schema.String),
|
||||
})
|
||||
|
||||
const SessionModel = Schema.Struct({
|
||||
id: ModelV2.ID,
|
||||
providerID: ProviderV2.ID,
|
||||
variant: optionalOmitUndefined(Schema.String),
|
||||
})
|
||||
|
||||
export const SessionInfo = Schema.Struct({
|
||||
id: SessionSchema.ID,
|
||||
slug: Schema.String,
|
||||
projectID: ProjectV2.ID,
|
||||
workspaceID: optionalOmitUndefined(WorkspaceV2.ID),
|
||||
directory: Schema.String,
|
||||
path: optionalOmitUndefined(Schema.String),
|
||||
parentID: optionalOmitUndefined(SessionSchema.ID),
|
||||
summary: optionalOmitUndefined(SessionSummary),
|
||||
cost: optionalOmitUndefined(Schema.Finite),
|
||||
tokens: optionalOmitUndefined(SessionTokens),
|
||||
share: optionalOmitUndefined(SessionShare),
|
||||
title: Schema.String,
|
||||
agent: optionalOmitUndefined(Schema.String),
|
||||
model: optionalOmitUndefined(SessionModel),
|
||||
version: Schema.String,
|
||||
metadata: optionalOmitUndefined(Schema.Record(Schema.String, Schema.Any)),
|
||||
time: Schema.Struct({
|
||||
created: NonNegativeInt,
|
||||
updated: NonNegativeInt,
|
||||
compacting: optionalOmitUndefined(NonNegativeInt),
|
||||
archived: optionalOmitUndefined(Schema.Finite),
|
||||
}),
|
||||
permission: optionalOmitUndefined(PermissionV1.Ruleset),
|
||||
revert: optionalOmitUndefined(SessionRevert),
|
||||
}).annotate({ identifier: "Session" })
|
||||
export type SessionInfo = typeof SessionInfo.Type
|
||||
|
||||
export const Event = {
|
||||
Created: EventV2.define({
|
||||
type: "session.created",
|
||||
...options,
|
||||
schema: {
|
||||
sessionID: SessionSchema.ID,
|
||||
info: SessionInfo,
|
||||
},
|
||||
}),
|
||||
Updated: EventV2.define({
|
||||
type: "session.updated",
|
||||
...options,
|
||||
schema: {
|
||||
sessionID: SessionSchema.ID,
|
||||
info: SessionInfo,
|
||||
},
|
||||
}),
|
||||
Deleted: EventV2.define({
|
||||
type: "session.deleted",
|
||||
...options,
|
||||
schema: {
|
||||
sessionID: SessionSchema.ID,
|
||||
info: SessionInfo,
|
||||
},
|
||||
}),
|
||||
MessageUpdated: EventV2.define({
|
||||
type: "message.updated",
|
||||
...options,
|
||||
schema: {
|
||||
sessionID: SessionSchema.ID,
|
||||
info: Info,
|
||||
},
|
||||
}),
|
||||
MessageRemoved: EventV2.define({
|
||||
type: "message.removed",
|
||||
...options,
|
||||
schema: {
|
||||
sessionID: SessionSchema.ID,
|
||||
messageID: MessageID,
|
||||
},
|
||||
}),
|
||||
PartUpdated: EventV2.define({
|
||||
type: "message.part.updated",
|
||||
...options,
|
||||
schema: {
|
||||
sessionID: SessionSchema.ID,
|
||||
part: Part,
|
||||
time: Schema.Finite,
|
||||
},
|
||||
}),
|
||||
PartRemoved: EventV2.define({
|
||||
type: "message.part.removed",
|
||||
...options,
|
||||
schema: {
|
||||
sessionID: SessionSchema.ID,
|
||||
messageID: MessageID,
|
||||
partID: PartID,
|
||||
},
|
||||
}),
|
||||
}
|
||||
export const ContentFilterError = NamedError.create("ContentFilterError", { message: Schema.String })
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
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"
|
||||
|
|
@ -16,10 +20,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: {
|
||||
|
|
@ -70,18 +70,16 @@ 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().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 +120,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)
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -363,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")],
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
|
@ -383,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")],
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
|
@ -415,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)))
|
||||
}),
|
||||
|
|
@ -433,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 })
|
||||
|
|
@ -441,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))]),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
|
@ -453,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])
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -487,23 +475,23 @@ describe("EventV2", () => {
|
|||
Effect.gen(function* () {
|
||||
const events = yield* EventV2.Service
|
||||
const received = new Array<EventV2.Payload>()
|
||||
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"))
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -511,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()
|
||||
|
|
@ -538,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<EventV2.Payload>()
|
||||
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)
|
||||
}),
|
||||
|
|
@ -551,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
|
||||
|
|
@ -580,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)
|
||||
|
||||
|
|
@ -606,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<typeof SyncTimestamp.Type>()
|
||||
yield* events.project(SyncTimestamp, (event) =>
|
||||
const aggregateID = Session.ID.create()
|
||||
const received = new Array<typeof SessionEvent.ContextUpdated.Type>()
|
||||
yield* events.project(SessionEvent.ContextUpdated, (event) =>
|
||||
Effect.sync(() => {
|
||||
received.push(event)
|
||||
}),
|
||||
|
|
@ -616,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))
|
||||
|
|
@ -646,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"),
|
||||
},
|
||||
])
|
||||
|
||||
|
|
@ -672,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
|
||||
|
|
@ -723,10 +711,10 @@ describe("EventV2", () => {
|
|||
Effect.gen(function* () {
|
||||
const events = yield* EventV2.Service
|
||||
const received = new Array<EventV2.Payload>()
|
||||
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)
|
||||
}),
|
||||
|
|
@ -735,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" },
|
||||
)
|
||||
|
|
@ -750,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" })
|
||||
|
||||
|
|
@ -771,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,
|
||||
|
|
@ -792,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)
|
||||
|
|
@ -804,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" },
|
||||
)
|
||||
|
|
@ -831,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" },
|
||||
)
|
||||
|
|
@ -875,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" },
|
||||
)
|
||||
|
|
@ -891,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 },
|
||||
)
|
||||
|
|
@ -908,14 +896,14 @@ describe("EventV2", () => {
|
|||
Effect.gen(function* () {
|
||||
const events = yield* EventV2.Service
|
||||
const received = new Array<EventV2.Payload>()
|
||||
const aggregateID = EventV2.ID.create()
|
||||
const aggregateID = Session.ID.create()
|
||||
yield* events.listen((event) => Effect.sync(() => received.push(event)))
|
||||
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 })
|
||||
|
|
@ -929,19 +917,19 @@ describe("EventV2", () => {
|
|||
Effect.gen(function* () {
|
||||
const events = yield* EventV2.Service
|
||||
const received = new Array<EventV2.Payload>()
|
||||
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")
|
||||
|
|
@ -952,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)
|
||||
|
||||
|
|
@ -980,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<EventV2.Payload>()
|
||||
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 },
|
||||
)
|
||||
|
|
@ -1047,10 +1035,10 @@ describe("EventV2", () => {
|
|||
Effect.gen(function* () {
|
||||
const events = yield* EventV2.Service
|
||||
const received = new Array<EventV2.Payload>()
|
||||
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)
|
||||
}),
|
||||
|
|
@ -1058,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"))
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
|
|
|||
16
packages/core/test/legacy-event-schema.test.ts
Normal file
16
packages/core/test/legacy-event-schema.test.ts
Normal file
|
|
@ -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 } })
|
||||
})
|
||||
})
|
||||
|
|
@ -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:*",
|
||||
|
|
|
|||
|
|
@ -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<string, Info>
|
||||
}
|
||||
|
||||
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({
|
||||
|
|
|
|||
|
|
@ -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<typeof ConnectionStatus>
|
||||
export const ConnectionStatus = WorkspaceEvent.ConnectionStatus
|
||||
export type ConnectionStatus = WorkspaceEvent.ConnectionStatus
|
||||
|
||||
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 Event = WorkspaceEvent
|
||||
|
||||
function fromRow(row: typeof WorkspaceTable.$inferSelect): Info {
|
||||
return {
|
||||
|
|
|
|||
3
packages/opencode/src/event-manifest.ts
Normal file
3
packages/opencode/src/event-manifest.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export * as EventManifest from "./event-manifest"
|
||||
|
||||
export { Definitions, Durable, Latest } from "@opencode-ai/schema/event-manifest"
|
||||
|
|
@ -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<Service, EventV2.Interface>()("@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<string, unknown>)[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,
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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", {})
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<typeof Resource>
|
||||
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
import { PermissionV1Event } from "@opencode-ai/schema/permission-v1"
|
||||
|
||||
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,
|
||||
},
|
||||
}),
|
||||
}
|
||||
export const Event = PermissionV1Event
|
||||
|
||||
export interface Interface {
|
||||
readonly ask: (input: PermissionV1.AskInput) => Effect.Effect<void, PermissionV1.Error>
|
||||
|
|
|
|||
|
|
@ -16,46 +16,18 @@ 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 { 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<Schema.Schema.Type<typeof Info>>
|
||||
|
||||
export const Event = {
|
||||
Updated: EventV2.define({ type: "project.updated", schema: Info.fields }),
|
||||
Updated: Project.Event.Updated,
|
||||
}
|
||||
|
||||
type Row = typeof ProjectTable.$inferSelect
|
||||
|
|
@ -72,7 +44,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 +60,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<Schema.Schema.Type<typeof UpdateInput>>
|
||||
|
||||
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<Schema.Schema.Type<typeof UpdatePayload>>
|
||||
|
||||
|
|
@ -135,7 +107,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 +139,7 @@ export const layer = Layer.effect(
|
|||
}),
|
||||
)
|
||||
|
||||
const fakeVcs = Schema.decodeUnknownSync(Schema.optional(ProjectVcs))(Flag.OPENCODE_FAKE_VCS)
|
||||
const fakeVcs = Schema.decodeUnknownSync(Schema.optional(Project.Vcs))(Flag.OPENCODE_FAKE_VCS)
|
||||
|
||||
const scope = yield* Scope.Scope
|
||||
|
||||
|
|
|
|||
|
|
@ -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<typeof Mode>
|
||||
|
||||
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),
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
import { QuestionV1 } from "@opencode-ai/schema/question-v1"
|
||||
|
||||
// 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<typeof Option>
|
||||
|
||||
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<typeof Info>
|
||||
|
||||
export const Prompt = Schema.Struct(base).annotate({ identifier: "QuestionPrompt" })
|
||||
export type Prompt = Schema.Schema.Type<typeof Prompt>
|
||||
|
||||
export const Tool = Schema.Struct({
|
||||
messageID: MessageID,
|
||||
callID: Schema.String,
|
||||
}).annotate({ identifier: "QuestionTool" })
|
||||
export type Tool = Schema.Schema.Type<typeof Tool>
|
||||
|
||||
export const Request = Schema.Struct({
|
||||
id: QuestionID,
|
||||
sessionID: SessionID,
|
||||
questions: Schema.Array(Info).annotate({
|
||||
description: "Questions to ask",
|
||||
}),
|
||||
tool: Schema.optional(Tool),
|
||||
}).annotate({ identifier: "QuestionRequest" })
|
||||
export type Request = Schema.Schema.Type<typeof Request>
|
||||
|
||||
export const Answer = Schema.Array(Schema.String).annotate({ identifier: "QuestionAnswer" })
|
||||
export type Answer = Schema.Schema.Type<typeof Answer>
|
||||
|
||||
export const Reply = Schema.Struct({
|
||||
answers: Schema.Array(Answer).annotate({
|
||||
description: "User answers in order of questions (each answer is an array of selected labels)",
|
||||
}),
|
||||
}).annotate({ identifier: "QuestionReply" })
|
||||
export type Reply = Schema.Schema.Type<typeof Reply>
|
||||
|
||||
export const Replied = Schema.Struct({
|
||||
sessionID: SessionID,
|
||||
requestID: QuestionID,
|
||||
answers: Schema.Array(Answer),
|
||||
}).annotate({ identifier: "QuestionReplied" })
|
||||
|
||||
export const Rejected = Schema.Struct({
|
||||
sessionID: SessionID,
|
||||
requestID: QuestionID,
|
||||
}).annotate({ identifier: "QuestionRejected" })
|
||||
|
||||
export const Event = {
|
||||
Asked: EventV2.define({ type: "question.asked", schema: Request.fields }),
|
||||
Replied: EventV2.define({ type: "question.replied", schema: Replied.fields }),
|
||||
Rejected: EventV2.define({ type: "question.rejected", schema: Rejected.fields }),
|
||||
}
|
||||
export 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<RejectedError>()("QuestionRejectedError", {}) {
|
||||
override get message() {
|
||||
|
|
|
|||
|
|
@ -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>()("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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 }),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -61,16 +58,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: SessionV1.Event.PartDelta,
|
||||
PartRemoved: SessionV1.Event.PartRemoved,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,7 +37,6 @@ 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"
|
||||
|
|
@ -309,69 +307,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: SessionV1.Event.Diff,
|
||||
Error: SessionV1.Event.Error,
|
||||
}
|
||||
|
||||
export function plan(input: { slug: string; time: { created: number } }, instance: InstanceContext) {
|
||||
|
|
|
|||
|
|
@ -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<typeof Info>
|
||||
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<Info>
|
||||
|
|
|
|||
|
|
@ -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<typeof Info>
|
||||
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<void>
|
||||
|
|
|
|||
|
|
@ -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 { Info } 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 = Info
|
||||
export type FileDiff = typeof FileDiff.Type
|
||||
|
||||
const prune = "7.days"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
24
packages/opencode/test/event-manifest.test.ts
Normal file
24
packages/opencode/test/event-manifest.test.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { describe, expect, test } from "bun:test"
|
||||
import { SessionEvent } from "@opencode-ai/core/session/event"
|
||||
import { EventManifest as SchemaEventManifest } from "@opencode-ai/schema/event-manifest"
|
||||
import { Todo } from "@/session/todo"
|
||||
import { EventManifest } from "@/event-manifest"
|
||||
|
||||
describe("public event manifest", () => {
|
||||
test("contains every latest public wire type once", () => {
|
||||
expect(EventManifest.Definitions).toBe(SchemaEventManifest.Definitions)
|
||||
expect(EventManifest.Latest).toBe(SchemaEventManifest.Latest)
|
||||
expect(EventManifest.Durable).toBe(SchemaEventManifest.Durable)
|
||||
expect(EventManifest.Latest.size).toBe(85)
|
||||
expect(EventManifest.Latest.get("session.next.step.ended")).toBe(SessionEvent.Step.Ended)
|
||||
expect(EventManifest.Latest.get("todo.updated")).toBe(Todo.Event.Updated)
|
||||
expect(EventManifest.Latest.has("ide.installed")).toBe(false)
|
||||
expect(EventManifest.Latest.has("server.connected")).toBe(true)
|
||||
expect(EventManifest.Latest.has("global.disposed")).toBe(true)
|
||||
})
|
||||
|
||||
test("contains only the current step settlement versions", () => {
|
||||
expect(EventManifest.Durable.has("session.next.step.ended.1")).toBe(false)
|
||||
expect(EventManifest.Durable.get("session.next.step.ended.2")).toBe(SessionEvent.Step.Ended)
|
||||
})
|
||||
})
|
||||
6
packages/schema/src/catalog.ts
Normal file
6
packages/schema/src/catalog.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export * as Catalog from "./catalog"
|
||||
|
||||
import { define, inventory } from "./event"
|
||||
|
||||
const Updated = define({ type: "catalog.updated", schema: {} })
|
||||
export const Event = { Updated, Definitions: inventory(Updated) }
|
||||
10
packages/schema/src/durable-event-manifest.ts
Normal file
10
packages/schema/src/durable-event-manifest.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
export * as DurableEventManifest from "./durable-event-manifest"
|
||||
|
||||
import { Event } from "./event"
|
||||
import { SessionEvent } from "./session-event"
|
||||
import { SessionV1 } from "./session-v1"
|
||||
|
||||
export const Durable = Event.durable([
|
||||
...SessionV1.Event.Definitions.filter((definition) => definition.durable !== undefined),
|
||||
...SessionEvent.DurableDefinitions,
|
||||
])
|
||||
84
packages/schema/src/event-manifest.ts
Normal file
84
packages/schema/src/event-manifest.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
export * as EventManifest from "./event-manifest"
|
||||
|
||||
import { Catalog } from "./catalog"
|
||||
import { Durable } from "./durable-event-manifest"
|
||||
import { Event } from "./event"
|
||||
import { FileSystem } from "./filesystem"
|
||||
import { FileSystemWatcher } from "./filesystem-watcher"
|
||||
import { InstallationEvent } from "./installation-event"
|
||||
import { Integration } from "./integration"
|
||||
import { LegacyEvent } from "./legacy-event"
|
||||
import { LspEvent } from "./lsp-event"
|
||||
import { McpEvent } from "./mcp-event"
|
||||
import { ModelsDev } from "./models-dev"
|
||||
import { Permission } from "./permission"
|
||||
import { PermissionV1 } from "./permission-v1"
|
||||
import { Plugin } from "./plugin"
|
||||
import { Project } from "./project"
|
||||
import { ProjectDirectories } from "./project-directories"
|
||||
import { Pty } from "./pty"
|
||||
import { Question } from "./question"
|
||||
import { QuestionV1 } from "./question-v1"
|
||||
import { Reference } from "./reference"
|
||||
import { ServerEvent } from "./server-event"
|
||||
import { SessionCompactionEvent } from "./session-compaction-event"
|
||||
import { SessionEvent } from "./session-event"
|
||||
import { SessionStatusEvent } from "./session-status-event"
|
||||
import { SessionTodo } from "./session-todo"
|
||||
import { SessionV1 } from "./session-v1"
|
||||
import { TuiEvent } from "./tui-event"
|
||||
import { VcsEvent } from "./vcs-event"
|
||||
import { WorkspaceEvent } from "./workspace-event"
|
||||
import { WorktreeEvent } from "./worktree-event"
|
||||
|
||||
const sessionV1DurableDefinitions = SessionV1.Event.Definitions.filter((definition) => definition.durable !== undefined)
|
||||
const sessionV1LiveDefinitions = SessionV1.Event.Definitions.filter((definition) => definition.durable === undefined)
|
||||
|
||||
const coreDefinitions = Event.inventory(...sessionV1DurableDefinitions, ...SessionEvent.Definitions)
|
||||
|
||||
const foundationDefinitions = Event.inventory(
|
||||
...ModelsDev.Event.Definitions,
|
||||
...Integration.Event.Definitions,
|
||||
...Catalog.Event.Definitions,
|
||||
...coreDefinitions,
|
||||
)
|
||||
|
||||
const featureDefinitions = Event.inventory(
|
||||
...FileSystem.Event.Definitions,
|
||||
...Reference.Event.Definitions,
|
||||
...Permission.Event.Definitions,
|
||||
...Plugin.Event.Definitions,
|
||||
...ProjectDirectories.Event.Definitions,
|
||||
...FileSystemWatcher.Event.Definitions,
|
||||
...Pty.Event.Definitions,
|
||||
...Question.Event.Definitions,
|
||||
)
|
||||
|
||||
export const ServerDefinitions = Event.inventory(
|
||||
...foundationDefinitions,
|
||||
...featureDefinitions,
|
||||
...SessionTodo.Event.Definitions,
|
||||
)
|
||||
|
||||
export const Definitions = Event.inventory(
|
||||
...foundationDefinitions,
|
||||
...sessionV1LiveDefinitions,
|
||||
...InstallationEvent.Definitions,
|
||||
...featureDefinitions,
|
||||
...SessionTodo.Event.Definitions,
|
||||
...LspEvent.Definitions,
|
||||
...PermissionV1.Event.Definitions,
|
||||
...TuiEvent.Definitions,
|
||||
...McpEvent.Definitions,
|
||||
...LegacyEvent.Definitions,
|
||||
...Project.Event.Definitions,
|
||||
...SessionStatusEvent.Definitions,
|
||||
...QuestionV1.Event.Definitions,
|
||||
...SessionCompactionEvent.Definitions,
|
||||
...VcsEvent.Definitions,
|
||||
...WorkspaceEvent.Definitions,
|
||||
...WorktreeEvent.Definitions,
|
||||
...ServerEvent.Definitions,
|
||||
)
|
||||
export const Latest = Event.latest(Definitions)
|
||||
export { Durable }
|
||||
125
packages/schema/src/event.ts
Normal file
125
packages/schema/src/event.ts
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
export * as Event from "./event"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { ascending } from "./identifier"
|
||||
import { Location } from "./location"
|
||||
import { withStatics } from "./schema"
|
||||
|
||||
export const ID = Schema.String.check(Schema.isStartsWith("evt_")).pipe(
|
||||
Schema.brand("Event.ID"),
|
||||
withStatics((schema) => ({ create: () => schema.make("evt_" + ascending()) })),
|
||||
)
|
||||
export type ID = typeof ID.Type
|
||||
|
||||
export type Definition<
|
||||
Type extends string = string,
|
||||
DataSchema extends Schema.Codec<unknown, unknown> = Schema.Codec<unknown, unknown>,
|
||||
> = Schema.Top & {
|
||||
readonly type: Type
|
||||
readonly durable?: {
|
||||
readonly version: number
|
||||
readonly aggregate: string
|
||||
}
|
||||
readonly data: DataSchema
|
||||
}
|
||||
|
||||
export type Data<D extends Definition> = Schema.Schema.Type<D["data"]>
|
||||
|
||||
export type Payload<D extends Definition = Definition> = {
|
||||
readonly id: ID
|
||||
readonly type: D["type"]
|
||||
readonly data: Data<D>
|
||||
readonly durable?: {
|
||||
readonly aggregateID: string
|
||||
readonly seq: number
|
||||
readonly version: number
|
||||
}
|
||||
readonly location?: Location.Ref
|
||||
readonly metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export function define<
|
||||
const Type extends string,
|
||||
Fields extends Readonly<Record<PropertyKey, Schema.Codec<unknown, unknown>>>,
|
||||
>(input: {
|
||||
readonly type: Type
|
||||
readonly durable?: {
|
||||
readonly version: number
|
||||
readonly aggregate: string
|
||||
}
|
||||
readonly schema: Fields
|
||||
}): Schema.Schema<Payload<Definition<Type, Schema.Struct<Fields>>>> & Definition<Type, Schema.Struct<Fields>> {
|
||||
const data = Schema.Struct(input.schema)
|
||||
return Object.assign(
|
||||
Schema.Struct({
|
||||
id: ID,
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
||||
type: Schema.Literal(input.type),
|
||||
durable: Schema.optional(
|
||||
Schema.Struct({ aggregateID: Schema.String, seq: Schema.Number, version: Schema.Number }),
|
||||
),
|
||||
location: Schema.optional(Location.Ref),
|
||||
data,
|
||||
}).annotate({ identifier: input.type }),
|
||||
{
|
||||
type: input.type,
|
||||
...(input.durable === undefined ? {} : { durable: input.durable }),
|
||||
data,
|
||||
},
|
||||
) as Schema.Schema<Payload<Definition<Type, Schema.Struct<Fields>>>> & Definition<Type, Schema.Struct<Fields>>
|
||||
}
|
||||
|
||||
export function inventory<const Definitions extends ReadonlyArray<Definition>>(...definitions: Definitions) {
|
||||
return Object.freeze(definitions)
|
||||
}
|
||||
|
||||
export function latest(definitions: ReadonlyArray<Definition>) {
|
||||
return readonlyMap(
|
||||
definitions.reduce((result, definition) => {
|
||||
const existing = result.get(definition.type)
|
||||
if (!existing) {
|
||||
result.set(definition.type, definition)
|
||||
return result
|
||||
}
|
||||
if (definition.durable && existing.durable && definition.durable.version !== existing.durable.version) {
|
||||
if (definition.durable.version > existing.durable.version) result.set(definition.type, definition)
|
||||
return result
|
||||
}
|
||||
if (definition !== existing) throw new Error(`Duplicate latest event definition for ${definition.type}`)
|
||||
return result
|
||||
}, new Map<string, Definition>()),
|
||||
)
|
||||
}
|
||||
|
||||
export function versionedType(type: string, version: number) {
|
||||
return `${type}.${version}`
|
||||
}
|
||||
|
||||
export function durable(definitions: ReadonlyArray<Definition>) {
|
||||
return readonlyMap(
|
||||
definitions.reduce((result, definition) => {
|
||||
if (!definition.durable) return result
|
||||
const key = versionedType(definition.type, definition.durable.version)
|
||||
if (result.has(key)) throw new Error(`Duplicate durable event definition for ${key}`)
|
||||
result.set(key, definition)
|
||||
return result
|
||||
}, new Map<string, Definition>()),
|
||||
)
|
||||
}
|
||||
|
||||
function readonlyMap<Key, Value>(map: Map<Key, Value>): ReadonlyMap<Key, Value> {
|
||||
const result: ReadonlyMap<Key, Value> = Object.freeze({
|
||||
get size() {
|
||||
return map.size
|
||||
},
|
||||
entries: () => map.entries(),
|
||||
forEach: (callback: (value: Value, key: Key, map: ReadonlyMap<Key, Value>) => void, thisArg?: unknown) =>
|
||||
map.forEach((value, key) => callback.call(thisArg, value, key, result)),
|
||||
get: (key: Key) => map.get(key),
|
||||
has: (key: Key) => map.has(key),
|
||||
keys: () => map.keys(),
|
||||
values: () => map.values(),
|
||||
[Symbol.iterator]: () => map[Symbol.iterator](),
|
||||
})
|
||||
return result
|
||||
}
|
||||
12
packages/schema/src/file-diff.ts
Normal file
12
packages/schema/src/file-diff.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
export * as FileDiff from "./file-diff"
|
||||
|
||||
import { Schema } from "effect"
|
||||
|
||||
export const Info = Schema.Struct({
|
||||
file: Schema.optional(Schema.String),
|
||||
patch: Schema.optional(Schema.String),
|
||||
additions: Schema.Finite,
|
||||
deletions: Schema.Finite,
|
||||
status: Schema.optional(Schema.Literals(["added", "deleted", "modified"])),
|
||||
}).annotate({ identifier: "SnapshotFileDiff" })
|
||||
export type Info = typeof Info.Type
|
||||
13
packages/schema/src/filesystem-watcher.ts
Normal file
13
packages/schema/src/filesystem-watcher.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
export * as FileSystemWatcher from "./filesystem-watcher"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { define, inventory } from "./event"
|
||||
|
||||
const Updated = define({
|
||||
type: "file.watcher.updated",
|
||||
schema: {
|
||||
file: Schema.String,
|
||||
event: Schema.Literals(["add", "change", "unlink"]),
|
||||
},
|
||||
})
|
||||
export const Event = { Updated, Definitions: inventory(Updated) }
|
||||
|
|
@ -1,8 +1,15 @@
|
|||
export * as FileSystem from "./filesystem"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { define, inventory } from "./event"
|
||||
import { NonNegativeInt, PositiveInt, RelativePath } from "./schema"
|
||||
|
||||
const Edited = define({
|
||||
type: "file.edited",
|
||||
schema: { file: Schema.String },
|
||||
})
|
||||
export const Event = { Edited, Definitions: inventory(Edited) }
|
||||
|
||||
export interface Entry extends Schema.Schema.Type<typeof Entry> {}
|
||||
export const Entry = Schema.Struct({
|
||||
path: RelativePath,
|
||||
|
|
|
|||
13
packages/schema/src/ide-event.ts
Normal file
13
packages/schema/src/ide-event.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
export * as IdeEvent from "./ide-event"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { Event } from "./event"
|
||||
|
||||
export const Installed = Event.define({
|
||||
type: "ide.installed",
|
||||
schema: {
|
||||
ide: Schema.String,
|
||||
},
|
||||
})
|
||||
|
||||
export const Definitions = Event.inventory(Installed)
|
||||
|
|
@ -2,6 +2,7 @@ export { Agent } from "./agent"
|
|||
export { Command } from "./command"
|
||||
export { Connection } from "./connection"
|
||||
export { Credential } from "./credential"
|
||||
export { Event } from "./event"
|
||||
export { FileSystem } from "./filesystem"
|
||||
export { Integration } from "./integration"
|
||||
export { LLM } from "./llm"
|
||||
|
|
|
|||
20
packages/schema/src/installation-event.ts
Normal file
20
packages/schema/src/installation-event.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
export * as InstallationEvent from "./installation-event"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { Event } from "./event"
|
||||
|
||||
export const Updated = Event.define({
|
||||
type: "installation.updated",
|
||||
schema: {
|
||||
version: Schema.String,
|
||||
},
|
||||
})
|
||||
|
||||
export const UpdateAvailable = Event.define({
|
||||
type: "installation.update-available",
|
||||
schema: {
|
||||
version: Schema.String,
|
||||
},
|
||||
})
|
||||
|
||||
export const Definitions = Event.inventory(Updated, UpdateAvailable)
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
export * as Integration from "./integration"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { define, inventory } from "./event"
|
||||
|
||||
export const ID = Schema.String.pipe(Schema.brand("Integration.ID"))
|
||||
export type ID = typeof ID.Type
|
||||
|
|
@ -72,6 +73,16 @@ export type Method = typeof Method.Type
|
|||
export const Inputs = Schema.Record(Schema.String, Schema.String).annotate({ identifier: "Integration.Inputs" })
|
||||
export type Inputs = typeof Inputs.Type
|
||||
|
||||
const Updated = define({
|
||||
type: "integration.updated",
|
||||
schema: {},
|
||||
})
|
||||
const ConnectionUpdated = define({
|
||||
type: "integration.connection.updated",
|
||||
schema: { integrationID: ID },
|
||||
})
|
||||
export const Event = { Updated, ConnectionUpdated, Definitions: inventory(Updated, ConnectionUpdated) }
|
||||
|
||||
export interface Ref extends Schema.Schema.Type<typeof Ref> {}
|
||||
export const Ref = Schema.Struct({
|
||||
id: ID,
|
||||
|
|
|
|||
18
packages/schema/src/legacy-event.ts
Normal file
18
packages/schema/src/legacy-event.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
export * as LegacyEvent from "./legacy-event"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { define, inventory } from "./event"
|
||||
import { SessionID } from "./session-id"
|
||||
import { SessionV1 } from "./session-v1"
|
||||
|
||||
export const CommandExecuted = define({
|
||||
type: "command.executed",
|
||||
schema: {
|
||||
name: Schema.String,
|
||||
sessionID: SessionID,
|
||||
arguments: Schema.String,
|
||||
messageID: SessionV1.MessageID,
|
||||
},
|
||||
})
|
||||
|
||||
export const Definitions = inventory(CommandExecuted)
|
||||
|
|
@ -2,12 +2,12 @@ export * as Location from "./location"
|
|||
|
||||
import { Effect, Schema } from "effect"
|
||||
import { AbsolutePath } from "./schema"
|
||||
import { Workspace } from "./workspace"
|
||||
import { WorkspaceID } from "./workspace-id"
|
||||
|
||||
export interface Ref extends Schema.Schema.Type<typeof Ref> {}
|
||||
export const Ref = Schema.Struct({
|
||||
directory: AbsolutePath,
|
||||
workspaceID: Schema.optional(Workspace.ID).pipe(
|
||||
workspaceID: Schema.optional(WorkspaceID).pipe(
|
||||
Schema.withDecodingDefault(Effect.succeed(undefined)),
|
||||
Schema.withConstructorDefault(Effect.succeed(undefined)),
|
||||
),
|
||||
|
|
|
|||
7
packages/schema/src/lsp-event.ts
Normal file
7
packages/schema/src/lsp-event.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export * as LspEvent from "./lsp-event"
|
||||
|
||||
import { Event } from "./event"
|
||||
|
||||
export const Updated = Event.define({ type: "lsp.updated", schema: {} })
|
||||
|
||||
export const Definitions = Event.inventory(Updated)
|
||||
21
packages/schema/src/mcp-event.ts
Normal file
21
packages/schema/src/mcp-event.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
export * as McpEvent from "./mcp-event"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { Event } from "./event"
|
||||
|
||||
export const ToolsChanged = Event.define({
|
||||
type: "mcp.tools.changed",
|
||||
schema: {
|
||||
server: Schema.String,
|
||||
},
|
||||
})
|
||||
|
||||
export const BrowserOpenFailed = Event.define({
|
||||
type: "mcp.browser.open.failed",
|
||||
schema: {
|
||||
mcpName: Schema.String,
|
||||
url: Schema.String,
|
||||
},
|
||||
})
|
||||
|
||||
export const Definitions = Event.inventory(ToolsChanged, BrowserOpenFailed)
|
||||
9
packages/schema/src/models-dev.ts
Normal file
9
packages/schema/src/models-dev.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
export * as ModelsDev from "./models-dev"
|
||||
|
||||
import { define, inventory } from "./event"
|
||||
|
||||
const Refreshed = define({
|
||||
type: "models-dev.refreshed",
|
||||
schema: {},
|
||||
})
|
||||
export const Event = { Refreshed, Definitions: inventory(Refreshed) }
|
||||
67
packages/schema/src/permission-v1.ts
Normal file
67
packages/schema/src/permission-v1.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
export * as PermissionV1 from "./permission-v1"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { define, inventory } from "./event"
|
||||
import { ascending } from "./identifier"
|
||||
import { Project } from "./project"
|
||||
import { withStatics } from "./schema"
|
||||
import { SessionID } from "./session-id"
|
||||
|
||||
export const ID = Schema.String.check(Schema.isStartsWith("per")).pipe(
|
||||
Schema.brand("PermissionID"),
|
||||
withStatics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "per_" + ascending()) })),
|
||||
)
|
||||
export type ID = typeof ID.Type
|
||||
|
||||
export const Action = Schema.Literals(["allow", "deny", "ask"]).annotate({ identifier: "PermissionAction" })
|
||||
export type Action = typeof Action.Type
|
||||
|
||||
export const Rule = Schema.Struct({ permission: Schema.String, pattern: Schema.String, action: Action }).annotate({
|
||||
identifier: "PermissionRule",
|
||||
})
|
||||
export type Rule = typeof Rule.Type
|
||||
|
||||
export const Ruleset = Schema.Array(Rule).annotate({ identifier: "PermissionRuleset" })
|
||||
export type Ruleset = typeof Ruleset.Type
|
||||
|
||||
export const Request = Schema.Struct({
|
||||
id: ID,
|
||||
sessionID: SessionID,
|
||||
permission: Schema.String,
|
||||
patterns: Schema.Array(Schema.String),
|
||||
metadata: Schema.Record(Schema.String, Schema.Unknown),
|
||||
always: Schema.Array(Schema.String),
|
||||
tool: Schema.optional(Schema.Struct({ messageID: Schema.String, callID: Schema.String })),
|
||||
}).annotate({ identifier: "PermissionRequest" })
|
||||
export type Request = typeof Request.Type
|
||||
|
||||
export const Reply = Schema.Literals(["once", "always", "reject"])
|
||||
export type Reply = typeof Reply.Type
|
||||
|
||||
export const ReplyBody = Schema.Struct({ reply: Reply, message: Schema.optional(Schema.String) }).annotate({
|
||||
identifier: "PermissionReplyBody",
|
||||
})
|
||||
export type ReplyBody = typeof ReplyBody.Type
|
||||
|
||||
export const Approval = Schema.Struct({ projectID: Project.ID, patterns: Schema.Array(Schema.String) }).annotate({
|
||||
identifier: "PermissionApproval",
|
||||
})
|
||||
export type Approval = typeof Approval.Type
|
||||
|
||||
export const AskInput = Schema.Struct({ ...Request.fields, id: Schema.optional(ID), ruleset: Ruleset }).annotate({
|
||||
identifier: "PermissionAskInput",
|
||||
})
|
||||
export type AskInput = typeof AskInput.Type
|
||||
|
||||
export const ReplyInput = Schema.Struct({ requestID: ID, ...ReplyBody.fields }).annotate({
|
||||
identifier: "PermissionReplyInput",
|
||||
})
|
||||
export type ReplyInput = typeof ReplyInput.Type
|
||||
|
||||
const Asked = define({ type: "permission.asked", schema: Request.fields })
|
||||
const Replied = define({
|
||||
type: "permission.replied",
|
||||
schema: { sessionID: SessionID, requestID: ID, reply: Reply },
|
||||
})
|
||||
export const Event = { Asked, Replied, Definitions: inventory(Asked, Replied) }
|
||||
export const PermissionV1Event = Event
|
||||
|
|
@ -1,6 +1,54 @@
|
|||
export * as Permission from "./permission"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { define, inventory } from "./event"
|
||||
import { ascending } from "./identifier"
|
||||
import { SessionID } from "./session-id"
|
||||
import { withStatics } from "./schema"
|
||||
|
||||
export const ID = Schema.String.check(Schema.isStartsWith("per")).pipe(
|
||||
Schema.brand("PermissionV2.ID"),
|
||||
withStatics((schema) => ({ create: (id?: string) => schema.make(id ?? "per_" + ascending()) })),
|
||||
)
|
||||
export type ID = typeof ID.Type
|
||||
|
||||
export const Source = Schema.Union([
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("tool"),
|
||||
messageID: Schema.String,
|
||||
callID: Schema.String,
|
||||
}),
|
||||
]).annotate({ identifier: "PermissionV2.Source" })
|
||||
export type Source = typeof Source.Type
|
||||
|
||||
const RequestFields = {
|
||||
sessionID: SessionID,
|
||||
action: Schema.String,
|
||||
resources: Schema.Array(Schema.String),
|
||||
save: Schema.Array(Schema.String).pipe(Schema.optional),
|
||||
metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
|
||||
source: Source.pipe(Schema.optional),
|
||||
}
|
||||
|
||||
export const Request = Schema.Struct({
|
||||
id: ID,
|
||||
...RequestFields,
|
||||
}).annotate({ identifier: "PermissionV2.Request" })
|
||||
export type Request = typeof Request.Type
|
||||
|
||||
export const Reply = Schema.Literals(["once", "always", "reject"]).annotate({ identifier: "PermissionV2.Reply" })
|
||||
export type Reply = typeof Reply.Type
|
||||
|
||||
const Asked = define({ type: "permission.v2.asked", schema: Request.fields })
|
||||
const Replied = define({
|
||||
type: "permission.v2.replied",
|
||||
schema: {
|
||||
sessionID: SessionID,
|
||||
requestID: ID,
|
||||
reply: Reply,
|
||||
},
|
||||
})
|
||||
export const Event = { Asked, Replied, Definitions: inventory(Asked, Replied) }
|
||||
|
||||
export const Effect = Schema.Literals(["allow", "deny", "ask"]).annotate({ identifier: "PermissionV2.Effect" })
|
||||
export type Effect = typeof Effect.Type
|
||||
|
|
|
|||
15
packages/schema/src/plugin.ts
Normal file
15
packages/schema/src/plugin.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
export * as Plugin from "./plugin"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { define, inventory } from "./event"
|
||||
|
||||
export const ID = Schema.String.pipe(Schema.brand("Plugin.ID"))
|
||||
export type ID = typeof ID.Type
|
||||
export const PluginID = ID
|
||||
|
||||
const Added = define({
|
||||
type: "plugin.added",
|
||||
schema: { id: ID },
|
||||
})
|
||||
export const Event = { Added, Definitions: inventory(Added) }
|
||||
export const PluginEvent = Event
|
||||
11
packages/schema/src/project-directories.ts
Normal file
11
packages/schema/src/project-directories.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
export * as ProjectDirectories from "./project-directories"
|
||||
|
||||
import { define, inventory } from "./event"
|
||||
import { Project } from "./project"
|
||||
|
||||
const Updated = define({
|
||||
type: "project.directories.updated",
|
||||
schema: { projectID: Project.ID },
|
||||
})
|
||||
export const Event = { Updated, Definitions: inventory(Updated) }
|
||||
export const ProjectDirectoriesEvent = Event
|
||||
|
|
@ -1,10 +1,43 @@
|
|||
export * as Project from "./project"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { withStatics } from "./schema"
|
||||
import { define, inventory } from "./event"
|
||||
import { NonNegativeInt, optionalOmitUndefined, withStatics } from "./schema"
|
||||
|
||||
export const ID = Schema.String.pipe(
|
||||
Schema.brand("Project.ID"),
|
||||
withStatics((schema) => ({ global: schema.make("global") })),
|
||||
)
|
||||
export type ID = typeof ID.Type
|
||||
|
||||
export const Vcs = Schema.Literal("git")
|
||||
export const Icon = Schema.Struct({
|
||||
url: optionalOmitUndefined(Schema.String),
|
||||
override: optionalOmitUndefined(Schema.String),
|
||||
color: optionalOmitUndefined(Schema.String),
|
||||
})
|
||||
export const Commands = Schema.Struct({
|
||||
start: optionalOmitUndefined(
|
||||
Schema.String.annotate({ description: "Startup script to run when creating a new workspace (worktree)" }),
|
||||
),
|
||||
})
|
||||
export const Time = Schema.Struct({
|
||||
created: NonNegativeInt,
|
||||
updated: NonNegativeInt,
|
||||
initialized: optionalOmitUndefined(NonNegativeInt),
|
||||
})
|
||||
|
||||
export const Info = Schema.Struct({
|
||||
id: ID,
|
||||
worktree: Schema.String,
|
||||
vcs: optionalOmitUndefined(Vcs),
|
||||
name: optionalOmitUndefined(Schema.String),
|
||||
icon: optionalOmitUndefined(Icon),
|
||||
commands: optionalOmitUndefined(Commands),
|
||||
time: Time,
|
||||
sandboxes: Schema.Array(Schema.String),
|
||||
}).annotate({ identifier: "Project" })
|
||||
export type Info = typeof Info.Type
|
||||
|
||||
const Updated = define({ type: "project.updated", schema: Info.fields })
|
||||
export const Event = { Updated, Definitions: inventory(Updated) }
|
||||
|
|
|
|||
35
packages/schema/src/pty.ts
Normal file
35
packages/schema/src/pty.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
export * as Pty from "./pty"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { define, inventory } from "./event"
|
||||
import { ascending } from "./identifier"
|
||||
import { NonNegativeInt } from "./schema"
|
||||
import { withStatics } from "./schema"
|
||||
|
||||
const IDSchema = Schema.String.check(Schema.isStartsWith("pty")).pipe(Schema.brand("PtyID"))
|
||||
|
||||
export const ID = IDSchema.pipe(
|
||||
withStatics((schema: typeof IDSchema) => ({
|
||||
ascending: (id?: string) => schema.make(id ?? "pty_" + ascending()),
|
||||
})),
|
||||
)
|
||||
export type ID = typeof ID.Type
|
||||
|
||||
export const Info = Schema.Struct({
|
||||
id: ID,
|
||||
title: Schema.String,
|
||||
command: Schema.String,
|
||||
args: Schema.Array(Schema.String),
|
||||
cwd: Schema.String,
|
||||
status: Schema.Literals(["running", "exited"]),
|
||||
pid: NonNegativeInt,
|
||||
exitCode: Schema.optional(NonNegativeInt),
|
||||
}).annotate({ identifier: "Pty" })
|
||||
export const PtyInfo = Info
|
||||
|
||||
const Created = define({ type: "pty.created", schema: { info: Info } })
|
||||
const Updated = define({ type: "pty.updated", schema: { info: Info } })
|
||||
const Exited = define({ type: "pty.exited", schema: { id: ID, exitCode: NonNegativeInt } })
|
||||
const Deleted = define({ type: "pty.deleted", schema: { id: ID } })
|
||||
export const Event = { Created, Updated, Exited, Deleted, Definitions: inventory(Created, Updated, Exited, Deleted) }
|
||||
export const PtyEvent = Event
|
||||
66
packages/schema/src/question-v1.ts
Normal file
66
packages/schema/src/question-v1.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
export * as QuestionV1 from "./question-v1"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { define, inventory } from "./event"
|
||||
import { ascending } from "./identifier"
|
||||
import { withStatics } from "./schema"
|
||||
import { SessionID } from "./session-id"
|
||||
import { SessionV1 } from "./session-v1"
|
||||
|
||||
export const ID = Schema.String.check(Schema.isStartsWith("que")).pipe(
|
||||
Schema.brand("QuestionID"),
|
||||
withStatics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "que_" + ascending()) })),
|
||||
)
|
||||
|
||||
export const Option = Schema.Struct({
|
||||
label: Schema.String.annotate({ description: "Display text (1-5 words, concise)" }),
|
||||
description: Schema.String.annotate({ description: "Explanation of choice" }),
|
||||
}).annotate({ identifier: "QuestionOption" })
|
||||
|
||||
const base = {
|
||||
question: Schema.String.annotate({ description: "Complete question" }),
|
||||
header: Schema.String.annotate({ description: "Very short label (max 30 chars)" }),
|
||||
options: Schema.Array(Option).annotate({ description: "Available choices" }),
|
||||
multiple: Schema.optional(Schema.Boolean).annotate({ description: "Allow selecting multiple choices" }),
|
||||
}
|
||||
|
||||
export const Info = Schema.Struct({
|
||||
...base,
|
||||
custom: Schema.optional(Schema.Boolean).annotate({ description: "Allow typing a custom answer (default: true)" }),
|
||||
}).annotate({ identifier: "QuestionInfo" })
|
||||
export const Prompt = Schema.Struct(base).annotate({ identifier: "QuestionPrompt" })
|
||||
export const Tool = Schema.Struct({ messageID: SessionV1.MessageID, callID: Schema.String }).annotate({
|
||||
identifier: "QuestionTool",
|
||||
})
|
||||
export const Request = Schema.Struct({
|
||||
id: ID,
|
||||
sessionID: SessionID,
|
||||
questions: Schema.Array(Info).annotate({ description: "Questions to ask" }),
|
||||
tool: Schema.optional(Tool),
|
||||
}).annotate({ identifier: "QuestionRequest" })
|
||||
export const Answer = Schema.Array(Schema.String).annotate({ identifier: "QuestionAnswer" })
|
||||
export const Reply = Schema.Struct({
|
||||
answers: Schema.Array(Answer).annotate({
|
||||
description: "User answers in order of questions (each answer is an array of selected labels)",
|
||||
}),
|
||||
}).annotate({ identifier: "QuestionReply" })
|
||||
export const Replied = Schema.Struct({
|
||||
sessionID: SessionID,
|
||||
requestID: ID,
|
||||
answers: Schema.Array(Answer),
|
||||
}).annotate({
|
||||
identifier: "QuestionReplied",
|
||||
})
|
||||
export const Rejected = Schema.Struct({ sessionID: SessionID, requestID: ID }).annotate({
|
||||
identifier: "QuestionRejected",
|
||||
})
|
||||
|
||||
const Asked = define({ type: "question.asked", schema: Request.fields })
|
||||
const RepliedEvent = define({ type: "question.replied", schema: Replied.fields })
|
||||
const RejectedEvent = define({ type: "question.rejected", schema: Rejected.fields })
|
||||
export const Event = {
|
||||
Asked,
|
||||
Replied: RepliedEvent,
|
||||
Rejected: RejectedEvent,
|
||||
Definitions: inventory(Asked, RepliedEvent, RejectedEvent),
|
||||
}
|
||||
79
packages/schema/src/question.ts
Normal file
79
packages/schema/src/question.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
export * as Question from "./question"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { define, inventory } from "./event"
|
||||
import { ascending } from "./identifier"
|
||||
import { SessionID } from "./session-id"
|
||||
import { withStatics } from "./schema"
|
||||
|
||||
export const ID = Schema.String.check(Schema.isStartsWith("que")).pipe(
|
||||
Schema.brand("QuestionV2.ID"),
|
||||
withStatics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "que_" + ascending()) })),
|
||||
)
|
||||
export type ID = typeof ID.Type
|
||||
|
||||
export const Option = Schema.Struct({
|
||||
label: Schema.String.annotate({ description: "Display text (1-5 words, concise)" }),
|
||||
description: Schema.String.annotate({ description: "Explanation of choice" }),
|
||||
}).annotate({ identifier: "QuestionV2.Option" })
|
||||
export type Option = typeof Option.Type
|
||||
|
||||
const base = {
|
||||
question: Schema.String.annotate({ description: "Complete question" }),
|
||||
header: Schema.String.annotate({ description: "Very short label (max 30 chars)" }),
|
||||
options: Schema.Array(Option).annotate({ description: "Available choices" }),
|
||||
multiple: Schema.Boolean.pipe(Schema.optional).annotate({ description: "Allow selecting multiple choices" }),
|
||||
}
|
||||
|
||||
export const Info = Schema.Struct({
|
||||
...base,
|
||||
custom: Schema.Boolean.pipe(Schema.optional).annotate({
|
||||
description: "Allow typing a custom answer (default: true)",
|
||||
}),
|
||||
}).annotate({ identifier: "QuestionV2.Info" })
|
||||
export type Info = typeof Info.Type
|
||||
|
||||
export const Prompt = Schema.Struct(base).annotate({ identifier: "QuestionV2.Prompt" })
|
||||
export type Prompt = typeof Prompt.Type
|
||||
|
||||
export const Tool = Schema.Struct({
|
||||
messageID: Schema.String,
|
||||
callID: Schema.String,
|
||||
}).annotate({ identifier: "QuestionV2.Tool" })
|
||||
export type Tool = typeof Tool.Type
|
||||
|
||||
export const Request = Schema.Struct({
|
||||
id: ID,
|
||||
sessionID: SessionID,
|
||||
questions: Schema.Array(Info).annotate({ description: "Questions to ask" }),
|
||||
tool: Tool.pipe(Schema.optional),
|
||||
}).annotate({ identifier: "QuestionV2.Request" })
|
||||
export type Request = typeof Request.Type
|
||||
|
||||
export const Answer = Schema.Array(Schema.String).annotate({ identifier: "QuestionV2.Answer" })
|
||||
export type Answer = typeof Answer.Type
|
||||
|
||||
export const Reply = Schema.Struct({
|
||||
answers: Schema.Array(Answer).annotate({
|
||||
description: "User answers in order of questions (each answer is an array of selected labels)",
|
||||
}),
|
||||
}).annotate({ identifier: "QuestionV2.Reply" })
|
||||
export type Reply = typeof Reply.Type
|
||||
|
||||
const Asked = define({ type: "question.v2.asked", schema: Request.fields })
|
||||
const Replied = define({
|
||||
type: "question.v2.replied",
|
||||
schema: {
|
||||
sessionID: SessionID,
|
||||
requestID: ID,
|
||||
answers: Schema.Array(Answer),
|
||||
},
|
||||
})
|
||||
const Rejected = define({
|
||||
type: "question.v2.rejected",
|
||||
schema: {
|
||||
sessionID: SessionID,
|
||||
requestID: ID,
|
||||
},
|
||||
})
|
||||
export const Event = { Asked, Replied, Rejected, Definitions: inventory(Asked, Replied, Rejected) }
|
||||
|
|
@ -1,8 +1,12 @@
|
|||
export * as Reference from "./reference"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { define, inventory } from "./event"
|
||||
import { AbsolutePath } from "./schema"
|
||||
|
||||
const Updated = define({ type: "reference.updated", schema: {} })
|
||||
export const Event = { Updated, Definitions: inventory(Updated) }
|
||||
|
||||
export interface LocalSource extends Schema.Schema.Type<typeof LocalSource> {}
|
||||
export const LocalSource = Schema.Struct({
|
||||
type: Schema.Literal("local"),
|
||||
|
|
|
|||
8
packages/schema/src/server-event.ts
Normal file
8
packages/schema/src/server-event.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export * as ServerEvent from "./server-event"
|
||||
|
||||
import { Event } from "./event"
|
||||
|
||||
export const Connected = Event.define({ type: "server.connected", schema: {} })
|
||||
export const Disposed = Event.define({ type: "global.disposed", schema: {} })
|
||||
|
||||
export const Definitions = Event.inventory(Connected, Disposed)
|
||||
13
packages/schema/src/session-compaction-event.ts
Normal file
13
packages/schema/src/session-compaction-event.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
export * as SessionCompactionEvent from "./session-compaction-event"
|
||||
|
||||
import { Event } from "./event"
|
||||
import { SessionID } from "./session-id"
|
||||
|
||||
export const Compacted = Event.define({
|
||||
type: "session.compacted",
|
||||
schema: {
|
||||
sessionID: SessionID,
|
||||
},
|
||||
})
|
||||
|
||||
export const Definitions = Event.inventory(Compacted)
|
||||
497
packages/schema/src/session-event.ts
Normal file
497
packages/schema/src/session-event.ts
Normal file
|
|
@ -0,0 +1,497 @@
|
|||
export * as SessionEvent from "./session-event"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { Event } from "./event"
|
||||
import { ProviderMetadata, ToolContent } from "./llm"
|
||||
import { Delivery } from "./session-delivery"
|
||||
import { Model } from "./model"
|
||||
import { DateTimeUtcFromMillis, NonNegativeInt, RelativePath } from "./schema"
|
||||
import { FileAttachment, Prompt } from "./prompt"
|
||||
import { SessionID } from "./session-id"
|
||||
import { Location } from "./location"
|
||||
import { SessionMessageID } from "./session-message-id"
|
||||
import { SessionMessage } from "./session-message"
|
||||
|
||||
export { FileAttachment }
|
||||
|
||||
export const Source = Schema.Struct({
|
||||
start: NonNegativeInt,
|
||||
end: NonNegativeInt,
|
||||
text: Schema.String,
|
||||
}).annotate({
|
||||
identifier: "session.next.event.source",
|
||||
})
|
||||
export type Source = typeof Source.Type
|
||||
|
||||
const Base = {
|
||||
timestamp: DateTimeUtcFromMillis,
|
||||
sessionID: SessionID,
|
||||
}
|
||||
const PromptFields = {
|
||||
...Base,
|
||||
messageID: SessionMessageID.ID,
|
||||
prompt: Prompt,
|
||||
delivery: Delivery,
|
||||
}
|
||||
|
||||
const options = {
|
||||
durable: {
|
||||
aggregate: "sessionID",
|
||||
version: 1,
|
||||
},
|
||||
} as const
|
||||
const stepSettlementOptions = {
|
||||
durable: {
|
||||
aggregate: "sessionID",
|
||||
version: 2,
|
||||
},
|
||||
} as const
|
||||
|
||||
export const UnknownError = SessionMessage.UnknownError
|
||||
export type UnknownError = SessionMessage.UnknownError
|
||||
|
||||
export const AgentSwitched = Event.define({
|
||||
type: "session.next.agent.switched",
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
messageID: SessionMessageID.ID,
|
||||
agent: Schema.String,
|
||||
},
|
||||
})
|
||||
export type AgentSwitched = typeof AgentSwitched.Type
|
||||
|
||||
export const ModelSwitched = Event.define({
|
||||
type: "session.next.model.switched",
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
messageID: SessionMessageID.ID,
|
||||
model: Model.Ref,
|
||||
},
|
||||
})
|
||||
export type ModelSwitched = typeof ModelSwitched.Type
|
||||
|
||||
export const Moved = Event.define({
|
||||
type: "session.next.moved",
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
location: Location.Ref,
|
||||
subdirectory: RelativePath.pipe(Schema.optional),
|
||||
},
|
||||
})
|
||||
export type Moved = typeof Moved.Type
|
||||
|
||||
export const Prompted = Event.define({
|
||||
type: "session.next.prompted",
|
||||
...options,
|
||||
schema: PromptFields,
|
||||
})
|
||||
export type Prompted = typeof Prompted.Type
|
||||
|
||||
export const PromptAdmitted = Event.define({
|
||||
type: "session.next.prompt.admitted",
|
||||
...options,
|
||||
schema: PromptFields,
|
||||
})
|
||||
export type PromptAdmitted = typeof PromptAdmitted.Type
|
||||
|
||||
export const ContextUpdated = Event.define({
|
||||
type: "session.next.context.updated",
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
messageID: SessionMessageID.ID,
|
||||
text: Schema.String,
|
||||
},
|
||||
})
|
||||
export type ContextUpdated = typeof ContextUpdated.Type
|
||||
|
||||
export const Synthetic = Event.define({
|
||||
type: "session.next.synthetic",
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
messageID: SessionMessageID.ID,
|
||||
text: Schema.String,
|
||||
},
|
||||
})
|
||||
export type Synthetic = typeof Synthetic.Type
|
||||
|
||||
export namespace Shell {
|
||||
export const Started = Event.define({
|
||||
type: "session.next.shell.started",
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
messageID: SessionMessageID.ID,
|
||||
callID: Schema.String,
|
||||
command: Schema.String,
|
||||
},
|
||||
})
|
||||
export type Started = typeof Started.Type
|
||||
|
||||
export const Ended = Event.define({
|
||||
type: "session.next.shell.ended",
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
callID: Schema.String,
|
||||
output: Schema.String,
|
||||
},
|
||||
})
|
||||
export type Ended = typeof Ended.Type
|
||||
}
|
||||
|
||||
export namespace Step {
|
||||
export const Started = Event.define({
|
||||
type: "session.next.step.started",
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
assistantMessageID: SessionMessageID.ID,
|
||||
agent: Schema.String,
|
||||
model: Model.Ref,
|
||||
snapshot: Schema.String.pipe(Schema.optional),
|
||||
},
|
||||
})
|
||||
export type Started = typeof Started.Type
|
||||
|
||||
export const Ended = Event.define({
|
||||
type: "session.next.step.ended",
|
||||
...stepSettlementOptions,
|
||||
schema: {
|
||||
...Base,
|
||||
assistantMessageID: SessionMessageID.ID,
|
||||
finish: Schema.String,
|
||||
cost: Schema.Finite,
|
||||
tokens: Schema.Struct({
|
||||
input: Schema.Finite,
|
||||
output: Schema.Finite,
|
||||
reasoning: Schema.Finite,
|
||||
cache: Schema.Struct({
|
||||
read: Schema.Finite,
|
||||
write: Schema.Finite,
|
||||
}),
|
||||
}),
|
||||
snapshot: Schema.String.pipe(Schema.optional),
|
||||
},
|
||||
})
|
||||
export type Ended = typeof Ended.Type
|
||||
|
||||
export const Failed = Event.define({
|
||||
type: "session.next.step.failed",
|
||||
...stepSettlementOptions,
|
||||
schema: {
|
||||
...Base,
|
||||
assistantMessageID: SessionMessageID.ID,
|
||||
error: UnknownError,
|
||||
},
|
||||
})
|
||||
export type Failed = typeof Failed.Type
|
||||
}
|
||||
|
||||
export namespace Text {
|
||||
export const Started = Event.define({
|
||||
type: "session.next.text.started",
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
assistantMessageID: SessionMessageID.ID,
|
||||
textID: Schema.String,
|
||||
},
|
||||
})
|
||||
export type Started = typeof Started.Type
|
||||
|
||||
// Stream fragments are live-only; Text.Ended is the replayable full-value boundary.
|
||||
export const Delta = Event.define({
|
||||
type: "session.next.text.delta",
|
||||
schema: {
|
||||
...Base,
|
||||
assistantMessageID: SessionMessageID.ID,
|
||||
textID: Schema.String,
|
||||
delta: Schema.String,
|
||||
},
|
||||
})
|
||||
export type Delta = typeof Delta.Type
|
||||
|
||||
export const Ended = Event.define({
|
||||
type: "session.next.text.ended",
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
assistantMessageID: SessionMessageID.ID,
|
||||
textID: Schema.String,
|
||||
text: Schema.String,
|
||||
},
|
||||
})
|
||||
export type Ended = typeof Ended.Type
|
||||
}
|
||||
|
||||
export namespace Reasoning {
|
||||
export const Started = Event.define({
|
||||
type: "session.next.reasoning.started",
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
assistantMessageID: SessionMessageID.ID,
|
||||
reasoningID: Schema.String,
|
||||
providerMetadata: ProviderMetadata.pipe(Schema.optional),
|
||||
},
|
||||
})
|
||||
export type Started = typeof Started.Type
|
||||
|
||||
// Stream fragments are live-only; Reasoning.Ended is the replayable full-value boundary.
|
||||
export const Delta = Event.define({
|
||||
type: "session.next.reasoning.delta",
|
||||
schema: {
|
||||
...Base,
|
||||
assistantMessageID: SessionMessageID.ID,
|
||||
reasoningID: Schema.String,
|
||||
delta: Schema.String,
|
||||
},
|
||||
})
|
||||
export type Delta = typeof Delta.Type
|
||||
|
||||
export const Ended = Event.define({
|
||||
type: "session.next.reasoning.ended",
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
assistantMessageID: SessionMessageID.ID,
|
||||
reasoningID: Schema.String,
|
||||
text: Schema.String,
|
||||
providerMetadata: ProviderMetadata.pipe(Schema.optional),
|
||||
},
|
||||
})
|
||||
export type Ended = typeof Ended.Type
|
||||
}
|
||||
|
||||
export namespace Tool {
|
||||
const ToolBase = {
|
||||
...Base,
|
||||
assistantMessageID: SessionMessageID.ID,
|
||||
callID: Schema.String,
|
||||
}
|
||||
|
||||
export namespace Input {
|
||||
export const Started = Event.define({
|
||||
type: "session.next.tool.input.started",
|
||||
...options,
|
||||
schema: {
|
||||
...ToolBase,
|
||||
name: Schema.String,
|
||||
},
|
||||
})
|
||||
export type Started = typeof Started.Type
|
||||
|
||||
// Stream fragments are live-only; Input.Ended is the replayable raw-input boundary.
|
||||
export const Delta = Event.define({
|
||||
type: "session.next.tool.input.delta",
|
||||
schema: {
|
||||
...ToolBase,
|
||||
delta: Schema.String,
|
||||
},
|
||||
})
|
||||
export type Delta = typeof Delta.Type
|
||||
|
||||
export const Ended = Event.define({
|
||||
type: "session.next.tool.input.ended",
|
||||
...options,
|
||||
schema: {
|
||||
...ToolBase,
|
||||
text: Schema.String,
|
||||
},
|
||||
})
|
||||
export type Ended = typeof Ended.Type
|
||||
}
|
||||
|
||||
export const Called = Event.define({
|
||||
type: "session.next.tool.called",
|
||||
...options,
|
||||
schema: {
|
||||
...ToolBase,
|
||||
tool: Schema.String,
|
||||
input: Schema.Record(Schema.String, Schema.Unknown),
|
||||
provider: Schema.Struct({
|
||||
executed: Schema.Boolean,
|
||||
metadata: ProviderMetadata.pipe(Schema.optional),
|
||||
}),
|
||||
},
|
||||
})
|
||||
export type Called = typeof Called.Type
|
||||
|
||||
/**
|
||||
* Replayable bounded running-tool state. Tools should checkpoint semantic
|
||||
* transitions or at a bounded cadence, not persist every stdout/stderr chunk.
|
||||
*/
|
||||
export const Progress = Event.define({
|
||||
type: "session.next.tool.progress",
|
||||
...options,
|
||||
schema: {
|
||||
...ToolBase,
|
||||
structured: Schema.Record(Schema.String, Schema.Any),
|
||||
content: Schema.Array(ToolContent),
|
||||
},
|
||||
})
|
||||
export type Progress = typeof Progress.Type
|
||||
|
||||
export const Success = Event.define({
|
||||
type: "session.next.tool.success",
|
||||
...options,
|
||||
schema: {
|
||||
...ToolBase,
|
||||
structured: Schema.Record(Schema.String, Schema.Any),
|
||||
content: Schema.Array(ToolContent),
|
||||
outputPaths: Schema.Array(Schema.String).pipe(Schema.optional),
|
||||
result: Schema.Unknown.pipe(Schema.optional),
|
||||
provider: Schema.Struct({
|
||||
executed: Schema.Boolean,
|
||||
metadata: ProviderMetadata.pipe(Schema.optional),
|
||||
}),
|
||||
},
|
||||
})
|
||||
export type Success = typeof Success.Type
|
||||
|
||||
export const Failed = Event.define({
|
||||
type: "session.next.tool.failed",
|
||||
...options,
|
||||
schema: {
|
||||
...ToolBase,
|
||||
error: UnknownError,
|
||||
result: Schema.Unknown.pipe(Schema.optional),
|
||||
provider: Schema.Struct({
|
||||
executed: Schema.Boolean,
|
||||
metadata: ProviderMetadata.pipe(Schema.optional),
|
||||
}),
|
||||
},
|
||||
})
|
||||
export type Failed = typeof Failed.Type
|
||||
}
|
||||
|
||||
export const RetryError = Schema.Struct({
|
||||
message: Schema.String,
|
||||
statusCode: Schema.Finite.pipe(Schema.optional),
|
||||
isRetryable: Schema.Boolean,
|
||||
responseHeaders: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
|
||||
responseBody: Schema.String.pipe(Schema.optional),
|
||||
metadata: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
|
||||
}).annotate({
|
||||
identifier: "session.next.retry_error",
|
||||
})
|
||||
export type RetryError = typeof RetryError.Type
|
||||
|
||||
export const Retried = Event.define({
|
||||
type: "session.next.retried",
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
attempt: Schema.Finite,
|
||||
error: RetryError,
|
||||
},
|
||||
})
|
||||
export type Retried = typeof Retried.Type
|
||||
|
||||
export namespace Compaction {
|
||||
export const Started = Event.define({
|
||||
type: "session.next.compaction.started",
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
messageID: SessionMessageID.ID,
|
||||
reason: Schema.Union([Schema.Literal("auto"), Schema.Literal("manual")]),
|
||||
},
|
||||
})
|
||||
export type Started = typeof Started.Type
|
||||
|
||||
export const Delta = Event.define({
|
||||
type: "session.next.compaction.delta",
|
||||
schema: {
|
||||
...Base,
|
||||
messageID: SessionMessageID.ID,
|
||||
text: Schema.String,
|
||||
},
|
||||
})
|
||||
export type Delta = typeof Delta.Type
|
||||
|
||||
export const Ended = Event.define({
|
||||
type: "session.next.compaction.ended",
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
messageID: SessionMessageID.ID,
|
||||
reason: Started.data.fields.reason,
|
||||
text: Schema.String,
|
||||
recent: Schema.String,
|
||||
},
|
||||
})
|
||||
export type Ended = typeof Ended.Type
|
||||
}
|
||||
|
||||
export const DurableDefinitions = Event.inventory(
|
||||
AgentSwitched,
|
||||
ModelSwitched,
|
||||
Moved,
|
||||
Prompted,
|
||||
PromptAdmitted,
|
||||
ContextUpdated,
|
||||
Synthetic,
|
||||
Shell.Started,
|
||||
Shell.Ended,
|
||||
Step.Started,
|
||||
Step.Ended,
|
||||
Step.Failed,
|
||||
Text.Started,
|
||||
Text.Ended,
|
||||
Tool.Input.Started,
|
||||
Tool.Input.Ended,
|
||||
Tool.Called,
|
||||
Tool.Progress,
|
||||
Tool.Success,
|
||||
Tool.Failed,
|
||||
Reasoning.Started,
|
||||
Reasoning.Ended,
|
||||
Retried,
|
||||
Compaction.Started,
|
||||
Compaction.Ended,
|
||||
)
|
||||
|
||||
export const Definitions = Event.inventory(
|
||||
AgentSwitched,
|
||||
ModelSwitched,
|
||||
Moved,
|
||||
Prompted,
|
||||
PromptAdmitted,
|
||||
ContextUpdated,
|
||||
Synthetic,
|
||||
Shell.Started,
|
||||
Shell.Ended,
|
||||
Step.Started,
|
||||
Step.Ended,
|
||||
Step.Failed,
|
||||
Text.Started,
|
||||
Text.Delta,
|
||||
Text.Ended,
|
||||
Reasoning.Started,
|
||||
Reasoning.Delta,
|
||||
Reasoning.Ended,
|
||||
Tool.Input.Started,
|
||||
Tool.Input.Delta,
|
||||
Tool.Input.Ended,
|
||||
Tool.Called,
|
||||
Tool.Progress,
|
||||
Tool.Success,
|
||||
Tool.Failed,
|
||||
Retried,
|
||||
Compaction.Started,
|
||||
Compaction.Delta,
|
||||
Compaction.Ended,
|
||||
)
|
||||
|
||||
export const Durable = Schema.Union(DurableDefinitions, { mode: "oneOf" }).pipe(Schema.toTaggedUnion("type"))
|
||||
export type DurableEvent = typeof Durable.Type
|
||||
|
||||
export const All = Schema.Union(Definitions, { mode: "oneOf" }).pipe(Schema.toTaggedUnion("type"))
|
||||
export type Event = typeof All.Type
|
||||
export type Type = Event["type"]
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export interface Admitted extends Schema.Schema.Type<typeof Admitted> {}
|
|||
export const Admitted = Schema.Struct({
|
||||
admittedSeq: NonNegativeInt,
|
||||
id: SessionMessageID.ID,
|
||||
sessionID: SessionID.ID,
|
||||
sessionID: SessionID,
|
||||
prompt: Prompt,
|
||||
delivery: Delivery,
|
||||
timeCreated: DateTimeUtcFromMillis,
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ export const User = Schema.Struct({
|
|||
export interface Synthetic extends Schema.Schema.Type<typeof Synthetic> {}
|
||||
export const Synthetic = Schema.Struct({
|
||||
...Base,
|
||||
sessionID: SessionID.ID,
|
||||
sessionID: SessionID,
|
||||
text: Schema.String,
|
||||
type: Schema.Literal("synthetic"),
|
||||
}).annotate({ identifier: "Session.Message.Synthetic" })
|
||||
|
|
|
|||
50
packages/schema/src/session-status-event.ts
Normal file
50
packages/schema/src/session-status-event.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
export * as SessionStatusEvent from "./session-status-event"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { Event } from "./event"
|
||||
import { NonNegativeInt } from "./schema"
|
||||
import { SessionID } from "./session-id"
|
||||
|
||||
export const Info = Schema.Union([
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("idle"),
|
||||
}),
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("retry"),
|
||||
attempt: NonNegativeInt,
|
||||
message: Schema.String,
|
||||
action: Schema.optional(
|
||||
Schema.Struct({
|
||||
reason: Schema.String,
|
||||
provider: Schema.String,
|
||||
title: Schema.String,
|
||||
message: Schema.String,
|
||||
label: Schema.String,
|
||||
link: Schema.optional(Schema.String),
|
||||
}),
|
||||
),
|
||||
next: NonNegativeInt,
|
||||
}),
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("busy"),
|
||||
}),
|
||||
]).annotate({ identifier: "SessionStatus" })
|
||||
export type Info = Schema.Schema.Type<typeof Info>
|
||||
|
||||
export const Status = Event.define({
|
||||
type: "session.status",
|
||||
schema: {
|
||||
sessionID: SessionID,
|
||||
status: Info,
|
||||
},
|
||||
})
|
||||
|
||||
// deprecated
|
||||
export const Idle = Event.define({
|
||||
type: "session.idle",
|
||||
schema: {
|
||||
sessionID: SessionID,
|
||||
},
|
||||
})
|
||||
|
||||
export const Definitions = Event.inventory(Status, Idle)
|
||||
24
packages/schema/src/session-todo.ts
Normal file
24
packages/schema/src/session-todo.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
export * as SessionTodo from "./session-todo"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { define, inventory } from "./event"
|
||||
import { SessionID } from "./session-id"
|
||||
|
||||
export const Info = Schema.Struct({
|
||||
content: Schema.String.annotate({ description: "Brief description of the task" }),
|
||||
status: Schema.String.annotate({
|
||||
description: "Current status of the task: pending, in_progress, completed, cancelled",
|
||||
}),
|
||||
priority: Schema.String.annotate({ description: "Priority level of the task: high, medium, low" }),
|
||||
}).annotate({ identifier: "Todo" })
|
||||
export type Info = typeof Info.Type
|
||||
export const SessionTodoInfo = Info
|
||||
|
||||
const Updated = define({
|
||||
type: "todo.updated",
|
||||
schema: {
|
||||
sessionID: SessionID,
|
||||
todos: Schema.Array(Info),
|
||||
},
|
||||
})
|
||||
export const Event = { Updated, Definitions: inventory(Updated) }
|
||||
676
packages/schema/src/session-v1.ts
Normal file
676
packages/schema/src/session-v1.ts
Normal file
|
|
@ -0,0 +1,676 @@
|
|||
export * as SessionV1 from "./session-v1"
|
||||
|
||||
import { Effect, Schema, Types } from "effect"
|
||||
import { define, inventory } from "./event"
|
||||
import { FileDiff } from "./file-diff"
|
||||
import { PermissionV1 } from "./permission-v1"
|
||||
import { Project } from "./project"
|
||||
import { Provider } from "./provider"
|
||||
import { Model } from "./model"
|
||||
import { NonNegativeInt, optionalOmitUndefined, withStatics } from "./schema"
|
||||
import { ascending } from "./identifier"
|
||||
import { SessionID } from "./session-id"
|
||||
import { WorkspaceID } from "./workspace-id"
|
||||
|
||||
const Timestamp = Schema.Finite.check(Schema.isGreaterThanOrEqualTo(0))
|
||||
|
||||
export const MessageID = Schema.String.check(Schema.isStartsWith("msg")).pipe(
|
||||
Schema.brand("MessageID"),
|
||||
withStatics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "msg_" + ascending()) })),
|
||||
)
|
||||
export type MessageID = typeof MessageID.Type
|
||||
|
||||
export const PartID = Schema.String.check(Schema.isStartsWith("prt")).pipe(
|
||||
Schema.brand("PartID"),
|
||||
withStatics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "prt_" + ascending()) })),
|
||||
)
|
||||
export type PartID = typeof PartID.Type
|
||||
|
||||
const namedError = <Name extends string, Fields extends Schema.Struct.Fields>(name: Name, fields: Fields) => {
|
||||
const schema = Schema.Struct({ name: Schema.Literal(name), data: Schema.Struct(fields) }).annotate({
|
||||
identifier: name,
|
||||
})
|
||||
return { Schema: schema, EffectSchema: schema }
|
||||
}
|
||||
|
||||
export const OutputLengthError = namedError("MessageOutputLengthError", {})
|
||||
|
||||
export const AuthError = namedError("ProviderAuthError", {
|
||||
providerID: Schema.String,
|
||||
message: Schema.String,
|
||||
})
|
||||
|
||||
export const AbortedError = namedError("MessageAbortedError", { message: Schema.String })
|
||||
export const StructuredOutputError = namedError("StructuredOutputError", {
|
||||
message: Schema.String,
|
||||
retries: NonNegativeInt,
|
||||
})
|
||||
export const APIError = namedError("APIError", {
|
||||
message: Schema.String,
|
||||
statusCode: Schema.optional(NonNegativeInt),
|
||||
isRetryable: Schema.Boolean,
|
||||
responseHeaders: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
||||
responseBody: Schema.optional(Schema.String),
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
||||
})
|
||||
export type APIError = Schema.Schema.Type<typeof APIError.Schema>
|
||||
export const ContextOverflowError = namedError("ContextOverflowError", {
|
||||
message: Schema.String,
|
||||
responseBody: Schema.optional(Schema.String),
|
||||
})
|
||||
export const ContentFilterError = namedError("ContentFilterError", {
|
||||
message: Schema.String,
|
||||
})
|
||||
|
||||
export class OutputFormatText extends Schema.Class<OutputFormatText>("OutputFormatText")({
|
||||
type: Schema.Literal("text"),
|
||||
}) {}
|
||||
|
||||
export class OutputFormatJsonSchema extends Schema.Class<OutputFormatJsonSchema>("OutputFormatJsonSchema")({
|
||||
type: Schema.Literal("json_schema"),
|
||||
schema: Schema.Record(Schema.String, Schema.Any).annotate({ identifier: "JSONSchema" }),
|
||||
retryCount: NonNegativeInt.pipe(Schema.optional, Schema.withDecodingDefault(Effect.succeed(2))),
|
||||
}) {}
|
||||
|
||||
export const Format = Schema.Union([OutputFormatText, OutputFormatJsonSchema]).annotate({
|
||||
discriminator: "type",
|
||||
identifier: "OutputFormat",
|
||||
})
|
||||
export type OutputFormat = Schema.Schema.Type<typeof Format>
|
||||
|
||||
const partBase = {
|
||||
id: PartID,
|
||||
sessionID: SessionID,
|
||||
messageID: MessageID,
|
||||
}
|
||||
|
||||
export const SnapshotPart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("snapshot"),
|
||||
snapshot: Schema.String,
|
||||
}).annotate({ identifier: "SnapshotPart" })
|
||||
export type SnapshotPart = Types.DeepMutable<Schema.Schema.Type<typeof SnapshotPart>>
|
||||
|
||||
export const PatchPart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("patch"),
|
||||
hash: Schema.String,
|
||||
files: Schema.Array(Schema.String),
|
||||
}).annotate({ identifier: "PatchPart" })
|
||||
export type PatchPart = Types.DeepMutable<Schema.Schema.Type<typeof PatchPart>>
|
||||
|
||||
export const TextPart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("text"),
|
||||
text: Schema.String,
|
||||
synthetic: Schema.optional(Schema.Boolean),
|
||||
ignored: Schema.optional(Schema.Boolean),
|
||||
time: Schema.optional(
|
||||
Schema.Struct({
|
||||
start: NonNegativeInt,
|
||||
end: Schema.optional(NonNegativeInt),
|
||||
}),
|
||||
),
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
|
||||
}).annotate({ identifier: "TextPart" })
|
||||
export type TextPart = Types.DeepMutable<Schema.Schema.Type<typeof TextPart>>
|
||||
|
||||
export const ReasoningPart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("reasoning"),
|
||||
text: Schema.String,
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
|
||||
time: Schema.Struct({
|
||||
start: NonNegativeInt,
|
||||
end: Schema.optional(NonNegativeInt),
|
||||
}),
|
||||
}).annotate({ identifier: "ReasoningPart" })
|
||||
export type ReasoningPart = Types.DeepMutable<Schema.Schema.Type<typeof ReasoningPart>>
|
||||
|
||||
const filePartSourceBase = {
|
||||
text: Schema.Struct({
|
||||
value: Schema.String,
|
||||
start: Schema.Finite,
|
||||
end: Schema.Finite,
|
||||
}).annotate({ identifier: "FilePartSourceText" }),
|
||||
}
|
||||
|
||||
export const Range = Schema.Struct({
|
||||
start: Schema.Struct({ line: NonNegativeInt, character: NonNegativeInt }),
|
||||
end: Schema.Struct({ line: NonNegativeInt, character: NonNegativeInt }),
|
||||
}).annotate({ identifier: "Range" })
|
||||
export type Range = typeof Range.Type
|
||||
|
||||
export const FileSource = Schema.Struct({
|
||||
...filePartSourceBase,
|
||||
type: Schema.Literal("file"),
|
||||
path: Schema.String,
|
||||
}).annotate({ identifier: "FileSource" })
|
||||
|
||||
export const SymbolSource = Schema.Struct({
|
||||
...filePartSourceBase,
|
||||
type: Schema.Literal("symbol"),
|
||||
path: Schema.String,
|
||||
range: Range,
|
||||
name: Schema.String,
|
||||
kind: NonNegativeInt,
|
||||
}).annotate({ identifier: "SymbolSource" })
|
||||
|
||||
export const ResourceSource = Schema.Struct({
|
||||
...filePartSourceBase,
|
||||
type: Schema.Literal("resource"),
|
||||
clientName: Schema.String,
|
||||
uri: Schema.String,
|
||||
}).annotate({ identifier: "ResourceSource" })
|
||||
|
||||
export const FilePartSource = Schema.Union([FileSource, SymbolSource, ResourceSource]).annotate({
|
||||
discriminator: "type",
|
||||
identifier: "FilePartSource",
|
||||
})
|
||||
|
||||
export const FilePart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("file"),
|
||||
mime: Schema.String,
|
||||
filename: Schema.optional(Schema.String),
|
||||
url: Schema.String,
|
||||
source: Schema.optional(FilePartSource),
|
||||
}).annotate({ identifier: "FilePart" })
|
||||
export type FilePart = Types.DeepMutable<Schema.Schema.Type<typeof FilePart>>
|
||||
|
||||
export const AgentPart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("agent"),
|
||||
name: Schema.String,
|
||||
source: Schema.optional(
|
||||
Schema.Struct({
|
||||
value: Schema.String,
|
||||
start: NonNegativeInt,
|
||||
end: NonNegativeInt,
|
||||
}),
|
||||
),
|
||||
}).annotate({ identifier: "AgentPart" })
|
||||
export type AgentPart = Types.DeepMutable<Schema.Schema.Type<typeof AgentPart>>
|
||||
|
||||
export const CompactionPart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("compaction"),
|
||||
auto: Schema.Boolean,
|
||||
overflow: Schema.optional(Schema.Boolean),
|
||||
tail_start_id: Schema.optional(MessageID),
|
||||
}).annotate({ identifier: "CompactionPart" })
|
||||
export type CompactionPart = Types.DeepMutable<Schema.Schema.Type<typeof CompactionPart>>
|
||||
|
||||
export const SubtaskPart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("subtask"),
|
||||
prompt: Schema.String,
|
||||
description: Schema.String,
|
||||
agent: Schema.String,
|
||||
model: Schema.optional(
|
||||
Schema.Struct({
|
||||
providerID: Provider.ID,
|
||||
modelID: Model.ID,
|
||||
}),
|
||||
),
|
||||
command: Schema.optional(Schema.String),
|
||||
}).annotate({ identifier: "SubtaskPart" })
|
||||
export type SubtaskPart = Types.DeepMutable<Schema.Schema.Type<typeof SubtaskPart>>
|
||||
|
||||
export const RetryPart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("retry"),
|
||||
attempt: NonNegativeInt,
|
||||
error: APIError.EffectSchema,
|
||||
time: Schema.Struct({
|
||||
created: NonNegativeInt,
|
||||
}),
|
||||
}).annotate({ identifier: "RetryPart" })
|
||||
export type RetryPart = Omit<Types.DeepMutable<Schema.Schema.Type<typeof RetryPart>>, "error"> & {
|
||||
error: APIError
|
||||
}
|
||||
|
||||
export const StepStartPart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("step-start"),
|
||||
snapshot: Schema.optional(Schema.String),
|
||||
}).annotate({ identifier: "StepStartPart" })
|
||||
export type StepStartPart = Types.DeepMutable<Schema.Schema.Type<typeof StepStartPart>>
|
||||
|
||||
export const StepFinishPart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("step-finish"),
|
||||
reason: Schema.String,
|
||||
snapshot: Schema.optional(Schema.String),
|
||||
cost: Schema.Finite,
|
||||
tokens: Schema.Struct({
|
||||
total: Schema.optional(Schema.Finite),
|
||||
input: Schema.Finite,
|
||||
output: Schema.Finite,
|
||||
reasoning: Schema.Finite,
|
||||
cache: Schema.Struct({
|
||||
read: Schema.Finite,
|
||||
write: Schema.Finite,
|
||||
}),
|
||||
}),
|
||||
}).annotate({ identifier: "StepFinishPart" })
|
||||
export type StepFinishPart = Types.DeepMutable<Schema.Schema.Type<typeof StepFinishPart>>
|
||||
|
||||
export const ToolStatePending = Schema.Struct({
|
||||
status: Schema.Literal("pending"),
|
||||
input: Schema.Record(Schema.String, Schema.Any),
|
||||
raw: Schema.String,
|
||||
}).annotate({ identifier: "ToolStatePending" })
|
||||
export type ToolStatePending = Types.DeepMutable<Schema.Schema.Type<typeof ToolStatePending>>
|
||||
|
||||
export const ToolStateRunning = Schema.Struct({
|
||||
status: Schema.Literal("running"),
|
||||
input: Schema.Record(Schema.String, Schema.Any),
|
||||
title: Schema.optional(Schema.String),
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
|
||||
time: Schema.Struct({
|
||||
start: NonNegativeInt,
|
||||
}),
|
||||
}).annotate({ identifier: "ToolStateRunning" })
|
||||
export type ToolStateRunning = Types.DeepMutable<Schema.Schema.Type<typeof ToolStateRunning>>
|
||||
|
||||
export const ToolStateCompleted = Schema.Struct({
|
||||
status: Schema.Literal("completed"),
|
||||
input: Schema.Record(Schema.String, Schema.Any),
|
||||
output: Schema.String,
|
||||
title: Schema.String,
|
||||
metadata: Schema.Record(Schema.String, Schema.Any),
|
||||
time: Schema.Struct({
|
||||
start: NonNegativeInt,
|
||||
end: NonNegativeInt,
|
||||
compacted: Schema.optional(NonNegativeInt),
|
||||
}),
|
||||
attachments: Schema.optional(Schema.Array(FilePart)),
|
||||
}).annotate({ identifier: "ToolStateCompleted" })
|
||||
export type ToolStateCompleted = Types.DeepMutable<Schema.Schema.Type<typeof ToolStateCompleted>>
|
||||
|
||||
export const ToolStateError = Schema.Struct({
|
||||
status: Schema.Literal("error"),
|
||||
input: Schema.Record(Schema.String, Schema.Any),
|
||||
error: Schema.String,
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
|
||||
time: Schema.Struct({
|
||||
start: NonNegativeInt,
|
||||
end: NonNegativeInt,
|
||||
}),
|
||||
}).annotate({ identifier: "ToolStateError" })
|
||||
export type ToolStateError = Types.DeepMutable<Schema.Schema.Type<typeof ToolStateError>>
|
||||
|
||||
export const ToolState = Schema.Union([
|
||||
ToolStatePending,
|
||||
ToolStateRunning,
|
||||
ToolStateCompleted,
|
||||
ToolStateError,
|
||||
]).annotate({
|
||||
discriminator: "status",
|
||||
identifier: "ToolState",
|
||||
})
|
||||
export type ToolState = ToolStatePending | ToolStateRunning | ToolStateCompleted | ToolStateError
|
||||
|
||||
export const ToolPart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("tool"),
|
||||
callID: Schema.String,
|
||||
tool: Schema.String,
|
||||
state: ToolState,
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
|
||||
}).annotate({ identifier: "ToolPart" })
|
||||
export type ToolPart = Omit<Types.DeepMutable<Schema.Schema.Type<typeof ToolPart>>, "state"> & {
|
||||
state: ToolState
|
||||
}
|
||||
|
||||
const messageBase = {
|
||||
id: MessageID,
|
||||
sessionID: partBase.sessionID,
|
||||
}
|
||||
|
||||
export const User = Schema.Struct({
|
||||
...messageBase,
|
||||
role: Schema.Literal("user"),
|
||||
time: Schema.Struct({
|
||||
created: Timestamp,
|
||||
}),
|
||||
format: Schema.optional(Format),
|
||||
summary: Schema.optional(
|
||||
Schema.Struct({
|
||||
title: Schema.optional(Schema.String),
|
||||
body: Schema.optional(Schema.String),
|
||||
diffs: Schema.Array(FileDiff.Info),
|
||||
}),
|
||||
),
|
||||
agent: Schema.String,
|
||||
model: Schema.Struct({
|
||||
providerID: Provider.ID,
|
||||
modelID: Model.ID,
|
||||
variant: Schema.optional(Schema.String),
|
||||
}),
|
||||
system: Schema.optional(Schema.String),
|
||||
tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)),
|
||||
}).annotate({ identifier: "UserMessage" })
|
||||
export type User = Types.DeepMutable<Schema.Schema.Type<typeof User>>
|
||||
|
||||
export const Part = Schema.Union([
|
||||
TextPart,
|
||||
SubtaskPart,
|
||||
ReasoningPart,
|
||||
FilePart,
|
||||
ToolPart,
|
||||
StepStartPart,
|
||||
StepFinishPart,
|
||||
SnapshotPart,
|
||||
PatchPart,
|
||||
AgentPart,
|
||||
RetryPart,
|
||||
CompactionPart,
|
||||
]).annotate({ discriminator: "type", identifier: "Part" })
|
||||
export type Part =
|
||||
| TextPart
|
||||
| SubtaskPart
|
||||
| ReasoningPart
|
||||
| FilePart
|
||||
| ToolPart
|
||||
| StepStartPart
|
||||
| StepFinishPart
|
||||
| SnapshotPart
|
||||
| PatchPart
|
||||
| AgentPart
|
||||
| RetryPart
|
||||
| CompactionPart
|
||||
|
||||
const AssistantErrorSchema = Schema.Union([
|
||||
AuthError.EffectSchema,
|
||||
namedError("UnknownError", { message: Schema.String, ref: Schema.optional(Schema.String) }).EffectSchema,
|
||||
OutputLengthError.EffectSchema,
|
||||
AbortedError.EffectSchema,
|
||||
StructuredOutputError.EffectSchema,
|
||||
ContextOverflowError.EffectSchema,
|
||||
ContentFilterError.EffectSchema,
|
||||
APIError.EffectSchema,
|
||||
]).annotate({ discriminator: "name" })
|
||||
type AssistantError = Schema.Schema.Type<typeof AssistantErrorSchema>
|
||||
|
||||
export const TextPartInput = Schema.Struct({
|
||||
id: Schema.optional(PartID),
|
||||
type: Schema.Literal("text"),
|
||||
text: Schema.String,
|
||||
synthetic: Schema.optional(Schema.Boolean),
|
||||
ignored: Schema.optional(Schema.Boolean),
|
||||
time: Schema.optional(
|
||||
Schema.Struct({
|
||||
start: NonNegativeInt,
|
||||
end: Schema.optional(NonNegativeInt),
|
||||
}),
|
||||
),
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
|
||||
}).annotate({ identifier: "TextPartInput" })
|
||||
export type TextPartInput = Types.DeepMutable<Schema.Schema.Type<typeof TextPartInput>>
|
||||
|
||||
export const FilePartInput = Schema.Struct({
|
||||
id: Schema.optional(PartID),
|
||||
type: Schema.Literal("file"),
|
||||
mime: Schema.String,
|
||||
filename: Schema.optional(Schema.String),
|
||||
url: Schema.String,
|
||||
source: Schema.optional(FilePartSource),
|
||||
}).annotate({ identifier: "FilePartInput" })
|
||||
export type FilePartInput = Types.DeepMutable<Schema.Schema.Type<typeof FilePartInput>>
|
||||
|
||||
export const AgentPartInput = Schema.Struct({
|
||||
id: Schema.optional(PartID),
|
||||
type: Schema.Literal("agent"),
|
||||
name: Schema.String,
|
||||
source: Schema.optional(
|
||||
Schema.Struct({
|
||||
value: Schema.String,
|
||||
start: NonNegativeInt,
|
||||
end: NonNegativeInt,
|
||||
}),
|
||||
),
|
||||
}).annotate({ identifier: "AgentPartInput" })
|
||||
export type AgentPartInput = Types.DeepMutable<Schema.Schema.Type<typeof AgentPartInput>>
|
||||
|
||||
export const SubtaskPartInput = Schema.Struct({
|
||||
id: Schema.optional(PartID),
|
||||
type: Schema.Literal("subtask"),
|
||||
prompt: Schema.String,
|
||||
description: Schema.String,
|
||||
agent: Schema.String,
|
||||
model: Schema.optional(
|
||||
Schema.Struct({
|
||||
providerID: Provider.ID,
|
||||
modelID: Model.ID,
|
||||
}),
|
||||
),
|
||||
command: Schema.optional(Schema.String),
|
||||
}).annotate({ identifier: "SubtaskPartInput" })
|
||||
export type SubtaskPartInput = Types.DeepMutable<Schema.Schema.Type<typeof SubtaskPartInput>>
|
||||
|
||||
export const Assistant = Schema.Struct({
|
||||
...messageBase,
|
||||
role: Schema.Literal("assistant"),
|
||||
time: Schema.Struct({
|
||||
created: NonNegativeInt,
|
||||
completed: Schema.optional(NonNegativeInt),
|
||||
}),
|
||||
error: Schema.optional(AssistantErrorSchema),
|
||||
parentID: MessageID,
|
||||
modelID: Model.ID,
|
||||
providerID: Provider.ID,
|
||||
mode: Schema.String,
|
||||
agent: Schema.String,
|
||||
path: Schema.Struct({
|
||||
cwd: Schema.String,
|
||||
root: Schema.String,
|
||||
}),
|
||||
summary: Schema.optional(Schema.Boolean),
|
||||
cost: Schema.Finite,
|
||||
tokens: Schema.Struct({
|
||||
total: Schema.optional(Schema.Finite),
|
||||
input: Schema.Finite,
|
||||
output: Schema.Finite,
|
||||
reasoning: Schema.Finite,
|
||||
cache: Schema.Struct({
|
||||
read: Schema.Finite,
|
||||
write: Schema.Finite,
|
||||
}),
|
||||
}),
|
||||
structured: Schema.optional(Schema.Any),
|
||||
variant: Schema.optional(Schema.String),
|
||||
finish: Schema.optional(Schema.String),
|
||||
}).annotate({ identifier: "AssistantMessage" })
|
||||
export type Assistant = Omit<Types.DeepMutable<Schema.Schema.Type<typeof Assistant>>, "error"> & {
|
||||
error?: AssistantError
|
||||
}
|
||||
|
||||
export const Info = Schema.Union([User, Assistant]).annotate({ discriminator: "role", identifier: "Message" })
|
||||
export type Info = User | Assistant
|
||||
|
||||
export const WithParts = Schema.Struct({
|
||||
info: Info,
|
||||
parts: Schema.Array(Part),
|
||||
})
|
||||
export type WithParts = {
|
||||
info: Info
|
||||
parts: Part[]
|
||||
}
|
||||
|
||||
const options = {
|
||||
durable: {
|
||||
aggregate: "sessionID",
|
||||
version: 1,
|
||||
},
|
||||
} as const
|
||||
|
||||
const SessionSummary = Schema.Struct({
|
||||
additions: Schema.Finite,
|
||||
deletions: Schema.Finite,
|
||||
files: Schema.Finite,
|
||||
diffs: optionalOmitUndefined(Schema.Array(FileDiff.Info)),
|
||||
})
|
||||
|
||||
const SessionTokens = Schema.Struct({
|
||||
input: Schema.Finite,
|
||||
output: Schema.Finite,
|
||||
reasoning: Schema.Finite,
|
||||
cache: Schema.Struct({
|
||||
read: Schema.Finite,
|
||||
write: Schema.Finite,
|
||||
}),
|
||||
})
|
||||
|
||||
const SessionShare = Schema.Struct({
|
||||
url: Schema.String,
|
||||
})
|
||||
|
||||
const SessionRevert = Schema.Struct({
|
||||
messageID: MessageID,
|
||||
partID: optionalOmitUndefined(PartID),
|
||||
snapshot: optionalOmitUndefined(Schema.String),
|
||||
diff: optionalOmitUndefined(Schema.String),
|
||||
})
|
||||
|
||||
const SessionModel = Schema.Struct({
|
||||
id: Model.ID,
|
||||
providerID: Provider.ID,
|
||||
variant: optionalOmitUndefined(Schema.String),
|
||||
})
|
||||
|
||||
export const SessionInfo = Schema.Struct({
|
||||
id: SessionID,
|
||||
slug: Schema.String,
|
||||
projectID: Project.ID,
|
||||
workspaceID: optionalOmitUndefined(WorkspaceID),
|
||||
directory: Schema.String,
|
||||
path: optionalOmitUndefined(Schema.String),
|
||||
parentID: optionalOmitUndefined(SessionID),
|
||||
summary: optionalOmitUndefined(SessionSummary),
|
||||
cost: optionalOmitUndefined(Schema.Finite),
|
||||
tokens: optionalOmitUndefined(SessionTokens),
|
||||
share: optionalOmitUndefined(SessionShare),
|
||||
title: Schema.String,
|
||||
agent: optionalOmitUndefined(Schema.String),
|
||||
model: optionalOmitUndefined(SessionModel),
|
||||
version: Schema.String,
|
||||
metadata: optionalOmitUndefined(Schema.Record(Schema.String, Schema.Any)),
|
||||
time: Schema.Struct({
|
||||
created: NonNegativeInt,
|
||||
updated: NonNegativeInt,
|
||||
compacting: optionalOmitUndefined(NonNegativeInt),
|
||||
archived: optionalOmitUndefined(Schema.Finite),
|
||||
}),
|
||||
permission: optionalOmitUndefined(PermissionV1.Ruleset),
|
||||
revert: optionalOmitUndefined(SessionRevert),
|
||||
}).annotate({ identifier: "Session" })
|
||||
export type SessionInfo = typeof SessionInfo.Type
|
||||
|
||||
const events = {
|
||||
Created: define({
|
||||
type: "session.created",
|
||||
...options,
|
||||
schema: {
|
||||
sessionID: SessionID,
|
||||
info: SessionInfo,
|
||||
},
|
||||
}),
|
||||
Updated: define({
|
||||
type: "session.updated",
|
||||
...options,
|
||||
schema: {
|
||||
sessionID: SessionID,
|
||||
info: SessionInfo,
|
||||
},
|
||||
}),
|
||||
Deleted: define({
|
||||
type: "session.deleted",
|
||||
...options,
|
||||
schema: {
|
||||
sessionID: SessionID,
|
||||
info: SessionInfo,
|
||||
},
|
||||
}),
|
||||
MessageUpdated: define({
|
||||
type: "message.updated",
|
||||
...options,
|
||||
schema: {
|
||||
sessionID: SessionID,
|
||||
info: Info,
|
||||
},
|
||||
}),
|
||||
MessageRemoved: define({
|
||||
type: "message.removed",
|
||||
...options,
|
||||
schema: {
|
||||
sessionID: SessionID,
|
||||
messageID: MessageID,
|
||||
},
|
||||
}),
|
||||
PartUpdated: define({
|
||||
type: "message.part.updated",
|
||||
...options,
|
||||
schema: {
|
||||
sessionID: SessionID,
|
||||
part: Part,
|
||||
time: Schema.Finite,
|
||||
},
|
||||
}),
|
||||
PartRemoved: define({
|
||||
type: "message.part.removed",
|
||||
...options,
|
||||
schema: {
|
||||
sessionID: SessionID,
|
||||
messageID: MessageID,
|
||||
partID: PartID,
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
export const PartDelta = define({
|
||||
type: "message.part.delta",
|
||||
schema: {
|
||||
sessionID: SessionID,
|
||||
messageID: MessageID,
|
||||
partID: PartID,
|
||||
field: Schema.String,
|
||||
delta: Schema.String,
|
||||
},
|
||||
})
|
||||
|
||||
export const Diff = define({
|
||||
type: "session.diff",
|
||||
schema: {
|
||||
sessionID: SessionID,
|
||||
diff: Schema.Array(FileDiff.Info),
|
||||
},
|
||||
})
|
||||
|
||||
export const Error = define({
|
||||
type: "session.error",
|
||||
schema: {
|
||||
sessionID: Schema.optional(SessionID),
|
||||
error: Assistant.fields.error,
|
||||
},
|
||||
})
|
||||
|
||||
export const Event = {
|
||||
...events,
|
||||
PartDelta,
|
||||
Diff,
|
||||
Error,
|
||||
Definitions: inventory(
|
||||
events.Created,
|
||||
events.Updated,
|
||||
events.Deleted,
|
||||
events.MessageUpdated,
|
||||
events.MessageRemoved,
|
||||
events.PartUpdated,
|
||||
events.PartRemoved,
|
||||
PartDelta,
|
||||
Diff,
|
||||
Error,
|
||||
),
|
||||
}
|
||||
|
|
@ -6,10 +6,13 @@ import { Location } from "./location"
|
|||
import { Model } from "./model"
|
||||
import { Project } from "./project"
|
||||
import { DateTimeUtcFromMillis, optionalOmitUndefined, RelativePath } from "./schema"
|
||||
import { SessionEvent } from "./session-event"
|
||||
import { SessionID } from "./session-id"
|
||||
|
||||
export const ID = SessionID.ID
|
||||
export type ID = SessionID.ID
|
||||
export const ID = SessionID
|
||||
export type ID = SessionID
|
||||
|
||||
export const Event = SessionEvent
|
||||
|
||||
export interface Info extends Schema.Schema.Type<typeof Info> {}
|
||||
export const Info = Schema.Struct({
|
||||
|
|
|
|||
58
packages/schema/src/tui-event.ts
Normal file
58
packages/schema/src/tui-event.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
export * as TuiEvent from "./tui-event"
|
||||
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Event } from "./event"
|
||||
import { PositiveInt } from "./schema"
|
||||
import { SessionID } from "./session-id"
|
||||
|
||||
const DEFAULT_TOAST_DURATION = 5000
|
||||
|
||||
export const PromptAppend = Event.define({ type: "tui.prompt.append", schema: { text: Schema.String } })
|
||||
|
||||
export const CommandExecute = Event.define({
|
||||
type: "tui.command.execute",
|
||||
schema: {
|
||||
command: Schema.Union([
|
||||
Schema.Literals([
|
||||
"session.list",
|
||||
"session.new",
|
||||
"session.share",
|
||||
"session.interrupt",
|
||||
"session.compact",
|
||||
"session.page.up",
|
||||
"session.page.down",
|
||||
"session.line.up",
|
||||
"session.line.down",
|
||||
"session.half.page.up",
|
||||
"session.half.page.down",
|
||||
"session.first",
|
||||
"session.last",
|
||||
"prompt.clear",
|
||||
"prompt.submit",
|
||||
"agent.cycle",
|
||||
]),
|
||||
Schema.String,
|
||||
]),
|
||||
},
|
||||
})
|
||||
|
||||
export const ToastShow = Event.define({
|
||||
type: "tui.toast.show",
|
||||
schema: {
|
||||
title: Schema.optional(Schema.String),
|
||||
message: Schema.String,
|
||||
variant: Schema.Literals(["info", "success", "warning", "error"]),
|
||||
duration: PositiveInt.pipe(Schema.withDecodingDefault(Effect.succeed(DEFAULT_TOAST_DURATION))).annotate({
|
||||
description: "Duration in milliseconds",
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export const SessionSelect = Event.define({
|
||||
type: "tui.session.select",
|
||||
schema: {
|
||||
sessionID: SessionID.annotate({ description: "Session ID to navigate to" }),
|
||||
},
|
||||
})
|
||||
|
||||
export const Definitions = Event.inventory(PromptAppend, CommandExecute, ToastShow, SessionSelect)
|
||||
13
packages/schema/src/vcs-event.ts
Normal file
13
packages/schema/src/vcs-event.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
export * as VcsEvent from "./vcs-event"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { Event } from "./event"
|
||||
|
||||
export const BranchUpdated = Event.define({
|
||||
type: "vcs.branch.updated",
|
||||
schema: {
|
||||
branch: Schema.optional(Schema.String),
|
||||
},
|
||||
})
|
||||
|
||||
export const Definitions = Event.inventory(BranchUpdated)
|
||||
32
packages/schema/src/workspace-event.ts
Normal file
32
packages/schema/src/workspace-event.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
export * as WorkspaceEvent from "./workspace-event"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { Event } from "./event"
|
||||
import { WorkspaceID } from "./workspace-id"
|
||||
|
||||
export const ConnectionStatus = Schema.Struct({
|
||||
workspaceID: WorkspaceID,
|
||||
status: Schema.Literals(["connected", "connecting", "disconnected", "error"]),
|
||||
})
|
||||
export type ConnectionStatus = typeof ConnectionStatus.Type
|
||||
|
||||
export const Ready = Event.define({
|
||||
type: "workspace.ready",
|
||||
schema: {
|
||||
name: Schema.String,
|
||||
},
|
||||
})
|
||||
|
||||
export const Failed = Event.define({
|
||||
type: "workspace.failed",
|
||||
schema: {
|
||||
message: Schema.String,
|
||||
},
|
||||
})
|
||||
|
||||
export const Status = Event.define({
|
||||
type: "workspace.status",
|
||||
schema: ConnectionStatus.fields,
|
||||
})
|
||||
|
||||
export const Definitions = Event.inventory(Ready, Failed, Status)
|
||||
19
packages/schema/src/workspace-id.ts
Normal file
19
packages/schema/src/workspace-id.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { Schema } from "effect"
|
||||
import { ascending } from "./identifier"
|
||||
import { withStatics } from "./schema"
|
||||
|
||||
export const WorkspaceID = Schema.String.check(Schema.isStartsWith("wrk")).pipe(
|
||||
Schema.brand("WorkspaceV2.ID"),
|
||||
withStatics((schema) => {
|
||||
const create = () => schema.make("wrk_" + ascending())
|
||||
return {
|
||||
ascending: (id?: string) => {
|
||||
if (!id) return create()
|
||||
if (!id.startsWith("wrk")) throw new Error(`ID ${id} does not start with wrk`)
|
||||
return schema.make(id)
|
||||
},
|
||||
create,
|
||||
}
|
||||
}),
|
||||
)
|
||||
export type WorkspaceID = typeof WorkspaceID.Type
|
||||
|
|
@ -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
|
||||
|
|
|
|||
21
packages/schema/src/worktree-event.ts
Normal file
21
packages/schema/src/worktree-event.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
export * as WorktreeEvent from "./worktree-event"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { Event } from "./event"
|
||||
|
||||
export const Ready = Event.define({
|
||||
type: "worktree.ready",
|
||||
schema: {
|
||||
name: Schema.String,
|
||||
branch: Schema.optional(Schema.String),
|
||||
},
|
||||
})
|
||||
|
||||
export const Failed = Event.define({
|
||||
type: "worktree.failed",
|
||||
schema: {
|
||||
message: Schema.String,
|
||||
},
|
||||
})
|
||||
|
||||
export const Definitions = Event.inventory(Ready, Failed)
|
||||
53
packages/schema/test/event-manifest.test.ts
Normal file
53
packages/schema/test/event-manifest.test.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { describe, expect, test } from "bun:test"
|
||||
import { FileSystem, Integration, Permission, Project, Reference, Session, Workspace } from "../src"
|
||||
import { EventManifest } from "../src/event-manifest"
|
||||
import { IdeEvent } from "../src/ide-event"
|
||||
import { SessionEvent } from "../src/session-event"
|
||||
import { SessionTodo } from "../src/session-todo"
|
||||
import { SessionV1 } from "../src/session-v1"
|
||||
import { WorkspaceEvent } from "../src/workspace-event"
|
||||
|
||||
describe("public event manifest", () => {
|
||||
test("owns the complete public event surface", () => {
|
||||
expect(EventManifest.ServerDefinitions.length).toBe(55)
|
||||
expect(EventManifest.Definitions.length).toBe(85)
|
||||
expect(SessionV1.Event.Definitions).toEqual([
|
||||
SessionV1.Event.Created,
|
||||
SessionV1.Event.Updated,
|
||||
SessionV1.Event.Deleted,
|
||||
SessionV1.Event.MessageUpdated,
|
||||
SessionV1.Event.MessageRemoved,
|
||||
SessionV1.Event.PartUpdated,
|
||||
SessionV1.Event.PartRemoved,
|
||||
SessionV1.Event.PartDelta,
|
||||
SessionV1.Event.Diff,
|
||||
SessionV1.Event.Error,
|
||||
])
|
||||
expect(EventManifest.Latest.size).toBe(85)
|
||||
expect(EventManifest.Durable.size).toBe(32)
|
||||
})
|
||||
|
||||
test("uses canonical definitions for current public events", () => {
|
||||
expect(Session.Event).toBe(SessionEvent)
|
||||
expect(Session.Event.Definitions).toBe(SessionEvent.Definitions)
|
||||
expect(Workspace.Event).toBe(WorkspaceEvent)
|
||||
expect(Workspace.Event.Definitions).toBe(WorkspaceEvent.Definitions)
|
||||
expect(EventManifest.Latest.get("session.next.step.ended")).toBe(SessionEvent.Step.Ended)
|
||||
expect(EventManifest.Latest.get("todo.updated")).toBe(SessionTodo.Event.Updated)
|
||||
expect(EventManifest.Latest.get("project.updated")).toBe(Project.Event.Updated)
|
||||
expect(Project.Event.Definitions).toEqual([Project.Event.Updated])
|
||||
expect(FileSystem.Event.Definitions).toEqual([FileSystem.Event.Edited])
|
||||
expect(Integration.Event.Definitions).toEqual([Integration.Event.Updated, Integration.Event.ConnectionUpdated])
|
||||
expect(Permission.Event.Definitions).toEqual([Permission.Event.Asked, Permission.Event.Replied])
|
||||
expect(Reference.Event.Definitions).toEqual([Reference.Event.Updated])
|
||||
expect(EventManifest.Latest.has("ide.installed")).toBe(false)
|
||||
expect(IdeEvent.Definitions).toEqual([IdeEvent.Installed])
|
||||
expect(EventManifest.Definitions.slice(40, 43)).toEqual([
|
||||
SessionV1.Event.PartDelta,
|
||||
SessionV1.Event.Diff,
|
||||
SessionV1.Event.Error,
|
||||
])
|
||||
expect(EventManifest.Durable.has("session.next.step.ended.1")).toBe(false)
|
||||
expect(EventManifest.Durable.get("session.next.step.ended.2")).toBe(SessionEvent.Step.Ended)
|
||||
})
|
||||
})
|
||||
37
packages/schema/test/event.test.ts
Normal file
37
packages/schema/test/event.test.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { describe, expect, test } from "bun:test"
|
||||
import { Schema } from "effect"
|
||||
import { Event } from "../src/event"
|
||||
|
||||
describe("public event schemas", () => {
|
||||
test("definition is pure", () => {
|
||||
const definitions = Event.inventory()
|
||||
Event.define({ type: "test.pure", schema: { value: Schema.String } })
|
||||
expect(definitions).toEqual([])
|
||||
})
|
||||
|
||||
test("latest selection is independent of declaration order", () => {
|
||||
const historical = Event.define({
|
||||
type: "test.versioned",
|
||||
durable: { aggregate: "id", version: 1 },
|
||||
schema: { id: Schema.String },
|
||||
})
|
||||
const current = Event.define({
|
||||
type: "test.versioned",
|
||||
durable: { aggregate: "id", version: 2 },
|
||||
schema: { id: Schema.String, value: Schema.String },
|
||||
})
|
||||
|
||||
expect(Event.latest([historical, current]).get(current.type)).toBe(current)
|
||||
expect(Event.latest([current, historical]).get(current.type)).toBe(current)
|
||||
})
|
||||
|
||||
test("durable definitions are indexed by type and version", () => {
|
||||
const definition = Event.define({
|
||||
type: "test.durable",
|
||||
durable: { aggregate: "id", version: 1 },
|
||||
schema: { id: Schema.String },
|
||||
})
|
||||
|
||||
expect(Event.durable([definition]).get("test.durable.1")).toBe(definition)
|
||||
})
|
||||
})
|
||||
53
packages/schema/test/legacy-event.test.ts
Normal file
53
packages/schema/test/legacy-event.test.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { describe, expect, test } from "bun:test"
|
||||
import { LegacyEvent } from "../src/legacy-event"
|
||||
import { PermissionV1 } from "../src/permission-v1"
|
||||
import { QuestionV1 } from "../src/question-v1"
|
||||
import { Project } from "../src/project"
|
||||
import { SessionV1 } from "../src/session-v1"
|
||||
|
||||
describe("legacy public event schemas", () => {
|
||||
test("owns all SessionV1 definitions", () => {
|
||||
expect(SessionV1.Event.Definitions.map((event) => event.type)).toEqual([
|
||||
"session.created",
|
||||
"session.updated",
|
||||
"session.deleted",
|
||||
"message.updated",
|
||||
"message.removed",
|
||||
"message.part.updated",
|
||||
"message.part.removed",
|
||||
"message.part.delta",
|
||||
"session.diff",
|
||||
"session.error",
|
||||
])
|
||||
const durable = SessionV1.Event.Definitions.filter((event) => event.durable !== undefined)
|
||||
expect(durable).toHaveLength(7)
|
||||
expect(durable.every((event) => event.durable?.aggregate === "sessionID")).toBe(true)
|
||||
expect(durable.every((event) => event.durable?.version === 1)).toBe(true)
|
||||
})
|
||||
|
||||
test("owns the legacy transient public definitions", () => {
|
||||
expect([
|
||||
SessionV1.PartDelta.type,
|
||||
SessionV1.Diff.type,
|
||||
SessionV1.Error.type,
|
||||
PermissionV1.Event.Asked.type,
|
||||
PermissionV1.Event.Replied.type,
|
||||
QuestionV1.Event.Asked.type,
|
||||
QuestionV1.Event.Replied.type,
|
||||
QuestionV1.Event.Rejected.type,
|
||||
Project.Event.Updated.type,
|
||||
LegacyEvent.CommandExecuted.type,
|
||||
]).toEqual([
|
||||
"message.part.delta",
|
||||
"session.diff",
|
||||
"session.error",
|
||||
"permission.asked",
|
||||
"permission.replied",
|
||||
"question.asked",
|
||||
"question.replied",
|
||||
"question.rejected",
|
||||
"project.updated",
|
||||
"command.executed",
|
||||
])
|
||||
})
|
||||
})
|
||||
|
|
@ -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,7 +8,8 @@ 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 { EventGroup, makeEventGroup } from "./groups/event"
|
||||
import type { Definition } from "@opencode-ai/core/event"
|
||||
import { AgentGroup } from "./groups/agent"
|
||||
import { HealthGroup } from "./groups/health"
|
||||
import { PtyGroup } from "./groups/pty"
|
||||
|
|
@ -20,31 +21,36 @@ 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)
|
||||
const makeApiFromGroup = <const Group extends HttpApiGroup.Any>(eventGroup: Group) =>
|
||||
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<Definition>) => makeApiFromGroup(makeEventGroup(definitions))
|
||||
|
||||
export const Api = makeApiFromGroup(EventGroup)
|
||||
|
|
|
|||
|
|
@ -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/core/event"
|
||||
import { Schema } from "effect"
|
||||
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
|
||||
|
|
@ -10,25 +12,47 @@ 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" })
|
||||
const schema = <const Definitions extends ReadonlyArray<Definition>>(definitions: Definitions) =>
|
||||
Schema.Union([
|
||||
...definitions.map((definition) =>
|
||||
Schema.Struct({
|
||||
...fields,
|
||||
type: Schema.Literal(definition.type),
|
||||
data: definition.data,
|
||||
}).annotate({ identifier: `V2Event.${definition.type}` }),
|
||||
),
|
||||
...(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 = <const Definitions extends ReadonlyArray<Definition>>(definitions: Definitions) =>
|
||||
HttpApiGroup.make("server.event")
|
||||
.add(
|
||||
HttpApiEndpoint.get("event.subscribe", "/api/event", {
|
||||
success: schema(definitions),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.event.subscribe",
|
||||
summary: "Subscribe to events",
|
||||
description: "Subscribe to native event payloads for the server.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(OpenApi.annotations({ title: "events", description: "Experimental event stream route." }))
|
||||
|
||||
const EventSchema = schema(PublicEventManifest.Definitions)
|
||||
|
||||
export const EventGroup = HttpApiGroup.make("server.event")
|
||||
.add(
|
||||
HttpApiEndpoint.get("event.subscribe", "/api/event", {
|
||||
success: Event,
|
||||
success: EventSchema,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.event.subscribe",
|
||||
|
|
@ -38,5 +62,4 @@ export const EventGroup = HttpApiGroup.make("server.event")
|
|||
),
|
||||
)
|
||||
.annotateMerge(OpenApi.annotations({ title: "events", description: "Experimental event stream route." }))
|
||||
|
||||
export type Event = typeof Event.Type
|
||||
export type Event = typeof EventSchema.Type
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue